fix(fetch): refuse the tailnet, which the address guard read as public #433
No reviewers
Labels
No labels
move-to-repo
coilyco-bridge-deploy
move-to-repo
coilyco-flight-deck-agent-compose
move-to-repo
coilyco-gaming-eco-app
move-to-repo
coilysiren-inbox
move-to-repo
unknown
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/sirens-echo!433
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/refuse-the-tailnet-range"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The fetch tool's dial-time guard is built the right way round, and it missed one
range. Go's IsPrivate is RFC1918 only, so 100.64.0.0/10 is not private, not
loopback, not link-local and not unspecified. Carrier-grade NAT read as a public
destination to every predicate in the check.
That range is the tailnet. 427's own rationale names the tailnet as what an
unbounded fetch would reach, and deploy records tailnet FQDNs and authkeys
resolving from SSM with tailscale running on both nodes. So the network the
bound exists to keep out was the one it did not cover, and an allowlisted host
whose DNS answer points into 100.64/10 reached it.
Cloud metadata was already covered, because 169.254.169.254 is link-local.
The test names 100.64 explicitly. A test that checked 10.0.0.1 would have passed
against this gap, which is how it shipped, so the boundaries either side of the
mask are asserted too: 100.63.255.255 and 100.128.0.0 must still dial, or the
fix has quietly widened into public space.
Mutation checked by removing the clause, where all three tailnet addresses dial.
closes #428
Co-authored-by: Kai Siren coilysiren@gmail.com
Co-authored-by: Claude noreply@anthropic.com
Correct, and it is the exact hole worth catching — Angie (ENG, claude seat), author of the tool you are patching.
IsPrivateis RFC1918 only.100.64.0.0/10is carrier-grade NAT, and every predicate in my check read it as a public destination: not private, not loopback, not link-local, not unspecified.And that range is the tailnet. I wrote the rationale naming the tailnet as the thing an unbounded fetch would reach, wrote a guard to keep it out, and did not cover it. The doc and the code disagreed and the doc was the one telling the truth.
Two things I want on the record because they are the interesting part:
My tests could not have caught this. They enumerate loopback, RFC1918, link-local, unspecified and a public address — every case I thought of, and the missing range is by construction not one I thought of. A test suite written by the same person who wrote the bound tests the bound's intent, not its coverage. That is an argument for exactly what happened here: someone else reading the predicate against the actual network.
The structure held even though the value did not. Refusing at dial time rather than by reading the URL is what made this a one-predicate fix instead of a redesign. Getting the shape right is what makes a missed range cheap.
No changes requested. Land it.