evo-cli 0.32.0__tar.gz → 0.32.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. {evo_cli-0.32.0 → evo_cli-0.32.1}/PKG-INFO +1 -1
  2. evo_cli-0.32.1/evo_cli/VERSION +1 -0
  3. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/route/_probe.py +22 -9
  4. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/route/check.py +5 -0
  5. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/route/fix.py +1 -1
  6. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/route/probe.py +3 -3
  7. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli.egg-info/PKG-INFO +1 -1
  8. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_route.py +46 -0
  9. evo_cli-0.32.0/evo_cli/VERSION +0 -1
  10. {evo_cli-0.32.0 → evo_cli-0.32.1}/Containerfile +0 -0
  11. {evo_cli-0.32.0 → evo_cli-0.32.1}/HISTORY.md +0 -0
  12. {evo_cli-0.32.0 → evo_cli-0.32.1}/LICENSE +0 -0
  13. {evo_cli-0.32.0 → evo_cli-0.32.1}/MANIFEST.in +0 -0
  14. {evo_cli-0.32.0 → evo_cli-0.32.1}/README.md +0 -0
  15. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/__init__.py +0 -0
  16. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/__main__.py +0 -0
  17. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/base.py +0 -0
  18. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/cli.py +0 -0
  19. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/__init__.py +0 -0
  20. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/agent_toy.py +0 -0
  21. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/claude_code.py +0 -0
  22. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/cloudflare.py +0 -0
  23. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/cred.py +0 -0
  24. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/download.py +0 -0
  25. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/fix_claude.py +0 -0
  26. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/gdrive.py +0 -0
  27. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/gh.py +0 -0
  28. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/__init__.py +0 -0
  29. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/_dag.py +0 -0
  30. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/_git.py +0 -0
  31. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/_hub.py +0 -0
  32. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/_mirror.py +0 -0
  33. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/_model.py +0 -0
  34. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/_mutate.py +0 -0
  35. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/_paths.py +0 -0
  36. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/_render.py +0 -0
  37. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/_server.py +0 -0
  38. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/check.py +0 -0
  39. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/clone.py +0 -0
  40. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/edit.py +0 -0
  41. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/pull.py +0 -0
  42. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/serve.py +0 -0
  43. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/show.py +0 -0
  44. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/views.py +0 -0
  45. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/index-Cs7hXRPJ.css +0 -0
  46. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/index-CuNOlaH7.js +0 -0
  47. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  48. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  49. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  50. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  51. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  52. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  53. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  54. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  55. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  56. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  57. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  58. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  59. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/harness/web/index.html +0 -0
  60. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/hwid.py +0 -0
  61. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/hwid_reset.py +0 -0
  62. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/image.py +0 -0
  63. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/localproxy.py +0 -0
  64. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/mcp.py +0 -0
  65. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/miniconda.py +0 -0
  66. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/netcheck.py +0 -0
  67. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/ngrok.py +0 -0
  68. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/opencode.py +0 -0
  69. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/openvpn.py +0 -0
  70. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/plantuml.py +0 -0
  71. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/route/__init__.py +0 -0
  72. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/route/_dns.py +0 -0
  73. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/route/_dpi.py +0 -0
  74. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/route/_hosts.py +0 -0
  75. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/route/_spoofdpi.py +0 -0
  76. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/route/status.py +0 -0
  77. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/serp.py +0 -0
  78. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/site2s.py +0 -0
  79. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/ssh.py +0 -0
  80. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/storage.py +0 -0
  81. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/sysmon.py +0 -0
  82. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/tts.py +0 -0
  83. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/update.py +0 -0
  84. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/commands/wifi.py +0 -0
  85. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/console.py +0 -0
  86. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/credentials/__init__.py +0 -0
  87. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/credentials/doctor.py +0 -0
  88. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/credentials/google_oauth.py +0 -0
  89. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/credentials/migrate.py +0 -0
  90. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/credentials/oauth_flow.py +0 -0
  91. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/credentials/registry.py +0 -0
  92. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/credentials/store.py +0 -0
  93. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/credentials/sync.py +0 -0
  94. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/imaging/__init__.py +0 -0
  95. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/imaging/align.py +0 -0
  96. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/imaging/core.py +0 -0
  97. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/imaging/creds.py +0 -0
  98. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/imaging/errors.py +0 -0
  99. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/imaging/gemini.py +0 -0
  100. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/imaging/ncnn.py +0 -0
  101. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/mcp_registry.py +0 -0
  102. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/openvpn.py +0 -0
  103. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/serp/__init__.py +0 -0
  104. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/serp/client.py +0 -0
  105. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/serp/creds.py +0 -0
  106. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/serp/errors.py +0 -0
  107. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/serp/install.py +0 -0
  108. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/serp/render.py +0 -0
  109. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/tts/__init__.py +0 -0
  110. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/tts/chunking.py +0 -0
  111. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/tts/core.py +0 -0
  112. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/tts/creds.py +0 -0
  113. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/tts/errors.py +0 -0
  114. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/tts/gemini.py +0 -0
  115. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/tts/http.py +0 -0
  116. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/tts/openai.py +0 -0
  117. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/tts/player.py +0 -0
  118. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli/tts/vbee.py +0 -0
  119. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli.egg-info/SOURCES.txt +0 -0
  120. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli.egg-info/dependency_links.txt +0 -0
  121. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli.egg-info/entry_points.txt +0 -0
  122. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli.egg-info/requires.txt +0 -0
  123. {evo_cli-0.32.0 → evo_cli-0.32.1}/evo_cli.egg-info/top_level.txt +0 -0
  124. {evo_cli-0.32.0 → evo_cli-0.32.1}/pyproject.toml +0 -0
  125. {evo_cli-0.32.0 → evo_cli-0.32.1}/setup.cfg +0 -0
  126. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/__init__.py +0 -0
  127. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/fake_evo_agents.py +0 -0
  128. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/fixtures/hub/contract-0.2.0.json +0 -0
  129. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/fixtures/hub/mirror-demo.yaml +0 -0
  130. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_agent_toy.py +0 -0
  131. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_claude_code.py +0 -0
  132. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_cli.py +0 -0
  133. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_console.py +0 -0
  134. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_cred.py +0 -0
  135. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_download.py +0 -0
  136. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_fix_claude.py +0 -0
  137. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_gdrive.py +0 -0
  138. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_gh.py +0 -0
  139. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_harness.py +0 -0
  140. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_harness_clone.py +0 -0
  141. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_harness_dag.py +0 -0
  142. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_harness_hub.py +0 -0
  143. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_harness_show.py +0 -0
  144. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_image.py +0 -0
  145. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_mcp.py +0 -0
  146. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_ngrok.py +0 -0
  147. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_opencode.py +0 -0
  148. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_openvpn.py +0 -0
  149. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_plantuml.py +0 -0
  150. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_serp.py +0 -0
  151. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_storage.py +0 -0
  152. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_sysmon.py +0 -0
  153. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_tts.py +0 -0
  154. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_update.py +0 -0
  155. {evo_cli-0.32.0 → evo_cli-0.32.1}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.32.0
3
+ Version: 0.32.1
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -0,0 +1 @@
1
+ 0.32.1
@@ -13,6 +13,7 @@ CONTROL_SNI = "example.com"
13
13
 
14
14
  OPEN = "open"
15
15
  DNS_POISONED = "dns-poisoned"
16
+ ADDRESS_BLOCKED = "address-blocked"
16
17
  SNI_BLOCKED = "sni-blocked"
17
18
  IP_BLOCKED = "ip-blocked"
18
19
  UNREACHABLE = "unreachable"
@@ -20,6 +21,7 @@ UNREACHABLE = "unreachable"
20
21
  VERDICT_TEXT = {
21
22
  OPEN: "reachable - nothing in the way",
22
23
  DNS_POISONED: "DNS is poisoned, the route itself is open",
24
+ ADDRESS_BLOCKED: "the address your system resolves is blocked, another one works",
23
25
  SNI_BLOCKED: "DPI blocks the TLS handshake by server name",
24
26
  IP_BLOCKED: "the address itself is blocked or dead",
25
27
  UNREACHABLE: "no address answers at all",
@@ -103,24 +105,32 @@ def rank(host, candidates, timeout=6.0):
103
105
  return rows
104
106
 
105
107
 
108
+ def candidates(answers):
109
+ """The system's own addresses first, since those are what a browser connects to, then every trusted one."""
110
+ system = [ip for ip in answers["system"] or [] if not _dns.is_bogus(ip)]
111
+ return system + [ip for ip in answers["trusted"] if ip not in system]
112
+
113
+
106
114
  def diagnose(host, timeout=6.0, include_vn=True):
107
115
  answers = _dns.gather(host, include_vn=include_vn)
108
116
  system_ips = answers["system"] or []
109
117
  trusted = answers["trusted"]
110
118
 
111
- poisoned = bool(system_ips) and all(_dns.is_bogus(ip) for ip in system_ips)
119
+ # A name the system cannot resolve at all, while trusted resolvers can, is as dead to a browser as a bogus one.
120
+ poisoned = (bool(system_ips) and all(_dns.is_bogus(ip) for ip in system_ips)) or (not system_ips and bool(trusted))
112
121
  if not poisoned and system_ips and trusted:
113
122
  poisoned = not set(system_ips) & set(trusted) and any(_dns.is_bogus(ip) for ip in system_ips)
114
123
 
115
- candidates = trusted or [ip for ip in system_ips if not _dns.is_bogus(ip)]
116
- probes = rank(host, candidates, timeout=timeout) if candidates else []
124
+ addresses = candidates(answers)
125
+ probes = rank(host, addresses, timeout=timeout) if addresses else []
117
126
  working = [row for row in probes if row["tls"]]
127
+ system_ok = any(row["ip"] in system_ips for row in working)
118
128
 
119
129
  control = None
120
- if candidates and not working:
121
- control = tls_probe(candidates[0], CONTROL_SNI, timeout=timeout)
130
+ if addresses and not working:
131
+ control = tls_probe(addresses[0], CONTROL_SNI, timeout=timeout)
122
132
 
123
- verdict = _verdict(candidates, probes, working, control, poisoned)
133
+ verdict = _verdict(addresses, probes, working, control, poisoned, system_ok)
124
134
 
125
135
  return {
126
136
  "host": host,
@@ -128,7 +138,7 @@ def diagnose(host, timeout=6.0, include_vn=True):
128
138
  "poisoned": poisoned,
129
139
  "system_ips": system_ips,
130
140
  "answers": answers["resolvers"],
131
- "candidates": candidates,
141
+ "candidates": addresses,
132
142
  "probes": probes,
133
143
  "working": working,
134
144
  "control": control,
@@ -136,11 +146,14 @@ def diagnose(host, timeout=6.0, include_vn=True):
136
146
  }
137
147
 
138
148
 
139
- def _verdict(candidates, probes, working, control, poisoned):
149
+ def _verdict(candidates, probes, working, control, poisoned, system_ok=True):
140
150
  if not candidates:
141
151
  return UNREACHABLE
142
152
  if working:
143
- return DNS_POISONED if poisoned else OPEN
153
+ if poisoned:
154
+ return DNS_POISONED
155
+ # Some address works, but not the one the system hands the browser: the name only opens once it is pinned.
156
+ return OPEN if system_ok else ADDRESS_BLOCKED
144
157
  if control and control["tls"]:
145
158
  return SNI_BLOCKED
146
159
  if any(row["tcp"] for row in probes):
@@ -24,6 +24,7 @@ EPILOG = Text.from_markup(
24
24
  VERDICT_STYLE = {
25
25
  _probe.OPEN: "success",
26
26
  _probe.DNS_POISONED: "warning",
27
+ _probe.ADDRESS_BLOCKED: "warning",
27
28
  _probe.SNI_BLOCKED: "error",
28
29
  _probe.IP_BLOCKED: "error",
29
30
  _probe.UNREACHABLE: "error",
@@ -32,6 +33,7 @@ VERDICT_STYLE = {
32
33
  ADVICE = {
33
34
  _probe.OPEN: "Nothing to do.",
34
35
  _probe.DNS_POISONED: "Run `evo route open {host}` to pin the working address, or fix DNS at the source.",
36
+ _probe.ADDRESS_BLOCKED: "Run `evo route open {host}` to pin the working address.",
35
37
  _probe.SNI_BLOCKED: "hosts and DNS cannot help here. Run `evo route open {host}` to bring up the DPI bypass.",
36
38
  _probe.IP_BLOCKED: "The address answers TCP but nothing completes - you need a tunnel or VPN.",
37
39
  _probe.UNREACHABLE: "No address resolved or answered. Check the name, then your link.",
@@ -92,6 +94,9 @@ def render(report):
92
94
  success(f"{host} is reachable. {message}")
93
95
  elif verdict == _probe.DNS_POISONED:
94
96
  warning(f"DNS hands back a bogus address for {host}, but {report['best']} works. {message}")
97
+ elif verdict == _probe.ADDRESS_BLOCKED:
98
+ dead = ", ".join(report["system_ips"])
99
+ warning(f"{host} resolves to {dead}, which is blocked, but {report['best']} works. {message}")
95
100
  else:
96
101
  error(f"{host}: {_probe.VERDICT_TEXT[verdict]}. {message}")
97
102
 
@@ -130,7 +130,7 @@ def open_route(host, method, dry_run, timeout):
130
130
  if verdict == _probe.OPEN:
131
131
  success(f"{host} is already reachable - nothing to do.")
132
132
  return report
133
- if verdict == _probe.DNS_POISONED:
133
+ if verdict in (_probe.DNS_POISONED, _probe.ADDRESS_BLOCKED):
134
134
  method = "hosts"
135
135
  elif verdict == _probe.SNI_BLOCKED:
136
136
  method = "dpi"
@@ -37,7 +37,7 @@ def probe(host, timeout, show_all, no_vn, as_json):
37
37
  with console.status(f"[info]collecting addresses for {host}...[/info]", spinner="dots"):
38
38
  answers = _dns.gather(host, include_vn=not no_vn)
39
39
 
40
- candidates = answers["trusted"] or [ip for ip in (answers["system"] or []) if not _dns.is_bogus(ip)]
40
+ candidates = _probe.candidates(answers)
41
41
  if not candidates:
42
42
  error(f"No usable address for {host}.")
43
43
  sys.exit(1)
@@ -61,13 +61,13 @@ def probe(host, timeout, show_all, no_vn, as_json):
61
61
  table.add_column("time")
62
62
  table.add_column("seen from")
63
63
 
64
- origin = _origin_map(answers["resolvers"])
64
+ origin = _origin_map({"system": answers["system"], **answers["resolvers"]})
65
65
  for row in shown:
66
66
  table.add_row(
67
67
  row["ip"],
68
68
  "[success]ok[/success]" if row["tls"] else "[error]blocked[/error]",
69
69
  f"{row['ms']:.0f}ms" if row["ms"] is not None else "-",
70
- ", ".join(origin.get(row["ip"], [])) or "[dim]system[/dim]",
70
+ ", ".join(origin.get(row["ip"], [])),
71
71
  )
72
72
  console.print()
73
73
  console.print(table)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.32.0
3
+ Version: 0.32.1
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -123,6 +123,52 @@ def test_verdict_unreachable_when_tcp_never_opens():
123
123
  assert _probe._verdict(["1.2.3.4"], probes, [], control, False) == _probe.UNREACHABLE
124
124
 
125
125
 
126
+ def test_verdict_address_blocked_when_only_a_foreign_address_works():
127
+ probes = [_probe_row("5.6.7.8", tls=True), _probe_row("1.2.3.4", tcp=False)]
128
+ assert _probe._verdict(["1.2.3.4", "5.6.7.8"], probes, probes[:1], None, False, False) == _probe.ADDRESS_BLOCKED
129
+
130
+
131
+ def _fake_network(monkeypatch, system, trusted, live):
132
+ """Resolvers answer with ``system`` and ``trusted``; only the addresses in ``live`` finish a handshake."""
133
+ answers = {"system": system, "resolvers": {"doh": trusted}, "trusted": trusted}
134
+ monkeypatch.setattr(_dns, "gather", lambda host, include_vn=True: answers)
135
+ tried = []
136
+
137
+ def tls_probe(ip, sni, timeout=6.0):
138
+ tried.append(ip)
139
+ return _probe_row(ip, tcp=ip in live, tls=ip in live)
140
+
141
+ monkeypatch.setattr(_probe, "tls_probe", tls_probe)
142
+ return tried
143
+
144
+
145
+ def test_diagnose_probes_the_address_the_system_uses(monkeypatch):
146
+ # www.facebook.com, 2026-10: the system answer is a real Meta address that the network drops,
147
+ # while the one DoH hands out works. Testing only the DoH address read that as open.
148
+ tried = _fake_network(monkeypatch, ["57.144.64.1"], ["157.240.13.35"], {"157.240.13.35"})
149
+ report = _probe.diagnose("www.facebook.com")
150
+ assert "57.144.64.1" in tried
151
+ assert report["verdict"] == _probe.ADDRESS_BLOCKED
152
+ assert report["best"] == "157.240.13.35"
153
+
154
+
155
+ def test_diagnose_open_when_the_system_address_works(monkeypatch):
156
+ _fake_network(monkeypatch, ["1.2.3.4"], ["5.6.7.8"], {"1.2.3.4", "5.6.7.8"})
157
+ assert _probe.diagnose("example.org")["verdict"] == _probe.OPEN
158
+
159
+
160
+ def test_diagnose_without_a_system_answer_needs_a_pin(monkeypatch):
161
+ _fake_network(monkeypatch, None, ["5.6.7.8"], {"5.6.7.8"})
162
+ assert _probe.diagnose("example.org")["verdict"] == _probe.DNS_POISONED
163
+
164
+
165
+ def test_open_pins_a_host_whose_system_address_is_blocked(monkeypatch):
166
+ _fake_network(monkeypatch, ["57.144.64.1"], ["157.240.13.35"], {"157.240.13.35"})
167
+ result = CliRunner().invoke(cli, ["route", "open", "www.facebook.com", "--dry-run"])
168
+ assert result.exit_code == 0, result.output
169
+ assert "Would pin" in result.output and "157.240.13.35" in result.output
170
+
171
+
126
172
  HOSTS_SAMPLE = "127.0.0.1 localhost\n192.168.1.9 nas.local\n"
127
173
 
128
174
 
@@ -1 +0,0 @@
1
- 0.32.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes