Skip to content

meshgw: extract a destination-explicit HandleMeshConn so Android/iOS VpnService can reuse the mesh gateway without SO_ORIGINAL_DST #3806

Description

@0pcom

Context

The mobile app wants .dmsg/.skynet names to resolve in a browser while the VPN client is active — the same capability the Linux vpn-router already ships via pkg/vpnrouter/meshgw. On Android (VpnService) / iOS (NEPacketTunnelProvider) the app reads the TUN itself, so there is no iptables REDIRECT and no kernel *net.TCPConn, and SO_ORIGINAL_DST does not apply.

What already works portably (no change needed)

meshgw splits cleanly; the transport-agnostic half is directly reusable in-process on mobile:

  • New(dial, cidr, aliases, log) — synthetic-IP pool + byIP: synthetic-IP → {scheme, dest PK} reverse map.
  • InstallDNS(mux *dns.ServeMux)dmsg./skynet. zone handlers on a miekg/dns mux; resolve() leases from the pool. Pure Go.
  • EnableTLSMITM(...), PoolCIDR(), and the whole dial + splice + TLS-MITM path.

One Gateway instance backs both the DNS resolver and the packet path (so resolve() writes the byIP entry the packet path reads) — already the design.

The one Linux coupling

ServeTransparenthandleConn (pkg/vpnrouter/meshgw/meshgw.go) hard-codes destination recovery via originalDst() (SO_ORIGINAL_DST) and requires conn.(*net.TCPConn). That is the only thing blocking reuse from a userspace-TUN path; skipping origdst is correct on mobile (the original dst IP+port are in the packet), but the dial+splice+MITM logic is currently private behind that origdst-coupled entry point.

Proposed change

Extract a destination-explicit entry point:

// dstIP/dstPort come from SO_ORIGINAL_DST on Linux, or from the TUN packet on mobile.
func (g *Gateway) HandleMeshConn(ctx context.Context, client net.Conn, dstIP net.IP, dstPort uint16) error
  • Linux handleConn becomes origIP, origPort := originalDst(tc); g.HandleMeshConn(ctx, conn, origIP, origPort) — byte-for-byte unchanged behaviour; redirect_netns_linux_test.go stays green.
  • Mobile calls HandleMeshConn directly with the dst it read from the TUN; client is the netstack net.Conn.
  • lookup stays internal.

Small, non-breaking refactor that unblocks Android/iOS while keeping the Linux path identical.

Out of meshgw scope — the mobile app must supply around it

  1. A userspace TCP/IP stack (gVisor netstack / wireguard-go tun+netstack) to turn TUN packets into net.Conn for HandleMeshConn.
  2. VpnService.Builder.addRoute(PoolCIDR()) so synthetic-IP packets reach the fd.
  3. A default DNS handler that forwards non-.dmsg/.skynet queries upstream (InstallDNS only handles the two zones).
  4. TLS-MITM CA trust: https://…dmsg requires the minted leaf CA to be user-trusted on the device (Android/iOS can’t install a system CA silently). Plaintext http://…dmsg needs none.
  5. iOS parity via NEPacketTunnelProvider (same refactor applies).

Alternative worth weighing

tun2socks → in-process pkg/dmsgweb SOCKS5 resolver. dmsgweb already does .dmsg resolution + browser TLS-MITM; feeding the TUN into it with an off-the-shelf tun2socks bridge may be less code than embedding a netstack + refactoring meshgw, and reuses the exact proxy a real browser already works against (CA-trust caveat still applies).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions