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 {