From 15a70243edefd70f73804c165119b443e63be363 Mon Sep 17 00:00:00 2001 From: Codinget Date: Tue, 28 Jul 2026 20:06:31 +0000 Subject: [PATCH] fix(ipnlocal): allow Funnel ingress from unsigned peer relays Upstream 0eb38dc2e denies peer capabilities to any peer with UnsignedPeerAPIOnly set. Funnel ingress relays are precisely that: per the docs on tailcfg.Node.UnsignedPeerAPIOnly they get no network access and exist only to reach this node's peerapi. Since canIngress() resolves PeerCapabilityIngress through the capability map, the relay can no longer reach the one endpoint it is allowed to use, and Funnel connections are refused after the client's ClientHello. Split peerCapsLocked so the unsigned-peer denial can be skipped for the ingress path alone. Every other caller keeps upstream's stricter behaviour. Fork-local patch, tracked in webnet/tailscale#16 for reverting once upstream restores Funnel. Co-Authored-By: Claude Opus 5 --- ipn/ipnlocal/local.go | 7 +++++++ ipn/ipnlocal/node_backend.go | 22 ++++++++++++++++++++++ ipn/ipnlocal/peerapi.go | 8 +++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 512c5884c..e5c2d8e65 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -1763,6 +1763,13 @@ func (b *LocalBackend) PeerCaps(src netip.Addr) tailcfg.PeerCapMap { return b.currentNode().PeerCaps(src) } +// PeerCapsIncludingUnsigned is like [LocalBackend.PeerCaps] but does not deny +// capabilities to peers with UnsignedPeerAPIOnly set. It exists only for the +// Funnel ingress path; see [nodeBackend.PeerCapsIncludingUnsigned]. +func (b *LocalBackend) PeerCapsIncludingUnsigned(src netip.Addr) tailcfg.PeerCapMap { + return b.currentNode().PeerCapsIncludingUnsigned(src) +} + // PeerCapsForIP returns the capabilities that remote src IP has when // talking to the given destination IP on this node. func (b *LocalBackend) PeerCapsForIP(src, dst netip.Addr) tailcfg.PeerCapMap { diff --git a/ipn/ipnlocal/node_backend.go b/ipn/ipnlocal/node_backend.go index 28c352766..ada72f80c 100644 --- a/ipn/ipnlocal/node_backend.go +++ b/ipn/ipnlocal/node_backend.go @@ -432,10 +432,32 @@ func (nb *nodeBackend) srcIsUnsignedPeerLocked(src netip.Addr) bool { return ok && n.UnsignedPeerAPIOnly() } +// PeerCapsIncludingUnsigned is like [nodeBackend.PeerCaps] but does not deny +// capabilities to peers with UnsignedPeerAPIOnly set. +// +// Funnel ingress relays are delivered as UnsignedPeerAPIOnly nodes: per the +// docs on [tailcfg.Node.UnsignedPeerAPIOnly] they get no network access at all +// and exist solely to reach this node's peerapi. The ingress endpoint they need +// is gated on [tailcfg.PeerCapabilityIngress], so denying them capabilities +// wholesale — as peerCapsLocked does upstream as of 0eb38dc2e — makes Funnel +// impossible. Callers must therefore be limited to the ingress path. +// +// This is a fork-local patch; drop it once upstream restores Funnel. +// See webnet/tailscale#16. +func (nb *nodeBackend) PeerCapsIncludingUnsigned(src netip.Addr) tailcfg.PeerCapMap { + nb.mu.Lock() + defer nb.mu.Unlock() + return nb.peerCapsIgnoringSignatureLocked(src) +} + func (nb *nodeBackend) peerCapsLocked(src netip.Addr) tailcfg.PeerCapMap { if nb.srcIsUnsignedPeerLocked(src) { return nil } + return nb.peerCapsIgnoringSignatureLocked(src) +} + +func (nb *nodeBackend) peerCapsIgnoringSignatureLocked(src netip.Addr) tailcfg.PeerCapMap { if nb.netMap == nil { return nil } diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 53d1e6d35..f1f1999a6 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -592,8 +592,14 @@ func (h *peerAPIHandler) canDebug() bool { var allowSelfIngress = envknob.RegisterBool("TS_ALLOW_SELF_INGRESS") // canIngress reports whether h can send ingress requests to this node. +// +// The ingress cap is resolved without the unsigned-peer denial that +// [nodeBackend.PeerCaps] applies, because Funnel ingress relays are by design +// UnsignedPeerAPIOnly nodes whose only permitted action is this endpoint. +// See [nodeBackend.PeerCapsIncludingUnsigned]. func (h *peerAPIHandler) canIngress() bool { - return h.peerHasCap(tailcfg.PeerCapabilityIngress) || (allowSelfIngress() && h.isSelf) + caps := h.ps.b.PeerCapsIncludingUnsigned(h.remoteAddr.Addr()) + return caps.HasCapability(tailcfg.PeerCapabilityIngress) || (allowSelfIngress() && h.isSelf) } func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {