Skip to content

feat(tunnel): IPv6 GUA pinholes — PCP v6 + manual forwards (PR2)#3403

Open
helix-nine wants to merge 5 commits into
masterfrom
helix/tunnel-ipv6-pcp
Open

feat(tunnel): IPv6 GUA pinholes — PCP v6 + manual forwards (PR2)#3403
helix-nine wants to merge 5 commits into
masterfrom
helix/tunnel-ipv6-pcp

Conversation

@helix-nine

@helix-nine helix-nine commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

PR2 — IPv6 GUA pinholes (PCP v6 + unified v4+v6 forwards)

Follow-up to #3388 (per-subnet IPv6 addressing). Opens inbound to a client's
own IPv6 GUA on the tunnel — the piece that makes v6 hosting work end-to-end —
and unifies v4 forwarding and v6 pinholes behind one dialog.

What a "pinhole" is

A client already gets a /128 GUA on any IPv6-enabled subnet (#3388). A pinhole
accepts inbound to that GUA on a port — a pure ip6 startos firewall accept,
no NAT (the GUA is directly routable, and the OS-side reply-routing sends the
return out the arrival interface). When the external port differs from the
internal (e.g. an 80→443 redirect) it becomes a port-only DNAT on the same
GUA. Because each client has its own GUA, external ports only need to be unique
per-GUA, so two clients can both publish :443.

Two ways a pinhole is opened

  • Automatic (PCP): the tunnel runs a v6 PCP server on a wg-bound v6-only
    socket; handle6 forces the target to the sender's own GUA and honors the PCP
    Suggested External Port field, so a remap needs no protocol change. StartOS
    servers already request GUA pinholes; this PR also adds the OS-side auto
    80→443 redirect on v6 (mirroring the existing v4 redirect_maps).
  • Manual: a pinhole add|remove|set-enabled|update-label CLI/RPC surface,
    and — in the UI — the reworked forward dialog.

UI (unified v4 + v6)

The Add Port Forward dialog now has an IP Version radio (IPv4 / IPv6 /
IPv4 + IPv6); the external/internal port fields apply to whichever is chosen,
IPv6 is gated on the server having a GUA, and the also-80 checkbox opens the
redirect on each selected version. The forwards tables gained an IP column,
and toggle/delete/relabel route to the pinhole API for v6 rows.

Backend

  • db: Pinhole { label, enabled, count, internalPort: Option<u16>, auto } +
    pinholes6 map (serde(default), no migration).
  • net/port_map/server: handle6 + v6 GatewayBackend methods (default-off).
  • tunnel/forward/pinhole: is_known_gua (reverse host_v6), nft rules via
    nft_rule_v6, db persistence, startup seed.
  • tunnel/forward/pcp: v6 socket + serve loop, TunnelContext v6 impl.
  • net/net_controller: OS-side v6 gua_redirect_maps (auto 80→443).
  • api: manual CRUD (+ 5-locale i18n, regenerated manpage); ApiService
    methods (live + mock) + regenerated osBindings.
  • docs: port-forwarding.md, cli-reference.md, ipv6.md, CHANGELOG.

Testing

  • cargo check -p start-core clean; 25 tunnel + 6 pinhole/handle6 unit tests
    pass; check:tunnel and the full build:tunnel (Angular AOT) green; prettier
    clean.
  • Not automated: end-to-end v6 reachability needs a real routed global
    prefix (a local libvirt VM can't provide one) — worth a real-VPS pass before
    release, same caveat as feat(tunnel): IPv6 support — prefix delegation + PCP pinholes (WIP) #3388.

Open inbound to a client's own IPv6 GUA on the tunnel: PCP v6 MAP requests
(over a wg-bound v6 socket) and a manual pinhole CRUD surface. A pinhole is a
pure ip6 firewall accept (no NAT); external != internal (e.g. 80->443) becomes
a port-only DNAT on the same GUA. The PCP MAP's Suggested External Port carries
the remap, so no protocol change is needed.

- db: Pinhole {label,enabled,count,internalPort,auto} + pinholes6 map (serde
  default, no migration)
- net/port_map/server: handle6 + v6 GatewayBackend methods (default-off)
- tunnel/forward/pinhole: is_known_gua (host_v6 reverse), nft_rule_v6 rule
  build, db persistence, startup seed
- tunnel/forward/pcp: v6 socket + serve loop, TunnelContext v6 impl
- api: pinhole add/remove/update-label/set-enabled (+ i18n, manpage)
Add addPinhole/deletePinhole/updatePinholeLabel/setPinholeEnabled to the
tunnel ApiService (live + mock), seed a mock pinholes6 entry, and regenerate
the osBindings for the new Pinhole/Pinholes6/*PinholeParams types. The unified
v4+v6 forward dialog builds on this next.
Rework the port-forward dialog into one form covering both stacks (start-wrt
Published Ports pattern): an IP Version radio (IPv4 / IPv6 / IPv4 + IPv6), the
external/internal port fields applying to whichever is picked, GUA-gated for v6.
IPv6 saves a pinhole (or a port-DNAT when external != internal); the also-80
checkbox opens the 80->443 on each selected version. The forwards list now
merges port_forwards + pinholes6 with an IP column, and toggle/delete/relabel
route to the pinhole API for v6 rows. Devices carry their computed GUA.
Mirror the v4 redirect_maps for IPv6: when a GUA exposes 443, also ask the
gateway for an 80->443 redirect pinhole (withdrawn when 443 stops being exposed
or 80 becomes a real pinhole). Tracks candidate v6 gateways per GUA so the
post-loop redirect reaches the same gateways the pinholes used.
@helix-nine helix-nine marked this pull request as ready for review July 4, 2026 20:36
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.

1 participant