Tie DisableRoutesAndAliases to exit nodes being in or not

This commit is contained in:
Owen
2026-09-28 15:08:23 -04:00
parent 8d58df58e6
commit 29d1d91ebb
8 changed files with 448 additions and 170 deletions
+19 -19
View File
@@ -19,25 +19,25 @@ import (
// non-empty, and is how olm later matches server-pushed gateway updates to the // non-empty, and is how olm later matches server-pushed gateway updates to the
// resource the user actually connected through. // resource the user actually connected through.
type ConnectionRequest struct { type ConnectionRequest struct {
ID string `json:"id"` ID string `json:"id"`
Secret string `json:"secret"` Secret string `json:"secret"`
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
UserToken string `json:"userToken,omitempty"` UserToken string `json:"userToken,omitempty"`
MTU int `json:"mtu,omitempty"` MTU int `json:"mtu,omitempty"`
DNS string `json:"dns,omitempty"` DNS string `json:"dns,omitempty"`
DNSProxyIP string `json:"dnsProxyIP,omitempty"` DNSProxyIP string `json:"dnsProxyIP,omitempty"`
UpstreamDNS []string `json:"upstreamDNS,omitempty"` UpstreamDNS []string `json:"upstreamDNS,omitempty"`
InterfaceName string `json:"interfaceName,omitempty"` InterfaceName string `json:"interfaceName,omitempty"`
Holepunch bool `json:"holepunch,omitempty"` Holepunch bool `json:"holepunch,omitempty"`
TlsClientCert string `json:"tlsClientCert,omitempty"` TlsClientCert string `json:"tlsClientCert,omitempty"`
PingInterval string `json:"pingInterval,omitempty"` PingInterval string `json:"pingInterval,omitempty"`
PingTimeout string `json:"pingTimeout,omitempty"` PingTimeout string `json:"pingTimeout,omitempty"`
OrgID string `json:"orgId,omitempty"` OrgID string `json:"orgId,omitempty"`
MatchDomains []string `json:"matchDomains,omitempty"` MatchDomains []string `json:"matchDomains,omitempty"`
SubnetRouter bool `json:"subnetRouter,omitempty"` SubnetRouter bool `json:"subnetRouter,omitempty"`
DisableRoutesAndAliases bool `json:"disableRoutesAndAliases,omitempty"` DisableRoutesAndAliasesOnExitNode bool `json:"disableRoutesAndAliasesOnExitNode,omitempty"`
GatewaySiteResourceId int `json:"gatewaySiteResourceId,omitempty"` GatewaySiteResourceId int `json:"gatewaySiteResourceId,omitempty"`
GatewaySiteIds []int `json:"gatewaySiteIds,omitempty"` GatewaySiteIds []int `json:"gatewaySiteIds,omitempty"`
} }
// SwitchOrgRequest defines the structure for switching organizations // SwitchOrgRequest defines the structure for switching organizations
+41 -41
View File
@@ -47,14 +47,14 @@ type OlmConfig struct {
PingTimeout string `json:"pingTimeout"` PingTimeout string `json:"pingTimeout"`
// Advanced // Advanced
DisableHolepunch bool `json:"disableHolepunch"` DisableHolepunch bool `json:"disableHolepunch"`
TlsClientCert string `json:"tlsClientCert"` TlsClientCert string `json:"tlsClientCert"`
OverrideDNS bool `json:"overrideDNS"` OverrideDNS bool `json:"overrideDNS"`
TunnelDNS bool `json:"tunnelDNS"` TunnelDNS bool `json:"tunnelDNS"`
DisableRelay bool `json:"disableRelay"` DisableRelay bool `json:"disableRelay"`
PreferLocalRoutes bool `json:"preferLocalRoutes"` PreferLocalRoutes bool `json:"preferLocalRoutes"`
SubnetRouter bool `json:"subnetRouter"` SubnetRouter bool `json:"subnetRouter"`
DisableRoutesAndAliases bool `json:"disableRoutesAndAliases"` DisableRoutesAndAliasesOnExitNode bool `json:"disableRoutesAndAliasesOnExitNode"`
// DoNotCreateNewClient bool `json:"doNotCreateNewClient"` // DoNotCreateNewClient bool `json:"doNotCreateNewClient"`
// Parsed values (not in JSON) // Parsed values (not in JSON)
@@ -123,7 +123,7 @@ func DefaultConfig() *OlmConfig {
config.sources["disableRelay"] = string(SourceDefault) config.sources["disableRelay"] = string(SourceDefault)
config.sources["preferLocalRoutes"] = string(SourceDefault) config.sources["preferLocalRoutes"] = string(SourceDefault)
config.sources["subnetRouter"] = string(SourceDefault) config.sources["subnetRouter"] = string(SourceDefault)
config.sources["disableRoutesAndAliases"] = string(SourceDefault) config.sources["disableRoutesAndAliasesOnExitNode"] = string(SourceDefault)
// config.sources["doNotCreateNewClient"] = string(SourceDefault) // config.sources["doNotCreateNewClient"] = string(SourceDefault)
return config return config
@@ -300,8 +300,8 @@ func loadConfigFromEnv(config *OlmConfig) {
config.sources["subnetRouter"] = string(SourceEnv) config.sources["subnetRouter"] = string(SourceEnv)
} }
if val := os.Getenv("DISABLE_ROUTES_AND_ALIASES"); val == "true" { if val := os.Getenv("DISABLE_ROUTES_AND_ALIASES"); val == "true" {
config.DisableRoutesAndAliases = true config.DisableRoutesAndAliasesOnExitNode = true
config.sources["disableRoutesAndAliases"] = string(SourceEnv) config.sources["disableRoutesAndAliasesOnExitNode"] = string(SourceEnv)
} }
// if val := os.Getenv("DO_NOT_CREATE_NEW_CLIENT"); val == "true" { // if val := os.Getenv("DO_NOT_CREATE_NEW_CLIENT"); val == "true" {
// config.DoNotCreateNewClient = true // config.DoNotCreateNewClient = true
@@ -315,29 +315,29 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
// Store original values to detect changes // Store original values to detect changes
origValues := map[string]interface{}{ origValues := map[string]interface{}{
"endpoint": config.Endpoint, "endpoint": config.Endpoint,
"id": config.ID, "id": config.ID,
"secret": config.Secret, "secret": config.Secret,
"org": config.OrgID, "org": config.OrgID,
"userToken": config.UserToken, "userToken": config.UserToken,
"mtu": config.MTU, "mtu": config.MTU,
"dns": config.DNS, "dns": config.DNS,
"upstreamDNS": fmt.Sprintf("%v", config.UpstreamDNS), "upstreamDNS": fmt.Sprintf("%v", config.UpstreamDNS),
"matchDomains": fmt.Sprintf("%v", config.MatchDomains), "matchDomains": fmt.Sprintf("%v", config.MatchDomains),
"logLevel": config.LogLevel, "logLevel": config.LogLevel,
"interface": config.InterfaceName, "interface": config.InterfaceName,
"httpAddr": config.HTTPAddr, "httpAddr": config.HTTPAddr,
"socketPath": config.SocketPath, "socketPath": config.SocketPath,
"pingInterval": config.PingInterval, "pingInterval": config.PingInterval,
"pingTimeout": config.PingTimeout, "pingTimeout": config.PingTimeout,
"enableApi": config.EnableAPI, "enableApi": config.EnableAPI,
"disableHolepunch": config.DisableHolepunch, "disableHolepunch": config.DisableHolepunch,
"overrideDNS": config.OverrideDNS, "overrideDNS": config.OverrideDNS,
"disableRelay": config.DisableRelay, "disableRelay": config.DisableRelay,
"preferLocalRoutes": config.PreferLocalRoutes, "preferLocalRoutes": config.PreferLocalRoutes,
"tunnelDNS": config.TunnelDNS, "tunnelDNS": config.TunnelDNS,
"subnetRouter": config.SubnetRouter, "subnetRouter": config.SubnetRouter,
"disableRoutesAndAliases": config.DisableRoutesAndAliases, "disableRoutesAndAliasesOnExitNode": config.DisableRoutesAndAliasesOnExitNode,
// "doNotCreateNewClient": config.DoNotCreateNewClient, // "doNotCreateNewClient": config.DoNotCreateNewClient,
} }
@@ -366,7 +366,7 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
serviceFlags.BoolVar(&config.PreferLocalRoutes, "prefer-local-routes", config.PreferLocalRoutes, "Add tunnel routes with a high metric so overlapping local/connected routes take precedence (default false)") serviceFlags.BoolVar(&config.PreferLocalRoutes, "prefer-local-routes", config.PreferLocalRoutes, "Add tunnel routes with a high metric so overlapping local/connected routes take precedence (default false)")
serviceFlags.BoolVar(&config.TunnelDNS, "tunnel-dns", config.TunnelDNS, "When enabled, DNS queries are routed through the tunnel for remote resolution. To ensure queries are tunneled correctly, you must define the DNS server as a Pangolin resource and enter its address as an Upstream DNS Server. (default false)") serviceFlags.BoolVar(&config.TunnelDNS, "tunnel-dns", config.TunnelDNS, "When enabled, DNS queries are routed through the tunnel for remote resolution. To ensure queries are tunneled correctly, you must define the DNS server as a Pangolin resource and enter its address as an Upstream DNS Server. (default false)")
serviceFlags.BoolVar(&config.SubnetRouter, "subnet-router", config.SubnetRouter, "Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false)") serviceFlags.BoolVar(&config.SubnetRouter, "subnet-router", config.SubnetRouter, "Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false)")
serviceFlags.BoolVar(&config.DisableRoutesAndAliases, "disable-routes-and-aliases", config.DisableRoutesAndAliases, "Do not add routes to the system routing table and do not run the DNS proxy (aliases are not resolved). Gateway routes are still added. (default false)") serviceFlags.BoolVar(&config.DisableRoutesAndAliasesOnExitNode, "disable-routes-and-aliases", config.DisableRoutesAndAliasesOnExitNode, "Make the exit node take precedence over individual resources: while an exit node is connected, remove routes/aliases for site resources, restoring them once it disconnects. Gateway routes are still added. (default false)")
// serviceFlags.BoolVar(&config.DoNotCreateNewClient, "do-not-create-new-client", config.DoNotCreateNewClient, "Do not create new client") // serviceFlags.BoolVar(&config.DoNotCreateNewClient, "do-not-create-new-client", config.DoNotCreateNewClient, "Do not create new client")
version := serviceFlags.Bool("version", false, "Print the version") version := serviceFlags.Bool("version", false, "Print the version")
@@ -459,8 +459,8 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
if config.SubnetRouter != origValues["subnetRouter"].(bool) { if config.SubnetRouter != origValues["subnetRouter"].(bool) {
config.sources["subnetRouter"] = string(SourceCLI) config.sources["subnetRouter"] = string(SourceCLI)
} }
if config.DisableRoutesAndAliases != origValues["disableRoutesAndAliases"].(bool) { if config.DisableRoutesAndAliasesOnExitNode != origValues["disableRoutesAndAliasesOnExitNode"].(bool) {
config.sources["disableRoutesAndAliases"] = string(SourceCLI) config.sources["disableRoutesAndAliasesOnExitNode"] = string(SourceCLI)
} }
// if config.DoNotCreateNewClient != origValues["doNotCreateNewClient"].(bool) { // if config.DoNotCreateNewClient != origValues["doNotCreateNewClient"].(bool) {
// config.sources["doNotCreateNewClient"] = string(SourceCLI) // config.sources["doNotCreateNewClient"] = string(SourceCLI)
@@ -598,9 +598,9 @@ func mergeConfigs(dest, src *OlmConfig) {
dest.SubnetRouter = src.SubnetRouter dest.SubnetRouter = src.SubnetRouter
dest.sources["subnetRouter"] = string(SourceFile) dest.sources["subnetRouter"] = string(SourceFile)
} }
if src.DisableRoutesAndAliases { if src.DisableRoutesAndAliasesOnExitNode {
dest.DisableRoutesAndAliases = src.DisableRoutesAndAliases dest.DisableRoutesAndAliasesOnExitNode = src.DisableRoutesAndAliasesOnExitNode
dest.sources["disableRoutesAndAliases"] = string(SourceFile) dest.sources["disableRoutesAndAliasesOnExitNode"] = string(SourceFile)
} }
// if src.DoNotCreateNewClient { // if src.DoNotCreateNewClient {
// dest.DoNotCreateNewClient = src.DoNotCreateNewClient // dest.DoNotCreateNewClient = src.DoNotCreateNewClient
@@ -696,7 +696,7 @@ func (c *OlmConfig) ShowConfig() {
fmt.Printf(" disable-relay = %v [%s]\n", c.DisableRelay, getSource("disableRelay")) fmt.Printf(" disable-relay = %v [%s]\n", c.DisableRelay, getSource("disableRelay"))
fmt.Printf(" prefer-local-routes = %v [%s]\n", c.PreferLocalRoutes, getSource("preferLocalRoutes")) fmt.Printf(" prefer-local-routes = %v [%s]\n", c.PreferLocalRoutes, getSource("preferLocalRoutes"))
fmt.Printf(" subnet-router = %v [%s]\n", c.SubnetRouter, getSource("subnetRouter")) fmt.Printf(" subnet-router = %v [%s]\n", c.SubnetRouter, getSource("subnetRouter"))
fmt.Printf(" disable-routes-and-aliases = %v [%s]\n", c.DisableRoutesAndAliases, getSource("disableRoutesAndAliases")) fmt.Printf(" disable-routes-and-aliases = %v [%s]\n", c.DisableRoutesAndAliasesOnExitNode, getSource("disableRoutesAndAliasesOnExitNode"))
// fmt.Printf(" do-not-create-new-client = %v [%s]\n", c.DoNotCreateNewClient, getSource("doNotCreateNewClient")) // fmt.Printf(" do-not-create-new-client = %v [%s]\n", c.DoNotCreateNewClient, getSource("doNotCreateNewClient"))
if c.TlsClientCert != "" { if c.TlsClientCert != "" {
fmt.Printf(" tls-cert = %s [%s]\n", c.TlsClientCert, getSource("tlsClientCert")) fmt.Printf(" tls-cert = %s [%s]\n", c.TlsClientCert, getSource("tlsClientCert"))
+20 -20
View File
@@ -256,26 +256,26 @@ func runOlmMainWithArgs(ctx context.Context, cancel context.CancelFunc, signalCt
if config.ID != "" && config.Secret != "" && config.Endpoint != "" { if config.ID != "" && config.Secret != "" && config.Endpoint != "" {
tunnelConfig := olmpkg.TunnelConfig{ tunnelConfig := olmpkg.TunnelConfig{
Endpoint: config.Endpoint, Endpoint: config.Endpoint,
ID: config.ID, ID: config.ID,
Secret: config.Secret, Secret: config.Secret,
UserToken: config.UserToken, UserToken: config.UserToken,
MTU: config.MTU, MTU: config.MTU,
DNS: config.DNS, DNS: config.DNS,
UpstreamDNS: config.UpstreamDNS, UpstreamDNS: config.UpstreamDNS,
MatchDomains: config.MatchDomains, MatchDomains: config.MatchDomains,
InterfaceName: config.InterfaceName, InterfaceName: config.InterfaceName,
Holepunch: !config.DisableHolepunch, Holepunch: !config.DisableHolepunch,
TlsClientCert: config.TlsClientCert, TlsClientCert: config.TlsClientCert,
PingIntervalDuration: config.PingIntervalDuration, PingIntervalDuration: config.PingIntervalDuration,
PingTimeoutDuration: config.PingTimeoutDuration, PingTimeoutDuration: config.PingTimeoutDuration,
OrgID: config.OrgID, OrgID: config.OrgID,
OverrideDNS: config.OverrideDNS, OverrideDNS: config.OverrideDNS,
DisableRelay: config.DisableRelay, DisableRelay: config.DisableRelay,
PreferLocalRoutes: config.PreferLocalRoutes, PreferLocalRoutes: config.PreferLocalRoutes,
SubnetRouter: config.SubnetRouter, SubnetRouter: config.SubnetRouter,
DisableRoutesAndAliases: config.DisableRoutesAndAliases, DisableRoutesAndAliasesOnExitNode: config.DisableRoutesAndAliasesOnExitNode,
EnableUAPI: true, EnableUAPI: true,
} }
go olm.StartTunnel(tunnelConfig) go olm.StartTunnel(tunnelConfig)
} else { } else {
+34 -27
View File
@@ -181,17 +181,15 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
logger.Warn("Failed to parse tunnel IP %q: %v", interfaceIP, err) logger.Warn("Failed to parse tunnel IP %q: %v", interfaceIP, err)
} }
// Create the DNS proxy, unless routes/aliases are disabled - the proxy is // The DNS proxy is always created - it resolves both exit node aliases
// what resolves aliases, and the utility subnet it lives on is only // (unaffected by DisableRoutesAndAliasesOnExitNode - see connectExitNode)
// reachable through a system route we wouldn't add. o.dnsProxy stays nil // and site resource aliases, which the peer manager created below adds
// in that case; everything that uses it is nil-checked. // and removes dynamically as an exit node connects/disconnects (see
if o.tunnelConfig.DisableRoutesAndAliases { // PeerManager.SetExitNode/ClearExitNode). o.dnsProxy is still nil-checked
logger.Info("Routes and aliases disabled: not adding system routes and not starting the DNS proxy (gateway routes are unaffected)") // everywhere it's used, in case creation itself fails.
} else { o.dnsProxy, err = dns.NewDNSProxy(o.middleDev, o.tunnelConfig.MTU, wgData.UtilitySubnet, o.tunnelConfig.UpstreamDNS, o.tunnelConfig.TunnelDNS, interfaceIP, o.tunnelConfig.MatchDomains, o.tunnelConfig.PublicDNS)
o.dnsProxy, err = dns.NewDNSProxy(o.middleDev, o.tunnelConfig.MTU, wgData.UtilitySubnet, o.tunnelConfig.UpstreamDNS, o.tunnelConfig.TunnelDNS, interfaceIP, o.tunnelConfig.MatchDomains, o.tunnelConfig.PublicDNS) if err != nil {
if err != nil { logger.Error("Failed to create DNS proxy: %v", err)
logger.Error("Failed to create DNS proxy: %v", err)
}
} }
// Tell the system DNS monitor to exclude the proxy IP so that subsequent // Tell the system DNS monitor to exclude the proxy IP so that subsequent
@@ -214,25 +212,34 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
} }
} }
if !o.tunnelConfig.DisableRoutesAndAliases { // The utility subnet route is what makes the DNS proxy (always created
if err := network.AddRoutesWithSource([]string{wgData.UtilitySubnet}, o.tunnelConfig.InterfaceName, interfaceIP); err != nil { // also route the utility subnet // above) reachable at all, so it's always added too, independent of
logger.Error("Failed to add route for utility subnet: %v", err) // DisableRoutesAndAliasesOnExitNode - which only gates routes/aliases for
} // individual site resources, applied dynamically below as the peer
// manager is told about the exit node's connection state.
if err := network.AddRoutesWithSource([]string{wgData.UtilitySubnet}, o.tunnelConfig.InterfaceName, interfaceIP); err != nil { // also route the utility subnet
logger.Error("Failed to add route for utility subnet: %v", err)
} }
// Create peer manager with integrated peer monitoring // Create peer manager with integrated peer monitoring. If
// DisableRoutesAndAliasesOnExitNode is enabled, resource routes/aliases
// are suppressed reactively once an exit node (ExitNodeConfig peer or
// gateway mode) actually activates below/later - see
// PeerManager.SetExitNode/SetGateway - rather than being pre-seeded here,
// so a failed initial exit-node/gateway setup can never leave routes
// stuck suppressed with nothing active to justify it.
o.peerManager = peers.NewPeerManager(peers.PeerManagerConfig{ o.peerManager = peers.NewPeerManager(peers.PeerManagerConfig{
Device: o.dev, Device: o.dev,
DNSProxy: o.dnsProxy, DNSProxy: o.dnsProxy,
InterfaceName: o.tunnelConfig.InterfaceName, InterfaceName: o.tunnelConfig.InterfaceName,
PrivateKey: o.privateKey, PrivateKey: o.privateKey,
MiddleDev: o.middleDev, MiddleDev: o.middleDev,
LocalIP: interfaceIP, LocalIP: interfaceIP,
SharedBind: o.sharedBind, SharedBind: o.sharedBind,
WSClient: o.websocket, WSClient: o.websocket,
APIServer: o.apiServer, APIServer: o.apiServer,
PublicDNS: o.tunnelConfig.PublicDNS, PublicDNS: o.tunnelConfig.PublicDNS,
DisableRoutes: o.tunnelConfig.DisableRoutesAndAliases, DisableRoutesAndAliasesOnExitNode: o.tunnelConfig.DisableRoutesAndAliasesOnExitNode,
}) })
for i := range wgData.Sites { for i := range wgData.Sites {
+16 -16
View File
@@ -301,22 +301,22 @@ func (o *Olm) registerAPICallbacks() {
logger.Info("Received connection request via HTTP: id=%s, endpoint=%s", req.ID, req.Endpoint) logger.Info("Received connection request via HTTP: id=%s, endpoint=%s", req.ID, req.Endpoint)
tunnelConfig := TunnelConfig{ tunnelConfig := TunnelConfig{
Endpoint: req.Endpoint, Endpoint: req.Endpoint,
ID: req.ID, ID: req.ID,
Secret: req.Secret, Secret: req.Secret,
UserToken: req.UserToken, UserToken: req.UserToken,
MTU: req.MTU, MTU: req.MTU,
DNS: req.DNS, DNS: req.DNS,
UpstreamDNS: req.UpstreamDNS, UpstreamDNS: req.UpstreamDNS,
InterfaceName: req.InterfaceName, InterfaceName: req.InterfaceName,
Holepunch: req.Holepunch, Holepunch: req.Holepunch,
TlsClientCert: req.TlsClientCert, TlsClientCert: req.TlsClientCert,
OrgID: req.OrgID, OrgID: req.OrgID,
MatchDomains: req.MatchDomains, MatchDomains: req.MatchDomains,
SubnetRouter: req.SubnetRouter, SubnetRouter: req.SubnetRouter,
DisableRoutesAndAliases: req.DisableRoutesAndAliases, DisableRoutesAndAliasesOnExitNode: req.DisableRoutesAndAliasesOnExitNode,
GatewaySiteIds: req.GatewaySiteIds, GatewaySiteIds: req.GatewaySiteIds,
GatewaySiteResourceId: req.GatewaySiteResourceId, GatewaySiteResourceId: req.GatewaySiteResourceId,
} }
var err error var err error
+14 -6
View File
@@ -182,15 +182,23 @@ type TunnelConfig struct {
// LAN address it originally arrived with. Linux only. Defaults to false. // LAN address it originally arrived with. Linux only. Defaults to false.
SubnetRouter bool SubnetRouter bool
// DisableRoutesAndAliases, when enabled, stops olm from adding routes to // DisableRoutesAndAliasesOnExitNode, when enabled, makes the exit node
// the host's routing table (server IPs, remote subnets, the utility // take precedence over individual resources: for as long as an exit node
// subnet) and from launching the DNS proxy, so aliases are not resolved // is connected, olm removes routes to the host's routing table for site
// either. WireGuard AllowedIPs are still configured, so the tunnel can // resources (server IPs, remote subnets) and their alias DNS records, and
// restores them the moment the exit node disconnects. If the tunnel
// starts with an exit node already selected, these routes/aliases are
// never added in the first place. The exit node's own routes and aliases
// are unaffected, and it can connect/disconnect at any time - via the
// initial connect, a server push (olm/wg/exitnode/connect|disconnect), or
// the local API - all of which converge through the same connect/
// disconnect path (see PeerManager.SetExitNode/ClearExitNode).
// WireGuard AllowedIPs are still configured throughout, so the tunnel can
// still be used by anything that reaches it without the OS routing table // still be used by anything that reaches it without the OS routing table
// (e.g. a file descriptor/netstack consumer). Gateway routes (the // (e.g. a file descriptor/netstack consumer). Gateway routes (the
// default-route-equivalent and its endpoint bypass routes) are unaffected // default-route-equivalent and its endpoint bypass routes) are unaffected
// and are still installed. Defaults to false. // and are always installed. Defaults to false.
DisableRoutesAndAliases bool DisableRoutesAndAliasesOnExitNode bool
// GatewaySiteIds, when non-empty, designates these site IDs as gateway // GatewaySiteIds, when non-empty, designates these site IDs as gateway
// (full-tunnel/default-route) candidates from the moment the tunnel // (full-tunnel/default-route) candidates from the moment the tunnel
+106
View File
@@ -0,0 +1,106 @@
package peers
import "testing"
// newExitNodeTestManager returns a PeerManager for exercising the
// DisableRoutesAndAliasesOnExitNode state machine (resourceRoutesSuppressed,
// exitNodeActive, gatewayActive) without touching the OS routing table, a
// WireGuard device, or a DNS proxy - safe as long as pm.peers stays empty,
// same constraint as newGatewayTestManager in gateway_test.go. Unlike
// NewPeerManager, disableRoutesAndAliasesOnExitNode is the only state seeded
// here; exitNodeActive/gatewayActive/resourceRoutesSuppressed always start
// false, matching NewPeerManager's real (purely reactive, no pre-seeding)
// behavior.
func newExitNodeTestManager(disableRoutesAndAliasesOnExitNode bool) *PeerManager {
return &PeerManager{
peers: make(map[int]SiteConfig),
allowedIPOwners: make(map[string]int),
allowedIPClaims: make(map[string]map[int]bool),
disableRoutesAndAliasesOnExitNode: disableRoutesAndAliasesOnExitNode,
}
}
func TestSetClearExitNodeSuppressesResourceRoutesWhenEnabled(t *testing.T) {
pm := newExitNodeTestManager(true)
if pm.resourceRoutesSuppressed {
t.Fatalf("must start unsuppressed")
}
pm.SetExitNode("100.64.0.1", "100.64.0.2")
if !pm.exitNodeActive {
t.Fatalf("SetExitNode must mark the exit node active")
}
if !pm.resourceRoutesSuppressed {
t.Fatalf("SetExitNode must suppress resource routes when the feature is enabled")
}
pm.ClearExitNode()
if pm.exitNodeActive {
t.Fatalf("ClearExitNode must mark the exit node inactive")
}
if pm.resourceRoutesSuppressed {
t.Fatalf("ClearExitNode must restore resource routes")
}
}
func TestSetClearExitNodeNoopWhenDisabled(t *testing.T) {
pm := newExitNodeTestManager(false)
pm.SetExitNode("100.64.0.1", "100.64.0.2")
if pm.resourceRoutesSuppressed {
t.Fatalf("resource routes must never be suppressed when the feature is disabled")
}
pm.ClearExitNode()
if pm.resourceRoutesSuppressed {
t.Fatalf("resource routes must stay unsuppressed when the feature is disabled")
}
}
// TestClearExitNodeKeepsSuppressedWhileGatewayActive covers the two
// independent "exit node" signals (see exitNodeOrGatewayActiveLocked):
// disconnecting the ExitNodeConfig WireGuard peer must not restore resource
// routes if gateway/full-tunnel mode - what client apps and the CLI actually
// call "select exit node" - is still active. gatewayActive is set directly
// here (bypassing SetGateway, which touches the OS routing table) purely to
// exercise ClearExitNode's OR-check.
func TestClearExitNodeKeepsSuppressedWhileGatewayActive(t *testing.T) {
pm := newExitNodeTestManager(true)
pm.SetExitNode("100.64.0.1", "100.64.0.2")
if !pm.resourceRoutesSuppressed {
t.Fatalf("SetExitNode must suppress resource routes")
}
pm.mu.Lock()
pm.gatewayActive = true
pm.mu.Unlock()
pm.ClearExitNode()
if pm.exitNodeActive {
t.Fatalf("ClearExitNode must mark the exit node inactive regardless of gateway state")
}
if !pm.resourceRoutesSuppressed {
t.Fatalf("resource routes must stay suppressed while gateway mode is still active")
}
}
func TestExitNodeOrGatewayActiveLocked(t *testing.T) {
pm := newExitNodeTestManager(true)
if pm.exitNodeOrGatewayActiveLocked() {
t.Fatalf("neither signal is active yet")
}
pm.exitNodeActive = true
if !pm.exitNodeOrGatewayActiveLocked() {
t.Fatalf("exit node signal alone must count as active")
}
pm.exitNodeActive = false
pm.gatewayActive = true
if !pm.exitNodeOrGatewayActiveLocked() {
t.Fatalf("gateway signal alone must count as active")
}
}
+198 -41
View File
@@ -36,10 +36,19 @@ type PeerManagerConfig struct {
WSClient *websocket.Client WSClient *websocket.Client
APIServer *api.API APIServer *api.API
PublicDNS []string PublicDNS []string
// DisableRoutes stops the manager from adding/removing routes in the // DisableRoutesAndAliasesOnExitNode, when true, suppresses routes and
// system routing table for server IPs and remote subnets. Gateway routes // alias DNS records for individual resources (server IPs, remote
// are unaffected. // subnets, alias records) for as long as an "exit node" is active, and
DisableRoutes bool // restores them as soon as it's no longer active. Two distinct
// mechanisms both count as "exit node" here and are ORed together (see
// exitNodeOrGatewayActiveLocked): the ExitNodeConfig WireGuard peer used
// for resources hosted directly on an exit node server (SetExitNode/
// ClearExitNode), and gateway/full-tunnel mode (SetGateway/
// clearGatewayLocked) - which is what client apps and the CLI actually
// mean by "select exit node" (a gateway-mode site resource). The
// gateway's own default-route-equivalent and bypass routes are
// unaffected either way.
DisableRoutesAndAliasesOnExitNode bool
} }
type PeerManager struct { type PeerManager struct {
@@ -62,7 +71,24 @@ type PeerManager struct {
allowedIPClaims map[string]map[int]bool allowedIPClaims map[string]map[int]bool
APIServer *api.API APIServer *api.API
publicDNS []string publicDNS []string
disableRoutes bool // disableRoutesAndAliasesOnExitNode is static config (see
// PeerManagerConfig) for whether resource routes/aliases should ever be
// suppressed while an "exit node" (see the doc comment on
// PeerManagerConfig.DisableRoutesAndAliasesOnExitNode) is active.
disableRoutesAndAliasesOnExitNode bool
// exitNodeActive tracks whether the ExitNodeConfig WireGuard peer is
// currently connected - see SetExitNode/ClearExitNode. gatewayActive
// (below, pre-existing) is the other "exit node" signal.
exitNodeActive bool
// resourceRoutesSuppressed is the live, computed state: true exactly
// when disableRoutesAndAliasesOnExitNode && (exitNodeActive ||
// gatewayActive) - see exitNodeOrGatewayActiveLocked. It gates
// addRoutes/removeRoutes/addServerRoute/removeServerRoute/addDNSRecord/
// removeDNSRecord/removeDNSRecordForSite below. Kept as its own field
// (rather than recomputed each time) so suppressResourceRoutesLocked/
// restoreResourceRoutesLocked can tell whether a transition actually
// occurred.
resourceRoutesSuppressed bool
PersistentKeepalive int PersistentKeepalive int
@@ -153,21 +179,21 @@ func normalizeServerRouteDestination(serverIP string) string {
// NewPeerManager creates a new PeerManager with an internal PeerMonitor // NewPeerManager creates a new PeerManager with an internal PeerMonitor
func NewPeerManager(config PeerManagerConfig) *PeerManager { func NewPeerManager(config PeerManagerConfig) *PeerManager {
pm := &PeerManager{ pm := &PeerManager{
device: config.Device, device: config.Device,
peers: make(map[int]SiteConfig), peers: make(map[int]SiteConfig),
dnsProxy: config.DNSProxy, dnsProxy: config.DNSProxy,
interfaceName: config.InterfaceName, interfaceName: config.InterfaceName,
localIP: config.LocalIP, localIP: config.LocalIP,
privateKey: config.PrivateKey, privateKey: config.PrivateKey,
allowedIPOwners: make(map[string]int), allowedIPOwners: make(map[string]int),
allowedIPClaims: make(map[string]map[int]bool), allowedIPClaims: make(map[string]map[int]bool),
APIServer: config.APIServer, APIServer: config.APIServer,
publicDNS: config.PublicDNS, publicDNS: config.PublicDNS,
disableRoutes: config.DisableRoutes, disableRoutesAndAliasesOnExitNode: config.DisableRoutesAndAliasesOnExitNode,
lastOwnerChange: make(map[string]time.Time), lastOwnerChange: make(map[string]time.Time),
gatewaySiteIds: make(map[int]bool), gatewaySiteIds: make(map[int]bool),
gatewayExcludedIPs: make(map[string]int), gatewayExcludedIPs: make(map[string]int),
gatewayExtraEndpoints: make(map[string]bool), gatewayExtraEndpoints: make(map[string]bool),
} }
// Create the peer monitor // Create the peer monitor
@@ -204,22 +230,36 @@ func (pm *PeerManager) GetPeerMonitor() *monitor.PeerMonitor {
// SetExitNode starts (or updates) ICMP connectivity monitoring of the given exit node. // SetExitNode starts (or updates) ICMP connectivity monitoring of the given exit node.
// tunnelIP is the secondary address assigned to us for this exit node, which the ping // tunnelIP is the secondary address assigned to us for this exit node, which the ping
// probe must be sourced from since the exit node's WireGuard peer entry only accepts // probe must be sourced from since the exit node's WireGuard peer entry only accepts
// traffic from that address. // traffic from that address. If DisableRoutesAndAliasesOnExitNode was enabled (see
// PeerManagerConfig), this also suppresses resource routes/aliases for every tracked
// site peer - see suppressResourceRoutesLocked.
func (pm *PeerManager) SetExitNode(serverIP, tunnelIP string) { func (pm *PeerManager) SetExitNode(serverIP, tunnelIP string) {
pm.mu.RLock() pm.mu.Lock()
defer pm.mu.RUnlock() defer pm.mu.Unlock()
if pm.peerMonitor != nil { if pm.peerMonitor != nil {
pm.peerMonitor.SetExitNode(serverIP, tunnelIP) pm.peerMonitor.SetExitNode(serverIP, tunnelIP)
} }
pm.exitNodeActive = true
if pm.disableRoutesAndAliasesOnExitNode {
pm.suppressResourceRoutesLocked()
}
} }
// ClearExitNode stops ICMP connectivity monitoring of the exit node // ClearExitNode stops ICMP connectivity monitoring of the exit node. If
// DisableRoutesAndAliasesOnExitNode was enabled (see PeerManagerConfig), this
// also restores resource routes/aliases for every tracked site peer - unless
// gateway mode is still active, since that's the other "exit node" signal
// (see exitNodeOrGatewayActiveLocked) - see restoreResourceRoutesLocked.
func (pm *PeerManager) ClearExitNode() { func (pm *PeerManager) ClearExitNode() {
pm.mu.RLock() pm.mu.Lock()
defer pm.mu.RUnlock() defer pm.mu.Unlock()
if pm.peerMonitor != nil { if pm.peerMonitor != nil {
pm.peerMonitor.ClearExitNode() pm.peerMonitor.ClearExitNode()
} }
pm.exitNodeActive = false
if pm.disableRoutesAndAliasesOnExitNode && !pm.exitNodeOrGatewayActiveLocked() {
pm.restoreResourceRoutesLocked()
}
} }
// SetPublicDNS replaces the DNS servers used to resolve WireGuard peer // SetPublicDNS replaces the DNS servers used to resolve WireGuard peer
@@ -442,6 +482,12 @@ func (pm *PeerManager) SetGateway(siteResourceId int, siteIds []int, controlEndp
return err return err
} }
pm.gatewayActive = true pm.gatewayActive = true
// Gateway/full-tunnel mode is what client apps and the CLI call
// "exit node" - see the doc comment on
// PeerManagerConfig.DisableRoutesAndAliasesOnExitNode.
if pm.disableRoutesAndAliasesOnExitNode {
pm.suppressResourceRoutesLocked()
}
} }
newSet := make(map[int]bool, len(siteIds)) newSet := make(map[int]bool, len(siteIds))
@@ -581,6 +627,12 @@ func (pm *PeerManager) clearGatewayLocked() {
pm.gatewaySiteResourceId = 0 pm.gatewaySiteResourceId = 0
pm.deactivateGatewayLocked() pm.deactivateGatewayLocked()
pm.gatewayActive = false pm.gatewayActive = false
// Mirror SetGateway's activation hook, restoring resource routes/aliases
// unless the ExitNodeConfig WireGuard peer is still active (the other
// "exit node" signal).
if pm.disableRoutesAndAliasesOnExitNode && !pm.exitNodeOrGatewayActiveLocked() {
pm.restoreResourceRoutesLocked()
}
logger.Info("Gateway cleared") logger.Info("Gateway cleared")
} }
@@ -648,55 +700,160 @@ func (pm *PeerManager) GetAllPeers() []SiteConfig {
// addRoutes/removeRoutes/addServerRoute/removeServerRoute wrap the system // addRoutes/removeRoutes/addServerRoute/removeServerRoute wrap the system
// route helpers for site traffic (server IPs and remote subnets) and are a // route helpers for site traffic (server IPs and remote subnets) and are a
// no-op when route management is disabled. They deliberately do NOT cover the // no-op while resource routes are suppressed (see resourceRoutesSuppressed).
// gateway default-route-equivalent or its bypass routes, which are always // They deliberately do NOT cover the gateway default-route-equivalent or its
// installed regardless (see activateGatewayLocked). // bypass routes, which are always installed regardless (see
// activateGatewayLocked).
func (pm *PeerManager) addRoutes(subnets []string) error { func (pm *PeerManager) addRoutes(subnets []string) error {
if pm.disableRoutes { if pm.resourceRoutesSuppressed {
return nil return nil
} }
return network.AddRoutesWithSource(subnets, pm.interfaceName, pm.localIP) return network.AddRoutesWithSource(subnets, pm.interfaceName, pm.localIP)
} }
func (pm *PeerManager) removeRoutes(subnets []string) error { func (pm *PeerManager) removeRoutes(subnets []string) error {
if pm.disableRoutes { if pm.resourceRoutesSuppressed {
return nil return nil
} }
return network.RemoveRoutes(subnets, pm.interfaceName) return network.RemoveRoutes(subnets, pm.interfaceName)
} }
func (pm *PeerManager) addServerRoute(serverIP string) error { func (pm *PeerManager) addServerRoute(serverIP string) error {
if pm.disableRoutes { if pm.resourceRoutesSuppressed {
return nil return nil
} }
return network.AddRouteForServerIPWithSource(normalizeServerRouteDestination(serverIP), pm.interfaceName, pm.localIP) return network.AddRouteForServerIPWithSource(normalizeServerRouteDestination(serverIP), pm.interfaceName, pm.localIP)
} }
func (pm *PeerManager) removeServerRoute(serverIP string) error { func (pm *PeerManager) removeServerRoute(serverIP string) error {
if pm.disableRoutes { if pm.resourceRoutesSuppressed {
return nil return nil
} }
return network.RemoveRouteForServerIPWithSource(normalizeServerRouteDestination(serverIP), pm.interfaceName, pm.localIP) return network.RemoveRouteForServerIPWithSource(normalizeServerRouteDestination(serverIP), pm.interfaceName, pm.localIP)
} }
// The DNS proxy is not created when aliases are disabled, so every alias // addDNSRecord/removeDNSRecord/removeDNSRecordForSite tolerate a nil proxy
// record operation must tolerate a nil proxy. // (defensively - the proxy is now always created, see olm's handleConnect)
// and are a no-op while resource routes/aliases are suppressed, same as the
// route helpers above.
func (pm *PeerManager) addDNSRecord(alias string, address net.IP, siteId int) { func (pm *PeerManager) addDNSRecord(alias string, address net.IP, siteId int) {
if pm.dnsProxy != nil { if pm.dnsProxy == nil || pm.resourceRoutesSuppressed {
pm.dnsProxy.AddDNSRecord(alias, address, siteId) return
} }
pm.dnsProxy.AddDNSRecord(alias, address, siteId)
} }
func (pm *PeerManager) removeDNSRecord(alias string, address net.IP) { func (pm *PeerManager) removeDNSRecord(alias string, address net.IP) {
if pm.dnsProxy != nil { if pm.dnsProxy == nil || pm.resourceRoutesSuppressed {
pm.dnsProxy.RemoveDNSRecord(alias, address) return
} }
pm.dnsProxy.RemoveDNSRecord(alias, address)
} }
func (pm *PeerManager) removeDNSRecordForSite(alias string, address net.IP, siteId int) { func (pm *PeerManager) removeDNSRecordForSite(alias string, address net.IP, siteId int) {
if pm.dnsProxy != nil { if pm.dnsProxy == nil || pm.resourceRoutesSuppressed {
pm.dnsProxy.RemoveDNSRecordForSite(alias, address, siteId) return
} }
pm.dnsProxy.RemoveDNSRecordForSite(alias, address, siteId)
}
// exitNodeOrGatewayActiveLocked reports whether either "exit node" signal is
// currently active - the ExitNodeConfig WireGuard peer (SetExitNode) or
// gateway/full-tunnel mode (SetGateway), which is what client apps and the
// CLI actually mean by "select exit node" - see the doc comment on
// PeerManagerConfig.DisableRoutesAndAliasesOnExitNode. Must be called with
// pm.mu held.
func (pm *PeerManager) exitNodeOrGatewayActiveLocked() bool {
return pm.exitNodeActive || pm.gatewayActive
}
// suppressResourceRoutesLocked removes routes and alias DNS records for every
// tracked site peer's resources (server IP, remote subnets, aliases) so that
// only the exit node's own routes remain in effect. WireGuard peer
// configuration (AllowedIps) is left untouched - the tunnel remains usable by
// anything that reaches it without relying on the OS routing table. Called
// when either "exit node" signal becomes active while
// DisableRoutesAndAliasesOnExitNode is enabled (see SetExitNode/SetGateway).
// No-op if already suppressed. Must be called with pm.mu held.
//
// Deliberately calls network.* and pm.dnsProxy directly rather than through
// the addRoutes/removeRoutes/addServerRoute/removeServerRoute/addDNSRecord/
// removeDNSRecord wrappers above: those are gated on resourceRoutesSuppressed,
// which this function is itself in the middle of flipping - going through
// them here would silently no-op the very removal this function exists to do.
func (pm *PeerManager) suppressResourceRoutesLocked() {
if pm.resourceRoutesSuppressed {
return
}
pm.resourceRoutesSuppressed = true
removedSubnets := make(map[string]bool, len(pm.peers))
for _, peer := range pm.peers {
if err := network.RemoveRouteForServerIPWithSource(normalizeServerRouteDestination(peer.ServerIP), pm.interfaceName, pm.localIP); err != nil {
logger.Warn("Exit node active: failed to remove route for server IP %s: %v", peer.ServerIP, err)
}
for _, subnet := range peer.RemoteSubnets {
if removedSubnets[subnet] {
continue
}
removedSubnets[subnet] = true
if err := network.RemoveRoutes([]string{subnet}, pm.interfaceName); err != nil {
logger.Warn("Exit node active: failed to remove route for remote subnet %s: %v", subnet, err)
}
}
if pm.dnsProxy != nil {
for _, alias := range peer.Aliases {
address := net.ParseIP(alias.AliasAddress)
if address == nil {
continue
}
pm.dnsProxy.RemoveDNSRecordForSite(alias.Alias, address, peer.SiteId)
}
}
}
logger.Info("Exit node active: removed resource routes/aliases for %d site(s)", len(pm.peers))
}
// restoreResourceRoutesLocked is suppressResourceRoutesLocked's inverse,
// re-adding routes and alias DNS records for every tracked site peer's
// resources. Called when neither "exit node" signal remains active (see
// ClearExitNode/clearGatewayLocked). No-op if not currently suppressed. Must
// be called with pm.mu held.
func (pm *PeerManager) restoreResourceRoutesLocked() {
if !pm.resourceRoutesSuppressed {
return
}
pm.resourceRoutesSuppressed = false
addedSubnets := make(map[string]bool, len(pm.peers))
for _, peer := range pm.peers {
if err := network.AddRouteForServerIPWithSource(normalizeServerRouteDestination(peer.ServerIP), pm.interfaceName, pm.localIP); err != nil {
logger.Warn("Exit node inactive: failed to add route for server IP %s: %v", peer.ServerIP, err)
}
for _, subnet := range peer.RemoteSubnets {
if addedSubnets[subnet] {
continue
}
addedSubnets[subnet] = true
if err := network.AddRoutesWithSource([]string{subnet}, pm.interfaceName, pm.localIP); err != nil {
logger.Warn("Exit node inactive: failed to add route for remote subnet %s: %v", subnet, err)
}
}
if pm.dnsProxy != nil {
for _, alias := range peer.Aliases {
address := net.ParseIP(alias.AliasAddress)
if address == nil {
continue
}
if err := pm.dnsProxy.AddDNSRecord(alias.Alias, address, peer.SiteId); err != nil {
logger.Warn("Exit node inactive: failed to add DNS record for alias %s: %v", alias.Alias, err)
}
}
}
}
logger.Info("Exit node inactive: restored resource routes/aliases for %d site(s)", len(pm.peers))
} }
func (pm *PeerManager) AddPeer(siteConfig SiteConfig) error { func (pm *PeerManager) AddPeer(siteConfig SiteConfig) error {