Add validation of telegram connectivity

This commit is contained in:
9seconds
2026-03-23 18:34:38 +01:00
parent a60523fed0
commit 8154f65e0e
5 changed files with 255 additions and 52 deletions
+30
View File
@@ -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",
},
}
+1 -1
View File
@@ -27,6 +27,7 @@ require (
) )
require ( require (
github.com/beevik/ntp v1.5.0
github.com/ncruces/go-dns v1.3.2 github.com/ncruces/go-dns v1.3.2
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/pires/go-proxyproto v0.11.0 github.com/pires/go-proxyproto v0.11.0
@@ -36,7 +37,6 @@ require (
) )
require ( require (
github.com/beevik/ntp v1.5.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
+188 -8
View File
@@ -1,13 +1,21 @@
package cli package cli
import ( import (
"context"
"fmt" "fmt"
"maps"
"net"
"os" "os"
"slices"
"strings"
"text/template" "text/template"
"time"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/internal/config" "github.com/9seconds/mtg/v2/internal/config"
"github.com/9seconds/mtg/v2/internal/utils" "github.com/9seconds/mtg/v2/internal/utils"
"github.com/9seconds/mtg/v2/mtglib" "github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/network/v2"
"github.com/beevik/ntp" "github.com/beevik/ntp"
) )
@@ -33,6 +41,20 @@ var (
template.New(""). template.New("").
Parse(" ❌ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}. You will get many rejected connections!\n"), 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 { 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 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 { func (d *Doctor) Run(cli *CLI, version string) error {
conf, err := utils.ReadConfig(d.ConfigPath) conf, err := utils.ReadConfig(d.ConfigPath)
if err != nil { if err != nil {
@@ -48,20 +92,42 @@ func (d *Doctor) Run(cli *CLI, version string) error {
} }
d.conf = conf d.conf = conf
everythingOK := true
fmt.Println("Deprecated options") fmt.Println("Deprecated options")
if !d.checkDeprecatedConfig() { everythingOK := d.checkDeprecatedConfig()
everythingOK = false
} else {
fmt.Println(" ✅ All good")
}
fmt.Println("Time skewness") fmt.Println("Time skewness")
if !d.checkTimeSkewness() { everythingOK = d.checkTimeSkewness() && everythingOK
everythingOK = false
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 { if !everythingOK {
os.Exit(1) os.Exit(1)
} }
@@ -116,6 +182,10 @@ func (d *Doctor) checkDeprecatedConfig() bool {
}) })
} }
if ok {
fmt.Println(" ✅ All good")
}
return ok return ok
} }
@@ -149,3 +219,113 @@ func (d *Doctor) checkTimeSkewness() bool {
return false 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
}
Executable
BIN
View File
Binary file not shown.
+36 -43
View File
@@ -2,7 +2,10 @@ package dc
import ( import (
"context" "context"
"net"
"time" "time"
"github.com/9seconds/mtg/v2/essentials"
) )
type preferIP uint8 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 // https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30
var defaultDCAddrSet = dcAddrSet{ var defaultDCAddrSet = (func() dcAddrSet {
v4: map[int][]Addr{ addrSet := dcAddrSet{
1: { v4: make(map[int][]Addr),
{Network: "tcp4", Address: "149.154.175.50:443"}, v6: make(map[int][]Addr),
}, }
2: {
{Network: "tcp4", Address: "149.154.167.51:443"}, for dcid, ips := range essentials.TelegramCoreAddresses {
{Network: "tcp4", Address: "95.161.76.100:443"}, for _, addr := range ips {
}, host, _, err := net.SplitHostPort(addr)
3: { if err != nil {
{Network: "tcp4", Address: "149.154.175.100:443"}, panic(err)
}, }
4: {
{Network: "tcp4", Address: "149.154.167.91:443"}, ip := net.ParseIP(host)
}, if ip == nil {
5: { panic(addr)
{Network: "tcp4", Address: "149.154.171.5:443"}, }
}, if ip.To4() == nil {
203: { addrSet.v6[dcid] = append(addrSet.v6[dcid], Addr{
{Network: "tcp4", Address: "91.105.192.100:443"}, Network: "tcp6",
}, Address: addr,
}, })
v6: map[int][]Addr{ } else {
1: { addrSet.v4[dcid] = append(addrSet.v4[dcid], Addr{
{Network: "tcp6", Address: "[2001:b28:f23d:f001::a]:443"}, Network: "tcp4",
}, Address: addr,
2: { })
{Network: "tcp6", Address: "[2001:67c:04e8:f002::a]:443"}, }
}, }
3: { }
{Network: "tcp6", Address: "[2001:b28:f23d:f003::a]:443"},
}, return addrSet
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"},
},
},
}