From 46894797453e31c616b710b17705281b9db834b3 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Fri, 5 Mar 2021 11:51:43 +0300 Subject: [PATCH] Add base dialers module --- example.config.toml | 7 +++ go.mod | 8 +++ go.sum | 36 +++++++++++ main.go | 19 ++++++ mtglib/dialers/consts.go | 9 +++ mtglib/dialers/default.go | 82 +++++++++++++++++++++++++ mtglib/dialers/dialer.go | 112 ++++++++++++++++++++++++++++++++++ mtglib/dialers/interfaces.go | 11 ++++ mtglib/dialers/shadowsocks.go | 68 +++++++++++++++++++++ mtglib/dialers/socks5.go | 23 +++++++ mtglib/secret.go | 71 +++++++++++++++++++++ raw_config.go | 60 ++++++++++++++++++ 12 files changed, 506 insertions(+) create mode 100644 mtglib/dialers/consts.go create mode 100644 mtglib/dialers/default.go create mode 100644 mtglib/dialers/dialer.go create mode 100644 mtglib/dialers/interfaces.go create mode 100644 mtglib/dialers/shadowsocks.go create mode 100644 mtglib/dialers/socks5.go create mode 100644 mtglib/secret.go create mode 100644 raw_config.go diff --git a/example.config.toml b/example.config.toml index a94a3cf..3f7bea7 100644 --- a/example.config.toml +++ b/example.config.toml @@ -44,6 +44,13 @@ prefer-ips = "prefer-ipv6" # access. cloak-port = 443 +# Path to access file. Each time when proxy starts up, it writes an +# access file. This file contains a JSON with settings how to access +# this proxy. +# +# Pass filepath here or '-' if you want to dump into stdout. +access-file = "-" + # FakeTLS can compare timestamps to prevent probes. Each message has # encrypted timestamp. So, mtg can compare this timestamp and decide if # we need to proceed with connection or not. diff --git a/go.mod b/go.mod index 2b87317..10e8001 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module github.com/9seconds/mtg/v2 go 1.16 + +require ( + github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 + github.com/libp2p/go-reuseport v0.0.2 + github.com/pelletier/go-toml v1.8.1 + github.com/shadowsocks/go-shadowsocks2 v0.1.4 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 +) diff --git a/go.sum b/go.sum index e69de29..fd68855 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,36 @@ +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/libp2p/go-reuseport v0.0.2 h1:XSG94b1FJfGA01BUrT82imejHQyTxO4jEWqheyCXYvU= +github.com/libp2p/go-reuseport v0.0.2/go.mod h1:SPD+5RwGC7rcnzngoYC86GjPzjSywuQyMVAheVBD9nQ= +github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= +github.com/shadowsocks/go-shadowsocks2 v0.1.4 h1:4VzajPL7RwwmImysBSvI+lm/UaegDGQq3hr42dYo3gs= +github.com/shadowsocks/go-shadowsocks2 v0.1.4/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index 8c4cef3..3281a3f 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,31 @@ package main import ( + "fmt" + "io/ioutil" "math/rand" + "os" "time" + + "github.com/9seconds/mtg/v2/mtglib/dialers" ) var version = "dev" // has to be set by ldflags func main() { rand.Seed(time.Now().UTC().UnixNano()) + + f, _ := os.Open("example.config.toml") + + fmt.Println(parseRawConfig(f)) + + bd, _ := dialers.NewDefaultBaseDialer(0, 0) + d, _ := dialers.MakeDialer(bd, "9.9.9.9", 0) + + r, err := d.HTTP.Get("https://ifconfig.co") + + fmt.Println(err) + body, _ := ioutil.ReadAll(r.Body) + + fmt.Println(string(body)) } diff --git a/mtglib/dialers/consts.go b/mtglib/dialers/consts.go new file mode 100644 index 0000000..40a022d --- /dev/null +++ b/mtglib/dialers/consts.go @@ -0,0 +1,9 @@ +package dialers + +import "time" + +const ( + DefaultTimeout = 10 * time.Second + DefaultHTTPTimeout = DefaultTimeout + DefaultBufferSize = 4096 +) diff --git a/mtglib/dialers/default.go b/mtglib/dialers/default.go new file mode 100644 index 0000000..feeda9d --- /dev/null +++ b/mtglib/dialers/default.go @@ -0,0 +1,82 @@ +package dialers + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/libp2p/go-reuseport" +) + +type defaultBaseDialer struct { + net.Dialer + + bufferSize int +} + +func (d *defaultBaseDialer) Dial(network, address string) (net.Conn, error) { + return d.DialContext(context.Background(), network, address) +} + +func (d *defaultBaseDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + switch network { + case "tcp", "tcp4", "tcp6": + default: + return nil, fmt.Errorf("unsupported network %s", network) + } + + conn, err := d.Dialer.DialContext(ctx, network, address) + if err != nil { + return nil, fmt.Errorf("cannot dial to %s: %w", address, err) + } + + tcpConn := conn.(*net.TCPConn) + + if err := tcpConn.SetNoDelay(true); err != nil { + conn.Close() + return nil, fmt.Errorf("cannot set TCP_NO_DELAY: %w", err) + } + + if err := tcpConn.SetReadBuffer(d.bufferSize); err != nil { + tcpConn.Close() + return nil, fmt.Errorf("cannot set read buffer size: %w", err) + } + + if err := tcpConn.SetWriteBuffer(d.bufferSize); err != nil { + tcpConn.Close() + return nil, fmt.Errorf("cannot set write buffer size: %w", err) + } + + if err := tcpConn.SetKeepAlive(true); err != nil { + tcpConn.Close() + return nil, fmt.Errorf("cannot enable keep-alive: %w", err) + } + + return tcpConn, nil +} + +func NewDefaultBaseDialer(timeout time.Duration, bufferSize int) (BaseDialer, error) { + switch { + case timeout < 0: + return nil, fmt.Errorf("timeout %v should be positive number", timeout) + case bufferSize < 0: + return nil, fmt.Errorf("buffer size %s should be positive number", bufferSize) + } + + if timeout == 0 { + timeout = DefaultTimeout + } + + if bufferSize == 0 { + bufferSize = DefaultBufferSize + } + + return &defaultBaseDialer{ + Dialer: net.Dialer{ + Timeout: timeout, + Control: reuseport.Control, + }, + bufferSize: bufferSize, + }, nil +} diff --git a/mtglib/dialers/dialer.go b/mtglib/dialers/dialer.go new file mode 100644 index 0000000..3f8d1be --- /dev/null +++ b/mtglib/dialers/dialer.go @@ -0,0 +1,112 @@ +package dialers + +import ( + "context" + "fmt" + "math/rand" + "net" + "net/http" + "time" + + doh "github.com/babolivier/go-doh-client" +) + +type Dialer struct { + HTTP http.Client + DNS doh.Resolver + + baseDialer BaseDialer +} + +func (d *Dialer) Dial(network, address string) (net.Conn, error) { + return d.DialContext(context.Background(), network, address) +} + +func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + host, port, _ := net.SplitHostPort(address) + + ips, err := d.resolveIPs(network, host) + if err != nil { + return nil, fmt.Errorf("cannot resolve dns names: %w", err) + } + + rand.Shuffle(len(ips), func(i, j int) { + ips[i], ips[j] = ips[j], ips[i] + }) + + for _, v := range ips { + if conn, err := d.baseDialer.DialContext(ctx, network, net.JoinHostPort(v, port)); err == nil { + return conn, nil + } + } + + return nil, fmt.Errorf("cannot dial to %s:%s", network, address) +} + +func (d *Dialer) resolveIPs(network, address string) ([]string, error) { + if net.ParseIP(address) != nil { + return []string{address}, nil + } + + var ips []string + + switch network { + case "tcp", "tcp4": + if recs, _, err := d.DNS.LookupA(address); err == nil { + for _, v := range recs { + ips = append(ips, v.IP4) + } + } + } + + switch network { + case "tcp", "tcp6": + if recs, _, err := d.DNS.LookupAAAA(address); err == nil { + for _, v := range recs { + ips = append(ips, v.IP6) + } + } + } + + if len(ips) == 0 { + return nil, fmt.Errorf("cannot find any ips for %s:%s", network, address) + } + + return ips, nil +} + +func MakeDialer(base BaseDialer, dohHostname string, httpTimeout time.Duration) (*Dialer, error) { + switch { + case httpTimeout < 0: + return nil, fmt.Errorf("timeout should be positive number %v", httpTimeout) + case httpTimeout == 0: + httpTimeout = DefaultHTTPTimeout + } + + if net.ParseIP(dohHostname) == nil { + return nil, fmt.Errorf("hostname %s should be IP address", dohHostname) + } + + dohHTTPClient := &http.Client{ + Timeout: httpTimeout, + Transport: &http.Transport{ + DialContext: base.DialContext, + }, + } + rv := &Dialer{ + baseDialer: base, + DNS: doh.Resolver{ + Host: dohHostname, + Class: doh.IN, + HTTPClient: dohHTTPClient, + }, + } + rv.HTTP = http.Client{ + Timeout: httpTimeout, + Transport: &http.Transport{ + DialContext: rv.DialContext, + }, + } + + return rv, nil +} diff --git a/mtglib/dialers/interfaces.go b/mtglib/dialers/interfaces.go new file mode 100644 index 0000000..774aa77 --- /dev/null +++ b/mtglib/dialers/interfaces.go @@ -0,0 +1,11 @@ +package dialers + +import ( + "context" + "net" +) + +type BaseDialer interface { + Dial(network, address string) (net.Conn, error) + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} diff --git a/mtglib/dialers/shadowsocks.go b/mtglib/dialers/shadowsocks.go new file mode 100644 index 0000000..1929fa2 --- /dev/null +++ b/mtglib/dialers/shadowsocks.go @@ -0,0 +1,68 @@ +package dialers + +import ( + "context" + "encoding/base64" + "fmt" + "net" + "net/url" + "strings" + "time" + + shadowsocks "github.com/shadowsocks/go-shadowsocks2/core" +) + +type shadowsocksBaseDialer struct { + base BaseDialer + cipher shadowsocks.StreamConnCipher +} + +func (s *shadowsocksBaseDialer) Dial(network, address string) (net.Conn, error) { + conn, err := s.base.Dial(network, address) + if err != nil { + return nil, err + } + + return s.cipher.StreamConn(conn), nil +} + +func (s *shadowsocksBaseDialer) DialContext(ctx context.Context, + network, address string) (net.Conn, error) { + conn, err := s.base.DialContext(ctx, network, address) + if err != nil { + return nil, err + } + + return s.cipher.StreamConn(conn), nil +} + +func NewShadowsocksBaseDialer(proxyUrl *url.URL, + timeout time.Duration, bufferSize int) (BaseDialer, error) { + username := proxyUrl.User.Username() + + decoded, err := base64.RawURLEncoding.DecodeString(username) + if err != nil { + return nil, fmt.Errorf("cannot decode payload: %w", err) + } + + chunks := strings.SplitN(string(decoded), ":", 2) + if len(chunks) != 2 { + return nil, fmt.Errorf("incorrect payload %s", username) + } + + cipher, err := shadowsocks.PickCipher(chunks[0], nil, chunks[1]) + if err != nil { + return nil, fmt.Errorf("cannot initialize shadowsocks cipher: %w", err) + } + + baseDialer, err := NewDefaultBaseDialer(timeout, bufferSize) + if err != nil { + return nil, fmt.Errorf("cannot initialize a base dialer: %w", err) + + } + + return &shadowsocksBaseDialer{ + base: baseDialer, + cipher: cipher, + }, nil +} diff --git a/mtglib/dialers/socks5.go b/mtglib/dialers/socks5.go new file mode 100644 index 0000000..f5f93ca --- /dev/null +++ b/mtglib/dialers/socks5.go @@ -0,0 +1,23 @@ +package dialers + +import ( + "fmt" + "net/url" + "time" + + "golang.org/x/net/proxy" +) + +func NewSocks5BaseDialer(proxyUrl *url.URL, timeout time.Duration, bufferSize int) (BaseDialer, error) { + baseDialer, err := NewDefaultBaseDialer(timeout, bufferSize) + if err != nil { + return nil, fmt.Errorf("cannot initialize base dialer: %w", err) + } + + rv, err := proxy.FromURL(proxyUrl, baseDialer.(*defaultBaseDialer)) + if err != nil { + return nil, fmt.Errorf("cannot initialize socks5 proxy dialer: %w", err) + } + + return rv.(BaseDialer), nil +} diff --git a/mtglib/secret.go b/mtglib/secret.go new file mode 100644 index 0000000..865ca60 --- /dev/null +++ b/mtglib/secret.go @@ -0,0 +1,71 @@ +package mtglib + +import ( + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "strings" +) + +type Secret struct { + Key []byte + Host string +} + +func (s *Secret) MarshalText() ([]byte, error) { + if s == nil { + return nil, nil + } + + return []byte(s.String()), nil +} + +func (s *Secret) UnmarshalText(text []byte) error { + sc, err := ParseSecret(string(text)) + if err != nil { + return err + } + + *s = sc + + return nil +} + +func (s Secret) Base64() string { + return s.String() +} + +func (s Secret) EE() string { + return "ee" + hex.EncodeToString(append(s.Key, s.Host...)) +} + +func (s Secret) String() string { + return base64.StdEncoding.EncodeToString(append(s.Key, s.Host...)) +} + +func ParseSecret(secret string) (Secret, error) { + rv := Secret{} + + if secret == "" { + return rv, errors.New("secret cannot be empty") + } + + decoded, err := base64.RawStdEncoding.DecodeString(secret) + if err != nil && strings.HasPrefix(secret, "ee") { + decoded, err = hex.DecodeString(strings.TrimPrefix(secret, "ee")) + } + + if err != nil { + return rv, fmt.Errorf("incorrect secret format: %w", err) + } + + if len(decoded) < 33 { + return rv, fmt.Errorf("secret %s has incorrect length", secret) + } + + rv.Key = decoded[:32] + rv.Host = string(decoded[32:]) + + return rv, nil +} diff --git a/raw_config.go b/raw_config.go new file mode 100644 index 0000000..f6789d2 --- /dev/null +++ b/raw_config.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "io" + + "github.com/pelletier/go-toml" +) + +type rawConfig struct { + Debug bool `toml:"debug"` + Secret string `toml:"secret"` + BindTo string `toml:"bind-to"` + TCPBuffer string `toml:"tcp-buffer"` + PreferIP string `toml:"prefer-ip"` + CloakPort uint `toml:"cloak-port"` + AccessFile string `toml:"access-file"` + Probes struct { + Time struct { + Enabled bool `toml:"enabled"` + AllowSkewness string `toml:"allow-skewness"` + } `toml:"time"` + AntiReplay struct { + Enabled bool `toml:"enabled"` + MaxSize string `toml:"max-size"` + TTL string `toml:"ttl"` + } `toml:"anti-replay"` + } `toml:"probes"` + PublicIP struct { + IPv4 string `toml:"ipv4"` + IPv6 string `toml:"ipv6"` + } `toml:"public-ip"` + Dialers struct { + Telegram string `toml:"telegram"` + Default string `toml:"default"` + } `toml:"dialers"` + Stats struct { + StatsD struct { + Enabled bool `toml:"enabled"` + Address string `toml:"address"` + MetricPrefix string `toml:"metric-prefix"` + } `toml:"statsd"` + Prometheus struct { + Enabled bool `toml:"enabled"` + BindTo string `toml:"bind-to"` + HttpPath string `toml:"http-path"` + MetricPrefix string `toml:"metric-prefix"` + } `toml:"prometheus"` + } `toml:"stats"` +} + +func parseRawConfig(reader io.Reader) (*rawConfig, error) { + conf := &rawConfig{} + + if err := toml.NewDecoder(reader).Decode(conf); err != nil { + return nil, fmt.Errorf("cannot parse config: %w", err) + } + + return conf, nil +}