Skip to content

Add Tor/Orbot SOCKS5 proxy support for .onion addresses#424

Merged
torlando-tech merged 10 commits intomainfrom
claude/tor-tcp-support-G8RsF
Feb 17, 2026
Merged

Add Tor/Orbot SOCKS5 proxy support for .onion addresses#424
torlando-tech merged 10 commits intomainfrom
claude/tor-tcp-support-G8RsF

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

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

  • Added Tor .onion server: Registered "interloper node (Tor)" community server with .onion address in the server list
  • SOCKS5 proxy configuration: Added socksProxyEnabled, socksProxyHost, and socksProxyPort fields to InterfaceConfig.TcpClient
  • Auto-detection for .onion addresses: SOCKS5 proxy is automatically enabled when a .onion address is entered or selected
  • UI controls: Added toggle switch and configurable host/port fields in the TCP client wizard review step with animated visibility
  • Python SOCKS5 implementation: Implemented a minimal SOCKS5 client in reticulum_wrapper.py with no external dependencies:
    • Handles SOCKS5 handshake and domain name resolution by proxy
    • Supports both standard and .onion addresses
    • Includes proper error handling with descriptive messages
    • Socket monkey-patching to transparently route configured targets through the proxy
  • State management: Updated TcpClientWizardViewModel to handle SOCKS proxy settings and auto-enable for .onion addresses
  • Serialization: Added SOCKS proxy fields to JSON serialization in both Android and Reticulum wrapper

Implementation Details

  • SOCKS5 proxy configuration is optional and defaults to Orbot's standard settings (127.0.0.1:9050)
  • The Python wrapper automatically installs socket interception only when SOCKS proxy targets are configured
  • .onion addresses automatically route through the configured SOCKS proxy without requiring manual toggle
  • The implementation uses SOCKS5 domain name resolution (ATYP=0x03) to support .onion addresses
  • Comprehensive error messages guide users to ensure Orbot is installed and connected

https://claude.ai/code/session_01DxBMKpvbEp6ckVEnLDb6Cv

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 9, 2026

Greptile Summary

This PR adds Tor/SOCKS5 proxy support for routing TCP connections through Orbot, enabling .onion address connectivity. The implementation introduces a clean TorClientInterface that subclasses RNS's TCPClientInterface with a full SOCKS5 handshake (including Username/Password auth for Tor stream isolation), and deploys it as an RNS external interface module.

  • New TorClientInterface.py: Standalone SOCKS5 client that overrides the parent's connect() method to tunnel through a proxy. Includes _recv_exact() for safe partial-read handling, hostname length validation, comprehensive error codes, and Tor-appropriate TCP keepalive timeouts.
  • Config generation: reticulum_wrapper.py conditionally emits type = TorClientInterface with proxy_host/proxy_port when SOCKS is enabled, and deploys the module file to {configdir}/interfaces/. Deployment failure is a hard error when SOCKS interfaces are configured.
  • Android UI: Toggle card with animated proxy host/port fields in the TCP client wizard. Auto-enables SOCKS for .onion addresses and prevents disabling it when .onion is the target.
  • Data model: socksProxyEnabled, socksProxyHost, socksProxyPort fields added to InterfaceConfig.TCPClient with consistent serialization across database and IPC paths.
  • Community server: Adds the interloper node Tor hidden service to the server list.

Previous review feedback has been thoroughly addressed — partial-read issues resolved with _recv_exact(), monkey-patching replaced with a proper subclass, serialization made consistent, premature self.online removed, and .onion toggle guard added.

Confidence Score: 4/5

  • This PR is well-structured and safe to merge; previous review feedback has been addressed comprehensively.
  • The implementation is clean and follows good patterns: proper subclassing instead of monkey-patching, safe byte reads, comprehensive error handling, and consistent serialization. Previous review issues have all been addressed. The SOCKS5 protocol implementation correctly follows RFC 1928/1929. Minor remaining concern is the lack of input validation feedback for proxy port (silent fallback to default), but this was already discussed in prior review threads and is a UX preference rather than a bug.
  • python/TorClientInterface.py and python/reticulum_wrapper.py are the core new logic — verify that RNS external interface loading works correctly on the target Chaquopy/Android environment.

Important Files Changed

Filename Overview
python/TorClientInterface.py New SOCKS5 proxy interface subclassing TCPClientInterface. Clean implementation with proper _recv_exact for partial reads, hostname length guard, unknown ATYP handling, and Tor stream isolation via username auth. Timeout configuration and online state handled correctly.
python/reticulum_wrapper.py Adds TorClientInterface deployment and config generation for SOCKS5-enabled TCP interfaces. Deployment failure correctly treated as hard error when SOCKS interfaces are configured. Config file generation conditionally adds proxy_host/proxy_port when socks_enabled.
app/src/main/java/com/lxmf/messenger/viewmodel/TcpClientWizardViewModel.kt Adds SOCKS proxy state fields with auto-enable for .onion addresses and guard against disabling SOCKS when .onion is the target. Port falls back to default 9050 on invalid input silently (previously discussed).
app/src/main/java/com/lxmf/messenger/ui/screens/tcpclient/ReviewConfigureStep.kt Adds Tor/Orbot SOCKS proxy toggle card with animated proxy host/port fields. Clean Compose UI with appropriate keyboard types and layout.
reticulum/src/main/java/com/lxmf/messenger/reticulum/model/InterfaceConfigExt.kt Always serializes SOCKS proxy fields for TCPClient, preserving custom host/port when proxy is disabled. Consistent with ServiceReticulumProtocol serialization. Also includes formatting refactor (expression body, chained apply).
reticulum/src/main/java/com/lxmf/messenger/reticulum/model/ReticulumConfig.kt Adds three SOCKS proxy fields to TCPClient data class with sensible defaults (disabled, 127.0.0.1:9050).
app/src/main/java/com/lxmf/messenger/data/model/TcpCommunityServer.kt Adds interloper node Tor hidden service (.onion address) to community server list.
app/src/main/java/com/lxmf/messenger/repository/InterfaceRepository.kt Deserializes SOCKS proxy fields from database JSON with safe defaults matching the data class.
app/src/main/java/com/lxmf/messenger/reticulum/protocol/ServiceReticulumProtocol.kt Serializes SOCKS proxy fields in IPC JSON to Python wrapper. Consistent with InterfaceConfigExt serialization.

Sequence Diagram

sequenceDiagram
    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
Loading

Last reviewed commit: 363f5c7

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Feb 9, 2026

Codecov Report

❌ Patch coverage is 10.63830% with 126 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
python/TorClientInterface.py 0.00% 110 Missing ⚠️
python/reticulum_wrapper.py 48.38% 16 Missing ⚠️

📢 Thoughts on this report? Let us know!

@torlando-tech torlando-tech linked an issue Feb 9, 2026 that may be closed by this pull request
@torlando-tech torlando-tech added this to the v0.9.0 milestone Feb 9, 2026
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 30 to 32
LXMF = None


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

1 similar comment
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +198 to +205
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +339 to +340
socksProxyHost = currentState.socksProxyHost.trim().ifEmpty { "127.0.0.1" },
socksProxyPort = currentState.socksProxyPort.toIntOrNull() ?: 9050,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +234 to +236
fun toggleSocksProxy(enabled: Boolean) {
_state.update { it.copy(socksProxyEnabled = enabled) }
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.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.

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +1641 to +1647
# 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}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
# 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.

Comment on lines +1087 to +1088
interfaces_dir = os.path.join(self.storage_path, "interfaces")
os.makedirs(interfaces_dir, exist_ok=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai review

claude and others added 10 commits February 16, 2026 18:53
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>
@torlando-tech torlando-tech force-pushed the claude/tor-tcp-support-G8RsF branch from 363f5c7 to 0bcf920 Compare February 17, 2026 00:48
@torlando-tech torlando-tech merged commit 9c3c464 into main Feb 17, 2026
12 of 13 checks passed
@torlando-tech torlando-tech deleted the claude/tor-tcp-support-G8RsF branch February 17, 2026 03:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Using Tor .onion for TCP with Orbot

2 participants