Skip to content

fix: redesign direct address allocation and membership authority - #24

Merged
orielhaim merged 3 commits into
mainfrom
fix/direct-addressing-authority
Sep 7, 2026
Merged

fix: redesign direct address allocation and membership authority#24
orielhaim merged 3 commits into
mainfrom
fix/direct-addressing-authority

Conversation

@orielhaim

Copy link
Copy Markdown
Collaborator

This PR addresses #18 by replacing Direct's hardcoded addressing model with a proper network-level architecture.

The root problem was broader than the 100.64.0.0/10 collision with Tailscale: Direct addressing was spread across hardcoded CIDRs, local derivation logic, DNS fake IPs, join-order precedence, and manual overrides, with no single authenticated source of truth.

The new design makes signed Genesis the authority for the network's peer allocation range, makes the coordinator the only peer-IP allocator, and makes signed membership the only EndpointId -> IP authority.

It also removes the broad connected subnet model in favor of exact /32 addresses/routes, removes legacy collision indexes and IP overrides, makes overlapping Direct networks fail explicitly instead of relying on precedence, and keeps PeerDNS/MagicDNS host-local rather than part of the Direct protocol.

The goal is a simpler zero-config model: users keep using tunnet create / invite / join, while address selection, validation, conflict detection, allocation, and routing are handled automatically.

@socket-security

socket-security Bot commented Sep 6, 2026

Copy link
Copy Markdown

@wighawag

wighawag commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

The direction is right, and better than what I was doing: making signed Genesis the allocation authority and signed membership the only EndpointId -> IP source removes the guessing entirely. accept_pending doing lookup_endpoint() instead of re-deriving is strictly better than the fix I had in #20, and moving PeerDNS to loopback sidesteps the CGNAT collision instead of just relocating inside it. Dropping the connected /10 for exact /32s should kill the route-stealing half of #18 outright.

Two things it doesn't fix, both still present in the branch and both on main today:

Two questions rather than objections:

  • 127.0.0.1:53 will collide with anything already there, dnsmasq/unbound/pi-hole are common. Is a dedicated loopback address worth it, the way resolved uses 127.0.0.53? LocalResolverEndpoint already carries a port so it may be configurable enough as-is.
  • port 53 is privileged, so PeerDNS still can't bind on Android (unprivileged app). Since the endpoint struct has a port field, a high port there might be all that's needed. Would close the MagicDNS gap in Android app: connect a phone to a Direct mesh #13.

Also: what's the migration story for networks that already exist? Removing collision indexes and overrides changes allocation, so I assume existing Direct networks renumber. Worth stating explicitly in the PR.

One heads-up: main got rebased and dropped the 8 datapath commits, so all my open PRs picked them up and look enormous. Rebasing them now, ignore the diffs until I've pushed.

wighawag added a commit to wighawag/Tunnet that referenced this pull request Sep 6, 2026
Captures the reproduction and what the evidence did and did not support,
including that the originally reported no_route figure was disproven (it
was multicast discovery noise) and that the anti-spoof drops were later
confirmed causally with a TCP probe against a live peer.

Notes that tunnetio#24 supersedes the proposal section, and which two defects it
does not address.
@orielhaim
orielhaim force-pushed the fix/direct-addressing-authority branch from 075e70d to a00f700 Compare September 7, 2026 21:27
@orielhaim
orielhaim merged commit cfa3bcb into main Sep 7, 2026
10 checks passed
wighawag added a commit to wighawag/Tunnet that referenced this pull request Sep 8, 2026
tunnetio#24 moved Direct addressing to exact /32s and `build_tun_multi` passes a
hardcoded prefix of 32. The Android app derived its route from that
prefix, so `networkAddress(ipv4, 32)` produced a host route to the node
itself: the tunnel captured no peer traffic at all while still reporting
connected. The Kotlin comment still described the old model ("Direct mode
uses a /10, so the route must be the truncated network").

Addresses and routes are separate facts, so `TunRequest` now carries them
as separate lists rather than one address plus a prefix the app has to
interpret. A node holds one /32 per joined network; the traffic that must
enter the tunnel is each network's peer range, taken from the signed
genesis address plan and threaded through `DataPlaneActorConfig`.

The JNI signature becomes
`([Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;I)I`, passing
addresses, routes and resolvers in canonical text form so the contract
does not change shape again as the lists grow. `establishTun` applies each
address as a /32, each supplied route, and each resolver, and refuses to
establish with no routes rather than producing a tunnel that silently
carries nothing.

The resolver list is deliberately empty for now. PeerDNS binds host
loopback, which Android cannot use: port 53 is privileged for an
unprivileged app, and `netd` resolves per app so 127.0.0.1 would name the
app rather than the agent. Making names work needs PeerDNS to answer on an
in-tunnel address, which is a feature rather than plumbing; the field is
in place for when it exists.
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.

2 participants