From 0c196b9d5d9a35416e264c6599f2634b16764aca Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 26 Jun 2026 23:24:40 -0400 Subject: [PATCH] Add the update endpoints --- main.go | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 207 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index a2ec00e..06049b1 100644 --- a/main.go +++ b/main.go @@ -55,15 +55,16 @@ type BrowserGatewayTarget struct { } type WgData struct { - Endpoint string `json:"endpoint"` - RelayPort uint16 `json:"relayPort"` - PublicKey string `json:"publicKey"` - ServerIP string `json:"serverIP"` - TunnelIP string `json:"tunnelIP"` - Targets TargetsByType `json:"targets"` - HealthCheckTargets []healthcheck.Config `json:"healthCheckTargets"` - BrowserGatewayTargets []BrowserGatewayTarget `json:"browserGatewayTargets"` - ChainId string `json:"chainId"` + Endpoint string `json:"endpoint"` + RelayPort uint16 `json:"relayPort"` + PublicKey string `json:"publicKey"` + ServerIP string `json:"serverIP"` + TunnelIP string `json:"tunnelIP"` + Targets TargetsByType `json:"targets"` + HealthCheckTargets []healthcheck.Config `json:"healthCheckTargets"` + BrowserGatewayTargets []BrowserGatewayTarget `json:"browserGatewayTargets"` + RemoteExitNodeSubnets []string `json:"remoteExitNodeSubnets"` + ChainId string `json:"chainId"` } type TargetsByType struct { @@ -198,6 +199,9 @@ var ( // Native main tunnel flags useNativeMainInterface bool nativeMainInterfaceName string + + // Subnets currently routed through the main tunnel (beyond the server IP) + activeRemoteSubnets []string ) // generateChainId generates a random chain ID for deduplicating round-trip messages. @@ -848,6 +852,21 @@ func runNewtMain(ctx context.Context) { pm = nil } + // Remove native main tunnel routes before closing the device + if useNativeMainInterface { + toRemove := make([]string, 0, len(activeRemoteSubnets)+1) + if wgData.ServerIP != "" { + toRemove = append(toRemove, wgData.ServerIP+"/32") + } + toRemove = append(toRemove, activeRemoteSubnets...) + if len(toRemove) > 0 { + if err := network.RemoveRoutes(toRemove); err != nil { + logger.Warn("Failed to remove native main tunnel routes: %v", err) + } + } + activeRemoteSubnets = nil + } + // Close WireGuard device first - this will automatically close the TUN device if dev != nil { dev.Close() @@ -1029,6 +1048,25 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey( } } + // Add remote exit node subnets: update WireGuard AllowedIPs and (for native + // mode) add kernel routes via the tunnel interface. + activeRemoteSubnets = nil + if len(wgData.RemoteExitNodeSubnets) > 0 { + for _, subnet := range wgData.RemoteExitNodeSubnets { + subnetCfg := fmt.Sprintf("public_key=%s\nallowed_ip=%s", util.FixKey(wgData.PublicKey), subnet) + if err := dev.IpcSet(subnetCfg); err != nil { + logger.Warn("Failed to add AllowedIP %s to main tunnel: %v", subnet, err) + } + } + if useNativeMainInterface { + if routeErr := network.AddRoutes(wgData.RemoteExitNodeSubnets, nativeMainInterfaceName); routeErr != nil { + logger.Warn("Failed to add routes for remote exit node subnets: %v", routeErr) + } + } + activeRemoteSubnets = append([]string{}, wgData.RemoteExitNodeSubnets...) + logger.Debug("Added %d remote exit node subnets", len(wgData.RemoteExitNodeSubnets)) + } + logger.Debug("WireGuard device created. Lets ping the server now...") // Even if pingWithRetry returns an error, it will continue trying in the background @@ -1510,6 +1548,166 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey( } }) + client.RegisterHandler("newt/wg/subnets/add", func(msg websocket.WSMessage) { + logger.Debug("Received subnet add message") + + var data struct { + Subnets []string `json:"subnets"` + } + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Error("Error marshaling subnet add data: %v", err) + return + } + if err := json.Unmarshal(jsonData, &data); err != nil { + logger.Error("Error unmarshaling subnet add data: %v", err) + return + } + if len(data.Subnets) == 0 || dev == nil { + return + } + + for _, subnet := range data.Subnets { + subnetCfg := fmt.Sprintf("public_key=%s\nallowed_ip=%s", util.FixKey(wgData.PublicKey), subnet) + if err := dev.IpcSet(subnetCfg); err != nil { + logger.Warn("Failed to add AllowedIP %s to main tunnel: %v", subnet, err) + } + } + if useNativeMainInterface { + if err := network.AddRoutes(data.Subnets, nativeMainInterfaceName); err != nil { + logger.Warn("Failed to add routes for subnets: %v", err) + } + } + activeRemoteSubnets = append(activeRemoteSubnets, data.Subnets...) + logger.Info("Added %d remote exit node subnets", len(data.Subnets)) + }) + + // newt/wg/subnets/update replaces the entire remote subnet list atomically. + // Matches the server-side "set resources" semantics used by setRemoteExitNodeResources. + client.RegisterHandler("newt/wg/subnets/update", func(msg websocket.WSMessage) { + logger.Debug("Received subnet update message") + + var data struct { + Subnets []string `json:"subnets"` + } + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Error("Error marshaling subnet update data: %v", err) + return + } + if err := json.Unmarshal(jsonData, &data); err != nil { + logger.Error("Error unmarshaling subnet update data: %v", err) + return + } + if dev == nil { + return + } + + // Remove kernel routes for subnets no longer in the list (native mode) + if useNativeMainInterface && len(activeRemoteSubnets) > 0 { + toRemove := make([]string, 0) + newSet := make(map[string]bool, len(data.Subnets)) + for _, s := range data.Subnets { + newSet[s] = true + } + for _, s := range activeRemoteSubnets { + if !newSet[s] { + toRemove = append(toRemove, s) + } + } + if len(toRemove) > 0 { + if err := network.RemoveRoutes(toRemove); err != nil { + logger.Warn("Failed to remove old subnet routes: %v", err) + } + } + } + + // Replace WireGuard AllowedIPs with serverIP/32 + new subnet list + if wgData.PublicKey != "" { + lines := fmt.Sprintf("public_key=%s\nreplace_allowed_ips=true\nallowed_ip=%s/32", + util.FixKey(wgData.PublicKey), wgData.ServerIP) + for _, s := range data.Subnets { + lines += "\nallowed_ip=" + s + } + if err := dev.IpcSet(lines); err != nil { + logger.Warn("Failed to update WireGuard AllowedIPs: %v", err) + } + } + + // Add kernel routes for any newly added subnets (native mode) + if useNativeMainInterface && len(data.Subnets) > 0 { + existing := make(map[string]bool, len(activeRemoteSubnets)) + for _, s := range activeRemoteSubnets { + existing[s] = true + } + toAdd := make([]string, 0) + for _, s := range data.Subnets { + if !existing[s] { + toAdd = append(toAdd, s) + } + } + if len(toAdd) > 0 { + if err := network.AddRoutes(toAdd, nativeMainInterfaceName); err != nil { + logger.Warn("Failed to add new subnet routes: %v", err) + } + } + } + + activeRemoteSubnets = append([]string{}, data.Subnets...) + logger.Info("Updated remote exit node subnets: %d total", len(data.Subnets)) + }) + + client.RegisterHandler("newt/wg/subnets/remove", func(msg websocket.WSMessage) { + logger.Debug("Received subnet remove message") + + var data struct { + Subnets []string `json:"subnets"` + } + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Error("Error marshaling subnet remove data: %v", err) + return + } + if err := json.Unmarshal(jsonData, &data); err != nil { + logger.Error("Error unmarshaling subnet remove data: %v", err) + return + } + if len(data.Subnets) == 0 { + return + } + + if useNativeMainInterface { + if err := network.RemoveRoutes(data.Subnets); err != nil { + logger.Warn("Failed to remove routes for subnets: %v", err) + } + } + + // Rebuild WireGuard AllowedIPs without the removed subnets + toRemove := make(map[string]bool, len(data.Subnets)) + for _, s := range data.Subnets { + toRemove[s] = true + } + remaining := activeRemoteSubnets[:0] + for _, s := range activeRemoteSubnets { + if !toRemove[s] { + remaining = append(remaining, s) + } + } + activeRemoteSubnets = remaining + + if dev != nil && wgData.PublicKey != "" { + lines := fmt.Sprintf("public_key=%s\nreplace_allowed_ips=true\nallowed_ip=%s/32", + util.FixKey(wgData.PublicKey), wgData.ServerIP) + for _, s := range remaining { + lines += "\nallowed_ip=" + s + } + if err := dev.IpcSet(lines); err != nil { + logger.Warn("Failed to update WireGuard AllowedIPs after subnet removal: %v", err) + } + } + logger.Info("Removed %d remote exit node subnets", len(data.Subnets)) + }) + // Register handler for syncing targets (TCP, UDP, and health checks) client.RegisterHandler("newt/sync", func(msg websocket.WSMessage) { logger.Info("Received sync message")