268 lines
6.6 KiB
Go
Executable file
268 lines
6.6 KiB
Go
Executable file
package main
|
|
|
|
import "os"
|
|
import "encoding/json"
|
|
import "log"
|
|
import "crypto/tls"
|
|
import "net"
|
|
import "sync"
|
|
import "strconv"
|
|
import "strings"
|
|
import "github.com/emersion/go-sasl"
|
|
import "github.com/emersion/go-smtp"
|
|
import "io"
|
|
import "io/ioutil"
|
|
import "time"
|
|
import "errors"
|
|
|
|
// SMTP
|
|
|
|
type Backend struct {
|
|
cfg TRouteConfig
|
|
}
|
|
func (b *Backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
|
|
return &Session{ cfg : b.cfg }, nil
|
|
}
|
|
|
|
type Session struct {
|
|
username string
|
|
password string
|
|
from string
|
|
to string
|
|
data string
|
|
cfg TRouteConfig
|
|
}
|
|
func (s *Session) AuthPlain(username, password string) error {
|
|
s.username = username
|
|
s.password = password
|
|
return nil
|
|
}
|
|
func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
|
|
s.from = from
|
|
return nil
|
|
}
|
|
func (s *Session) Rcpt(to string) error {
|
|
s.to = to
|
|
return nil
|
|
}
|
|
func (s *Session) Data(r io.Reader) error {
|
|
if b, err := ioutil.ReadAll(r); err != nil {
|
|
return err
|
|
} else {
|
|
s.data += string(b)
|
|
return nil
|
|
}
|
|
}
|
|
func (s *Session) Reset() {
|
|
if Config.Is_verbose_log {
|
|
log.Println("smtp: ", s.username, " sends from ", s.from, " to ", s.to, " ", len(s.data), " bytes")
|
|
}
|
|
auth := sasl.NewPlainClient("", s.username, s.password)
|
|
to := []string{s.to}
|
|
err := smtp.SendMail(s.cfg.Destination + ":" + strconv.FormatUint(uint64(s.cfg.Port_out), 10), auth, s.from, to, strings.NewReader(s.data))
|
|
if err != nil {
|
|
log.Println("smtp: ", err)
|
|
}
|
|
}
|
|
func (s *Session) Logout() error {
|
|
s.username = ""
|
|
s.password = ""
|
|
s.from = ""
|
|
s.to = ""
|
|
s.data = ""
|
|
return nil
|
|
}
|
|
|
|
// TCP and general
|
|
|
|
type TRouteConfig struct {
|
|
Port_in uint16 `json:"port-in"`
|
|
Port_out uint16 `json:"port-out"`
|
|
Destination string `json:"destination"`
|
|
Type string `json:"type"`
|
|
}
|
|
type TConfig struct {
|
|
Routes []TRouteConfig `json:"routes"`
|
|
Is_verbose_log bool `json:"verbose-log"`
|
|
SMTP_data_limit int `json:"smtp-data-limit"`
|
|
SMTP_timeout int `json:"smtp-timeout"`
|
|
SMTP_max_recipients int `json:"smtp-max-recipients"`
|
|
}
|
|
type TState struct {
|
|
is_routing bool
|
|
}
|
|
|
|
var Config = TConfig {
|
|
Is_verbose_log : true,
|
|
}
|
|
|
|
var State = TState {
|
|
is_routing : false,
|
|
}
|
|
|
|
// functions
|
|
|
|
func load_config() {
|
|
content, err_file := os.ReadFile("./config.json")
|
|
if err_file != nil {
|
|
log.Fatal(err_file)
|
|
}
|
|
err_json := json.Unmarshal(content, &Config)
|
|
if err_json != nil {
|
|
log.Fatal(err_json)
|
|
}
|
|
}
|
|
|
|
func validate_config() {
|
|
/* no validation required at this point */
|
|
}
|
|
|
|
func is_net_closed(err error) bool {
|
|
switch {
|
|
case
|
|
errors.Is(err, net.ErrClosed),
|
|
errors.Is(err, io.EOF):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func feedback_tcp(route TRouteConfig, in net.Conn, out net.Conn, wg sync.WaitGroup, is_active *bool) {
|
|
buffer := make([]byte, 1024)
|
|
defer wg.Done()
|
|
for State.is_routing {
|
|
n, err := out.Read(buffer)
|
|
if err != nil {
|
|
if is_net_closed(err) {
|
|
break
|
|
} else {
|
|
log.Println("tcp: ", err)
|
|
continue
|
|
}
|
|
} else {
|
|
if Config.Is_verbose_log {
|
|
log.Println("tcp: received ", n, " bytes")
|
|
}
|
|
if n > 0 {
|
|
_, err := in.Write(buffer[:n])
|
|
if err != nil {
|
|
if is_net_closed(err) {
|
|
break
|
|
} else {
|
|
log.Println("tcp: ", n, ": ", err)
|
|
continue
|
|
}
|
|
} else {
|
|
log.Println("tcp: relayed successfully")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
log.Print("tcp: closing client")
|
|
*is_active = false
|
|
}
|
|
|
|
func handle_tcp(route TRouteConfig, in net.Conn, wg sync.WaitGroup) {
|
|
defer wg.Done()
|
|
defer in.Close()
|
|
tls_cfg := tls.Config{
|
|
PreferServerCipherSuites: true,
|
|
CipherSuites: []uint16 {
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
|
},
|
|
}
|
|
is_out_active := true
|
|
out, err := tls.Dial("tcp", route.Destination + ":" + strconv.FormatUint(uint64(route.Port_out), 10), &tls_cfg)
|
|
log.Print("tcp: opening client")
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
defer out.Close()
|
|
wg.Add(1)
|
|
go feedback_tcp(route, in, out, wg, &is_out_active)
|
|
buffer := make([]byte, 1024)
|
|
for State.is_routing && is_out_active {
|
|
n, err := in.Read(buffer)
|
|
if err != nil {
|
|
log.Println("tcp: ", err)
|
|
return
|
|
}
|
|
if n > 0 {
|
|
_, err := out.Write(buffer[:n])
|
|
if err != nil {
|
|
log.Println("tcp: ", n, ": ", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func listen_route_tcp(route TRouteConfig, wg sync.WaitGroup) {
|
|
defer wg.Done()
|
|
server, err := net.Listen("tcp", ":" + strconv.FormatUint(uint64(route.Port_in), 10))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer server.Close()
|
|
var conwg sync.WaitGroup
|
|
for State.is_routing {
|
|
con, err := server.Accept()
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
conwg.Add(1)
|
|
go handle_tcp(route, con, conwg)
|
|
}
|
|
_ = server
|
|
conwg.Wait()
|
|
}
|
|
|
|
func listen_route_smtp(route TRouteConfig, wg sync.WaitGroup) {
|
|
defer wg.Done()
|
|
be := &Backend{ cfg : route }
|
|
s := smtp.NewServer(be)
|
|
s.Addr = ":" + strconv.FormatUint(uint64(route.Port_in), 10)
|
|
s.Domain = "localhost"
|
|
s.ReadTimeout = time.Duration(Config.SMTP_timeout) * time.Second
|
|
s.WriteTimeout = time.Duration(Config.SMTP_timeout) * time.Second
|
|
s.MaxMessageBytes = Config.SMTP_data_limit
|
|
s.MaxRecipients = Config.SMTP_max_recipients
|
|
s.AllowInsecureAuth = true
|
|
if err := s.ListenAndServe(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func listen_route(route TRouteConfig, wg sync.WaitGroup) {
|
|
switch(route.Type) {
|
|
case "tcp":
|
|
listen_route_tcp(route, wg)
|
|
case "smtp":
|
|
listen_route_smtp(route, wg)
|
|
}
|
|
}
|
|
|
|
func run_listeners() {
|
|
var wg sync.WaitGroup
|
|
State.is_routing = true
|
|
if Config.Is_verbose_log {
|
|
log.Printf("starting %d routes", len(Config.Routes))
|
|
}
|
|
for _, route := range Config.Routes {
|
|
wg.Add(1)
|
|
go listen_route(route, wg)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func main() {
|
|
load_config()
|
|
validate_config()
|
|
run_listeners()
|
|
}
|