diff --git a/essentials/addresses.go b/essentials/addresses.go new file mode 100644 index 0000000..7a91754 --- /dev/null +++ b/essentials/addresses.go @@ -0,0 +1,30 @@ +package essentials + +// TelegramCoreAddresses are publicly known addresses of Telegram core network. +var TelegramCoreAddresses = map[int][]string{ + 1: { + "149.154.175.50:443", + "[2001:b28:f23d:f001::a]:443", + }, + 2: { + "149.154.167.51:443", + "95.161.76.100:443", + "[2001:67c:04e8:f002::a]:443", + }, + 3: { + "149.154.175.100:443", + "[2001:b28:f23d:f003::a]:443", + }, + 4: { + "149.154.167.91:443", + "[2001:67c:04e8:f004::a]:443", + }, + 5: { + "149.154.171.5:443", + "[2001:b28:f23f:f005::a]:443", + }, + 203: { + "91.105.192.100:443", + "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443", + }, +} diff --git a/go.mod b/go.mod index 85140ab..c76e51c 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( ) require ( + github.com/beevik/ntp v1.5.0 github.com/ncruces/go-dns v1.3.2 github.com/pelletier/go-toml/v2 v2.2.4 github.com/pires/go-proxyproto v0.11.0 @@ -36,7 +37,6 @@ require ( ) require ( - github.com/beevik/ntp v1.5.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 3ca6334..140d892 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -1,13 +1,21 @@ package cli import ( + "context" "fmt" + "maps" + "net" "os" + "slices" + "strings" "text/template" + "time" + "github.com/9seconds/mtg/v2/essentials" "github.com/9seconds/mtg/v2/internal/config" "github.com/9seconds/mtg/v2/internal/utils" "github.com/9seconds/mtg/v2/mtglib" + "github.com/9seconds/mtg/v2/network/v2" "github.com/beevik/ntp" ) @@ -33,6 +41,20 @@ var ( template.New(""). Parse(" ❌ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}. You will get many rejected connections!\n"), ) + + tplODCConnect = template.Must( + template.New("").Parse(" ✅ DC {{ .dc }}\n"), + ) + tplEDCConnect = template.Must( + template.New("").Parse(" ❌ DC {{ .dc }}: {{ .error }}\n"), + ) + + tplODNSSNIMatch = template.Must( + template.New("").Parse(" ✅ IP address {{ .ip }} matches secret hostname {{ .hostname }}\n"), + ) + tplEDNSSNIMatch = template.Must( + template.New("").Parse(" ❌ Hostname {{ .hostname }} {{ if .resolved }}is resolved to {{ .resolved }} addresses, not {{ if .ip4 }}{{ .ip4 }}{{ else }}{{ .ip6 }}{{ end }}{{ else }}cannot be resolved to any host{{ end }}\n"), + ) ) type Doctor struct { @@ -41,6 +63,28 @@ type Doctor struct { ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` //nolint: lll } +type wrappedNetwork struct { + mtglib.Network +} + +func (w wrappedNetwork) Dial(network, address string) (net.Conn, error) { + rv, err := w.Network.Dial(network, address) + if err != nil { + return nil, err + } + + return rv.(net.Conn), nil +} + +func (w wrappedNetwork) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + rv, err := w.Network.DialContext(ctx, network, address) + if err != nil { + return nil, err + } + + return rv.(net.Conn), nil +} + func (d *Doctor) Run(cli *CLI, version string) error { conf, err := utils.ReadConfig(d.ConfigPath) if err != nil { @@ -48,20 +92,42 @@ func (d *Doctor) Run(cli *CLI, version string) error { } d.conf = conf - everythingOK := true fmt.Println("Deprecated options") - if !d.checkDeprecatedConfig() { - everythingOK = false - } else { - fmt.Println(" ✅ All good") - } + everythingOK := d.checkDeprecatedConfig() fmt.Println("Time skewness") - if !d.checkTimeSkewness() { - everythingOK = false + everythingOK = d.checkTimeSkewness() && everythingOK + + resolver, err := network.GetDNS(conf.GetDNS()) + if err != nil { + return fmt.Errorf("cannot create DNS resolver: %w", err) } + base := network.New( + resolver, + "", + conf.Network.Timeout.TCP.Get(10*time.Second), + conf.Network.Timeout.HTTP.Get(0), + conf.Network.Timeout.Idle.Get(0), + ) + + fmt.Println("Validate native network connectivity") + everythingOK = d.checkNetwork(base) && everythingOK + + for _, url := range conf.Network.Proxies { + value, err := network.NewProxyNetwork(base, url.Get(nil)) + if err != nil { + return err + } + + fmt.Printf("Validate network connectivity with proxy %s\n", url.Get(nil)) + everythingOK = d.checkNetwork(value) && everythingOK + } + + fmt.Println("Validate SNI-DNS match") + everythingOK = d.checkSecretHost(resolver, base) && everythingOK + if !everythingOK { os.Exit(1) } @@ -116,6 +182,10 @@ func (d *Doctor) checkDeprecatedConfig() bool { }) } + if ok { + fmt.Println(" ✅ All good") + } + return ok } @@ -149,3 +219,113 @@ func (d *Doctor) checkTimeSkewness() bool { return false } + +func (d *Doctor) checkNetwork(ntw mtglib.Network) bool { + dcs := slices.Collect(maps.Keys(essentials.TelegramCoreAddresses)) + slices.Sort(dcs) + + ok := true + + for _, dc := range dcs { + err := d.checkNetworkAddresses(ntw, essentials.TelegramCoreAddresses[dc]) + if err == nil { + tplODCConnect.Execute(os.Stdout, map[string]any{ + "dc": dc, + }) + } else { + tplEDCConnect.Execute(os.Stdout, map[string]any{ + "dc": dc, + "error": err, + }) + ok = false + } + } + + return ok +} + +func (d *Doctor) checkNetworkAddresses(ntw mtglib.Network, addresses []string) error { + checkAddresses := []string{} + + switch d.conf.PreferIP.Get("prefer-ip4") { + case "only-ipv4": + for _, addr := range addresses { + host, _, err := net.SplitHostPort(addr) + if err != nil { + panic(err) + } + + if ip := net.ParseIP(host); ip != nil && ip.To4() != nil { + checkAddresses = append(checkAddresses, addr) + } + } + case "only-ipv6": + for _, addr := range addresses { + host, _, err := net.SplitHostPort(addr) + if err != nil { + panic(err) + } + + if ip := net.ParseIP(host); ip != nil && ip.To4() == nil { + checkAddresses = append(checkAddresses, addr) + } + } + default: + checkAddresses = addresses + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var ( + conn net.Conn + err error + ) + + for _, addr := range checkAddresses { + conn, err = ntw.DialContext(ctx, "tcp", addr) + if err != nil { + continue + } + + conn.Close() + + return nil + } + + return err +} + +func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool { + addresses, err := resolver.LookupIPAddr(context.Background(), d.conf.Secret.Host) + if err != nil { + // TODO + return false + } + + access := &Access{} + ourIP4 := access.getIP(ntw, "tcp4") + ourIP6 := access.getIP(ntw, "tcp6") + + strAddresses := []string{} + for _, value := range addresses { + if value.IP.String() == ourIP4.String() || value.IP.String() == ourIP6.String() { + tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ + "ip": value.IP, + "hostname": d.conf.Secret.Host, + }) + return true + } + + strAddresses = append(strAddresses, `"`+value.IP.String()+`"`) + } + + tplEDNSSNIMatch.Execute(os.Stdout, map[string]any{ + "hostname": d.conf.Secret.Host, + "resolved": strings.Join(strAddresses, ", "), + "ip4": ourIP4, + "ip6": ourIP6, + }) + + return false +} diff --git a/mtg-linux b/mtg-linux new file mode 100755 index 0000000..6c3e66d Binary files /dev/null and b/mtg-linux differ diff --git a/mtglib/internal/dc/init.go b/mtglib/internal/dc/init.go index 1af0545..f7db83f 100644 --- a/mtglib/internal/dc/init.go +++ b/mtglib/internal/dc/init.go @@ -2,7 +2,10 @@ package dc import ( "context" + "net" "time" + + "github.com/9seconds/mtg/v2/essentials" ) type preferIP uint8 @@ -39,46 +42,36 @@ type Updater interface { } // https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30 -var defaultDCAddrSet = dcAddrSet{ - v4: map[int][]Addr{ - 1: { - {Network: "tcp4", Address: "149.154.175.50:443"}, - }, - 2: { - {Network: "tcp4", Address: "149.154.167.51:443"}, - {Network: "tcp4", Address: "95.161.76.100:443"}, - }, - 3: { - {Network: "tcp4", Address: "149.154.175.100:443"}, - }, - 4: { - {Network: "tcp4", Address: "149.154.167.91:443"}, - }, - 5: { - {Network: "tcp4", Address: "149.154.171.5:443"}, - }, - 203: { - {Network: "tcp4", Address: "91.105.192.100:443"}, - }, - }, - v6: map[int][]Addr{ - 1: { - {Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"}, - }, - 2: { - {Network: "tcp6", Address: "[2001:67c:04e8:f002::a]:443"}, - }, - 3: { - {Network: "tcp6", Address: "[2001:b28:f23d:f003::a]:443"}, - }, - 4: { - {Network: "tcp6", Address: "[2001:67c:04e8:f004::a]:443"}, - }, - 5: { - {Network: "tcp6", Address: "[2001:b28:f23f:f005::a]:443"}, - }, - 203: { - {Network: "tcp6", Address: "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443"}, - }, - }, -} +var defaultDCAddrSet = (func() dcAddrSet { + addrSet := dcAddrSet{ + v4: make(map[int][]Addr), + v6: make(map[int][]Addr), + } + + for dcid, ips := range essentials.TelegramCoreAddresses { + for _, addr := range ips { + host, _, err := net.SplitHostPort(addr) + if err != nil { + panic(err) + } + + ip := net.ParseIP(host) + if ip == nil { + panic(addr) + } + if ip.To4() == nil { + addrSet.v6[dcid] = append(addrSet.v6[dcid], Addr{ + Network: "tcp6", + Address: addr, + }) + } else { + addrSet.v4[dcid] = append(addrSet.v4[dcid], Addr{ + Network: "tcp4", + Address: addr, + }) + } + } + } + + return addrSet +})()