diff --git a/olm/data.go b/olm/data.go index 879e61e..587d102 100644 --- a/olm/data.go +++ b/olm/data.go @@ -202,6 +202,10 @@ func (o *Olm) handleSync(msg websocket.WSMessage) { // Sync exit nodes for hole punching o.syncExitNodes(syncData.ExitNodes) + // Reconcile the client's own exit node connection (connect/switch/update + // aliases/disconnect), same as what the initial olm/wg/connect message does + o.syncExitNodeConnection(syncData.ExitNode) + // Build a map of expected peers from the incoming data expectedPeers := make(map[int]peers.SiteConfig) for _, site := range syncData.Sites { diff --git a/olm/exitnode.go b/olm/exitnode.go index 2d781df..c1ca808 100644 --- a/olm/exitnode.go +++ b/olm/exitnode.go @@ -41,6 +41,18 @@ func (o *Olm) connectExitNode(cfg ExitNodeConfig) error { return fmt.Errorf("wireguard device not initialized") } + if o.exitNode != nil && o.exitNode.PublicKey == cfg.PublicKey && + o.exitNode.Endpoint == cfg.Endpoint && o.exitNode.ServerIP == cfg.ServerIP && + o.exitNode.TunnelIP == cfg.TunnelIP { + if !slicesEqual(o.exitNode.Aliases, cfg.Aliases) { + logger.Info("Already connected to exit node %s, updating aliases", cfg.PublicKey) + o.updateExitNodeAliasesLocked(cfg.Aliases) + } else { + logger.Info("Already connected to exit node %s, ignoring duplicate connect message", cfg.PublicKey) + } + return nil + } + if o.exitNode != nil && o.exitNode.PublicKey != cfg.PublicKey { logger.Info("Switching exit nodes, removing previous exit node peer") if err := o.removeExitNodePeerLocked(); err != nil { @@ -159,6 +171,71 @@ func (o *Olm) removeExitNodePeerLocked() error { return nil } +// syncExitNodeConnection reconciles the client's own exit node connection (used +// for site resources hosted on the exit node) with the desired state sent in a +// sync message - connecting, switching, updating aliases, or disconnecting as +// needed. This mirrors what the initial "olm/wg/connect" message does, so a +// client that reconnects with a stale exit node assignment (or none at all) +// converges without needing to fully re-register. +func (o *Olm) syncExitNodeConnection(cfg *ExitNodeConfig) { + if !o.tunnelRunning { + logger.Debug("Tunnel stopped, ignoring exit node sync") + return + } + + if cfg == nil || !cfg.Connect { + if err := o.disconnectExitNode(); err != nil { + logger.Error("Sync: Failed to disconnect from exit node: %v", err) + } + return + } + + if err := o.connectExitNode(*cfg); err != nil { + logger.Error("Sync: Failed to connect to exit node: %v", err) + } +} + +// updateExitNodeAliasesLocked reconciles the currently connected exit node's +// aliases with newAliases, adding new ones before removing stale ones so a +// rename never has a gap in resolution. Must be called with exitNodeMu held. +func (o *Olm) updateExitNodeAliasesLocked(newAliases []string) { + if o.exitNode == nil { + return + } + + added := stringSliceDiff(newAliases, o.exitNode.Aliases) + removed := stringSliceDiff(o.exitNode.Aliases, newAliases) + + serverIP := net.ParseIP(o.exitNode.ServerIP) + if o.dnsProxy != nil && serverIP != nil { + for _, alias := range added { + if err := o.dnsProxy.AddDNSRecord(alias, serverIP, exitNodeAliasSiteId); err != nil { + logger.Warn("Failed to add DNS record for exit node alias %s: %v", alias, err) + } + } + for _, alias := range removed { + o.dnsProxy.RemoveDNSRecordForSite(alias, serverIP, exitNodeAliasSiteId) + } + } + + o.exitNode.Aliases = applyStringListUpdate(o.exitNode.Aliases, removed, added) +} + +// stringSliceDiff returns the elements of a that are not present in b. +func stringSliceDiff(a, b []string) []string { + inB := make(map[string]struct{}, len(b)) + for _, s := range b { + inB[s] = struct{}{} + } + diff := make([]string, 0, len(a)) + for _, s := range a { + if _, ok := inB[s]; !ok { + diff = append(diff, s) + } + } + return diff +} + // handleExitNodeConnect handles a server-initiated request to connect to (or switch to) // an exit node, delivered as a full ExitNodeConfig payload. func (o *Olm) handleExitNodeConnect(msg websocket.WSMessage) { diff --git a/olm/types.go b/olm/types.go index 22a8a1f..a51f9e9 100644 --- a/olm/types.go +++ b/olm/types.go @@ -43,6 +43,11 @@ type ExitNodeUpdateData struct { type SyncData struct { Sites []peers.SiteConfig `json:"sites"` ExitNodes []SyncExitNode `json:"exitNodes"` + // ExitNode is the exit node the client itself is assigned to (for site + // resources hosted on it, e.g. inference), mirroring the ExitNode field + // on WgData sent at registration. It is separate from ExitNodes above, + // which is only the set of exit nodes used for hole punching. + ExitNode *ExitNodeConfig `json:"exitNode,omitempty"` } type SyncExitNode struct {