Highly-opinionated (ex-bullshit-free) MTPROTO proxy for Telegram. If you use v1.0 or upgrade broke you proxy, please read the chapter Version 2
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

doctor.go 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. package cli
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "maps"
  7. "net"
  8. "os"
  9. "slices"
  10. "strconv"
  11. "strings"
  12. "text/template"
  13. "time"
  14. "github.com/9seconds/mtg/v2/essentials"
  15. "github.com/9seconds/mtg/v2/internal/config"
  16. "github.com/9seconds/mtg/v2/internal/utils"
  17. "github.com/9seconds/mtg/v2/mtglib"
  18. "github.com/9seconds/mtg/v2/network/v2"
  19. "github.com/beevik/ntp"
  20. )
  21. var (
  22. tplError = template.Must(
  23. template.New("").Parse(" ‼️ {{ .description }}: {{ .error }}\n"),
  24. )
  25. tplWDeprecatedConfig = template.Must(
  26. template.New("").
  27. Parse(` ⚠️ Option {{ .old | printf "%q" }}{{ if .old_section }} from section [{{ .old_section }}]{{ end }} is deprecated and will be removed in v{{ .when }}. Please use {{ .new | printf "%q" }}{{ if .new_section }} in [{{ .new_section }}] section{{ end }} instead.` + "\n"),
  28. )
  29. tplOTimeSkewness = template.Must(
  30. template.New("").
  31. Parse(" ✅ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}\n"),
  32. )
  33. tplWTimeSkewness = template.Must(
  34. template.New("").
  35. Parse(" ⚠️ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}. Please check ntp.\n"),
  36. )
  37. tplETimeSkewness = template.Must(
  38. template.New("").
  39. Parse(" ❌ Time drift is {{ .drift }}, but tolerate-time-skewness is {{ .value }}. You will get many rejected connections!\n"),
  40. )
  41. tplODCConnect = template.Must(
  42. template.New("").Parse(" ✅ DC {{ .dc }}\n"),
  43. )
  44. tplEDCConnect = template.Must(
  45. template.New("").Parse(" ❌ DC {{ .dc }}: {{ .error }}\n"),
  46. )
  47. tplODNSSNIMatch = template.Must(
  48. template.New("").Parse(" ✅ Secret hostname {{ .hostname }} matches our public IP ({{ .our }}); resolved: {{ .resolved }}\n"),
  49. )
  50. tplEDNSSNIMatch = template.Must(
  51. template.New("").Parse(" ❌ Secret hostname {{ .hostname }} resolves to {{ .resolved }} but our public IP is {{ .our }}{{ if .families }} (mismatched families: {{ .families }}){{ end }}\n"),
  52. )
  53. tplEDNSSNINoResolve = template.Must(
  54. template.New("").Parse(" ❌ Secret hostname {{ .hostname }} cannot be resolved to any address\n"),
  55. )
  56. tplOFrontingDomain = template.Must(
  57. template.New("").Parse(" ✅ {{ .address }} is reachable\n"),
  58. )
  59. tplEFrontingDomain = template.Must(
  60. template.New("").Parse(" ❌ {{ .address }}: {{ .error }}\n"),
  61. )
  62. )
  63. type Doctor struct {
  64. conf *config.Config
  65. ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` //nolint: lll
  66. }
  67. func (d *Doctor) Run(cli *CLI, version string) error {
  68. conf, err := utils.ReadConfig(d.ConfigPath)
  69. if err != nil {
  70. return fmt.Errorf("cannot init config: %w", err)
  71. }
  72. d.conf = conf
  73. fmt.Println("Deprecated options")
  74. everythingOK := d.checkDeprecatedConfig()
  75. fmt.Println("Time skewness")
  76. everythingOK = d.checkTimeSkewness() && everythingOK
  77. resolver, err := network.GetDNS(conf.GetDNS())
  78. if err != nil {
  79. return fmt.Errorf("cannot create DNS resolver: %w", err)
  80. }
  81. base := network.New(
  82. resolver,
  83. "",
  84. conf.Network.Timeout.TCP.Get(10*time.Second),
  85. conf.Network.Timeout.HTTP.Get(0),
  86. conf.Network.Timeout.Idle.Get(0),
  87. net.KeepAliveConfig{
  88. Enable: !conf.Network.KeepAlive.Disabled.Get(false),
  89. Idle: conf.Network.KeepAlive.Idle.Get(0),
  90. Interval: conf.Network.KeepAlive.Interval.Get(0),
  91. Count: int(conf.Network.KeepAlive.Count.Get(0)),
  92. },
  93. )
  94. fmt.Println("Validate native network connectivity")
  95. everythingOK = d.checkNetwork(base) && everythingOK
  96. for _, url := range conf.Network.Proxies {
  97. value, err := network.NewProxyNetwork(base, url.Get(nil))
  98. if err != nil {
  99. return err
  100. }
  101. fmt.Printf("Validate network connectivity with proxy %s\n", url.Get(nil))
  102. everythingOK = d.checkNetwork(value) && everythingOK
  103. }
  104. fmt.Println("Validate fronting domain connectivity")
  105. everythingOK = d.checkFrontingDomain(base) && everythingOK
  106. fmt.Println("Validate SNI-DNS match")
  107. everythingOK = d.checkSecretHost(resolver, base) && everythingOK
  108. if !everythingOK {
  109. os.Exit(1)
  110. }
  111. return nil
  112. }
  113. func (d *Doctor) checkDeprecatedConfig() bool {
  114. ok := true
  115. if d.conf.DomainFrontingIP.Value != nil {
  116. ok = false
  117. tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
  118. "when": "2.3.0",
  119. "old": "domain-fronting-ip",
  120. "old_section": "",
  121. "new": "ip",
  122. "new_section": "domain-fronting",
  123. })
  124. }
  125. if d.conf.DomainFrontingPort.Value != 0 {
  126. ok = false
  127. tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
  128. "when": "2.3.0",
  129. "old": "domain-fronting-port",
  130. "old_section": "",
  131. "new": "port",
  132. "new_section": "domain-fronting",
  133. })
  134. }
  135. if d.conf.DomainFrontingProxyProtocol.Value {
  136. ok = false
  137. tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
  138. "when": "2.3.0",
  139. "old": "domain-fronting-proxy-protocol",
  140. "old_section": "",
  141. "new": "proxy-protocol",
  142. "new_section": "domain-fronting",
  143. })
  144. }
  145. if d.conf.Network.DOHIP.Value != nil {
  146. ok = false
  147. tplWDeprecatedConfig.Execute(os.Stdout, map[string]string{ //nolint: errcheck
  148. "when": "2.3.0",
  149. "old": "doh-ip",
  150. "old_section": "network",
  151. "new": "dns",
  152. "new_section": "network",
  153. })
  154. }
  155. if ok {
  156. fmt.Println(" ✅ All good")
  157. }
  158. return ok
  159. }
  160. func (d *Doctor) checkTimeSkewness() bool {
  161. response, err := ntp.Query("0.pool.ntp.org")
  162. if err != nil {
  163. tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
  164. "description": "cannot access ntp pool",
  165. "error": err,
  166. })
  167. return false
  168. }
  169. skewness := response.ClockOffset.Abs()
  170. confValue := d.conf.TolerateTimeSkewness.Get(mtglib.DefaultTolerateTimeSkewness)
  171. diff := float64(skewness) / float64(confValue)
  172. tplData := map[string]any{
  173. "drift": response.ClockOffset,
  174. "value": confValue,
  175. }
  176. switch {
  177. case diff < 0.3:
  178. tplOTimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
  179. return true
  180. case diff < 0.7:
  181. tplWTimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
  182. default:
  183. tplETimeSkewness.Execute(os.Stdout, tplData) //nolint: errcheck
  184. }
  185. return false
  186. }
  187. func (d *Doctor) checkNetwork(ntw mtglib.Network) bool {
  188. dcs := slices.Collect(maps.Keys(essentials.TelegramCoreAddresses))
  189. slices.Sort(dcs)
  190. ok := true
  191. for _, dc := range dcs {
  192. err := d.checkNetworkAddresses(ntw, essentials.TelegramCoreAddresses[dc])
  193. if err == nil {
  194. tplODCConnect.Execute(os.Stdout, map[string]any{ //nolint: errcheck
  195. "dc": dc,
  196. })
  197. } else {
  198. tplEDCConnect.Execute(os.Stdout, map[string]any{ //nolint: errcheck
  199. "dc": dc,
  200. "error": err,
  201. })
  202. ok = false
  203. }
  204. }
  205. return ok
  206. }
  207. func (d *Doctor) checkNetworkAddresses(ntw mtglib.Network, addresses []string) error {
  208. checkAddresses := []string{}
  209. switch d.conf.PreferIP.Get("prefer-ip4") {
  210. case "only-ipv4":
  211. for _, addr := range addresses {
  212. host, _, err := net.SplitHostPort(addr)
  213. if err != nil {
  214. panic(err)
  215. }
  216. if ip := net.ParseIP(host); ip != nil && ip.To4() != nil {
  217. checkAddresses = append(checkAddresses, addr)
  218. }
  219. }
  220. case "only-ipv6":
  221. for _, addr := range addresses {
  222. host, _, err := net.SplitHostPort(addr)
  223. if err != nil {
  224. panic(err)
  225. }
  226. if ip := net.ParseIP(host); ip != nil && ip.To4() == nil {
  227. checkAddresses = append(checkAddresses, addr)
  228. }
  229. }
  230. default:
  231. checkAddresses = addresses
  232. }
  233. if len(checkAddresses) == 0 {
  234. return fmt.Errorf("no suitable addresses after IP version filtering")
  235. }
  236. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  237. defer cancel()
  238. var (
  239. conn net.Conn
  240. err error
  241. )
  242. for _, addr := range checkAddresses {
  243. conn, err = ntw.DialContext(ctx, "tcp", addr)
  244. if err != nil {
  245. continue
  246. }
  247. conn.Close() //nolint: errcheck
  248. return nil
  249. }
  250. return err
  251. }
  252. func (d *Doctor) checkFrontingDomain(ntw mtglib.Network) bool {
  253. host := d.conf.Secret.Host
  254. if ip := d.conf.GetDomainFrontingIP(nil); ip != "" {
  255. host = ip
  256. }
  257. port := d.conf.GetDomainFrontingPort(mtglib.DefaultDomainFrontingPort)
  258. address := net.JoinHostPort(host, strconv.Itoa(int(port)))
  259. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  260. defer cancel()
  261. dialer := ntw.NativeDialer()
  262. conn, err := dialer.DialContext(ctx, "tcp", address)
  263. if err != nil {
  264. tplEFrontingDomain.Execute(os.Stdout, map[string]any{ //nolint: errcheck
  265. "address": address,
  266. "error": err,
  267. })
  268. return false
  269. }
  270. conn.Close() //nolint: errcheck
  271. tplOFrontingDomain.Execute(os.Stdout, map[string]any{ //nolint: errcheck
  272. "address": address,
  273. })
  274. return true
  275. }
  276. func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool {
  277. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  278. defer cancel()
  279. res := runSNICheck(ctx, resolver, d.conf, ntw)
  280. if res.ResolveErr != nil {
  281. tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
  282. "description": fmt.Sprintf("cannot resolve DNS name of %s", res.Host),
  283. "error": res.ResolveErr,
  284. })
  285. return false
  286. }
  287. if !res.Known() {
  288. tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
  289. "description": "cannot detect public IP address",
  290. "error": errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"),
  291. })
  292. return false
  293. }
  294. if len(res.Resolved) == 0 {
  295. tplEDNSSNINoResolve.Execute(os.Stdout, map[string]any{ //nolint: errcheck
  296. "hostname": res.Host,
  297. })
  298. return false
  299. }
  300. resolved := make([]string, 0, len(res.Resolved))
  301. for _, ip := range res.Resolved {
  302. resolved = append(resolved, `"`+ip.String()+`"`)
  303. }
  304. our := ""
  305. if res.OurIPv4 != nil {
  306. our = res.OurIPv4.String()
  307. }
  308. if res.OurIPv6 != nil {
  309. if our != "" {
  310. our += "/"
  311. }
  312. our += res.OurIPv6.String()
  313. }
  314. if res.OK() {
  315. tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
  316. "hostname": res.Host,
  317. "resolved": strings.Join(resolved, ", "),
  318. "our": our,
  319. })
  320. return true
  321. }
  322. mismatched := []string{}
  323. if res.OurIPv4 != nil && !res.IPv4Match {
  324. mismatched = append(mismatched, "IPv4")
  325. }
  326. if res.OurIPv6 != nil && !res.IPv6Match {
  327. mismatched = append(mismatched, "IPv6")
  328. }
  329. tplEDNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
  330. "hostname": res.Host,
  331. "resolved": strings.Join(resolved, ", "),
  332. "our": our,
  333. "families": strings.Join(mismatched, ", "),
  334. })
  335. return false
  336. }