diff --git a/olm/exitnode.go b/olm/exitnode.go index 37dc9a9..2d781df 100644 --- a/olm/exitnode.go +++ b/olm/exitnode.go @@ -208,3 +208,80 @@ func (o *Olm) handleExitNodeDisconnect(msg websocket.WSMessage) { logger.Error("Failed to disconnect from exit node: %v", err) } } + +// handleExitNodeUpdateData handles a server-initiated request to change data +// associated with the currently connected exit node, such as its aliases (e.g. a +// resource was renamed). Unlike site aliases, there is no per-alias address to +// track since every exit node alias resolves to the exit node's own ServerIP. +func (o *Olm) handleExitNodeUpdateData(msg websocket.WSMessage) { + logger.Debug("Received exit node update data message: %v", msg.Data) + + if !o.tunnelRunning { + logger.Debug("Tunnel stopped, ignoring exit node update data message") + return + } + + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Error("Error marshaling exit node update data: %v", err) + return + } + + var update ExitNodeUpdateData + if err := json.Unmarshal(jsonData, &update); err != nil { + logger.Error("Error unmarshaling exit node update data: %v", err) + return + } + + o.exitNodeMu.Lock() + defer o.exitNodeMu.Unlock() + + if o.exitNode == nil { + logger.Debug("Ignoring exit node update data message: no exit node connected") + return + } + + serverIP := net.ParseIP(o.exitNode.ServerIP) + + // Add new aliases BEFORE removing old ones, same as site aliases, so a rename + // that keeps the same underlying address never has a gap in resolution. + if o.dnsProxy != nil && serverIP != nil { + for _, alias := range update.NewAliases { + 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) + } + } + } + + if o.dnsProxy != nil && serverIP != nil { + for _, alias := range update.OldAliases { + o.dnsProxy.RemoveDNSRecordForSite(alias, serverIP, exitNodeAliasSiteId) + } + } + + o.exitNode.Aliases = applyStringListUpdate(o.exitNode.Aliases, update.OldAliases, update.NewAliases) + + logger.Info("Successfully updated exit node data") +} + +// applyStringListUpdate returns list with every entry in removed dropped and every +// entry in added appended, preserving the add-before-remove semantics of the caller. +func applyStringListUpdate(list, removed, added []string) []string { + next := make([]string, 0, len(list)+len(added)) + next = append(next, list...) + next = append(next, added...) + + removedSet := make(map[string]struct{}, len(removed)) + for _, alias := range removed { + removedSet[alias] = struct{}{} + } + + filtered := next[:0] + for _, alias := range next { + if _, ok := removedSet[alias]; ok { + continue + } + filtered = append(filtered, alias) + } + return filtered +} diff --git a/olm/olm.go b/olm/olm.go index ef5573c..1484983 100644 --- a/olm/olm.go +++ b/olm/olm.go @@ -565,6 +565,7 @@ func (o *Olm) StartTunnel(config TunnelConfig) { // Handlers for the server to direct connecting/disconnecting an exit node after registration o.websocket.RegisterHandler("olm/wg/exitnode/connect", o.handleExitNodeConnect) o.websocket.RegisterHandler("olm/wg/exitnode/disconnect", o.handleExitNodeDisconnect) + o.websocket.RegisterHandler("olm/wg/exitnode/data/update", o.handleExitNodeUpdateData) o.websocket.RegisterHandler("olm/ping/exitNodes", func(msg websocket.WSMessage) { logger.Debug("Received exit node ping request") diff --git a/olm/types.go b/olm/types.go index 98c1d35..22a8a1f 100644 --- a/olm/types.go +++ b/olm/types.go @@ -30,6 +30,16 @@ type ExitNodeConfig struct { Aliases []string `json:"aliases,omitempty"` } +// ExitNodeUpdateData describes a change to data associated with the currently +// connected exit node, e.g. when a resource's alias is renamed on the server. +// Aliases have no per-alias address here since every exit node alias resolves +// to the exit node's own ServerIP. More fields can be added here in the +// future as other exit node data becomes updatable. +type ExitNodeUpdateData struct { + OldAliases []string `json:"oldAliases,omitempty"` + NewAliases []string `json:"newAliases,omitempty"` +} + type SyncData struct { Sites []peers.SiteConfig `json:"sites"` ExitNodes []SyncExitNode `json:"exitNodes"`