Axon Update: We Stopped Lying to Dart (And Built a Raw Socket Client)

In our last update, I shared a dirty little secret about how Axon was talking to the Tor network.

We had a problem: Tor’s HTTP tunnel requires a CONNECT handshake, but Dart’s standard HttpClient only sends that handshake if you are using HTTPS. Our solution at the time was the "Fake HTTPS" hack—we forced every .onion URL to look like https://, ignored the SSL errors, and tricked Dart into triggering the handshake.

It was clever. It worked. But it was also fragile.

As we pushed Axon further—moving large files and dealing with the spotty connectivity inherent in peer-to-peer mobile mesh networks—the high-level HttpClient started fighting us. We saw "Unexpected end of input" errors when Tor circuits jittered. We lacked granular control over response buffering.

We realized that if we wanted a robust mesh network, we had to stop abstracting the network layer and start owning it. We had to stop lying to Dart and teach it how to speak "Tor" natively.

The New Architecture: TorOnionClient

We scrapped the HttpClient wrapper for .onion addresses entirely. Instead, we built TorOnionClient, a custom class that operates at the TCP socket layer.

Instead of asking a high-level client to fetch a URL, we now manually open a socket to the local Tor proxy and handle the handshake ourselves. This gives us total control over the bytes on the wire.

Here is the logic that replaced our "Fake HTTPS" hack:

// The new "Manual CONNECT" approach
Future<TorResponse> _send(String method, String url) async {
  final uri = Uri.parse(url);

  // 1. Connect to the local Tor SOCKS/HTTP proxy (usually 9080)
  Socket socket = await Socket.connect('127.0.0.1', proxyPort);

  // 2. Perform the CONNECT Handshake manually
  // We tell the proxy: "Open a tunnel to this hidden service"
  final targetPort = uri.port == 0 ? 80 : uri.port;
  final handshake = 'CONNECT ${uri.host}:$targetPort HTTP/1.1\r\n'
                    'Host: ${uri.host}:$targetPort\r\n'
                    '\r\n';

  socket.write(handshake);
  await socket.flush();

  // 3. Listen to the raw stream
  socket.listen((data) {
    // Logic here checks for "HTTP/1.1 200 Connection Established"
    // Once received, we know the tunnel is open.
    // Then we write the ACTUAL request (GET/POST) into the tunnel.
  });
}

Why this is better

1. No More SSL Spoofing

We no longer have to pretend our connections are HTTPS. We can treat .onion addresses as the unsecure HTTP endpoints they technically are (knowing that Tor provides the encryption layer automatically). The code is honest.

2. Robust Buffering

The standard HttpClient can be finicky about stream termination. In a P2P context, bytes might arrive in weird chunks. By using a raw Socket, we can buffer the data ourselves.

In our new implementation, we collect the response bytes into a buffer and only attempt to decode them when the server explicitly closes the connection or the Content-Length is met. This solved our "Unexpected end of input" issues overnight.

3. Simpler Debugging

Because we are constructing the raw HTTP headers string manually:

sb.write('$method $path HTTP/1.1\r\n');
sb.write('Host: ${uri.host}\r\n');
sb.write('Connection: close\r\n');

We know exactly what is leaving the device. There is no magic injection of headers we didn't ask for.

The Loopback Test

To prove this works, we added an integration test right inside the app. We spin up a local standard HTTP server inside the Flutter app on port 8080, point the Tor Hidden Service at it, and then use our new TorOnionClient to hit our own .onion address.

Traffic flows out of the app, into the local Tor proxy, through the Tor network (finding the rendezvous point), back to our device, and into our local server.

// From our main.dart test logic
_logs.add("➡️ Sending POST to $url");
final postResponse = await _onionClient.post(
  url,
  body: '{"msg": "Hello Tor"}',
  headers: {'Content-Type': 'application/json'}
);

The result? A perfect 200 OK without a single SSL certificate warning in sight.

What’s Next for Axon

With the networking layer stabilized, we are back to focusing on File Swarm. Now that we can reliably hold sockets open and buffer raw data, we are optimizing the chunking algorithm to download file parts from multiple anonymous peers simultaneously.

The tor_hidden_service plugin is getting cleaner by the day.

Stay tuned. The mesh is getting stronger.