Carrying a fork-local patch to canIngress() because upstream 0eb38dc2e breaks Funnel. This issue is the reminder to drop the patch once upstream fixes it.
What breaks
Funnel ingress is refused by the receiving node. Externally it looks like a TLS failure, because the ingress relay closes the client connection after the ClientHello:
Drop: TCP{[fd7a:115c:a1e0::a801:26ab]:32617 > [...]:37384} 80 no rules matched
peerapi: ingress: denied; no ingress cap from [fd7a:115c:a1e0::a801:26ab]:32617
Cause
Upstream 0eb38dc2e ("ipn,magicsock: deny peer capabilities to unsigned peers", 2026-07-22, tailscale/tailscale#20561) added srcIsUnsignedPeerLocked and made peerCapsLocked return nil for any peer with UnsignedPeerAPIOnly set.
Funnel ingress relays are exactly that class of node. From the doc comment on tailcfg.Node.UnsignedPeerAPIOnly:
UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA restrictions. However, in exchange for that privilege, it does not get network access. It can only access this node's peerapi, which may not let it do anything.
Both halves of that are visible in the log above: the relay's direct traffic is dropped by the packet filter (no rules matched), and its one sanctioned action — the peerapi ingress endpoint — is now refused too, because canIngress() is resolved purely through the capability map:
Note that canDebug() directly above it has its own explicit UnsignedPeerAPIOnly check. The pre-existing design was granular per handler; the new blanket denial at the capability layer catches Funnel as collateral damage.
This does not require Tailnet Lock to be enabled on the tailnet — the relay is delivered unsigned regardless — so it should affect any node running upstream main, not just this fork.
The patch
nodeBackend.peerCapsLocked is split, factoring the lookup into peerCapsIgnoringSignatureLocked. peerCapsLocked keeps the unsigned-peer denial for every existing caller.
New nodeBackend.PeerCapsIncludingUnsigned / LocalBackend.PeerCapsIncludingUnsigned skip only that denial.
canIngress() is the sole caller.
Every other capability keeps upstream's stricter behaviour; only the ingress cap on the peerapi ingress endpoint is exempted.
To do
Verify against upstream tailscaled on main to confirm it reproduces outside this fork
Report upstream if it reproduces and isn't already known
Revert this patch once upstream lands a fix, and check whether their fix conflicts with the split of peerCapsLocked
Carrying a fork-local patch to `canIngress()` because upstream `0eb38dc2e` breaks Funnel. **This issue is the reminder to drop the patch once upstream fixes it.**
## What breaks
Funnel ingress is refused by the receiving node. Externally it looks like a TLS failure, because the ingress relay closes the client connection after the ClientHello:
```
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (OUT), TLS alert, decode error (562):
* TLS connect error: error:0A000126:SSL routines::unexpected eof while reading
curl: (35) TLS connect error
```
The node logs the real reason:
```
Drop: TCP{[fd7a:115c:a1e0::a801:26ab]:32617 > [...]:37384} 80 no rules matched
peerapi: ingress: denied; no ingress cap from [fd7a:115c:a1e0::a801:26ab]:32617
```
## Cause
Upstream `0eb38dc2e` ("ipn,magicsock: deny peer capabilities to unsigned peers", 2026-07-22, tailscale/tailscale#20561) added `srcIsUnsignedPeerLocked` and made `peerCapsLocked` return `nil` for any peer with `UnsignedPeerAPIOnly` set.
Funnel ingress relays are exactly that class of node. From the doc comment on `tailcfg.Node.UnsignedPeerAPIOnly`:
> UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA restrictions. However, in exchange for that privilege, it does not get network access. It can only access this node's peerapi, which may not let it do anything.
Both halves of that are visible in the log above: the relay's direct traffic is dropped by the packet filter (`no rules matched`), and its one sanctioned action — the peerapi ingress endpoint — is now refused too, because `canIngress()` is resolved purely through the capability map:
```go
func (h *peerAPIHandler) canIngress() bool {
return h.peerHasCap(tailcfg.PeerCapabilityIngress) || (allowSelfIngress() && h.isSelf)
}
```
Note that `canDebug()` directly above it has its own explicit `UnsignedPeerAPIOnly` check. The pre-existing design was granular per handler; the new blanket denial at the capability layer catches Funnel as collateral damage.
This does not require Tailnet Lock to be enabled on the tailnet — the relay is delivered unsigned regardless — so it should affect any node running upstream main, not just this fork.
## The patch
- `nodeBackend.peerCapsLocked` is split, factoring the lookup into `peerCapsIgnoringSignatureLocked`. `peerCapsLocked` keeps the unsigned-peer denial for every existing caller.
- New `nodeBackend.PeerCapsIncludingUnsigned` / `LocalBackend.PeerCapsIncludingUnsigned` skip only that denial.
- `canIngress()` is the sole caller.
Every other capability keeps upstream's stricter behaviour; only the ingress cap on the peerapi ingress endpoint is exempted.
## To do
- [ ] Verify against upstream `tailscaled` on main to confirm it reproduces outside this fork
- [ ] Report upstream if it reproduces and isn't already known
- [ ] Revert this patch once upstream lands a fix, and check whether their fix conflicts with the split of `peerCapsLocked`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Carrying a fork-local patch to
canIngress()because upstream0eb38dc2ebreaks Funnel. This issue is the reminder to drop the patch once upstream fixes it.What breaks
Funnel ingress is refused by the receiving node. Externally it looks like a TLS failure, because the ingress relay closes the client connection after the ClientHello:
The node logs the real reason:
Cause
Upstream
0eb38dc2e("ipn,magicsock: deny peer capabilities to unsigned peers", 2026-07-22, tailscale/tailscale#20561) addedsrcIsUnsignedPeerLockedand madepeerCapsLockedreturnnilfor any peer withUnsignedPeerAPIOnlyset.Funnel ingress relays are exactly that class of node. From the doc comment on
tailcfg.Node.UnsignedPeerAPIOnly:Both halves of that are visible in the log above: the relay's direct traffic is dropped by the packet filter (
no rules matched), and its one sanctioned action — the peerapi ingress endpoint — is now refused too, becausecanIngress()is resolved purely through the capability map:Note that
canDebug()directly above it has its own explicitUnsignedPeerAPIOnlycheck. The pre-existing design was granular per handler; the new blanket denial at the capability layer catches Funnel as collateral damage.This does not require Tailnet Lock to be enabled on the tailnet — the relay is delivered unsigned regardless — so it should affect any node running upstream main, not just this fork.
The patch
nodeBackend.peerCapsLockedis split, factoring the lookup intopeerCapsIgnoringSignatureLocked.peerCapsLockedkeeps the unsigned-peer denial for every existing caller.nodeBackend.PeerCapsIncludingUnsigned/LocalBackend.PeerCapsIncludingUnsignedskip only that denial.canIngress()is the sole caller.Every other capability keeps upstream's stricter behaviour; only the ingress cap on the peerapi ingress endpoint is exempted.
To do
tailscaledon main to confirm it reproduces outside this forkpeerCapsLocked🤖 Generated with Claude Code