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
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

doctor.go 11KB

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