Handle exit node connection status and sync messages

This commit is contained in:
Owen
2026-08-04 11:49:03 -04:00
parent bdb5870c04
commit af6592d538
3 changed files with 86 additions and 0 deletions

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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 {