fosrl / fosrl/pangolin

Feature: per-site-resource opt-out from advertising the destination as a client route (alias-address-only mode)

Open
#3,548 8 comments 0 reactions 1 assignee Claimed by @escooterclinic View on GitHub
Improvement
Dominant language
TypeScript
Stars
22.8k
Forks
783
Avg merge
1d 11h
Merged PRs (30d)
47

Description

### Summary

There is currently no way to tell Pangolin *"serve this site resource through its alias address, but do not put its destination in the client's route table."* For a deployment with many host/ssh site resources on a LAN the client already reaches another way, that missing knob is the difference between the tunnel being additive and the tunnel taking over the LAN.

I'd like a per-resource boolean — `advertiseDestination` (default `true`, i.e. today's behaviour) — honoured by `generateRemoteSubnets()`.

### Why the current behaviour is a problem

In `generateRemoteSubnets()` (`ee-1.19.4`):

```js
if (sr.mode === "host" || sr.mode === "ssh") {
const parseResult = ipSchema.safeParse(sr.destination);
return parseResult.success; // -> `${sr.destination}/32`
}
```

`buildSiteConfigurationForOlmClient()` hands the result to the client as `remoteSubnets`, and the client installs one route per prefix. So **every** host/ssh site resource with a bare-IP destination becomes a `/32` on the client, unconditionally.

In our case that is ~158 `/32` routes for LAN addresses. On macOS the route table selects by **longest prefix before metric**, so those `/32`s beat every less-specific route the machine already had for the same LAN — an SD-WAN route and a Tailscale route in our case. There is no client-side setting that reorders it, because there is nothing to reorder: a `/32` simply wins. The result is that connecting the client silently blackholes paths that worked a moment earlier, for hosts the tunnel was never meant to take over.

This is not about *reachability* — the resources work. It is that the client cannot express "I want this resource, but I already have a better path to its address."

### Why the existing workarounds don't cover it

- **`aliasAddress` already proves the concept.** `generateSubnetProxyTargetV2()` emits `destPrefix = /32, rewriteTo: `, which is a complete, collision-free path to the resource that does not require the destination to be routed at all. Notably, the `jitMode` branch of `buildSiteConfigurationForOlmClient()` already has `remoteSubnets` commented out and sends only `aliases` — so JIT connections *already* work purely off alias addresses. The requested flag is essentially "let a non-JIT client opt into that same shape, per resource."
- **A non-IP destination** (hostname) does skip the route, since `ipSchema.safeParse` fails — but that changes `rewriteTo` into a hostname and makes the data path depend on name resolution inside the site. That's a side effect of a validation check, not a supported switch.
- **`mode: "cidr"` aggregation** would collapse the routes, but CIDR resources can't carry the per-host access grants that the per-resource model provides. Trading the grant model to reduce route count isn't acceptable for us.
- **Client-side route withdrawal** works and is what we're shipping in the meantime, but it is a second writer racing the server's own config — exactly the kind of thing that turns into a churn loop. A supported server-side flag retires it.

### Proposed change

Add a nullable boolean column to `siteResources` (default `true`) and honour it in the filter:

```js
if (sr.mode === "host" || sr.mode === "ssh") {
if (sr.advertiseDestination === false) return false;
const parseResult = ipSchema.safeParse(sr.destination);
return parseResult.success;
}
```

`generateSubnetProxyTargetV2()` would keep emitting the alias target unchanged, so the resource stays fully reachable via `aliasAddress`. Defaulting to `true` makes it a no-op for every existing deployment.

Exposing it on the site-resource create/update API (and the UI checkbox, "Advertise destination as a client route") would be enough for our use; we drive resources through the Integration API.

### Environment

- Pangolin `ee-1.19.4`
- ~158 host/ssh site resources with bare-IP LAN destinations
- macOS client, with pre-existing SD-WAN and Tailscale routes covering the same LAN

Happy to put up a PR if the shape above is acceptable — the change looks small and additive, but I'd rather agree the column name and default first than guess.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.