Skip to content

Linux: prevent the proxy server from looping back into its own VPN #246

@compscidr

Description

@compscidr

Background

When the kanonproxy server and client run on the same Linux host with traffic
routed into the kanon TUN device (e.g. via ip route add <target> dev kanon),
the server's own outbound TCP/UDP socket — opened in
AnonymousTcpSession / UdpSession against the same destination — gets routed
right back into kanon. The client picks it up, tunnels it to the server, the
server tries to connect again, and the cycle repeats until curl times out.

This is the Linux equivalent of what VpnService.protect() solves on Android.
Today there is no Linux protector implementation, so kanonproxy itself has no
loop protection on Linux.

The local Linux demo added in #244 sidesteps the issue at the caller by
pinning curl with --interface kanon (SO_BINDTODEVICE), so curl's packets
go through the TUN but the server's outbound packets don't. That's a usable
demo workaround but not a real fix — any caller that doesn't bind to kanon
will still loop.

Options

Option 3 (preferred): LinuxProtector in kanonproxy

Implement a Linux-specific VpnProtector that pins every outbound
SocketChannel / DatagramChannel opened by Session / KAnonProxy to the
host's real egress interface via SO_BINDTODEVICE (or equivalent). Wire it
into ProxyServer.main (and into LinuxProxyClient if it grows the same
need).

Pros:

  • Loop becomes structurally impossible on Linux regardless of how callers
    route traffic into the TUN.
  • Makes the Linux story symmetric with Android (IcmpAndroid +
    VpnService.protect() already do the same thing on the icmp side).
  • No special instructions for users — the obvious curl http://1.1.1.1/
    through a dev kanon route would Just Work.

Cons:

  • Requires a code change in core (a new VpnProtector impl) plus a small
    hook so the protector sees every newly-opened socket. The Java NIO API
    doesn't expose SO_BINDTODEVICE directly, so the impl needs to grab the
    underlying file descriptor (likely via reflection or a small JNI/JNA
    helper, similar to how TunTapDevice already uses JNA / jnr-enxio).
  • The JVM running the proxy needs CAP_NET_RAW (or root) for
    SO_BINDTODEVICE to succeed.

Option 1: UID-based policy routing (demo / ops workaround)

Run the server as a dedicated user; add ip rule add uidrange <srv-uid>-<srv-uid> lookup main priority 50 so the server's own packets bypass any kanon route.
Then ip route add <target> dev kanon works for everyone else without
caller-side flags.

Pros:

  • No code change.
  • Any client on the host (curl, wget, browser) goes through the proxy.

Cons:

  • Requires a second user account and an ip rule setup outside the kanonproxy
    process — easy to misconfigure / forget to tear down.

Option 2: Network namespace (demo / ops workaround)

Run the server inside a netns connected to the host by a veth pair with
SNAT/MASQUERADE. The server's routing table is independent of the host's,
so the kanon route doesn't exist inside the netns and the loop is
structurally impossible.

Pros:

  • Cleanest isolation; closest to a realistic deployment model.

Cons:

  • Most setup of the three (netns + veth + NAT + cross-namespace UDP between
    the client on the host and the server in the netns).
  • Still no help for any caller actually running on the host.

Recommendation

Implement Option 3 as the real fix. It belongs in the library (matches
the Android model), removes a footgun for any future Linux user of
kanonproxy, and makes Options 1 and 2 unnecessary.

Options 1 and 2 are documented here only for reference / as fallback
workarounds in the meantime.

Related

  • Add a runnable local Linux demo (server + client + curl) #244 (this PR) — adds the local Linux demo and uses --interface kanon
    on the curl side as a demo-only workaround.
  • core/src/main/kotlin/com/jasonernst/kanonproxy/VpnProtector.kt
    current VpnProtector interface; DummyProtector is the default on Linux.
  • android/src/main/kotlin/com/jasonernst/kanonproxy/KAnonVpnService.kt
    Android wires its own protect() via the VpnService; that's the model
    to mirror on Linux.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions