diff --git a/cli_access.go b/cli_access.go new file mode 100644 index 0000000..ab356a8 --- /dev/null +++ b/cli_access.go @@ -0,0 +1,156 @@ +package main + +import ( + "context" + "encoding/json" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "strconv" + "strings" + + "github.com/9seconds/mtg/v2/mtglib/network" +) + +type runAccessResponse struct { + IPv4 *runAccessResponseURLs `json:"ipv4,omitempty"` + IPv6 *runAccessResponseURLs `json:"ipv6,omitempty"` + Secret struct { + Hex string `json:"hex"` + Base64 string `json:"base64"` + } `json:"secret"` +} + +type runAccessResponseURLs struct { + IP net.IP `json:"ip"` + TgURL string `json:"tg_url"` + TgQrCode string `json:"tg_qrcode"` + TmeURL string `json:"tme_url"` + TmeQrCode string `json:"tme_qrcode"` +} + +func runAccess(cli *CLI) { + filefp, err := os.Open(cli.Access.ConfigPath) + if err != nil { + exit(err) + } + + defer filefp.Close() + + conf, err := parseConfig(filefp) + if err != nil { + exit(err) + } + + ntw, err := makeNetwork(conf) + if err != nil { + exit(err) + } + + ipv4 := conf.Network.PublicIP.IPv4.Value(nil) + ipv6 := conf.Network.PublicIP.IPv6.Value(nil) + + if ipv4 == nil { + ipv4 = runAccessGetIP(ntw, "tcp4") + } + + if ipv6 == nil { + ipv6 = runAccessGetIP(ntw, "tcp6") + } + + resp := runAccessResponse{ + IPv4: runMakeAccessResponseURLs(ipv4, conf, cli), + IPv6: runMakeAccessResponseURLs(ipv6, conf, cli), + } + resp.Secret.Base64 = conf.Secret.Base64() + resp.Secret.Hex = conf.Secret.EE() + + encoder := json.NewEncoder(os.Stdout) + + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + + if err := encoder.Encode(resp); err != nil { + exit(err) + } +} + +func runAccessGetIP(ntw *network.Network, protocol string) net.IP { + client := &http.Client{ + Timeout: ntw.HTTP.Timeout, + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return ntw.DialContext(ctx, protocol, address) + }, + }, + } + + resp, err := client.Get("https://ifconfig.co") + if err != nil { + return nil + } + + defer exhaustResponse(resp) + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil + } + + return net.ParseIP(strings.TrimSpace(string(data))) +} + +func runMakeAccessResponseURLs(ip net.IP, conf *config, cli *CLI) *runAccessResponseURLs { + if ip == nil { + return nil + } + + values := url.Values{} + + values.Set("server", ip.String()) + values.Set("port", strconv.Itoa(int(conf.BindTo.port.Value(0)))) + values.Set("secret", conf.Secret.Base64()) + + if cli.Access.Hex { + values.Set("secret", conf.Secret.EE()) + } + + urlQuery := values.Encode() + + rv := &runAccessResponseURLs{ + IP: ip, + TgURL: (&url.URL{ + Scheme: "tg", + Host: "proxy", + RawQuery: urlQuery, + }).String(), + TmeURL: (&url.URL{ + Scheme: "https", + Host: "t.me", + Path: "proxy", + RawQuery: urlQuery, + }).String(), + } + + rv.TgQrCode = runMakeAccessResponseURLsQRCode(rv.TgURL) + rv.TmeQrCode = runMakeAccessResponseURLsQRCode(rv.TmeURL) + + return rv +} + +func runMakeAccessResponseURLsQRCode(data string) string { + values := url.Values{} + + values.Set("qzone", "4") + values.Set("format", "svg") + values.Set("data", data) + + return (&url.URL{ + Scheme: "https", + Host: "api.qrserver.com", + Path: "v1/create-qr-code", + RawQuery: values.Encode(), + }).String() +} diff --git a/config.go b/config.go index c496453..4a11b50 100644 --- a/config.go +++ b/config.go @@ -410,6 +410,10 @@ type config struct { IPv4 configTypeIP `json:"ipv4"` IPv6 configTypeIP `json:"ipv6"` } `json:"public-ip"` + Timeout struct { + TCP configTypeDuration `json:"tcp"` + HTTP configTypeDuration `json:"http"` + } `json:"timeout"` DOHIP configTypeIP `json:"doh-ip"` Proxies []configTypeURL `json:"proxies"` } `json:"network"` @@ -472,6 +476,10 @@ type configRaw struct { IPv4 string `toml:"ipv4" json:"ipv4"` IPv6 string `toml:"ipv6" json:"ipv6"` } `toml:"public-ip" json:"public-ip"` + Timeout struct { + TCP string `toml:"tcp" json:"tcp"` + HTTP string `toml:"http" json:"http"` + } `toml:"timeout" json:"timeout"` DOHIP string `toml:"doh-ip" json:"doh-ip"` Proxies []string `toml:"proxies" json:"proxies"` } `toml:"network" json:"network"` diff --git a/example.config.toml b/example.config.toml index ee896d0..0f74c79 100644 --- a/example.config.toml +++ b/example.config.toml @@ -101,6 +101,12 @@ proxies = [ ipv4 = "" ipv6 = "" +# network timeouts define different settings for timeouts. HTTP timeout +# is required only for DOH. +[network.timeout] +tcp = "5s" +http = "10s" + # 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/main.go b/main.go index a45e0a8..184b198 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ type CLI struct { } `cmd help:"Generate new proxy secret."` Access struct { ConfigPath string `arg required type:"existingfile" help:"Path to the configuration file." name:"config-path"` + Hex bool `help:"Print secret in hex encoding."` } `cmd help:"Print access information."` Run struct { ConfigPath string `arg required type:"existingfile" help:"Path to the configuration file." name:"config-path"` @@ -34,8 +35,11 @@ func main() { switch ctx.Command() { case "generate-secret": runGenerateSecret(cli) - case "access": - case "run": + case "access ": + runAccess(cli) + case "run ": panic("not implemented yet") + default: + panic(ctx.Command()) } } diff --git a/mtglib/network/init.go b/mtglib/network/init.go index d07f2b0..593b935 100644 --- a/mtglib/network/init.go +++ b/mtglib/network/init.go @@ -16,6 +16,8 @@ const ( ProxyDialerOpenThreshold = 5 ProxyDialerHalfOpenTimeout = time.Minute ProxyDialerResetFailuresTimeout = 10 * time.Second + + DefaultDOHHostname = "9.9.9.9" ) var ( diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..7299518 --- /dev/null +++ b/utils.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + + "github.com/9seconds/mtg/v2/mtglib/network" +) + +func exit(err error) { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) +} + +func makeNetwork(conf *config) (*network.Network, error) { + tcpTimeout := conf.Network.Timeout.TCP.Value(network.DefaultTimeout) + httpTimeout := conf.Network.Timeout.TCP.Value(network.DefaultHTTPTimeout) + dohIP := conf.Network.DOHIP.Value(net.ParseIP(network.DefaultDOHHostname)).String() + bufferSize := conf.TCPBuffer.Value(network.DefaultBufferSize) + + baseDialer, err := network.NewDefaultDialer(tcpTimeout, int(bufferSize)) + if err != nil { + return nil, fmt.Errorf("cannot build a default dialer: %w", err) + } + + proxyURLs := make([]*url.URL, len(conf.Network.Proxies)) + + for _, v := range conf.Network.Proxies { + if value := v.Value(nil); value != nil { + proxyURLs = append(proxyURLs, v.Value(nil)) + } + } + + switch len(proxyURLs) { + case 0: + return network.NewNetwork(baseDialer, dohIP, httpTimeout) + case 1: + socksDialer, err := network.NewSocks5Dialer(baseDialer, proxyURLs[0]) + + if err != nil { + return nil, fmt.Errorf("cannot build socks5 dialer: %w", err) + } + + return network.NewNetwork(socksDialer, dohIP, httpTimeout) + } + + socksDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, proxyURLs) + if err != nil { + + return nil, fmt.Errorf("cannot build socks5 dialer: %w", err) + } + + return network.NewNetwork(socksDialer, dohIP, httpTimeout) +} + +func exhaustResponse(response *http.Response) { + io.Copy(ioutil.Discard, response.Body) + response.Body.Close() +}