mirror of
https://github.com/fosrl/olm.git
synced 2026-10-01 18:29:08 +02:00
Exclude the holepunch endpoints as well
This commit is contained in:
@@ -251,6 +251,12 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
|
||||
|
||||
o.peerManager.Start()
|
||||
|
||||
// OnTokenUpdate (see olm.go) typically fires before this peer manager
|
||||
// existed - it runs during the initial token/auth fetch, well before this
|
||||
// "olm/wg/connect" message - so push in whatever hole-punch bypass
|
||||
// endpoints it already recorded now that there's somewhere to put them.
|
||||
o.flushPendingHolepunchBypassEndpoints()
|
||||
|
||||
if err := o.dnsProxy.Start(); err != nil { // start DNS proxy first so there is no downtime
|
||||
logger.Error("Failed to start DNS proxy: %v", err)
|
||||
}
|
||||
|
||||
@@ -210,7 +210,14 @@ persistent_keepalive_interval=%d`, util.FixKey(cfg.PublicKey), allowedIP, resolv
|
||||
|
||||
if pm := o.getPeerManager(); pm != nil {
|
||||
pm.SetExitNode(strings.Split(cfg.ServerIP, "/")[0], strings.Split(cfg.TunnelIP, "/")[0])
|
||||
// Distinct from the hole-punch exit nodes registered in olm.go's
|
||||
// OnTokenUpdate handler: this is the exit node actually connected as a
|
||||
// WireGuard peer above. Its own traffic must stay off the gateway
|
||||
// route the same way a site peer's endpoint does, or it would loop
|
||||
// through the tunnel it's part of maintaining.
|
||||
pm.AddGatewayBypassEndpoint(resolvedEndpoint)
|
||||
}
|
||||
o.exitNodeResolvedEndpoint = resolvedEndpoint
|
||||
|
||||
logger.Info("Connected to exit node at %s", resolvedEndpoint)
|
||||
return nil
|
||||
@@ -232,9 +239,14 @@ func (o *Olm) removeExitNodePeerLocked() error {
|
||||
}
|
||||
cfg := o.exitNode
|
||||
o.exitNode = nil
|
||||
resolvedEndpoint := o.exitNodeResolvedEndpoint
|
||||
o.exitNodeResolvedEndpoint = ""
|
||||
|
||||
if pm := o.getPeerManager(); pm != nil {
|
||||
pm.ClearExitNode()
|
||||
if resolvedEndpoint != "" {
|
||||
pm.RemoveGatewayBypassEndpoint(resolvedEndpoint)
|
||||
}
|
||||
}
|
||||
|
||||
if o.dnsProxy != nil {
|
||||
|
||||
@@ -81,6 +81,30 @@ func (o *Olm) applyPendingGatewayConfig(requestedSiteIds []int) {
|
||||
}
|
||||
}
|
||||
|
||||
// flushPendingHolepunchBypassEndpoints re-registers every currently-known
|
||||
// hole-punch bypass endpoint (see the OnTokenUpdate handler in olm.go) with
|
||||
// the peer manager. OnTokenUpdate typically fires before the peer manager
|
||||
// exists - it runs during the initial token/auth fetch in
|
||||
// websocket.Client.establishConnection, well before the server's
|
||||
// "olm/wg/connect" message creates the peer manager here in handleConnect -
|
||||
// so anything recorded into o.hpBypassEndpoints while pm was nil needs to be
|
||||
// pushed in once it becomes available. AddGatewayBypassEndpoint is
|
||||
// idempotent, so calling it again for an endpoint OnTokenUpdate already
|
||||
// managed to register directly (e.g. a later token refresh, once the peer
|
||||
// manager already existed) is harmless.
|
||||
func (o *Olm) flushPendingHolepunchBypassEndpoints() {
|
||||
pm := o.getPeerManager()
|
||||
if pm == nil {
|
||||
return
|
||||
}
|
||||
|
||||
o.hpBypassMu.Lock()
|
||||
defer o.hpBypassMu.Unlock()
|
||||
for hostport := range o.hpBypassEndpoints {
|
||||
pm.AddGatewayBypassEndpoint(hostport)
|
||||
}
|
||||
}
|
||||
|
||||
// extractControlEndpointHost returns the bare host (no scheme/port) of the
|
||||
// Pangolin server olm is registered against, for gateway bypass-route
|
||||
// purposes. Falls back to the raw endpoint string on parse failure -
|
||||
|
||||
+63
-8
@@ -12,6 +12,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -64,6 +65,20 @@ type Olm struct {
|
||||
// secondary address on the same interface/WireGuard device as the site peers.
|
||||
exitNode *ExitNodeConfig
|
||||
exitNodeMu sync.Mutex
|
||||
// exitNodeResolvedEndpoint is the exit node's WireGuard endpoint (already
|
||||
// DNS-resolved to "ip:port") as of the last successful connectExitNode
|
||||
// call, kept alongside exitNode purely so removeExitNodePeerLocked can
|
||||
// unregister the exact same gateway bypass-route target it registered -
|
||||
// see connectExitNode's AddGatewayBypassEndpoint call. Guarded by exitNodeMu.
|
||||
exitNodeResolvedEndpoint string
|
||||
|
||||
// hpBypassEndpoints tracks the "host:relayPort" endpoints currently
|
||||
// registered as gateway bypass targets for hole-punch exit nodes (STUN-like
|
||||
// probing, distinct from a connected exit node's own WireGuard peer above) -
|
||||
// diffed against each OnTokenUpdate so stale entries are unregistered and
|
||||
// new ones protected while gateway mode is active.
|
||||
hpBypassEndpoints map[string]bool
|
||||
hpBypassMu sync.Mutex
|
||||
|
||||
// primaryTunnelIP is the site tunnel's own address (wgData.TunnelIP), set once
|
||||
// per connect in handleConnect. It's the interface's first/primary address -
|
||||
@@ -222,13 +237,14 @@ func Init(ctx context.Context, config OlmConfig) (*Olm, error) {
|
||||
apiServer.SetAgent(config.Agent)
|
||||
|
||||
newOlm := &Olm{
|
||||
logFile: logFile,
|
||||
olmCtx: ctx,
|
||||
apiServer: apiServer,
|
||||
olmConfig: config,
|
||||
stopPeerSends: make(map[string]func()),
|
||||
stopPeerInits: make(map[string]func()),
|
||||
jitPendingSites: make(map[int]string),
|
||||
logFile: logFile,
|
||||
olmCtx: ctx,
|
||||
apiServer: apiServer,
|
||||
olmConfig: config,
|
||||
stopPeerSends: make(map[string]func()),
|
||||
stopPeerInits: make(map[string]func()),
|
||||
jitPendingSites: make(map[int]string),
|
||||
hpBypassEndpoints: make(map[string]bool),
|
||||
}
|
||||
|
||||
newOlm.registerAPICallbacks()
|
||||
@@ -746,6 +762,36 @@ func (o *Olm) StartTunnel(config TunnelConfig) {
|
||||
|
||||
logger.Debug("Updated hole punch exit nodes: %v", hpExitNodes)
|
||||
|
||||
// pm can be nil here: this callback runs from establishConnection, as
|
||||
// part of the initial token/auth fetch, which happens well before the
|
||||
// server's "olm/wg/connect" message creates the peer manager in
|
||||
// handleConnect - so on a fresh connect there usually isn't one yet.
|
||||
// o.hpBypassEndpoints is still updated unconditionally so it reflects
|
||||
// the current set regardless; flushPendingHolepunchBypassEndpoints
|
||||
// (called from handleConnect once the peer manager exists) pushes
|
||||
// whatever was recorded here into it.
|
||||
pm := o.getPeerManager()
|
||||
newBypassEndpoints := make(map[string]bool, len(hpExitNodes))
|
||||
for _, node := range hpExitNodes {
|
||||
newBypassEndpoints[net.JoinHostPort(node.Endpoint, strconv.Itoa(int(node.RelayPort)))] = true
|
||||
}
|
||||
|
||||
o.hpBypassMu.Lock()
|
||||
if pm != nil {
|
||||
for hostport := range newBypassEndpoints {
|
||||
if !o.hpBypassEndpoints[hostport] {
|
||||
pm.AddGatewayBypassEndpoint(hostport)
|
||||
}
|
||||
}
|
||||
for hostport := range o.hpBypassEndpoints {
|
||||
if !newBypassEndpoints[hostport] {
|
||||
pm.RemoveGatewayBypassEndpoint(hostport)
|
||||
}
|
||||
}
|
||||
}
|
||||
o.hpBypassEndpoints = newBypassEndpoints
|
||||
o.hpBypassMu.Unlock()
|
||||
|
||||
// Start hole punching using the manager
|
||||
logger.Info("Starting hole punch for %d exit nodes", len(exitNodes))
|
||||
if err := o.holePunchManager.StartMultipleExitNodes(hpExitNodes); err != nil {
|
||||
@@ -878,11 +924,20 @@ func (o *Olm) Close() {
|
||||
|
||||
// The WireGuard device and TUN interface are being torn down below, which takes
|
||||
// the exit node peer and its secondary address with them - just clear the
|
||||
// in-memory record so a stale config isn't reused on the next connect.
|
||||
// in-memory record so a stale config isn't reused on the next connect. The
|
||||
// peer manager (and its gateway bypass-route state, including anything
|
||||
// registered for this exit node or for hole-punch nodes below) was already
|
||||
// torn down above, so these resets are purely to avoid stale diffing state
|
||||
// carrying into the next connect, not for route cleanup.
|
||||
o.exitNodeMu.Lock()
|
||||
o.exitNode = nil
|
||||
o.exitNodeResolvedEndpoint = ""
|
||||
o.exitNodeMu.Unlock()
|
||||
|
||||
o.hpBypassMu.Lock()
|
||||
o.hpBypassEndpoints = make(map[string]bool)
|
||||
o.hpBypassMu.Unlock()
|
||||
|
||||
if o.uapiListener != nil {
|
||||
_ = o.uapiListener.Close()
|
||||
o.uapiListener = nil
|
||||
|
||||
+73
-13
@@ -81,6 +81,17 @@ type PeerManager struct {
|
||||
gatewaySiteIds map[int]bool
|
||||
gatewayExcludedIPs map[string]int
|
||||
gatewayControlIP string
|
||||
|
||||
// gatewayExtraEndpoints tracks "host:port" (or already-resolved "ip:port")
|
||||
// endpoints registered by callers outside the normal site-peer lifecycle -
|
||||
// currently hole-punch exit node probing endpoints and a connected exit
|
||||
// node's own WireGuard endpoint (see olm's OnTokenUpdate handler and
|
||||
// connectExitNode) - that must stay off the gateway route the same way a
|
||||
// site peer's own endpoint does, so hole punching / the exit node
|
||||
// connection still originates from the local network rather than looping
|
||||
// through the tunnel. Business intent, independent of gatewayActive - see
|
||||
// AddGatewayBypassEndpoint/RemoveGatewayBypassEndpoint.
|
||||
gatewayExtraEndpoints map[string]bool
|
||||
}
|
||||
|
||||
// gatewayCIDR is the WireGuard AllowedIPs claim key for "this site is the
|
||||
@@ -130,19 +141,20 @@ func normalizeServerRouteDestination(serverIP string) string {
|
||||
// NewPeerManager creates a new PeerManager with an internal PeerMonitor
|
||||
func NewPeerManager(config PeerManagerConfig) *PeerManager {
|
||||
pm := &PeerManager{
|
||||
device: config.Device,
|
||||
peers: make(map[int]SiteConfig),
|
||||
dnsProxy: config.DNSProxy,
|
||||
interfaceName: config.InterfaceName,
|
||||
localIP: config.LocalIP,
|
||||
privateKey: config.PrivateKey,
|
||||
allowedIPOwners: make(map[string]int),
|
||||
allowedIPClaims: make(map[string]map[int]bool),
|
||||
APIServer: config.APIServer,
|
||||
publicDNS: config.PublicDNS,
|
||||
lastOwnerChange: make(map[string]time.Time),
|
||||
gatewaySiteIds: make(map[int]bool),
|
||||
gatewayExcludedIPs: make(map[string]int),
|
||||
device: config.Device,
|
||||
peers: make(map[int]SiteConfig),
|
||||
dnsProxy: config.DNSProxy,
|
||||
interfaceName: config.InterfaceName,
|
||||
localIP: config.LocalIP,
|
||||
privateKey: config.PrivateKey,
|
||||
allowedIPOwners: make(map[string]int),
|
||||
allowedIPClaims: make(map[string]map[int]bool),
|
||||
APIServer: config.APIServer,
|
||||
publicDNS: config.PublicDNS,
|
||||
lastOwnerChange: make(map[string]time.Time),
|
||||
gatewaySiteIds: make(map[int]bool),
|
||||
gatewayExcludedIPs: make(map[string]int),
|
||||
gatewayExtraEndpoints: make(map[string]bool),
|
||||
}
|
||||
|
||||
// Create the peer monitor
|
||||
@@ -302,6 +314,12 @@ func (pm *PeerManager) activateGatewayLocked(controlEndpointHost string) error {
|
||||
}
|
||||
}
|
||||
|
||||
for hostport := range pm.gatewayExtraEndpoints {
|
||||
if ip, ok := pm.resolveEndpointIPLocked(hostport); ok {
|
||||
pm.excludeEndpointLocked(ip)
|
||||
}
|
||||
}
|
||||
|
||||
if err := network.AddGatewayDefaultRoute(pm.interfaceName, pm.localIP); err != nil {
|
||||
return fmt.Errorf("failed to install gateway route: %v", err)
|
||||
}
|
||||
@@ -454,6 +472,48 @@ func (pm *PeerManager) ClearGateway() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddGatewayBypassEndpoint registers hostport (a "host:port" string, or an
|
||||
// already-resolved "ip:port") as needing protection from the gateway
|
||||
// default-route-equivalent, for endpoints outside the normal site-peer
|
||||
// lifecycle - hole-punch exit node probing endpoints and a connected exit
|
||||
// node's own WireGuard endpoint. If gateway mode is currently active, the
|
||||
// bypass route is installed immediately; otherwise this only records intent,
|
||||
// applied the next time gateway activates. Safe to call repeatedly with the
|
||||
// same hostport (idempotent).
|
||||
func (pm *PeerManager) AddGatewayBypassEndpoint(hostport string) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
if pm.gatewayExtraEndpoints[hostport] {
|
||||
return
|
||||
}
|
||||
pm.gatewayExtraEndpoints[hostport] = true
|
||||
|
||||
if pm.gatewayActive {
|
||||
if ip, ok := pm.resolveEndpointIPLocked(hostport); ok {
|
||||
pm.excludeEndpointLocked(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveGatewayBypassEndpoint reverses AddGatewayBypassEndpoint. Safe to call
|
||||
// on a hostport that was never registered (no-op).
|
||||
func (pm *PeerManager) RemoveGatewayBypassEndpoint(hostport string) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
if !pm.gatewayExtraEndpoints[hostport] {
|
||||
return
|
||||
}
|
||||
delete(pm.gatewayExtraEndpoints, hostport)
|
||||
|
||||
if pm.gatewayActive {
|
||||
if ip, ok := pm.resolveEndpointIPLocked(hostport); ok {
|
||||
pm.unexcludeEndpointLocked(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *PeerManager) GetAllPeers() []SiteConfig {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
|
||||
Reference in New Issue
Block a user