mirror of
https://github.com/fosrl/olm.git
synced 2026-09-21 05:19:07 +02:00
Basic subnet router and dns server config added
This commit is contained in:
@@ -30,6 +30,7 @@ type ConnectionRequest struct {
|
||||
PingTimeout string `json:"pingTimeout,omitempty"`
|
||||
OrgID string `json:"orgId,omitempty"`
|
||||
MatchDomains []string `json:"matchDomains,omitempty"`
|
||||
SubnetRouter bool `json:"subnetRouter,omitempty"`
|
||||
}
|
||||
|
||||
// SwitchOrgRequest defines the structure for switching organizations
|
||||
|
||||
@@ -53,6 +53,7 @@ type OlmConfig struct {
|
||||
TunnelDNS bool `json:"tunnelDNS"`
|
||||
DisableRelay bool `json:"disableRelay"`
|
||||
PreferLocalRoutes bool `json:"preferLocalRoutes"`
|
||||
SubnetRouter bool `json:"subnetRouter"`
|
||||
// DoNotCreateNewClient bool `json:"doNotCreateNewClient"`
|
||||
|
||||
// Parsed values (not in JSON)
|
||||
@@ -120,6 +121,7 @@ func DefaultConfig() *OlmConfig {
|
||||
config.sources["tunnelDNS"] = string(SourceDefault)
|
||||
config.sources["disableRelay"] = string(SourceDefault)
|
||||
config.sources["preferLocalRoutes"] = string(SourceDefault)
|
||||
config.sources["subnetRouter"] = string(SourceDefault)
|
||||
// config.sources["doNotCreateNewClient"] = string(SourceDefault)
|
||||
|
||||
return config
|
||||
@@ -291,6 +293,10 @@ func loadConfigFromEnv(config *OlmConfig) {
|
||||
config.TunnelDNS = true
|
||||
config.sources["tunnelDNS"] = string(SourceEnv)
|
||||
}
|
||||
if val := os.Getenv("SUBNET_ROUTER"); val == "true" {
|
||||
config.SubnetRouter = true
|
||||
config.sources["subnetRouter"] = string(SourceEnv)
|
||||
}
|
||||
// if val := os.Getenv("DO_NOT_CREATE_NEW_CLIENT"); val == "true" {
|
||||
// config.DoNotCreateNewClient = true
|
||||
// config.sources["doNotCreateNewClient"] = string(SourceEnv)
|
||||
@@ -324,6 +330,7 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
|
||||
"disableRelay": config.DisableRelay,
|
||||
"preferLocalRoutes": config.PreferLocalRoutes,
|
||||
"tunnelDNS": config.TunnelDNS,
|
||||
"subnetRouter": config.SubnetRouter,
|
||||
// "doNotCreateNewClient": config.DoNotCreateNewClient,
|
||||
}
|
||||
|
||||
@@ -351,6 +358,7 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
|
||||
serviceFlags.BoolVar(&config.DisableRelay, "disable-relay", config.DisableRelay, "Disable relay connections")
|
||||
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.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.DoNotCreateNewClient, "do-not-create-new-client", config.DoNotCreateNewClient, "Do not create new client")
|
||||
|
||||
version := serviceFlags.Bool("version", false, "Print the version")
|
||||
@@ -440,6 +448,9 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
|
||||
if config.TunnelDNS != origValues["tunnelDNS"].(bool) {
|
||||
config.sources["tunnelDNS"] = string(SourceCLI)
|
||||
}
|
||||
if config.SubnetRouter != origValues["subnetRouter"].(bool) {
|
||||
config.sources["subnetRouter"] = string(SourceCLI)
|
||||
}
|
||||
// if config.DoNotCreateNewClient != origValues["doNotCreateNewClient"].(bool) {
|
||||
// config.sources["doNotCreateNewClient"] = string(SourceCLI)
|
||||
// }
|
||||
@@ -572,6 +583,10 @@ func mergeConfigs(dest, src *OlmConfig) {
|
||||
dest.PreferLocalRoutes = src.PreferLocalRoutes
|
||||
dest.sources["preferLocalRoutes"] = string(SourceFile)
|
||||
}
|
||||
if src.SubnetRouter {
|
||||
dest.SubnetRouter = src.SubnetRouter
|
||||
dest.sources["subnetRouter"] = string(SourceFile)
|
||||
}
|
||||
// if src.DoNotCreateNewClient {
|
||||
// dest.DoNotCreateNewClient = src.DoNotCreateNewClient
|
||||
// dest.sources["doNotCreateNewClient"] = string(SourceFile)
|
||||
@@ -665,6 +680,7 @@ func (c *OlmConfig) ShowConfig() {
|
||||
fmt.Printf(" tunnel-dns = %v [%s]\n", c.TunnelDNS, getSource("tunnelDNS"))
|
||||
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(" subnet-router = %v [%s]\n", c.SubnetRouter, getSource("subnetRouter"))
|
||||
// fmt.Printf(" do-not-create-new-client = %v [%s]\n", c.DoNotCreateNewClient, getSource("doNotCreateNewClient"))
|
||||
if c.TlsClientCert != "" {
|
||||
fmt.Printf(" tls-cert = %s [%s]\n", c.TlsClientCert, getSource("tlsClientCert"))
|
||||
|
||||
@@ -866,6 +866,29 @@ func (p *DNSProxy) SetUpstreamDNS(servers []string) {
|
||||
p.upstreamDNS = servers
|
||||
}
|
||||
|
||||
// SetTunnelDNS changes whether upstream DNS queries are sent over the
|
||||
// WireGuard tunnel (true) or directly via host networking (false). Only
|
||||
// takes effect for queries issued after the call; in-flight queries keep
|
||||
// using whichever path they already started on. Switching to true after the
|
||||
// proxy has already started lazily brings up the tunnel netstack and its
|
||||
// packet-forwarding goroutine if they weren't already running - NewDNSProxy
|
||||
// only does that eagerly when tunnelDns is true from the start.
|
||||
func (p *DNSProxy) SetTunnelDNS(tunnelDNS bool) {
|
||||
if tunnelDNS && p.tunnelStack == nil {
|
||||
if !p.tunnelIP.IsValid() {
|
||||
logger.Warn("Cannot enable tunnel DNS: tunnel IP not set")
|
||||
return
|
||||
}
|
||||
if err := p.initTunnelNetstack(); err != nil {
|
||||
logger.Error("Failed to initialize tunnel netstack for tunnel DNS: %v", err)
|
||||
return
|
||||
}
|
||||
p.wg.Add(1)
|
||||
go p.runTunnelPacketSender()
|
||||
}
|
||||
p.tunnelDNS = tunnelDNS
|
||||
}
|
||||
|
||||
// AddDNSRecord adds a DNS record to the local store
|
||||
// domain should be a domain name (e.g., "example.com" or "example.com.")
|
||||
// ip should be a valid IPv4 or IPv6 address
|
||||
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/fosrl/newt v1.16.0
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
github.com/google/nftables v0.3.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/miekg/dns v1.1.70
|
||||
golang.org/x/net v0.56.0
|
||||
@@ -19,6 +20,8 @@ require (
|
||||
require (
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
|
||||
@@ -8,8 +8,14 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
|
||||
github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
|
||||
@@ -273,6 +273,7 @@ func runOlmMainWithArgs(ctx context.Context, cancel context.CancelFunc, signalCt
|
||||
OverrideDNS: config.OverrideDNS,
|
||||
DisableRelay: config.DisableRelay,
|
||||
PreferLocalRoutes: config.PreferLocalRoutes,
|
||||
SubnetRouter: config.SubnetRouter,
|
||||
EnableUAPI: true,
|
||||
}
|
||||
go olm.StartTunnel(tunnelConfig)
|
||||
|
||||
+18
-20
@@ -15,8 +15,8 @@ import (
|
||||
"github.com/fosrl/newt/util"
|
||||
olmDevice "github.com/fosrl/olm/device"
|
||||
"github.com/fosrl/olm/dns"
|
||||
dnsOverride "github.com/fosrl/olm/dns/override"
|
||||
"github.com/fosrl/olm/peers"
|
||||
"github.com/fosrl/olm/subnetrouter"
|
||||
"github.com/fosrl/olm/websocket"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
@@ -75,6 +75,12 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
// A server-provided DNS config always overrides the client's own local
|
||||
// config (from CLI flags / API connect request) - see applyDNSConfigUpdate.
|
||||
if wgData.DNSConfig != nil {
|
||||
o.applyDNSConfigUpdate(*wgData.DNSConfig)
|
||||
}
|
||||
|
||||
// When handed an already-open FD (mobile/NetworkExtension platforms), the
|
||||
// TUN device's addresses and routes are owned and reconciled by the host
|
||||
// platform from NetworkSettings (e.g. Apple's NEPacketTunnelProvider via
|
||||
@@ -193,6 +199,14 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
|
||||
logger.Error("Failed to o.tunnelConfigure interface: %v", err)
|
||||
}
|
||||
|
||||
if o.tunnelConfig.SubnetRouter {
|
||||
if err := subnetrouter.Enable(o.tunnelConfig.InterfaceName, o.primaryTunnelIP); err != nil {
|
||||
logger.Error("Failed to enable subnet router: %v", err)
|
||||
} else {
|
||||
logger.Info("Subnet router enabled on %s (SNAT to %s)", o.tunnelConfig.InterfaceName, o.primaryTunnelIP)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -273,26 +287,10 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
|
||||
})
|
||||
|
||||
if o.tunnelConfig.OverrideDNS {
|
||||
// When the host platform already applies DNS natively (NEDNSSettings on
|
||||
// macOS/iOS, scoped to the tunnel session and auto-cleaned by the OS no
|
||||
// matter how the session ends), skip olm's own raw scutil-based override -
|
||||
// there is nothing for it to add and, unlike NEDNSSettings, it has no way
|
||||
// to guarantee cleanup if this process dies uncleanly. See NativeDNSManaged.
|
||||
if !o.tunnelConfig.NativeDNSManaged {
|
||||
// Set up DNS override to use our DNS proxy
|
||||
if err := dnsOverride.SetupDNSOverride(o.tunnelConfig.InterfaceName, o.dnsProxy.GetProxyIP()); err != nil {
|
||||
logger.Error("Failed to setup DNS override: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the external watchdog (if configured). The watchdog will
|
||||
// reset DNS if this process dies before it can call
|
||||
// RestoreDNSOverride. This is a no-op when no watchdog
|
||||
// subcommand has been configured on the OlmConfig.
|
||||
o.startDNSWatchdog(o.tunnelConfig.InterfaceName)
|
||||
if err := o.applyDNSOverride(true); err != nil {
|
||||
logger.Error("%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
network.SetDNSServers([]string{o.dnsProxy.GetProxyIP().String()})
|
||||
}
|
||||
|
||||
if wgData.ExitNode != nil && wgData.ExitNode.Connect {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package olm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/fosrl/newt/logger"
|
||||
"github.com/fosrl/newt/network"
|
||||
dnsOverride "github.com/fosrl/olm/dns/override"
|
||||
"github.com/fosrl/olm/websocket"
|
||||
)
|
||||
|
||||
// applyDNSConfigUpdate merges a server-provided DNS config override into the
|
||||
// running tunnel config. Every set field always overrides the client's own
|
||||
// local config (from CLI flags / API connect request, or a previous update);
|
||||
// an unset field leaves the current value alone. Called both from the
|
||||
// initial "olm/wg/connect" message, before the DNS proxy exists yet (the
|
||||
// updated tunnelConfig feeds into its construction in handleConnect), and
|
||||
// from a later live "olm/wg/dns/update" push, where the running proxy is
|
||||
// updated directly.
|
||||
func (o *Olm) applyDNSConfigUpdate(cfg DNSConfigUpdate) {
|
||||
logger.Info("Applying DNS config from server: %+v", cfg)
|
||||
|
||||
if len(cfg.UpstreamDNS) > 0 {
|
||||
o.tunnelConfig.UpstreamDNS = cfg.UpstreamDNS
|
||||
if o.dnsProxy != nil {
|
||||
o.dnsProxy.SetUpstreamDNS(cfg.UpstreamDNS)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.MatchDomains) > 0 {
|
||||
o.tunnelConfig.MatchDomains = cfg.MatchDomains
|
||||
if o.dnsProxy != nil {
|
||||
o.dnsProxy.SetMatchDomains(cfg.MatchDomains)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.TunnelDNS != nil {
|
||||
o.tunnelConfig.TunnelDNS = *cfg.TunnelDNS
|
||||
if o.dnsProxy != nil {
|
||||
o.dnsProxy.SetTunnelDNS(*cfg.TunnelDNS)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.OverrideDNS != nil && *cfg.OverrideDNS != o.tunnelConfig.OverrideDNS {
|
||||
if o.dnsProxy != nil {
|
||||
if err := o.applyDNSOverride(*cfg.OverrideDNS); err != nil {
|
||||
logger.Error("Failed to apply DNS override update: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
o.tunnelConfig.OverrideDNS = *cfg.OverrideDNS
|
||||
}
|
||||
}
|
||||
|
||||
// applyDNSOverride installs or removes olm's own system DNS override
|
||||
// (pointing the host resolver at the DNS proxy) and requires the DNS proxy
|
||||
// to already be running. When the host platform already manages DNS
|
||||
// natively (see NativeDNSManaged), this only updates the OS resolver list -
|
||||
// there's no raw override to add or remove. Used both at initial connect
|
||||
// (see handleConnect) and for a live "olm/wg/dns/update" toggle of
|
||||
// OverrideDNS via applyDNSConfigUpdate.
|
||||
func (o *Olm) applyDNSOverride(enable bool) error {
|
||||
if o.dnsProxy == nil {
|
||||
return fmt.Errorf("cannot toggle DNS override: DNS proxy is not running")
|
||||
}
|
||||
|
||||
if enable {
|
||||
if !o.tunnelConfig.NativeDNSManaged {
|
||||
if err := dnsOverride.SetupDNSOverride(o.tunnelConfig.InterfaceName, o.dnsProxy.GetProxyIP()); err != nil {
|
||||
return fmt.Errorf("failed to setup DNS override: %w", err)
|
||||
}
|
||||
|
||||
// Start the external watchdog (if configured). The watchdog will
|
||||
// reset DNS if this process dies before it can call
|
||||
// RestoreDNSOverride. This is a no-op when no watchdog
|
||||
// subcommand has been configured on the OlmConfig.
|
||||
o.startDNSWatchdog(o.tunnelConfig.InterfaceName)
|
||||
}
|
||||
|
||||
network.SetDNSServers([]string{o.dnsProxy.GetProxyIP().String()})
|
||||
} else {
|
||||
if !o.tunnelConfig.NativeDNSManaged {
|
||||
if err := dnsOverride.RestoreDNSOverride(); err != nil {
|
||||
return fmt.Errorf("failed to restore DNS: %w", err)
|
||||
}
|
||||
|
||||
o.stopDNSWatchdog()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDNSConfigUpdate handles a server-initiated request to change the
|
||||
// client's DNS configuration (upstream DNS, tunnel DNS, override DNS, match
|
||||
// domains) after it is already connected, without requiring a full
|
||||
// reconnect. Mirrors the DNSConfig field sent on the initial
|
||||
// "olm/wg/connect" message - see DNSConfigUpdate.
|
||||
func (o *Olm) handleDNSConfigUpdate(msg websocket.WSMessage) {
|
||||
logger.Debug("Received DNS config update message: %v", msg.Data)
|
||||
|
||||
if !o.tunnelRunning {
|
||||
logger.Debug("Tunnel stopped, ignoring DNS config update message")
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(msg.Data)
|
||||
if err != nil {
|
||||
logger.Error("Error marshaling DNS config update data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var update DNSConfigUpdate
|
||||
if err := json.Unmarshal(jsonData, &update); err != nil {
|
||||
logger.Error("Error unmarshaling DNS config update data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
o.applyDNSConfigUpdate(update)
|
||||
}
|
||||
+13
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/fosrl/olm/dns"
|
||||
dnsOverride "github.com/fosrl/olm/dns/override"
|
||||
"github.com/fosrl/olm/peers"
|
||||
"github.com/fosrl/olm/subnetrouter"
|
||||
"github.com/fosrl/olm/websocket"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
@@ -254,6 +255,7 @@ func (o *Olm) registerAPICallbacks() {
|
||||
TlsClientCert: req.TlsClientCert,
|
||||
OrgID: req.OrgID,
|
||||
MatchDomains: req.MatchDomains,
|
||||
SubnetRouter: req.SubnetRouter,
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -577,6 +579,11 @@ func (o *Olm) StartTunnel(config TunnelConfig) {
|
||||
o.websocket.RegisterHandler("olm/wg/exitnode/disconnect", o.handleExitNodeDisconnect)
|
||||
o.websocket.RegisterHandler("olm/wg/exitnode/data/update", o.handleExitNodeUpdateData)
|
||||
|
||||
// Handler for the server to push a live DNS config override (upstream DNS,
|
||||
// tunnel DNS, override DNS, match domains) after registration, mirroring the
|
||||
// DNSConfig field sent on the initial "olm/wg/connect" message.
|
||||
o.websocket.RegisterHandler("olm/wg/dns/update", o.handleDNSConfigUpdate)
|
||||
|
||||
o.websocket.RegisterHandler("olm/ping/exitNodes", func(msg websocket.WSMessage) {
|
||||
logger.Debug("Received exit node ping request")
|
||||
|
||||
@@ -831,6 +838,12 @@ func (o *Olm) Close() {
|
||||
o.stopDNSWatchdog()
|
||||
}
|
||||
|
||||
if o.tunnelConfig.SubnetRouter {
|
||||
if err := subnetrouter.Disable(o.tunnelConfig.InterfaceName); err != nil {
|
||||
logger.Error("Failed to disable subnet router: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if o.holePunchManager != nil {
|
||||
o.holePunchManager.Stop()
|
||||
o.holePunchManager = nil
|
||||
|
||||
@@ -11,6 +11,22 @@ type WgData struct {
|
||||
TunnelIP string `json:"tunnelIP"`
|
||||
UtilitySubnet string `json:"utilitySubnet"` // this is for things like the DNS server, and alias addresses
|
||||
ExitNode *ExitNodeConfig `json:"exitNode,omitempty"`
|
||||
DNSConfig *DNSConfigUpdate `json:"dnsConfig,omitempty"`
|
||||
}
|
||||
|
||||
// DNSConfigUpdate describes a server-driven override of the olm client's DNS
|
||||
// configuration - the same settings that can otherwise only be set locally
|
||||
// (see TunnelConfig's UpstreamDNS/OverrideDNS/TunnelDNS/MatchDomains). It
|
||||
// arrives on the initial "olm/wg/connect" message and can also be sent later
|
||||
// via "olm/wg/dns/update" to change the running config without reconnecting.
|
||||
// Every field is optional/nil-able: an omitted field leaves the client's
|
||||
// current value (local config, or whatever a previous update set) unchanged,
|
||||
// while a present field always overrides it.
|
||||
type DNSConfigUpdate struct {
|
||||
UpstreamDNS []string `json:"upstreamDns,omitempty"`
|
||||
OverrideDNS *bool `json:"overrideDns,omitempty"`
|
||||
TunnelDNS *bool `json:"tunnelDns,omitempty"`
|
||||
MatchDomains []string `json:"matchDomains,omitempty"`
|
||||
}
|
||||
|
||||
// ExitNodeConfig describes an exit node the olm client can connect to for
|
||||
@@ -158,4 +174,11 @@ type TunnelConfig struct {
|
||||
// false, preserving the routing behavior from before this option was
|
||||
// introduced.
|
||||
PreferLocalRoutes bool
|
||||
|
||||
// SubnetRouter, when enabled, lets this client forward traffic from its
|
||||
// local network out over the tunnel: forwarded packets are NATed to the
|
||||
// client's own tunnel IP before being encrypted, since the server side
|
||||
// authorizes traffic by the client's tunnel identity, not by whatever
|
||||
// LAN address it originally arrived with. Linux only. Defaults to false.
|
||||
SubnetRouter bool
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
//go:build linux
|
||||
|
||||
// Package subnetrouter lets this client forward LAN traffic out over its own
|
||||
// WireGuard tunnel, source-NAT'd to the tunnel's own IP. Pangolin's
|
||||
// server-side routing/ACLs are keyed on the client's tunnel IP as its
|
||||
// identity, so traffic merely forwarded from the LAN (which arrives with the
|
||||
// LAN device's own source address) would not be recognized - it must be
|
||||
// rewritten to look like it came from this client before it goes out over
|
||||
// the tunnel, the same way a NAT router masquerades LAN traffic behind its
|
||||
// WAN IP.
|
||||
package subnetrouter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/fosrl/newt/logger"
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/expr"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
tableName = "olm_subnet_router"
|
||||
ipForwardSys = "/proc/sys/net/ipv4/ip_forward"
|
||||
)
|
||||
|
||||
// ipForwardMu guards the two package-level fields below, which record
|
||||
// whether Enable had to flip ip_forward on itself, so Disable only ever
|
||||
// restores a value it actually changed - mirroring dns/override's
|
||||
// instance-free save/restore convention.
|
||||
var (
|
||||
ipForwardMu sync.Mutex
|
||||
weEnabledForward bool
|
||||
)
|
||||
|
||||
// Enable turns this host into a subnet router: it enables IPv4 forwarding
|
||||
// (if not already on) and installs an nftables table that SNATs anything
|
||||
// leaving interfaceName whose source isn't already tunnelIP, and accepts
|
||||
// forwarding to/from interfaceName so a default-deny FORWARD policy
|
||||
// elsewhere on the host doesn't drop it.
|
||||
//
|
||||
// It is idempotent: any table left behind by a previous run (e.g. after a
|
||||
// crash) is torn down first, so repeated Enable/Disable cycles across
|
||||
// reconnects never conflict with stale state.
|
||||
func Enable(interfaceName string, tunnelIP netip.Addr) error {
|
||||
if !tunnelIP.Is4() {
|
||||
return fmt.Errorf("subnet router requires an IPv4 tunnel address, got %v", tunnelIP)
|
||||
}
|
||||
|
||||
// Best-effort cleanup of anything left over from a previous run.
|
||||
if err := Disable(interfaceName); err != nil {
|
||||
logger.Debug("subnetrouter: pre-enable cleanup: %v", err)
|
||||
}
|
||||
|
||||
if err := enableIPForward(); err != nil {
|
||||
return fmt.Errorf("failed to enable IPv4 forwarding: %w", err)
|
||||
}
|
||||
|
||||
conn := &nftables.Conn{}
|
||||
|
||||
table := conn.AddTable(&nftables.Table{
|
||||
Family: nftables.TableFamilyIPv4,
|
||||
Name: tableName,
|
||||
})
|
||||
|
||||
postrouting := conn.AddChain(&nftables.Chain{
|
||||
Name: "postrouting",
|
||||
Table: table,
|
||||
Type: nftables.ChainTypeNAT,
|
||||
Hooknum: nftables.ChainHookPostrouting,
|
||||
Priority: nftables.ChainPriorityNATSource,
|
||||
})
|
||||
|
||||
addr := tunnelIP.As4()
|
||||
conn.AddRule(&nftables.Rule{
|
||||
Table: table,
|
||||
Chain: postrouting,
|
||||
Exprs: []expr.Any{
|
||||
// oifname == interfaceName
|
||||
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
|
||||
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: ifname(interfaceName)},
|
||||
// ip saddr != tunnelIP
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseNetworkHeader,
|
||||
Offset: 12, // IPv4 source address offset
|
||||
Len: 4,
|
||||
},
|
||||
&expr.Cmp{Op: expr.CmpOpNeq, Register: 1, Data: addr[:]},
|
||||
// snat to tunnelIP
|
||||
&expr.Immediate{Register: 1, Data: addr[:]},
|
||||
&expr.NAT{
|
||||
Type: expr.NATTypeSourceNAT,
|
||||
Family: unix.NFPROTO_IPV4,
|
||||
RegAddrMin: 1,
|
||||
RegAddrMax: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
forward := conn.AddChain(&nftables.Chain{
|
||||
Name: "forward",
|
||||
Table: table,
|
||||
Type: nftables.ChainTypeFilter,
|
||||
Hooknum: nftables.ChainHookForward,
|
||||
Priority: nftables.ChainPriorityFilter,
|
||||
})
|
||||
|
||||
for _, key := range []expr.MetaKey{expr.MetaKeyIIFNAME, expr.MetaKeyOIFNAME} {
|
||||
conn.AddRule(&nftables.Rule{
|
||||
Table: table,
|
||||
Chain: forward,
|
||||
Exprs: []expr.Any{
|
||||
&expr.Meta{Key: key, Register: 1},
|
||||
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: ifname(interfaceName)},
|
||||
&expr.Verdict{Kind: expr.VerdictAccept},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err := conn.Flush(); err != nil {
|
||||
// Roll back the forwarding sysctl change too, so a failed Enable
|
||||
// doesn't leave the host with forwarding on and no NAT rules.
|
||||
_ = disableIPForwardIfWeEnabledIt()
|
||||
return fmt.Errorf("failed to apply nftables rules: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("subnetrouter: enabled on %s (snat to %s)", interfaceName, tunnelIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disable removes the nftables table added by Enable (a no-op if it doesn't
|
||||
// exist) and restores ip_forward to whatever it was before Enable, but only
|
||||
// if Enable is what changed it.
|
||||
func Disable(interfaceName string) error {
|
||||
conn := &nftables.Conn{}
|
||||
|
||||
tables, err := conn.ListTables()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list nftables tables: %w", err)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, t := range tables {
|
||||
if t.Name == tableName && t.Family == nftables.TableFamilyIPv4 {
|
||||
conn.DelTable(t)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var flushErr error
|
||||
if found {
|
||||
flushErr = conn.Flush()
|
||||
}
|
||||
|
||||
forwardErr := disableIPForwardIfWeEnabledIt()
|
||||
|
||||
if flushErr != nil {
|
||||
return fmt.Errorf("failed to remove nftables table: %w", flushErr)
|
||||
}
|
||||
return forwardErr
|
||||
}
|
||||
|
||||
// enableIPForward turns on IPv4 forwarding if it isn't already on, recording
|
||||
// whether this call is the one that changed it.
|
||||
func enableIPForward() error {
|
||||
ipForwardMu.Lock()
|
||||
defer ipForwardMu.Unlock()
|
||||
|
||||
current, err := readIPForward()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current {
|
||||
weEnabledForward = false
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.WriteFile(ipForwardSys, []byte("1\n"), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
weEnabledForward = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// disableIPForwardIfWeEnabledIt restores ip_forward to 0, but only if a
|
||||
// prior enableIPForward call is what turned it on.
|
||||
func disableIPForwardIfWeEnabledIt() error {
|
||||
ipForwardMu.Lock()
|
||||
defer ipForwardMu.Unlock()
|
||||
|
||||
if !weEnabledForward {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.WriteFile(ipForwardSys, []byte("0\n"), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
weEnabledForward = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func readIPForward() (bool, error) {
|
||||
data, err := os.ReadFile(ipForwardSys)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.TrimSpace(string(data)) == "1", nil
|
||||
}
|
||||
|
||||
// ifname encodes an interface name the way nftables expects it: NUL-padded
|
||||
// to IFNAMSIZ (16) bytes.
|
||||
func ifname(name string) []byte {
|
||||
b := make([]byte, 16)
|
||||
copy(b, name)
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build !linux
|
||||
|
||||
package subnetrouter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// Enable always fails on non-Linux platforms: there is no nftables/netfilter
|
||||
// to install SNAT rules into. Callers should log this as a warning, not
|
||||
// treat it as fatal.
|
||||
func Enable(interfaceName string, tunnelIP netip.Addr) error {
|
||||
return fmt.Errorf("subnet router is only supported on Linux")
|
||||
}
|
||||
|
||||
// Disable is a no-op on non-Linux platforms, since Enable never succeeds
|
||||
// there and so never leaves anything to clean up.
|
||||
func Disable(interfaceName string) error {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user