At Sarah's Forge, we’ve been putting a lot of energy into Axon — our decentralized, peer-to-peer mesh network built to keep communicating even when the wider internet goes dark. Axon started life entirely in Go, taking full advantage of its clean networking layer and the ease of piping traffic through Tor.
But at some point, we had to stop thinking like backend engineers and start thinking like our users. Axon needed to live on phones. It needed a real mobile experience. That meant Flutter.
The plan sounded simple enough: wrap the binary, talk to it from Dart, and call it a port. But the moment we tried to route traffic through Tor on the Dart side, we hit a wall the size of an overfed proxy daemon.
The Problem: Dart and SOCKS5 don’t get along
Tor speaks SOCKS5. Go is perfectly happy to dial SOCKS5. Flutter? Flutter politely shakes its head and slides your request straight into the void.
There’s no native SOCKS5 support in Dart’s HttpClient, and while there are packages out there, bolting them into Flutter’s internal networking stack would have shaved a few years off my life, but somehow The Flutter app itself had to be the node.
The Solution: Build our own Tor plugin
So we built tor_hidden_service, a Flutter plugin that bundles the Guardian Project’s Tor binaries and exposes everything we need directly to Dart.
The key move was convincing Tor to give us something more Flutter-shaped than a SOCKS5 port. Instead of relying only on SOCKS, we enabled an HTTPTunnelPort on 9080 — which lets Dart treat Tor like a normal HTTP proxy:
HttpClient client = HttpClient();
client.findProxy = (uri) => "PROXY localhost:9080";
This looked perfect. We hit a .onion address, waited for the magic… and got slapped with 502 Bad Gateway.
The Bug That Ate an Afternoon
Tor’s HTTP tunnel isn’t actually a normal HTTP proxy. It requires a CONNECT handshake. Dart’s HttpClient only sends CONNECT when the request is HTTPS. Onion Services don’t need HTTPS because the encryption is already baked in.
So we ended up in an absurd standoff:
- Tor refused to proceed without CONNECT.
- Dart refused to use CONNECT without HTTPS.
- Onion Services refused to pretend to be HTTPS.
Something had to give.
The Fix: Lie boldly
The escape hatch was to simply lie to Dart.
Inside Axon, we rewrote the outbound logic so every onion URL is forced to use https:// even though TLS is nowhere in sight:
// logic/outbox.dart
Future<HttpClientResponse> _sendOverTor(String method, String onionUrl) async {
if (onionUrl.startsWith('http://')) {
onionUrl = onionUrl.replaceFirst('http://', 'https://');
}
final client = tor.getTorHttpClient();
// ...send request...
}
Pair that with a badCertificateCallback that always returns true — because Tor already provides end-to-end encryption — and suddenly everything lines up:
- Flutter makes an HTTPS request to
xyz.onion. - Dart sends
CONNECT xyz.onion:443to the Tor proxy. - Tor accepts it and builds the circuit.
- Data flows exactly where we want it.
The New Architecture
Now that the plugin works, Axon is shifting from Go to Dart piece by piece.
- Inbound: Tor maps public port 80 on your onion address to
localhost:8080, where a tiny Shelf server waits to handle messages and file requests. - Outbound: All traffic leaves through
localhost:9080, flowing through the Tor tunnel. - Storage: SQLite moves from the Go driver to
sqfliteon device.
No backend process. No sidecar. The Flutter app is the node.
What’s next
We’re finishing the new File Swarm system — the part that lets Axon discover files across the network and download them in parallel from multiple peers, anonymously, automatically, and fully distributed.
The tor_hidden_service plugin will be open-sourced shortly so other devs can build real P2P systems in Flutter without fighting the same proxy battles we did.
Follow Sarah’s Forge.Dev for more updates as Axon grows into the network it’s meant to be.