Skip to content

feat: per-site-resource opt-out from advertising the destination as a client route - #3621

Open
escooterclinic wants to merge 2 commits into
fosrl:mainfrom
escooterclinic:feat/site-resource-advertise-destination
Open

escooterclinic wants to merge 2 commits into
fosrl:mainfrom
escooterclinic:feat/site-resource-advertise-destination

Conversation

@escooterclinic

Copy link
Copy Markdown
By creating this pull request, I grant the project maintainers an unlimited,
perpetual license to use, modify, and redistribute these contributions under any terms they
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
represent that I have the right to grant this license for all contributed content.

@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 a host or ssh resource with a bare-IP destination no longer becomes a /32 in the client's route table. The resource stays reachable through its alias, because generateSubnetProxyTargetV2() and generateAliasConfig() 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 set shouldUpdateTargets — the newt targets key off the alias address and don't change.
  • The "is this destination still in use by a sibling resource?" query now requires the sibling to be advertising, otherwise a non-advertising sibling blocks withdrawal of a route it isn't keeping alive.
  • Exposed on create/update, in blueprints as 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 --noEmit is clean under oss+sqlite, enterprise+pg and saas+pg, and prettier --check passes on every changed file.

The second commit adds unit tests to server/lib/ip.test.ts covering the default, the opt-out, the ssh case, and a row with the flag unset still advertising. Deleting the one-line filter from generateRemoteSubnets() makes them fail, so they're testing the thing they claim to.

That commit also adds import "@server/extendZod" to the top of ip.test.ts. On current main that file throws on import — ip.ts evaluates zod .openapi() schemas at module scope, so without the extension registered the existing findNextAvailableCidr tests 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 fieldset is gone from main, so the switch there no longer takes a disabled prop and now matches its siblings.

Two questions

It's based on main rather than dev, where #3567 sat, because dev is currently ~360 commits behind main and branching from it would have made an unreadable diff. Happy to rebase onto whichever you prefer.

The flag is only honoured for host and ssh, the modes that produce a /32. A cidr resource with advertise-destination: false in 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.

… 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.
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