Add Tor/Orbot SOCKS5 proxy support for .onion addresses#424
Add Tor/Orbot SOCKS5 proxy support for .onion addresses#424torlando-tech merged 10 commits intomainfrom
Conversation
Greptile SummaryThis PR adds Tor/SOCKS5 proxy support for routing TCP connections through Orbot, enabling
Previous review feedback has been thoroughly addressed — partial-read issues resolved with Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant UI as Android UI (Wizard)
participant VM as TcpClientWizardViewModel
participant DB as InterfaceRepository (DB)
participant SRP as ServiceReticulumProtocol
participant PY as reticulum_wrapper.py
participant TOR as TorClientInterface
participant SOCKS as SOCKS5 Proxy (Orbot)
participant TARGET as .onion Target
UI->>VM: selectServer(.onion) / updateTargetHost(.onion)
VM->>VM: Auto-enable socksProxyEnabled
UI->>VM: saveConfiguration()
VM->>DB: insertInterface(TCPClient + SOCKS fields)
Note over SRP,PY: On RNS initialization
SRP->>PY: IPC JSON with socks_proxy_enabled=true
PY->>PY: _deploy_tor_interface() → copy to interfaces/
PY->>PY: _create_config_file() → type = TorClientInterface
Note over TOR,TARGET: RNS loads TorClientInterface
TOR->>TOR: __init__: extract proxy_host/port
TOR->>TOR: super().__init__() triggers connect()
TOR->>SOCKS: TCP connect to 127.0.0.1:9050
TOR->>SOCKS: SOCKS5 greeting (auth method 0x02)
SOCKS-->>TOR: Accept Username/Password auth
TOR->>SOCKS: Username (target:port) + Password
SOCKS-->>TOR: Auth success
TOR->>SOCKS: CONNECT to .onion:port (ATYP=0x03)
SOCKS->>TARGET: Tor circuit established
SOCKS-->>TOR: Connect success + bind address
TOR->>TOR: set_timeouts_linux(), online = True
Last reviewed commit: 363f5c7 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
| LXMF = None | ||
|
|
||
|
|
There was a problem hiding this comment.
Missing struct import
The new SOCKS5 code calls struct.pack('!H', dest_port) but this doesn’t import struct in this file scope (the existing import struct at the top was removed in the diff context). This will raise NameError: name 'struct' is not defined the first time a proxied connection is attempted.
Prompt To Fix With AI
This is a comment left during a code review.
Path: python/reticulum_wrapper.py
Line: 30:32
Comment:
**Missing `struct` import**
The new SOCKS5 code calls `struct.pack('!H', dest_port)` but this doesn’t import `struct` in this file scope (the existing `import struct` at the top was removed in the diff context). This will raise `NameError: name 'struct' is not defined` the first time a proxied connection is attempted.
How can I resolve this? If you propose a fix, please make it concise.
1 similar comment
| atyp = resp[3] | ||
| if atyp == 0x01: # IPv4: 4 addr + 2 port | ||
| self._recv_exact(6) | ||
| elif atyp == 0x03: # Domain: 1 len + domain + 2 port | ||
| domain_len = self._recv_exact(1)[0] | ||
| self._recv_exact(domain_len + 2) | ||
| elif atyp == 0x04: # IPv6: 16 addr + 2 port | ||
| self._recv_exact(18) |
There was a problem hiding this comment.
Missing handler for unknown ATYP values
If the proxy returns an ATYP value other than 0x01, 0x03, or 0x04 (e.g., a malformed response or future extension), the code will silently skip consuming the bind address, causing the socket stream to become desynchronized. All subsequent reads will fail.
Add an else clause:
else:
raise ConnectionError(f"SOCKS5 proxy returned unsupported address type: 0x{atyp:02x}")Prompt To Fix With AI
This is a comment left during a code review.
Path: python/TorClientInterface.py
Line: 198:205
Comment:
Missing handler for unknown ATYP values
If the proxy returns an ATYP value other than 0x01, 0x03, or 0x04 (e.g., a malformed response or future extension), the code will silently skip consuming the bind address, causing the socket stream to become desynchronized. All subsequent reads will fail.
Add an else clause:
```python
else:
raise ConnectionError(f"SOCKS5 proxy returned unsupported address type: 0x{atyp:02x}")
```
How can I resolve this? If you propose a fix, please make it concise.| socksProxyHost = currentState.socksProxyHost.trim().ifEmpty { "127.0.0.1" }, | ||
| socksProxyPort = currentState.socksProxyPort.toIntOrNull() ?: 9050, |
There was a problem hiding this comment.
Port validation silently accepts invalid input
socksProxyPort.toIntOrNull() returns null for non-numeric input (e.g., "abc"), falling back to 9050. Users entering invalid ports won't see an error and may be confused when the default is used silently.
Same issue exists for targetPort on line 334.
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/viewmodel/TcpClientWizardViewModel.kt
Line: 339:340
Comment:
Port validation silently accepts invalid input
`socksProxyPort.toIntOrNull()` returns null for non-numeric input (e.g., "abc"), falling back to 9050. Users entering invalid ports won't see an error and may be confused when the default is used silently.
Same issue exists for `targetPort` on line 334.
How can I resolve this? If you propose a fix, please make it concise.| fun toggleSocksProxy(enabled: Boolean) { | ||
| _state.update { it.copy(socksProxyEnabled = enabled) } | ||
| } |
There was a problem hiding this comment.
.onion addresses can be saved without SOCKS proxy
toggleSocksProxy(false) allows the user to disable the SOCKS proxy even when a .onion address is entered. Since .onion addresses are only resolvable through Tor, saving such a configuration will always fail to connect at runtime.
The auto-enable logic in updateTargetHost() (line 219) only fires while the user is typing, but doesn't prevent manual toggle-off afterwards. Consider either:
- Preventing the toggle from being disabled when the target host ends with
.onion, or - Re-enabling it in
saveConfiguration()as a safety net
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/viewmodel/TcpClientWizardViewModel.kt
Line: 234:236
Comment:
**`.onion` addresses can be saved without SOCKS proxy**
`toggleSocksProxy(false)` allows the user to disable the SOCKS proxy even when a `.onion` address is entered. Since `.onion` addresses are only resolvable through Tor, saving such a configuration will always fail to connect at runtime.
The auto-enable logic in `updateTargetHost()` (line 219) only fires while the user is typing, but doesn't prevent manual toggle-off afterwards. Consider either:
- Preventing the toggle from being disabled when the target host ends with `.onion`, or
- Re-enabling it in `saveConfiguration()` as a safety net
How can I resolve this? If you propose a fix, please make it concise.
reticulum/src/main/java/com/lxmf/messenger/reticulum/model/InterfaceConfigExt.kt
Outdated
Show resolved
Hide resolved
| # Deploy TorClientInterface to RNS external interfaces directory | ||
| # so RNS can load it when config has type = TorClientInterface | ||
| try: | ||
| self._deploy_tor_interface() | ||
| except Exception as tor_err: | ||
| log_warning("ReticulumWrapper", "initialize", | ||
| f"Failed to deploy TorClientInterface: {tor_err}") |
There was a problem hiding this comment.
Silent failure masks broken Tor interfaces
If _deploy_tor_interface() fails here, initialization proceeds and the config file will reference type = TorClientInterface, but RNS won't find the module in the interfaces/ directory. The user will see a cryptic RNS module-loading error instead of a clear indication that Tor support failed to deploy.
Consider checking whether any enabled interface actually uses SOCKS, and if so, treating deployment failure as a hard error:
| # Deploy TorClientInterface to RNS external interfaces directory | |
| # so RNS can load it when config has type = TorClientInterface | |
| try: | |
| self._deploy_tor_interface() | |
| except Exception as tor_err: | |
| log_warning("ReticulumWrapper", "initialize", | |
| f"Failed to deploy TorClientInterface: {tor_err}") | |
| # Deploy TorClientInterface to RNS external interfaces directory | |
| # so RNS can load it when config has type = TorClientInterface | |
| has_socks = any( | |
| iface.get("socks_proxy_enabled", False) | |
| for iface in enabled_interfaces | |
| ) | |
| try: | |
| self._deploy_tor_interface() | |
| except Exception as tor_err: | |
| if has_socks: | |
| return {"success": False, | |
| "error": f"Failed to deploy Tor interface module: {tor_err}"} | |
| log_warning("ReticulumWrapper", "initialize", | |
| f"Failed to deploy TorClientInterface: {tor_err}") |
Prompt To Fix With AI
This is a comment left during a code review.
Path: python/reticulum_wrapper.py
Line: 1641:1647
Comment:
**Silent failure masks broken Tor interfaces**
If `_deploy_tor_interface()` fails here, initialization proceeds and the config file will reference `type = TorClientInterface`, but RNS won't find the module in the `interfaces/` directory. The user will see a cryptic RNS module-loading error instead of a clear indication that Tor support failed to deploy.
Consider checking whether any enabled interface actually uses SOCKS, and if so, treating deployment failure as a hard error:
```suggestion
# Deploy TorClientInterface to RNS external interfaces directory
# so RNS can load it when config has type = TorClientInterface
has_socks = any(
iface.get("socks_proxy_enabled", False)
for iface in enabled_interfaces
)
try:
self._deploy_tor_interface()
except Exception as tor_err:
if has_socks:
return {"success": False,
"error": f"Failed to deploy Tor interface module: {tor_err}"}
log_warning("ReticulumWrapper", "initialize",
f"Failed to deploy TorClientInterface: {tor_err}")
```
How can I resolve this? If you propose a fix, please make it concise.| interfaces_dir = os.path.join(self.storage_path, "interfaces") | ||
| os.makedirs(interfaces_dir, exist_ok=True) |
There was a problem hiding this comment.
Missing __init__.py in interfaces directory
RNS loads external interface modules from {configdir}/interfaces/. Depending on the RNS version and Python environment, the interfaces/ directory may need an __init__.py file to be importable as a package. Without it, some Python environments (especially Chaquopy on Android) might fail to load TorClientInterface.py via import or exec.
Is this something you've verified works on the target Android/Chaquopy environment? Have you tested that RNS successfully loads external interface modules from this directory on Chaquopy/Android without an __init__.py?
Prompt To Fix With AI
This is a comment left during a code review.
Path: python/reticulum_wrapper.py
Line: 1087:1088
Comment:
**Missing `__init__.py` in interfaces directory**
RNS loads external interface modules from `{configdir}/interfaces/`. Depending on the RNS version and Python environment, the `interfaces/` directory may need an `__init__.py` file to be importable as a package. Without it, some Python environments (especially Chaquopy on Android) might fail to load `TorClientInterface.py` via `import` or `exec`.
Is this something you've verified works on the target Android/Chaquopy environment? Have you tested that RNS successfully loads external interface modules from this directory on Chaquopy/Android without an `__init__.py`?
How can I resolve this? If you propose a fix, please make it concise.|
@greptileai review |
Enable TCP client interfaces to route connections through a SOCKS5 proxy (e.g., Orbot) for Tor .onion address support. When a user enters a .onion hostname, the SOCKS proxy toggle auto-enables. Changes across all layers of the stack: - Data model: add socksProxyEnabled/Host/Port fields to TCPClient - Serialization: JSON round-trip through DB and Python config builder - Python wrapper: minimal SOCKS5 handshake implementation that monkey-patches socket.create_connection for proxied targets - UI: "Connect via Tor (Orbot)" toggle with proxy host/port fields in the TCP client wizard review step - Community servers: add interloper node .onion address https://claude.ai/code/session_01DxBMKpvbEp6ckVEnLDb6Cv
- Replace bare sock.recv() calls with _recv_exact() helper that loops until all expected bytes arrive, preventing misparse when TCP delivers data in smaller chunks - Parse SOCKS5 connect response in two stages: 4-byte header first, then variable-length bind address based on ATYP - Add @Suppress("TooManyFunctions") to TcpClientWizardViewModel (17 functions, threshold 15) since each function maps to a UI control https://claude.ai/code/session_01DxBMKpvbEp6ckVEnLDb6Cv
The previous socket.create_connection monkey-patch wasn't actually
intercepting Reticulum's TCP connections (RNS uses socket.connect()
directly, never create_connection). Replace with a proper RNS external
interface module that subclasses TCPClientInterface.
- Add TorClientInterface.py: overrides connect() with SOCKS5 handshake,
uses Username/Password auth for Tor stream isolation (different
circuits per interface), Tor-appropriate keepalive timeouts
- Remove ~175 lines of monkey-patch code from reticulum_wrapper.py
- Deploy interface to {configdir}/interfaces/ via pkgutil (RNS loads
external interfaces from this directory)
- Config generation emits type=TorClientInterface with proxy_host/port
when socks_proxy_enabled is set
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add else clause for unsupported SOCKS5 ATYP values to prevent socket desynchronization from unconsumed bind address bytes - Move hostname length validation into _socks5_handshake() where self.target_ip is used, rather than checking config value pre-init Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ion UX - Use shutil.copy2 (sibling file via __file__) as primary deployment method for TorClientInterface.py, with pkgutil.get_data as fallback - Remove premature self.online = True before timeout configuration - Prevent disabling SOCKS proxy toggle when .onion address is entered Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
If any enabled interface uses SOCKS proxy and deployment fails, return an error instead of letting RNS hit a confusing module-not-found error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When both deployment methods fail (filesystem copy and pkgutil), raise FileNotFoundError so the caller's try/except and has_socks check can properly fail-fast. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, socks_proxy_host and socks_proxy_port were only written to the database JSON when socksProxyEnabled was true. This caused custom proxy settings to be lost on round-trip if the user disabled the proxy, saved, then re-enabled it. Now matches ServiceReticulumProtocol which always serializes these fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up the existing TorClientInterface backend to the UI by adding SOCKS5 proxy toggle, host, and port fields under TCP Client advanced options. Also fix socket resource leak during reconnect cycles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
363f5c7 to
0bcf920
Compare
Summary
This PR adds support for routing TCP connections through a SOCKS5 proxy (e.g., Orbot for Tor), enabling the application to connect to .onion addresses and other services behind a SOCKS5 proxy.
Key Changes
socksProxyEnabled,socksProxyHost, andsocksProxyPortfields toInterfaceConfig.TcpClientreticulum_wrapper.pywith no external dependencies:TcpClientWizardViewModelto handle SOCKS proxy settings and auto-enable for .onion addressesImplementation Details
https://claude.ai/code/session_01DxBMKpvbEp6ckVEnLDb6Cv