Skip to content

fix: classify Tailscale CGNAT (100.64.0.0/10) as LAN, not public - #89

Closed
PhilipEriksson wants to merge 1 commit into
MiaAI-Lab:mainfrom
PhilipEriksson:fix/tailscale-cgnat-scope
Closed

PhilipEriksson wants to merge 1 commit into
MiaAI-Lab:mainfrom
PhilipEriksson:fix/tailscale-cgnat-scope

Conversation

@PhilipEriksson

Copy link
Copy Markdown

Problem

A spark whose lanIp is a Tailscale address (100.64.0.0/10) shows a red "Open · Public" (danger) LLM-posture badge, even though the endpoint is only reachable inside the tailnet.

classifyHostScope() in server/validate.js only recognizes loopback and the RFC1918 private ranges (10/8, 172.16/12, 192.168/16) plus link-local. Tailscale's CGNAT range is none of those, so any 100.x address falls through to the final return "public".

The posture badge is auth × scope:

  • auth = open (the LLM API answers unauthenticated — no key configured)
  • scope = public (the Tailscale IP misclassified)

open + publiclevel = "danger" → the red "Open · Public" badge. That's a false alarm: the 100.x address is not on the public internet, it's a private tailnet interface.

Why it shouldn't show as "public" on Tailscale addresses

Tailscale assigns every node a stable IP from 100.64.0.0/10 (RFC 6598 shared-address space, the same CGNAT block carriers use). Traffic to that IP is only routable by peers in the same tailnet — it is never exposed to the public internet. Classifying it as "public" misrepresents the exposure and triggers the danger badge for a setup that is, in practice, as private as a LAN.

Fix

Treat 100.64.0.0/10 as lan in classifyHostScope():

if (a === 100 && b >= 64 && b <= 127) return "lan"; // Tailscale CGNAT (100.64.0.0/10)

A Tailscale-hosted spark now reports "Open · LAN" (warn) instead of "Open · Public" (danger) — accurate, since the endpoint is unauthenticated but confined to the tailnet.

Note: the "Open" part is intentionally unchanged. It still correctly reflects that the LLM API accepts unauthenticated requests; the fix only corrects the scope (network reachability), not the auth posture. If you want the badge to drop below "warn", set an API key on the endpoint and it becomes "API key · LAN".

Tests

Added a unit test in server/collectors/__tests__/LlmProbe.posture.test.js covering the CGNAT block and its edges:

  • 100.64.0.1, 100.89.38.120, 100.127.255.254lan
  • 100.63.0.1, 100.128.0.1 (just outside the block) → public

All 13 tests pass (node --test server/collectors/__tests__/LlmProbe.posture.test.js).

Verified in production

Applied the same one-line change to a live sparkDash install (v1.8.5, node --watch auto-reload). A spark with lanIp: 100.89.38.120 (Tailscale) flipped from Open · Public (danger) to Open · LAN (warn) on the next probe cycle, confirmed via the live WebSocket snapshot.

classifyHostScope only recognized RFC1918 + loopback, so a spark whose
lanIp is a Tailscale 100.x address fell through to 'public'. Combined
with an unauthenticated LLM endpoint this produced a red 'Open · Public'
(danger) badge even though the address is only reachable inside the
tailnet. Treat 100.64.0.0/10 as LAN so the badge reflects reality
('Open · LAN', warn). Add unit tests for the CGNAT block and its edges.
@PhilipEriksson
PhilipEriksson marked this pull request as ready for review September 9, 2026 14:25
@PhilipEriksson

Copy link
Copy Markdown
Author

Hi @MikeGibbsOnyx @MiaAI-Lab — small nudge. One-line fix + tests: Tailscale 100.64.0.0/10 addresses classify as "public", producing a false "Open · Public" danger badge. Verified live — badge flips to "Open · LAN". Happy to adjust if you'd rather handle CGNAT differently.

@Lesilva

Lesilva commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

ACK — Tailscale CGNAT classified as LAN

Production Readiness: GO

classifyHostScope has exactly one consumer (LlmProbe's endpoint-exposure indicator), so this cannot move target validation or any auth decision — the blast radius is the posture chip.

The boundary is right: 100.64.0.0/10 is 100.64.0.0–100.127.255.255, the new guard sits ahead of the 169.254 and a >= 224 cases, and the test pins both edges (100.63 and 100.128 → public).

Suite on your branch: 317/317 + 23/23; merges clean against current main. Landing it in a batch merge — thank you. Non-blocking: 100.64/10 is also ISP carrier-grade NAT, so a genuinely public CGNAT'd address now reads as "lan" too; for a Tailscale-first fleet that is the useful default, and a line in the README's posture section would make it explicit.

— Wei

@Lesilva

Lesilva commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Landed via #107 (Batch B ACK merge onto main) as 8bbcbcb, tests included. Closing this one as superseded — thanks for the contribution!

@Lesilva Lesilva closed this Sep 17, 2026
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.

2 participants