feat: per-site-resource opt-out from advertising the destination as a client route - #3621
Open
escooterclinic wants to merge 2 commits into
Open
escooterclinic wants to merge 2 commits into
escooterclinic wants to merge 2 commits into
Conversation
… client route Adds siteResources.advertiseDestination (boolean, default true). When it is false, generateRemoteSubnets() skips the resource, so a host/ssh resource with a bare-IP destination no longer becomes a /32 in the client's route table. The resource stays fully reachable through its aliasAddress, since generateSubnetProxyTargetV2() and generateAliasConfig() are untouched. Defaulting to true makes this a no-op for existing deployments. Motivation: on a client that already has a route to the destination LAN (an SD-WAN or Tailscale route, say), the per-resource /32 wins route selection on longest-prefix match before any metric is consulted, so the tunnel silently takes over paths it was never meant to own. Closes fosrl#3548. Also: - handleMessagingForUpdatedSiteResource() diffs the flag so toggling it pushes the route add/withdraw to connected clients. It deliberately does not set shouldUpdateTargets, since the newt targets key off the alias address and do not change. - The "is this destination still in use by a sibling resource" query now requires the sibling to advertise its destination, otherwise a non-advertising sibling would block withdrawal of a route it isn't keeping alive. - Exposed on create/update, blueprints ("advertise-destination"), and as a switch on the host and ssh settings pages plus the create wizard, shown under the same condition as the alias field.
Pins the four cases that matter: a host resource advertises a /32 by default, opting out withdraws it, the same holds for ssh, and a row without the flag set still advertises, which is what keeps existing deployments unchanged. Removing the one-line filter in generateRemoteSubnets makes the second case fail. server/lib/ip.test.ts also needed "@server/extendZod" importing before "./ip": ip.ts evaluates zod .openapi() schemas at module scope, so without the extension registered the file throws on import and the pre-existing findNextAvailableCidr tests never run either. That one line is unrelated to this feature and can be taken separately.
escooterclinic
requested review from
miloschwartz and
oschwartz10612
as code owners
August 20, 2026 09:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@AstralDestiny asked me to say whether AI was involved, so up front: yes. Claude (Claude Code) wrote the code, the commit messages and this description. I drove it and read every line, and the verification below is stuff that was actually run, not asserted.
This implements #3548. It replaces #3567, which closed on 13 Aug — not because I withdrew it, but because I deleted the fork it was branched from and GitHub closes a PR when its head repository goes away. Sorry for the silence that followed; nothing about the change was in doubt. The branch is rebased and re-pushed here.
What it changes
siteResources.advertiseDestination, boolean, not null, default true. When false,generateRemoteSubnets()skips the resource, so ahostorsshresource with a bare-IP destination no longer becomes a/32in the client's route table. The resource stays reachable through its alias, becausegenerateSubnetProxyTargetV2()andgenerateAliasConfig()are untouched. Defaulting to true makes this a no-op for existing deployments.The motivation, briefly: those
/32s win route selection by longest prefix before any metric is consulted, so a client that already reaches the destination LAN another way has that path pre-empted, including when the tunnel half-dies.Three things beyond the one-line filter, each separable if you'd rather not take them:
handleMessagingForUpdatedSiteResource()diffs the flag, so toggling it withdraws or adds the route on already-connected clients instead of waiting for a reconnect. It deliberately doesn't setshouldUpdateTargets— the newt targets key off the alias address and don't change.advertise-destination, and as a switch on the host and ssh settings pages and the create wizard, shown under the same condition as the alias field.No migration script — the convention looks like feature commits touch the schema files and the release migration collects the ALTERs, so I followed that. Tell me which version to name one after and I'll add it.
Verification
tsc --noEmitis clean under oss+sqlite, enterprise+pg and saas+pg, andprettier --checkpasses on every changed file.The second commit adds unit tests to
server/lib/ip.test.tscovering the default, the opt-out, the ssh case, and a row with the flag unset still advertising. Deleting the one-line filter fromgenerateRemoteSubnets()makes them fail, so they're testing the thing they claim to.That commit also adds
import "@server/extendZod"to the top ofip.test.ts. On currentmainthat file throws on import —ip.tsevaluates zod.openapi()schemas at module scope, so without the extension registered the existingfindNextAvailableCidrtests don't run either. It's unrelated to this feature and easy to take separately or drop.The limits, plainly: the runtime checks I reported on #3567 — the flag round-tripping through the API and dashboard, toggling pushing the change, re-sending the same value no-opping — were run against the older base and I have not re-run them since rebasing over ~360 commits. The static checks and the new tests are the evidence for this revision. One thing the rebase changed: the ssh page's license-gated
fieldsetis gone frommain, so the switch there no longer takes adisabledprop and now matches its siblings.Two questions
It's based on
mainrather thandev, where #3567 sat, becausedevis currently ~360 commits behindmainand branching from it would have made an unreadable diff. Happy to rebase onto whichever you prefer.The flag is only honoured for
hostandssh, the modes that produce a/32. Acidrresource withadvertise-destination: falsein a blueprint is silently ignored today. I kept the change to what #3548 described, but if you'd rather it applied there too, or was rejected outright, that's small and I'll do it.