Skip to content

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

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

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.

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.

1 participant