From e8bd55bed9d9cf31b2c1b21322348ee6132c8621 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 20 Feb 2025 20:04:01 -0500 Subject: [PATCH 01/87] Copy in gerbil wg config --- go.mod | 16 +- go.sum | 26 +++ wg/wg.go | 646 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 684 insertions(+), 4 deletions(-) create mode 100644 wg/wg.go diff --git a/go.mod b/go.mod index 2cc0c19..7812b1b 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,21 @@ require golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 require ( github.com/google/btree v1.1.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - golang.org/x/crypto v0.28.0 // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/mdlayher/genetlink v1.3.2 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.5.1 // indirect + github.com/vishvananda/netlink v1.3.0 // indirect + github.com/vishvananda/netns v0.0.4 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.7.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect ) diff --git a/go.sum b/go.sum index d95ab3a..f453d4f 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,39 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= +github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= @@ -18,5 +42,7 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= diff --git a/wg/wg.go b/wg/wg.go new file mode 100644 index 0000000..ed868ea --- /dev/null +++ b/wg/wg.go @@ -0,0 +1,646 @@ +package wg + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "net" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "sync" + "time" + + "github.com/fosrl/newt/logger" + "github.com/fosrl/newt/websocket" + "github.com/vishvananda/netlink" + "golang.zx2c4.com/wireguard/wgctrl" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +var ( + interfaceName string + listenAddr string + mtuInt int + lastReadings = make(map[string]PeerReading) + mu sync.Mutex +) + +type WgConfig struct { + PrivateKey string `json:"privateKey"` + ListenPort int `json:"listenPort"` + IpAddress string `json:"ipAddress"` + Peers []Peer `json:"peers"` +} + +type Peer struct { + PublicKey string `json:"publicKey"` + AllowedIPs []string `json:"allowedIps"` +} + +type PeerBandwidth struct { + PublicKey string `json:"publicKey"` + BytesIn float64 `json:"bytesIn"` + BytesOut float64 `json:"bytesOut"` +} + +type PeerReading struct { + BytesReceived int64 + BytesTransmitted int64 + LastChecked time.Time +} + +var ( + wgClient *wgctrl.Client +) + +func main() { + var ( + err error + wgconfig WgConfig + remoteConfigURL string + generateAndSaveKeyTo string + reachableAt string + logLevel string + mtu string + ) + + interfaceName = os.Getenv("INTERFACE") + remoteConfigURL = os.Getenv("REMOTE_CONFIG") + listenAddr = os.Getenv("LISTEN") + generateAndSaveKeyTo = os.Getenv("GENERATE_AND_SAVE_KEY_TO") + reachableAt = os.Getenv("REACHABLE_AT") + logLevel = os.Getenv("LOG_LEVEL") + mtu = os.Getenv("MTU") + + if interfaceName == "" { + flag.StringVar(&interfaceName, "interface", "wg0", "Name of the WireGuard interface") + } + if remoteConfigURL == "" { + flag.StringVar(&remoteConfigURL, "remoteConfig", "", "URL to fetch remote configuration") + } + if generateAndSaveKeyTo == "" { + flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "", "Path to save generated private key") + } + if reachableAt == "" { + flag.StringVar(&reachableAt, "reachableAt", "", "Endpoint of the http server to tell remote config about") + } + if logLevel == "" { + flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)") + } + if mtu == "" { + flag.StringVar(&mtu, "mtu", "1280", "MTU of the WireGuard interface") + } + flag.Parse() + + mtuInt, err = strconv.Atoi(mtu) + if err != nil { + logger.Fatal("Failed to parse MTU: %v", err) + } + + var key wgtypes.Key + // if generateAndSaveKeyTo is provided, generate a private key and save it to the file. if the file already exists, load the key from the file + if generateAndSaveKeyTo != "" { + if _, err := os.Stat(generateAndSaveKeyTo); os.IsNotExist(err) { + // generate a new private key + key, err = wgtypes.GeneratePrivateKey() + if err != nil { + logger.Fatal("Failed to generate private key: %v", err) + } + // save the key to the file + err = os.WriteFile(generateAndSaveKeyTo, []byte(key.String()), 0644) + if err != nil { + logger.Fatal("Failed to save private key: %v", err) + } + } else { + keyData, err := os.ReadFile(generateAndSaveKeyTo) + if err != nil { + logger.Fatal("Failed to read private key: %v", err) + } + key, err = wgtypes.ParseKey(string(keyData)) + if err != nil { + logger.Fatal("Failed to parse private key: %v", err) + } + } + } else { + // if no generateAndSaveKeyTo is provided, ensure that the private key is provided + if wgconfig.PrivateKey == "" { + // generate a new one + key, err = wgtypes.GeneratePrivateKey() + if err != nil { + logger.Fatal("Failed to generate private key: %v", err) + } + } + } + + // loop until we get the config + for wgconfig.PrivateKey == "" { + logger.Info("Fetching remote config from %s", remoteConfigURL) + wgconfig, err = loadRemoteConfig(remoteConfigURL, key, reachableAt) + if err != nil { + logger.Error("Failed to load configuration: %v", err) + time.Sleep(5 * time.Second) + continue + } + wgconfig.PrivateKey = key.String() + } + + wgClient, err = wgctrl.New() + if err != nil { + logger.Fatal("Failed to create WireGuard client: %v", err) + } + defer wgClient.Close() + + // Ensure the WireGuard interface exists and is configured + if err := ensureWireguardInterface(wgconfig); err != nil { + logger.Fatal("Failed to ensure WireGuard interface: %v", err) + } + + // Ensure the WireGuard peers exist + ensureWireguardPeers(wgconfig.Peers) + + // go periodicBandwidthCheck(reportBandwidthTo) +} + +func loadRemoteConfig(url string, key wgtypes.Key, reachableAt string) (WgConfig, error) { + var body *bytes.Buffer + if reachableAt == "" { + body = bytes.NewBuffer([]byte(fmt.Sprintf(`{"publicKey": "%s"}`, key.PublicKey().String()))) + } else { + body = bytes.NewBuffer([]byte(fmt.Sprintf(`{"publicKey": "%s", "reachableAt": "%s"}`, key.PublicKey().String(), reachableAt))) + } + resp, err := http.Post(url, "application/json", body) + if err != nil { + // print the error + logger.Error("Error fetching remote config %s: %v", url, err) + return WgConfig{}, err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return WgConfig{}, err + } + + var config WgConfig + err = json.Unmarshal(data, &config) + + return config, err +} + +func ensureWireguardInterface(wgconfig WgConfig) error { + // Check if the WireGuard interface exists + _, err := netlink.LinkByName(interfaceName) + if err != nil { + if _, ok := err.(netlink.LinkNotFoundError); ok { + // Interface doesn't exist, so create it + err = createWireGuardInterface() + if err != nil { + logger.Fatal("Failed to create WireGuard interface: %v", err) + } + logger.Info("Created WireGuard interface %s\n", interfaceName) + } else { + logger.Fatal("Error checking for WireGuard interface: %v", err) + } + } else { + logger.Info("WireGuard interface %s already exists\n", interfaceName) + return nil + } + + // Assign IP address to the interface + err = assignIPAddress(wgconfig.IpAddress) + if err != nil { + logger.Fatal("Failed to assign IP address: %v", err) + } + logger.Info("Assigned IP address %s to interface %s\n", wgconfig.IpAddress, interfaceName) + + // Check if the interface already exists + _, err = wgClient.Device(interfaceName) + if err != nil { + return fmt.Errorf("interface %s does not exist", interfaceName) + } + + // Parse the private key + key, err := wgtypes.ParseKey(wgconfig.PrivateKey) + if err != nil { + return fmt.Errorf("failed to parse private key: %v", err) + } + + // Create a new WireGuard configuration + config := wgtypes.Config{ + PrivateKey: &key, + ListenPort: new(int), + } + *config.ListenPort = wgconfig.ListenPort + + // Create and configure the WireGuard interface + err = wgClient.ConfigureDevice(interfaceName, config) + if err != nil { + return fmt.Errorf("failed to configure WireGuard device: %v", err) + } + + // bring up the interface + link, err := netlink.LinkByName(interfaceName) + if err != nil { + return fmt.Errorf("failed to get interface: %v", err) + } + + if err := netlink.LinkSetMTU(link, mtuInt); err != nil { + return fmt.Errorf("failed to set MTU: %v", err) + } + + if err := netlink.LinkSetUp(link); err != nil { + return fmt.Errorf("failed to bring up interface: %v", err) + } + + if err := ensureMSSClamping(); err != nil { + logger.Warn("Failed to ensure MSS clamping: %v", err) + } + + logger.Info("WireGuard interface %s created and configured", interfaceName) + + return nil +} + +func createWireGuardInterface() error { + wgLink := &netlink.GenericLink{ + LinkAttrs: netlink.LinkAttrs{Name: interfaceName}, + LinkType: "wireguard", + } + return netlink.LinkAdd(wgLink) +} + +func assignIPAddress(ipAddress string) error { + link, err := netlink.LinkByName(interfaceName) + if err != nil { + return fmt.Errorf("failed to get interface: %v", err) + } + + addr, err := netlink.ParseAddr(ipAddress) + if err != nil { + return fmt.Errorf("failed to parse IP address: %v", err) + } + + return netlink.AddrAdd(link, addr) +} + +func ensureWireguardPeers(peers []Peer) error { + // get the current peers + device, err := wgClient.Device(interfaceName) + if err != nil { + return fmt.Errorf("failed to get device: %v", err) + } + + // get the peer public keys + var currentPeers []string + for _, peer := range device.Peers { + currentPeers = append(currentPeers, peer.PublicKey.String()) + } + + // remove any peers that are not in the config + for _, peer := range currentPeers { + found := false + for _, configPeer := range peers { + if peer == configPeer.PublicKey { + found = true + break + } + } + if !found { + err := removePeer(peer) + if err != nil { + return fmt.Errorf("failed to remove peer: %v", err) + } + } + } + + // add any peers that are in the config but not in the current peers + for _, configPeer := range peers { + found := false + for _, peer := range currentPeers { + if configPeer.PublicKey == peer { + found = true + break + } + } + if !found { + err := addPeer(configPeer) + if err != nil { + return fmt.Errorf("failed to add peer: %v", err) + } + } + } + + return nil +} + +func ensureMSSClamping() error { + // Calculate MSS value (MTU - 40 for IPv4 header (20) and TCP header (20)) + mssValue := mtuInt - 40 + + // Rules to be managed - just the chains, we'll construct the full command separately + chains := []string{"INPUT", "OUTPUT", "FORWARD"} + + // First, try to delete any existing rules + for _, chain := range chains { + deleteCmd := exec.Command("/usr/sbin/iptables", + "-t", "mangle", + "-D", chain, + "-p", "tcp", + "--tcp-flags", "SYN,RST", "SYN", + "-j", "TCPMSS", + "--set-mss", fmt.Sprintf("%d", mssValue)) + + logger.Info("Attempting to delete existing MSS clamping rule for chain %s", chain) + + // Try deletion multiple times to handle multiple existing rules + for i := 0; i < 3; i++ { + out, err := deleteCmd.CombinedOutput() + if err != nil { + // Convert exit status 1 to string for better logging + if exitErr, ok := err.(*exec.ExitError); ok { + logger.Debug("Deletion stopped for chain %s: %v (output: %s)", + chain, exitErr.String(), string(out)) + } + break // No more rules to delete + } + logger.Info("Deleted MSS clamping rule for chain %s (attempt %d)", chain, i+1) + } + } + + // Then add the new rules + var errors []error + for _, chain := range chains { + addCmd := exec.Command("/usr/sbin/iptables", + "-t", "mangle", + "-A", chain, + "-p", "tcp", + "--tcp-flags", "SYN,RST", "SYN", + "-j", "TCPMSS", + "--set-mss", fmt.Sprintf("%d", mssValue)) + + logger.Info("Adding MSS clamping rule for chain %s", chain) + + if out, err := addCmd.CombinedOutput(); err != nil { + errMsg := fmt.Sprintf("Failed to add MSS clamping rule for chain %s: %v (output: %s)", + chain, err, string(out)) + logger.Error(errMsg) + errors = append(errors, fmt.Errorf(errMsg)) + continue + } + + // Verify the rule was added + checkCmd := exec.Command("/usr/sbin/iptables", + "-t", "mangle", + "-C", chain, + "-p", "tcp", + "--tcp-flags", "SYN,RST", "SYN", + "-j", "TCPMSS", + "--set-mss", fmt.Sprintf("%d", mssValue)) + + if out, err := checkCmd.CombinedOutput(); err != nil { + errMsg := fmt.Sprintf("Rule verification failed for chain %s: %v (output: %s)", + chain, err, string(out)) + logger.Error(errMsg) + errors = append(errors, fmt.Errorf(errMsg)) + continue + } + + logger.Info("Successfully added and verified MSS clamping rule for chain %s", chain) + } + + // If we encountered any errors, return them combined + if len(errors) > 0 { + var errMsgs []string + for _, err := range errors { + errMsgs = append(errMsgs, err.Error()) + } + return fmt.Errorf("MSS clamping setup encountered errors:\n%s", + strings.Join(errMsgs, "\n")) + } + + return nil +} + +func handleAddPeer(msg websocket.WSMessage) { + var peer Peer + + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Info("Error marshaling data: %v", err) + } + + if err := json.Unmarshal(jsonData, &peer); err != nil { + logger.Info("Error unmarshaling target data: %v", err) + } + + err = addPeer(peer) + if err != nil { + return + } +} + +func addPeer(peer Peer) error { + pubKey, err := wgtypes.ParseKey(peer.PublicKey) + if err != nil { + return fmt.Errorf("failed to parse public key: %v", err) + } + + // parse allowed IPs into array of net.IPNet + var allowedIPs []net.IPNet + for _, ipStr := range peer.AllowedIPs { + _, ipNet, err := net.ParseCIDR(ipStr) + if err != nil { + return fmt.Errorf("failed to parse allowed IP: %v", err) + } + allowedIPs = append(allowedIPs, *ipNet) + } + + peerConfig := wgtypes.PeerConfig{ + PublicKey: pubKey, + AllowedIPs: allowedIPs, + } + + config := wgtypes.Config{ + Peers: []wgtypes.PeerConfig{peerConfig}, + } + + if err := wgClient.ConfigureDevice(interfaceName, config); err != nil { + return fmt.Errorf("failed to add peer: %v", err) + } + + logger.Info("Peer %s added successfully", peer.PublicKey) + + return nil +} + +func handleRemovePeer(msg websocket.WSMessage) { + // parse the publicKey from the message which is json { "publicKey": "asdfasdfl;akjsdf" } + type RemoveRequest struct { + PublicKey string `json:"publicKey"` + } + + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Info("Error marshaling data: %v", err) + } + + var request RemoveRequest + if err := json.Unmarshal(jsonData, &request); err != nil { + logger.Info("Error unmarshaling data: %v", err) + return + } + + if err := removePeer(request.PublicKey); err != nil { + logger.Info("Error removing peer: %v", err) + return + } +} + +func removePeer(publicKey string) error { + pubKey, err := wgtypes.ParseKey(publicKey) + if err != nil { + return fmt.Errorf("failed to parse public key: %v", err) + } + + peerConfig := wgtypes.PeerConfig{ + PublicKey: pubKey, + Remove: true, + } + + config := wgtypes.Config{ + Peers: []wgtypes.PeerConfig{peerConfig}, + } + + if err := wgClient.ConfigureDevice(interfaceName, config); err != nil { + return fmt.Errorf("failed to remove peer: %v", err) + } + + logger.Info("Peer %s removed successfully", publicKey) + + return nil +} + +func periodicBandwidthCheck(endpoint string) { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for range ticker.C { + if err := reportPeerBandwidth(endpoint); err != nil { + logger.Info("Failed to report peer bandwidth: %v", err) + } + } +} + +func calculatePeerBandwidth() ([]PeerBandwidth, error) { + device, err := wgClient.Device(interfaceName) + if err != nil { + return nil, fmt.Errorf("failed to get device: %v", err) + } + + peerBandwidths := []PeerBandwidth{} + now := time.Now() + + mu.Lock() + defer mu.Unlock() + + for _, peer := range device.Peers { + publicKey := peer.PublicKey.String() + currentReading := PeerReading{ + BytesReceived: peer.ReceiveBytes, + BytesTransmitted: peer.TransmitBytes, + LastChecked: now, + } + + var bytesInDiff, bytesOutDiff float64 + lastReading, exists := lastReadings[publicKey] + + if exists { + timeDiff := currentReading.LastChecked.Sub(lastReading.LastChecked).Seconds() + if timeDiff > 0 { + // Calculate bytes transferred since last reading + bytesInDiff = float64(currentReading.BytesReceived - lastReading.BytesReceived) + bytesOutDiff = float64(currentReading.BytesTransmitted - lastReading.BytesTransmitted) + + // Handle counter wraparound (if the counter resets or overflows) + if bytesInDiff < 0 { + bytesInDiff = float64(currentReading.BytesReceived) + } + if bytesOutDiff < 0 { + bytesOutDiff = float64(currentReading.BytesTransmitted) + } + + // Convert to MB + bytesInMB := bytesInDiff / (1024 * 1024) + bytesOutMB := bytesOutDiff / (1024 * 1024) + + peerBandwidths = append(peerBandwidths, PeerBandwidth{ + PublicKey: publicKey, + BytesIn: bytesInMB, + BytesOut: bytesOutMB, + }) + } else { + // If readings are too close together or time hasn't passed, report 0 + peerBandwidths = append(peerBandwidths, PeerBandwidth{ + PublicKey: publicKey, + BytesIn: 0, + BytesOut: 0, + }) + } + } else { + // For first reading of a peer, report 0 to establish baseline + peerBandwidths = append(peerBandwidths, PeerBandwidth{ + PublicKey: publicKey, + BytesIn: 0, + BytesOut: 0, + }) + } + + // Update the last reading + lastReadings[publicKey] = currentReading + } + + // Clean up old peers + for publicKey := range lastReadings { + found := false + for _, peer := range device.Peers { + if peer.PublicKey.String() == publicKey { + found = true + break + } + } + if !found { + delete(lastReadings, publicKey) + } + } + + return peerBandwidths, nil +} + +func reportPeerBandwidth(apiURL string) error { + bandwidths, err := calculatePeerBandwidth() + if err != nil { + return fmt.Errorf("failed to calculate peer bandwidth: %v", err) + } + + jsonData, err := json.Marshal(bandwidths) + if err != nil { + return fmt.Errorf("failed to marshal bandwidth data: %v", err) + } + + resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to send bandwidth data: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API returned non-OK status: %s", resp.Status) + } + + return nil +} From f69a7f647d8cbfcafc8ee72c4600d9a89c8b4b76 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 20 Feb 2025 20:37:31 -0500 Subject: [PATCH 02/87] Move wg into more of a class --- main.go | 49 +++++++-- wg/wg.go | 295 +++++++++++++++++++++++++++---------------------------- 2 files changed, 184 insertions(+), 160 deletions(-) diff --git a/main.go b/main.go index 786ecbd..9cee671 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "github.com/fosrl/newt/logger" "github.com/fosrl/newt/proxy" "github.com/fosrl/newt/websocket" + "github.com/fosrl/newt/wg" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" @@ -246,15 +247,18 @@ func resolveDomain(domain string) (string, error) { func main() { var ( - endpoint string - id string - secret string - mtu string - mtuInt int - dns string - privateKey wgtypes.Key - err error - logLevel string + endpoint string + id string + secret string + mtu string + mtuInt int + dns string + privateKey wgtypes.Key + err error + logLevel string + interfaceName string + generateAndSaveKeyTo string + reachableAt string ) // if PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET are set as environment variables, they will be used as default values @@ -264,6 +268,9 @@ func main() { mtu = os.Getenv("MTU") dns = os.Getenv("DNS") logLevel = os.Getenv("LOG_LEVEL") + interfaceName = os.Getenv("INTERFACE") + generateAndSaveKeyTo = os.Getenv("GENERATE_AND_SAVE_KEY_TO") + reachableAt = os.Getenv("REACHABLE_AT") if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -283,6 +290,15 @@ func main() { if logLevel == "" { flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)") } + if interfaceName == "" { + flag.StringVar(&interfaceName, "interface", "wg-1", "Name of the WireGuard interface") + } + if generateAndSaveKeyTo == "" { + flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "", "Path to save generated private key") + } + if reachableAt == "" { + flag.StringVar(&reachableAt, "reachableAt", "", "Endpoint of the http server to tell remote config about") + } // do a --version check version := flag.Bool("version", false, "Print the version") @@ -319,6 +335,21 @@ func main() { logger.Fatal("Failed to create client: %v", err) } + // Create WireGuard service + wgService, err := wg.NewWireGuardService(interfaceName, mtuInt, reachableAt, generateAndSaveKeyTo, client) + if err != nil { + logger.Fatal("Failed to create WireGuard service: %v", err) + } + // defer wgService.Close() + + // Start the WireGuard service + if err := wgService.Start(); err != nil { + logger.Fatal("Failed to start WireGuard service: %v", err) + } + + // Start bandwidth reporting + wgService.StartBandwidthReporting() + // Create TUN device and network stack var tun tun.Device var tnet *netstack.Net diff --git a/wg/wg.go b/wg/wg.go index ed868ea..dc5e337 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -3,14 +3,10 @@ package wg import ( "bytes" "encoding/json" - "flag" "fmt" - "io" "net" - "net/http" "os" "os/exec" - "strconv" "strings" "sync" "time" @@ -58,147 +54,149 @@ var ( wgClient *wgctrl.Client ) -func main() { - var ( - err error - wgconfig WgConfig - remoteConfigURL string - generateAndSaveKeyTo string - reachableAt string - logLevel string - mtu string - ) +type WireGuardService struct { + interfaceName string + mtu int + client *websocket.Client + wgClient *wgctrl.Client + config WgConfig + key wgtypes.Key + reachableAt string + generateAndSaveKeyTo string + lastReadings map[string]PeerReading + mu sync.Mutex +} - interfaceName = os.Getenv("INTERFACE") - remoteConfigURL = os.Getenv("REMOTE_CONFIG") - listenAddr = os.Getenv("LISTEN") - generateAndSaveKeyTo = os.Getenv("GENERATE_AND_SAVE_KEY_TO") - reachableAt = os.Getenv("REACHABLE_AT") - logLevel = os.Getenv("LOG_LEVEL") - mtu = os.Getenv("MTU") - - if interfaceName == "" { - flag.StringVar(&interfaceName, "interface", "wg0", "Name of the WireGuard interface") - } - if remoteConfigURL == "" { - flag.StringVar(&remoteConfigURL, "remoteConfig", "", "URL to fetch remote configuration") - } - if generateAndSaveKeyTo == "" { - flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "", "Path to save generated private key") - } - if reachableAt == "" { - flag.StringVar(&reachableAt, "reachableAt", "", "Endpoint of the http server to tell remote config about") - } - if logLevel == "" { - flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)") - } - if mtu == "" { - flag.StringVar(&mtu, "mtu", "1280", "MTU of the WireGuard interface") - } - flag.Parse() - - mtuInt, err = strconv.Atoi(mtu) +func NewWireGuardService(interfaceName string, mtu int, reachableAt string, generateAndSaveKeyTo string, wsClient *websocket.Client) (*WireGuardService, error) { + wgClient, err := wgctrl.New() if err != nil { - logger.Fatal("Failed to parse MTU: %v", err) + return nil, fmt.Errorf("failed to create WireGuard client: %v", err) } - var key wgtypes.Key + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + return nil, fmt.Errorf("failed to generate private key: %v", err) + } + + service := &WireGuardService{ + interfaceName: interfaceName, + mtu: mtu, + client: wsClient, + wgClient: wgClient, + key: key, + reachableAt: reachableAt, + generateAndSaveKeyTo: generateAndSaveKeyTo, + lastReadings: make(map[string]PeerReading), + } + + // Register websocket handlers + wsClient.RegisterHandler("wg/peer/config", service.handleConfig) + wsClient.RegisterHandler("wg/peer/add", service.handleAddPeer) + wsClient.RegisterHandler("wg/peer/remove", service.handleRemovePeer) + + // Register connect handler to initiate configuration + wsClient.OnConnect(service.handleConnect) + + return service, nil +} + +func (s *WireGuardService) handleConnect() error { + logger.Debug("Public key: %s", s.key.PublicKey()) + + err := s.client.SendMessage("wg/register", map[string]interface{}{ + "publicKey": fmt.Sprintf("%s", s.key.PublicKey()), + }) + if err != nil { + logger.Error("Failed to send registration message: %v", err) + return err + } + + logger.Info("Sent registration message") + return nil +} + +func (s *WireGuardService) Start() error { + // if generateAndSaveKeyTo is provided, generate a private key and save it to the file. if the file already exists, load the key from the file - if generateAndSaveKeyTo != "" { - if _, err := os.Stat(generateAndSaveKeyTo); os.IsNotExist(err) { - // generate a new private key - key, err = wgtypes.GeneratePrivateKey() - if err != nil { - logger.Fatal("Failed to generate private key: %v", err) - } - // save the key to the file - err = os.WriteFile(generateAndSaveKeyTo, []byte(key.String()), 0644) - if err != nil { - logger.Fatal("Failed to save private key: %v", err) - } - } else { - keyData, err := os.ReadFile(generateAndSaveKeyTo) - if err != nil { - logger.Fatal("Failed to read private key: %v", err) - } - key, err = wgtypes.ParseKey(string(keyData)) - if err != nil { - logger.Fatal("Failed to parse private key: %v", err) - } - } - } else { - // if no generateAndSaveKeyTo is provided, ensure that the private key is provided - if wgconfig.PrivateKey == "" { - // generate a new one - key, err = wgtypes.GeneratePrivateKey() - if err != nil { - logger.Fatal("Failed to generate private key: %v", err) - } - } - } - - // loop until we get the config - for wgconfig.PrivateKey == "" { - logger.Info("Fetching remote config from %s", remoteConfigURL) - wgconfig, err = loadRemoteConfig(remoteConfigURL, key, reachableAt) + if _, err := os.Stat(s.generateAndSaveKeyTo); os.IsNotExist(err) { + // generate a new private key + s.key, err = wgtypes.GeneratePrivateKey() if err != nil { - logger.Error("Failed to load configuration: %v", err) - time.Sleep(5 * time.Second) - continue + logger.Fatal("Failed to generate private key: %v", err) + } + // save the key to the file + err = os.WriteFile(s.generateAndSaveKeyTo, []byte(s.key.String()), 0644) + if err != nil { + logger.Fatal("Failed to save private key: %v", err) } - wgconfig.PrivateKey = key.String() - } - - wgClient, err = wgctrl.New() - if err != nil { - logger.Fatal("Failed to create WireGuard client: %v", err) - } - defer wgClient.Close() - - // Ensure the WireGuard interface exists and is configured - if err := ensureWireguardInterface(wgconfig); err != nil { - logger.Fatal("Failed to ensure WireGuard interface: %v", err) - } - - // Ensure the WireGuard peers exist - ensureWireguardPeers(wgconfig.Peers) - - // go periodicBandwidthCheck(reportBandwidthTo) -} - -func loadRemoteConfig(url string, key wgtypes.Key, reachableAt string) (WgConfig, error) { - var body *bytes.Buffer - if reachableAt == "" { - body = bytes.NewBuffer([]byte(fmt.Sprintf(`{"publicKey": "%s"}`, key.PublicKey().String()))) } else { - body = bytes.NewBuffer([]byte(fmt.Sprintf(`{"publicKey": "%s", "reachableAt": "%s"}`, key.PublicKey().String(), reachableAt))) + keyData, err := os.ReadFile(s.generateAndSaveKeyTo) + if err != nil { + logger.Fatal("Failed to read private key: %v", err) + } + s.key, err = wgtypes.ParseKey(string(keyData)) + if err != nil { + logger.Fatal("Failed to parse private key: %v", err) + } } - resp, err := http.Post(url, "application/json", body) + + // Get initial configuration + err := s.loadRemoteConfig() if err != nil { - // print the error - logger.Error("Error fetching remote config %s: %v", url, err) - return WgConfig{}, err - } - defer resp.Body.Close() - - data, err := io.ReadAll(resp.Body) - if err != nil { - return WgConfig{}, err + return fmt.Errorf("failed to load initial configuration: %v", err) } - var config WgConfig - err = json.Unmarshal(data, &config) - - return config, err + return nil } -func ensureWireguardInterface(wgconfig WgConfig) error { +func (s *WireGuardService) StartBandwidthReporting() { + go s.periodicBandwidthCheck() +} + +func (s *WireGuardService) loadRemoteConfig() error { + body := bytes.NewBuffer([]byte(fmt.Sprintf(`{"publicKey": "%s", "reachableAt": "%s"}`, s.key.PublicKey().String(), s.reachableAt))) + + // send a ws message to the server to get the config + + err := s.client.SendMessage("wg/config/get", body) + if err != nil { + return fmt.Errorf("failed to send config request: %v", err) + } + + return nil +} + +func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { + var config WgConfig + + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Info("Error marshaling data: %v", err) + } + + if err := json.Unmarshal(jsonData, &config); err != nil { + logger.Info("Error unmarshaling target data: %v", err) + } + + s.config = config + + // Ensure the WireGuard interface and peers are configured + if err := s.ensureWireguardInterface(config); err != nil { + logger.Error("Failed to ensure WireGuard interface: %v", err) + } + + if err := s.ensureWireguardPeers(config.Peers); err != nil { + logger.Error("Failed to ensure WireGuard peers: %v", err) + } +} + +func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { // Check if the WireGuard interface exists _, err := netlink.LinkByName(interfaceName) if err != nil { if _, ok := err.(netlink.LinkNotFoundError); ok { // Interface doesn't exist, so create it - err = createWireGuardInterface() + err = s.createWireGuardInterface() if err != nil { logger.Fatal("Failed to create WireGuard interface: %v", err) } @@ -212,7 +210,7 @@ func ensureWireguardInterface(wgconfig WgConfig) error { } // Assign IP address to the interface - err = assignIPAddress(wgconfig.IpAddress) + err = s.assignIPAddress(wgconfig.IpAddress) if err != nil { logger.Fatal("Failed to assign IP address: %v", err) } @@ -257,7 +255,7 @@ func ensureWireguardInterface(wgconfig WgConfig) error { return fmt.Errorf("failed to bring up interface: %v", err) } - if err := ensureMSSClamping(); err != nil { + if err := s.ensureMSSClamping(); err != nil { logger.Warn("Failed to ensure MSS clamping: %v", err) } @@ -266,7 +264,7 @@ func ensureWireguardInterface(wgconfig WgConfig) error { return nil } -func createWireGuardInterface() error { +func (s *WireGuardService) createWireGuardInterface() error { wgLink := &netlink.GenericLink{ LinkAttrs: netlink.LinkAttrs{Name: interfaceName}, LinkType: "wireguard", @@ -274,7 +272,7 @@ func createWireGuardInterface() error { return netlink.LinkAdd(wgLink) } -func assignIPAddress(ipAddress string) error { +func (s *WireGuardService) assignIPAddress(ipAddress string) error { link, err := netlink.LinkByName(interfaceName) if err != nil { return fmt.Errorf("failed to get interface: %v", err) @@ -288,7 +286,7 @@ func assignIPAddress(ipAddress string) error { return netlink.AddrAdd(link, addr) } -func ensureWireguardPeers(peers []Peer) error { +func (s *WireGuardService) ensureWireguardPeers(peers []Peer) error { // get the current peers device, err := wgClient.Device(interfaceName) if err != nil { @@ -311,7 +309,7 @@ func ensureWireguardPeers(peers []Peer) error { } } if !found { - err := removePeer(peer) + err := s.removePeer(peer) if err != nil { return fmt.Errorf("failed to remove peer: %v", err) } @@ -328,7 +326,7 @@ func ensureWireguardPeers(peers []Peer) error { } } if !found { - err := addPeer(configPeer) + err := s.addPeer(configPeer) if err != nil { return fmt.Errorf("failed to add peer: %v", err) } @@ -338,7 +336,7 @@ func ensureWireguardPeers(peers []Peer) error { return nil } -func ensureMSSClamping() error { +func (s *WireGuardService) ensureMSSClamping() error { // Calculate MSS value (MTU - 40 for IPv4 header (20) and TCP header (20)) mssValue := mtuInt - 40 @@ -426,7 +424,7 @@ func ensureMSSClamping() error { return nil } -func handleAddPeer(msg websocket.WSMessage) { +func (s *WireGuardService) handleAddPeer(msg websocket.WSMessage) { var peer Peer jsonData, err := json.Marshal(msg.Data) @@ -438,13 +436,13 @@ func handleAddPeer(msg websocket.WSMessage) { logger.Info("Error unmarshaling target data: %v", err) } - err = addPeer(peer) + err = s.addPeer(peer) if err != nil { return } } -func addPeer(peer Peer) error { +func (s *WireGuardService) addPeer(peer Peer) error { pubKey, err := wgtypes.ParseKey(peer.PublicKey) if err != nil { return fmt.Errorf("failed to parse public key: %v", err) @@ -478,7 +476,7 @@ func addPeer(peer Peer) error { return nil } -func handleRemovePeer(msg websocket.WSMessage) { +func (s *WireGuardService) handleRemovePeer(msg websocket.WSMessage) { // parse the publicKey from the message which is json { "publicKey": "asdfasdfl;akjsdf" } type RemoveRequest struct { PublicKey string `json:"publicKey"` @@ -495,13 +493,13 @@ func handleRemovePeer(msg websocket.WSMessage) { return } - if err := removePeer(request.PublicKey); err != nil { + if err := s.removePeer(request.PublicKey); err != nil { logger.Info("Error removing peer: %v", err) return } } -func removePeer(publicKey string) error { +func (s *WireGuardService) removePeer(publicKey string) error { pubKey, err := wgtypes.ParseKey(publicKey) if err != nil { return fmt.Errorf("failed to parse public key: %v", err) @@ -525,18 +523,18 @@ func removePeer(publicKey string) error { return nil } -func periodicBandwidthCheck(endpoint string) { +func (s *WireGuardService) periodicBandwidthCheck() { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for range ticker.C { - if err := reportPeerBandwidth(endpoint); err != nil { + if err := s.reportPeerBandwidth(); err != nil { logger.Info("Failed to report peer bandwidth: %v", err) } } } -func calculatePeerBandwidth() ([]PeerBandwidth, error) { +func (s *WireGuardService) calculatePeerBandwidth() ([]PeerBandwidth, error) { device, err := wgClient.Device(interfaceName) if err != nil { return nil, fmt.Errorf("failed to get device: %v", err) @@ -621,8 +619,8 @@ func calculatePeerBandwidth() ([]PeerBandwidth, error) { return peerBandwidths, nil } -func reportPeerBandwidth(apiURL string) error { - bandwidths, err := calculatePeerBandwidth() +func (s *WireGuardService) reportPeerBandwidth() error { + bandwidths, err := s.calculatePeerBandwidth() if err != nil { return fmt.Errorf("failed to calculate peer bandwidth: %v", err) } @@ -632,15 +630,10 @@ func reportPeerBandwidth(apiURL string) error { return fmt.Errorf("failed to marshal bandwidth data: %v", err) } - resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonData)) + err = s.client.SendMessage("wg/bandwidth", jsonData) if err != nil { return fmt.Errorf("failed to send bandwidth data: %v", err) } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("API returned non-OK status: %s", resp.Status) - } return nil } From 66edae42885341958ca019c9f60cc60c2226d21d Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 20 Feb 2025 21:01:44 -0500 Subject: [PATCH 03/87] Clean up implementation --- main.go | 10 +-- wg/wg.go | 263 ++++++++++++++++++++++++------------------------------- 2 files changed, 116 insertions(+), 157 deletions(-) diff --git a/main.go b/main.go index 9cee671..942acef 100644 --- a/main.go +++ b/main.go @@ -340,15 +340,7 @@ func main() { if err != nil { logger.Fatal("Failed to create WireGuard service: %v", err) } - // defer wgService.Close() - - // Start the WireGuard service - if err := wgService.Start(); err != nil { - logger.Fatal("Failed to start WireGuard service: %v", err) - } - - // Start bandwidth reporting - wgService.StartBandwidthReporting() + defer wgService.Close() // Create TUN device and network stack var tun tun.Device diff --git a/wg/wg.go b/wg/wg.go index dc5e337..4699ed7 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -6,8 +6,6 @@ import ( "fmt" "net" "os" - "os/exec" - "strings" "sync" "time" @@ -55,16 +53,15 @@ var ( ) type WireGuardService struct { - interfaceName string - mtu int - client *websocket.Client - wgClient *wgctrl.Client - config WgConfig - key wgtypes.Key - reachableAt string - generateAndSaveKeyTo string - lastReadings map[string]PeerReading - mu sync.Mutex + interfaceName string + mtu int + client *websocket.Client + wgClient *wgctrl.Client + config WgConfig + key wgtypes.Key + reachableAt string + lastReadings map[string]PeerReading + mu sync.Mutex } func NewWireGuardService(interfaceName string, mtu int, reachableAt string, generateAndSaveKeyTo string, wsClient *websocket.Client) (*WireGuardService, error) { @@ -73,90 +70,60 @@ func NewWireGuardService(interfaceName string, mtu int, reachableAt string, gene return nil, fmt.Errorf("failed to create WireGuard client: %v", err) } - key, err := wgtypes.GeneratePrivateKey() - if err != nil { - return nil, fmt.Errorf("failed to generate private key: %v", err) - } - - service := &WireGuardService{ - interfaceName: interfaceName, - mtu: mtu, - client: wsClient, - wgClient: wgClient, - key: key, - reachableAt: reachableAt, - generateAndSaveKeyTo: generateAndSaveKeyTo, - lastReadings: make(map[string]PeerReading), - } - - // Register websocket handlers - wsClient.RegisterHandler("wg/peer/config", service.handleConfig) - wsClient.RegisterHandler("wg/peer/add", service.handleAddPeer) - wsClient.RegisterHandler("wg/peer/remove", service.handleRemovePeer) - - // Register connect handler to initiate configuration - wsClient.OnConnect(service.handleConnect) - - return service, nil -} - -func (s *WireGuardService) handleConnect() error { - logger.Debug("Public key: %s", s.key.PublicKey()) - - err := s.client.SendMessage("wg/register", map[string]interface{}{ - "publicKey": fmt.Sprintf("%s", s.key.PublicKey()), - }) - if err != nil { - logger.Error("Failed to send registration message: %v", err) - return err - } - - logger.Info("Sent registration message") - return nil -} - -func (s *WireGuardService) Start() error { - + key := wgtypes.Key{} // if generateAndSaveKeyTo is provided, generate a private key and save it to the file. if the file already exists, load the key from the file - if _, err := os.Stat(s.generateAndSaveKeyTo); os.IsNotExist(err) { + if _, err := os.Stat(generateAndSaveKeyTo); os.IsNotExist(err) { // generate a new private key - s.key, err = wgtypes.GeneratePrivateKey() + key, err = wgtypes.GeneratePrivateKey() if err != nil { logger.Fatal("Failed to generate private key: %v", err) } // save the key to the file - err = os.WriteFile(s.generateAndSaveKeyTo, []byte(s.key.String()), 0644) + err = os.WriteFile(generateAndSaveKeyTo, []byte(key.String()), 0644) if err != nil { logger.Fatal("Failed to save private key: %v", err) } } else { - keyData, err := os.ReadFile(s.generateAndSaveKeyTo) + keyData, err := os.ReadFile(generateAndSaveKeyTo) if err != nil { logger.Fatal("Failed to read private key: %v", err) } - s.key, err = wgtypes.ParseKey(string(keyData)) + key, err = wgtypes.ParseKey(string(keyData)) if err != nil { logger.Fatal("Failed to parse private key: %v", err) } } - // Get initial configuration - err := s.loadRemoteConfig() - if err != nil { - return fmt.Errorf("failed to load initial configuration: %v", err) + service := &WireGuardService{ + interfaceName: interfaceName, + mtu: mtu, + client: wsClient, + wgClient: wgClient, + key: key, + reachableAt: reachableAt, + lastReadings: make(map[string]PeerReading), } - return nil + // Register websocket handlers + wsClient.RegisterHandler("wg/config/receive", service.handleConfig) + wsClient.RegisterHandler("wg/peer/add", service.handleAddPeer) + wsClient.RegisterHandler("wg/peer/remove", service.handleRemovePeer) + + // Register connect handler to initiate configuration + wsClient.OnConnect(service.loadRemoteConfig) + + return service, nil } -func (s *WireGuardService) StartBandwidthReporting() { - go s.periodicBandwidthCheck() +func (s *WireGuardService) Close() { + s.client.Close() + wgClient.Close() } func (s *WireGuardService) loadRemoteConfig() error { - body := bytes.NewBuffer([]byte(fmt.Sprintf(`{"publicKey": "%s", "reachableAt": "%s"}`, s.key.PublicKey().String(), s.reachableAt))) + body := bytes.NewBuffer([]byte(fmt.Sprintf(`{"publicKey": "%s", "endpoint": "%s"}`, s.key.PublicKey().String(), s.reachableAt))) - // send a ws message to the server to get the config + go s.periodicBandwidthCheck() err := s.client.SendMessage("wg/config/get", body) if err != nil { @@ -255,9 +222,9 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { return fmt.Errorf("failed to bring up interface: %v", err) } - if err := s.ensureMSSClamping(); err != nil { - logger.Warn("Failed to ensure MSS clamping: %v", err) - } + // if err := s.ensureMSSClamping(); err != nil { + // logger.Warn("Failed to ensure MSS clamping: %v", err) + // } logger.Info("WireGuard interface %s created and configured", interfaceName) @@ -336,93 +303,93 @@ func (s *WireGuardService) ensureWireguardPeers(peers []Peer) error { return nil } -func (s *WireGuardService) ensureMSSClamping() error { - // Calculate MSS value (MTU - 40 for IPv4 header (20) and TCP header (20)) - mssValue := mtuInt - 40 +// func (s *WireGuardService) ensureMSSClamping() error { +// // Calculate MSS value (MTU - 40 for IPv4 header (20) and TCP header (20)) +// mssValue := mtuInt - 40 - // Rules to be managed - just the chains, we'll construct the full command separately - chains := []string{"INPUT", "OUTPUT", "FORWARD"} +// // Rules to be managed - just the chains, we'll construct the full command separately +// chains := []string{"INPUT", "OUTPUT", "FORWARD"} - // First, try to delete any existing rules - for _, chain := range chains { - deleteCmd := exec.Command("/usr/sbin/iptables", - "-t", "mangle", - "-D", chain, - "-p", "tcp", - "--tcp-flags", "SYN,RST", "SYN", - "-j", "TCPMSS", - "--set-mss", fmt.Sprintf("%d", mssValue)) +// // First, try to delete any existing rules +// for _, chain := range chains { +// deleteCmd := exec.Command("/usr/sbin/iptables", +// "-t", "mangle", +// "-D", chain, +// "-p", "tcp", +// "--tcp-flags", "SYN,RST", "SYN", +// "-j", "TCPMSS", +// "--set-mss", fmt.Sprintf("%d", mssValue)) - logger.Info("Attempting to delete existing MSS clamping rule for chain %s", chain) +// logger.Info("Attempting to delete existing MSS clamping rule for chain %s", chain) - // Try deletion multiple times to handle multiple existing rules - for i := 0; i < 3; i++ { - out, err := deleteCmd.CombinedOutput() - if err != nil { - // Convert exit status 1 to string for better logging - if exitErr, ok := err.(*exec.ExitError); ok { - logger.Debug("Deletion stopped for chain %s: %v (output: %s)", - chain, exitErr.String(), string(out)) - } - break // No more rules to delete - } - logger.Info("Deleted MSS clamping rule for chain %s (attempt %d)", chain, i+1) - } - } +// // Try deletion multiple times to handle multiple existing rules +// for i := 0; i < 3; i++ { +// out, err := deleteCmd.CombinedOutput() +// if err != nil { +// // Convert exit status 1 to string for better logging +// if exitErr, ok := err.(*exec.ExitError); ok { +// logger.Debug("Deletion stopped for chain %s: %v (output: %s)", +// chain, exitErr.String(), string(out)) +// } +// break // No more rules to delete +// } +// logger.Info("Deleted MSS clamping rule for chain %s (attempt %d)", chain, i+1) +// } +// } - // Then add the new rules - var errors []error - for _, chain := range chains { - addCmd := exec.Command("/usr/sbin/iptables", - "-t", "mangle", - "-A", chain, - "-p", "tcp", - "--tcp-flags", "SYN,RST", "SYN", - "-j", "TCPMSS", - "--set-mss", fmt.Sprintf("%d", mssValue)) +// // Then add the new rules +// var errors []error +// for _, chain := range chains { +// addCmd := exec.Command("/usr/sbin/iptables", +// "-t", "mangle", +// "-A", chain, +// "-p", "tcp", +// "--tcp-flags", "SYN,RST", "SYN", +// "-j", "TCPMSS", +// "--set-mss", fmt.Sprintf("%d", mssValue)) - logger.Info("Adding MSS clamping rule for chain %s", chain) +// logger.Info("Adding MSS clamping rule for chain %s", chain) - if out, err := addCmd.CombinedOutput(); err != nil { - errMsg := fmt.Sprintf("Failed to add MSS clamping rule for chain %s: %v (output: %s)", - chain, err, string(out)) - logger.Error(errMsg) - errors = append(errors, fmt.Errorf(errMsg)) - continue - } +// if out, err := addCmd.CombinedOutput(); err != nil { +// errMsg := fmt.Sprintf("Failed to add MSS clamping rule for chain %s: %v (output: %s)", +// chain, err, string(out)) +// logger.Error(errMsg) +// errors = append(errors, fmt.Errorf(errMsg)) +// continue +// } - // Verify the rule was added - checkCmd := exec.Command("/usr/sbin/iptables", - "-t", "mangle", - "-C", chain, - "-p", "tcp", - "--tcp-flags", "SYN,RST", "SYN", - "-j", "TCPMSS", - "--set-mss", fmt.Sprintf("%d", mssValue)) +// // Verify the rule was added +// checkCmd := exec.Command("/usr/sbin/iptables", +// "-t", "mangle", +// "-C", chain, +// "-p", "tcp", +// "--tcp-flags", "SYN,RST", "SYN", +// "-j", "TCPMSS", +// "--set-mss", fmt.Sprintf("%d", mssValue)) - if out, err := checkCmd.CombinedOutput(); err != nil { - errMsg := fmt.Sprintf("Rule verification failed for chain %s: %v (output: %s)", - chain, err, string(out)) - logger.Error(errMsg) - errors = append(errors, fmt.Errorf(errMsg)) - continue - } +// if out, err := checkCmd.CombinedOutput(); err != nil { +// errMsg := fmt.Sprintf("Rule verification failed for chain %s: %v (output: %s)", +// chain, err, string(out)) +// logger.Error(errMsg) +// errors = append(errors, fmt.Errorf(errMsg)) +// continue +// } - logger.Info("Successfully added and verified MSS clamping rule for chain %s", chain) - } +// logger.Info("Successfully added and verified MSS clamping rule for chain %s", chain) +// } - // If we encountered any errors, return them combined - if len(errors) > 0 { - var errMsgs []string - for _, err := range errors { - errMsgs = append(errMsgs, err.Error()) - } - return fmt.Errorf("MSS clamping setup encountered errors:\n%s", - strings.Join(errMsgs, "\n")) - } +// // If we encountered any errors, return them combined +// if len(errors) > 0 { +// var errMsgs []string +// for _, err := range errors { +// errMsgs = append(errMsgs, err.Error()) +// } +// return fmt.Errorf("MSS clamping setup encountered errors:\n%s", +// strings.Join(errMsgs, "\n")) +// } - return nil -} +// return nil +// } func (s *WireGuardService) handleAddPeer(msg websocket.WSMessage) { var peer Peer From fb199cc94be248891effff07cabd42c35415f7df Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 20 Feb 2025 22:07:27 -0500 Subject: [PATCH 04/87] Tidy --- go.mod | 15 ++++++++------- go.sum | 12 ++---------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 7812b1b..08b4716 100644 --- a/go.mod +++ b/go.mod @@ -4,25 +4,26 @@ go 1.23.1 toolchain go1.23.2 -require golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 +require ( + github.com/gorilla/websocket v1.5.3 + github.com/vishvananda/netlink v1.3.0 + golang.org/x/net v0.33.0 + golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 + gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 +) require ( github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/josharian/native v1.1.0 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.1 // indirect - github.com/vishvananda/netlink v1.3.0 // indirect github.com/vishvananda/netns v0.0.4 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect - golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.7.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect - gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect ) diff --git a/go.sum b/go.sum index f453d4f..2f56ede 100644 --- a/go.sum +++ b/go.sum @@ -12,26 +12,20 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= @@ -40,8 +34,6 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= -golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= -golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= From 45a1ab91d7f68acaf223aef4d619d5ebcf26bc1d Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 20 Feb 2025 22:10:02 -0500 Subject: [PATCH 05/87] Dont always do wg --- main.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 942acef..29e25ee 100644 --- a/main.go +++ b/main.go @@ -335,12 +335,14 @@ func main() { logger.Fatal("Failed to create client: %v", err) } - // Create WireGuard service - wgService, err := wg.NewWireGuardService(interfaceName, mtuInt, reachableAt, generateAndSaveKeyTo, client) - if err != nil { - logger.Fatal("Failed to create WireGuard service: %v", err) + if reachableAt != "" { + // Create WireGuard service + wgService, err := wg.NewWireGuardService(interfaceName, mtuInt, reachableAt, generateAndSaveKeyTo, client) + if err != nil { + logger.Fatal("Failed to create WireGuard service: %v", err) + } + defer wgService.Close() } - defer wgService.Close() // Create TUN device and network stack var tun tun.Device @@ -417,7 +419,7 @@ func main() { public_key=%s allowed_ip=%s/32 endpoint=%s -persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(wgData.PublicKey), wgData.ServerIP, endpoint) +persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.PublicKey), wgData.ServerIP, endpoint) err = dev.IpcSet(config) if err != nil { @@ -549,7 +551,7 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( logger.Debug("Public key: %s", publicKey) err := client.SendMessage("newt/wg/register", map[string]interface{}{ - "publicKey": fmt.Sprintf("%s", publicKey), + "publicKey": publicKey.PublicKey(), }) if err != nil { logger.Error("Failed to send registration message: %v", err) From 56e75902e3941db8bbeac406fae0065d6ede55aa Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 21 Feb 2025 12:44:52 -0500 Subject: [PATCH 06/87] Adjust ws types --- main.go | 2 +- wg/wg.go | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index 29e25ee..0592a51 100644 --- a/main.go +++ b/main.go @@ -55,7 +55,7 @@ func fixKey(key string) string { // Decode from base64 decoded, err := base64.StdEncoding.DecodeString(key) if err != nil { - logger.Fatal("Error decoding base64:", err) + logger.Fatal("Error decoding base64") } // Convert to hex diff --git a/wg/wg.go b/wg/wg.go index 4699ed7..7e19958 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -18,7 +18,6 @@ import ( var ( interfaceName string - listenAddr string mtuInt int lastReadings = make(map[string]PeerReading) mu sync.Mutex @@ -61,7 +60,6 @@ type WireGuardService struct { key wgtypes.Key reachableAt string lastReadings map[string]PeerReading - mu sync.Mutex } func NewWireGuardService(interfaceName string, mtu int, reachableAt string, generateAndSaveKeyTo string, wsClient *websocket.Client) (*WireGuardService, error) { @@ -70,7 +68,7 @@ func NewWireGuardService(interfaceName string, mtu int, reachableAt string, gene return nil, fmt.Errorf("failed to create WireGuard client: %v", err) } - key := wgtypes.Key{} + var key wgtypes.Key // if generateAndSaveKeyTo is provided, generate a private key and save it to the file. if the file already exists, load the key from the file if _, err := os.Stat(generateAndSaveKeyTo); os.IsNotExist(err) { // generate a new private key @@ -105,9 +103,9 @@ func NewWireGuardService(interfaceName string, mtu int, reachableAt string, gene } // Register websocket handlers - wsClient.RegisterHandler("wg/config/receive", service.handleConfig) - wsClient.RegisterHandler("wg/peer/add", service.handleAddPeer) - wsClient.RegisterHandler("wg/peer/remove", service.handleRemovePeer) + wsClient.RegisterHandler("newt/wg/receive-config", service.handleConfig) + wsClient.RegisterHandler("newt/wg/peer/add", service.handleAddPeer) + wsClient.RegisterHandler("newt/wg/peer/remove", service.handleRemovePeer) // Register connect handler to initiate configuration wsClient.OnConnect(service.loadRemoteConfig) @@ -121,11 +119,11 @@ func (s *WireGuardService) Close() { } func (s *WireGuardService) loadRemoteConfig() error { - body := bytes.NewBuffer([]byte(fmt.Sprintf(`{"publicKey": "%s", "endpoint": "%s"}`, s.key.PublicKey().String(), s.reachableAt))) + body := bytes.NewBuffer([]byte(fmt.Sprintf(`{ "publicKey": "%s", "endpoint": "%s" }`, s.key.PublicKey().String(), s.reachableAt))) go s.periodicBandwidthCheck() - err := s.client.SendMessage("wg/config/get", body) + err := s.client.SendMessage("newt/wg/get-config", body) if err != nil { return fmt.Errorf("failed to send config request: %v", err) } From 95eab504fac0c87077d75152c03c149c3fc23efd Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 21 Feb 2025 16:12:12 -0500 Subject: [PATCH 07/87] Get wg working --- main.go | 29 ++++++++++-------- wg/wg.go | 90 ++++++++++++++++++++++++-------------------------------- 2 files changed, 56 insertions(+), 63 deletions(-) diff --git a/main.go b/main.go index 0592a51..1f0b289 100644 --- a/main.go +++ b/main.go @@ -291,7 +291,7 @@ func main() { flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)") } if interfaceName == "" { - flag.StringVar(&interfaceName, "interface", "wg-1", "Name of the WireGuard interface") + flag.StringVar(&interfaceName, "interface", "wg0", "Name of the WireGuard interface") } if generateAndSaveKeyTo == "" { flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "", "Path to save generated private key") @@ -335,15 +335,7 @@ func main() { logger.Fatal("Failed to create client: %v", err) } - if reachableAt != "" { - // Create WireGuard service - wgService, err := wg.NewWireGuardService(interfaceName, mtuInt, reachableAt, generateAndSaveKeyTo, client) - if err != nil { - logger.Fatal("Failed to create WireGuard service: %v", err) - } - defer wgService.Close() - } - + var wgService *wg.WireGuardService // Create TUN device and network stack var tun tun.Device var tnet *netstack.Net @@ -352,6 +344,16 @@ func main() { var connected bool var wgData WgData + if reachableAt != "" { + logger.Info("Sending reachableAt to server: %s", reachableAt) + // Create WireGuard service + wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, reachableAt, generateAndSaveKeyTo, client) + if err != nil { + logger.Fatal("Failed to create WireGuard service: %v", err) + } + defer wgService.Close() + } + client.RegisterHandler("newt/terminate", func(msg websocket.WSMessage) { logger.Info("Received terminate message") if pm != nil { @@ -419,7 +421,7 @@ func main() { public_key=%s allowed_ip=%s/32 endpoint=%s -persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.PublicKey), wgData.ServerIP, endpoint) +persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(wgData.PublicKey), wgData.ServerIP, endpoint) err = dev.IpcSet(config) if err != nil { @@ -439,6 +441,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub if err != nil { // Handle complete failure after all retries logger.Error("Failed to ping %s: %v", wgData.ServerIP, err) + fmt.Sprintf("%s", privateKey) } if !connected { @@ -551,13 +554,15 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Debug("Public key: %s", publicKey) err := client.SendMessage("newt/wg/register", map[string]interface{}{ - "publicKey": publicKey.PublicKey(), + "publicKey": fmt.Sprintf("%s", publicKey), }) if err != nil { logger.Error("Failed to send registration message: %v", err) return err } + wgService.LoadRemoteConfig() + logger.Info("Sent registration message") return nil }) diff --git a/wg/wg.go b/wg/wg.go index 7e19958..6df69a7 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -1,7 +1,6 @@ package wg import ( - "bytes" "encoding/json" "fmt" "net" @@ -16,13 +15,6 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) -var ( - interfaceName string - mtuInt int - lastReadings = make(map[string]PeerReading) - mu sync.Mutex -) - type WgConfig struct { PrivateKey string `json:"privateKey"` ListenPort int `json:"listenPort"` @@ -47,10 +39,6 @@ type PeerReading struct { LastChecked time.Time } -var ( - wgClient *wgctrl.Client -) - type WireGuardService struct { interfaceName string mtu int @@ -60,6 +48,7 @@ type WireGuardService struct { key wgtypes.Key reachableAt string lastReadings map[string]PeerReading + mu sync.Mutex } func NewWireGuardService(interfaceName string, mtu int, reachableAt string, generateAndSaveKeyTo string, wsClient *websocket.Client) (*WireGuardService, error) { @@ -107,27 +96,29 @@ func NewWireGuardService(interfaceName string, mtu int, reachableAt string, gene wsClient.RegisterHandler("newt/wg/peer/add", service.handleAddPeer) wsClient.RegisterHandler("newt/wg/peer/remove", service.handleRemovePeer) - // Register connect handler to initiate configuration - wsClient.OnConnect(service.loadRemoteConfig) - return service, nil } func (s *WireGuardService) Close() { s.client.Close() - wgClient.Close() + s.wgClient.Close() } -func (s *WireGuardService) loadRemoteConfig() error { - body := bytes.NewBuffer([]byte(fmt.Sprintf(`{ "publicKey": "%s", "endpoint": "%s" }`, s.key.PublicKey().String(), s.reachableAt))) +func (s *WireGuardService) LoadRemoteConfig() error { + + err := s.client.SendMessage("newt/wg/get-config", map[string]interface{}{ + "publicKey": fmt.Sprintf("%s", s.key.PublicKey().String()), + "endpoint": s.reachableAt, + }) + if err != nil { + logger.Error("Failed to send registration message: %v", err) + return err + } + + logger.Info("Requesting WireGuard configuration from remote server") go s.periodicBandwidthCheck() - err := s.client.SendMessage("newt/wg/get-config", body) - if err != nil { - return fmt.Errorf("failed to send config request: %v", err) - } - return nil } @@ -157,7 +148,7 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { // Check if the WireGuard interface exists - _, err := netlink.LinkByName(interfaceName) + _, err := netlink.LinkByName(s.interfaceName) if err != nil { if _, ok := err.(netlink.LinkNotFoundError); ok { // Interface doesn't exist, so create it @@ -165,12 +156,12 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { if err != nil { logger.Fatal("Failed to create WireGuard interface: %v", err) } - logger.Info("Created WireGuard interface %s\n", interfaceName) + logger.Info("Created WireGuard interface %s\n", s.interfaceName) } else { logger.Fatal("Error checking for WireGuard interface: %v", err) } } else { - logger.Info("WireGuard interface %s already exists\n", interfaceName) + logger.Info("WireGuard interface %s already exists\n", s.interfaceName) return nil } @@ -179,12 +170,12 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { if err != nil { logger.Fatal("Failed to assign IP address: %v", err) } - logger.Info("Assigned IP address %s to interface %s\n", wgconfig.IpAddress, interfaceName) + logger.Info("Assigned IP address %s to interface %s\n", wgconfig.IpAddress, s.interfaceName) // Check if the interface already exists - _, err = wgClient.Device(interfaceName) + _, err = s.wgClient.Device(s.interfaceName) if err != nil { - return fmt.Errorf("interface %s does not exist", interfaceName) + return fmt.Errorf("interface %s does not exist", s.interfaceName) } // Parse the private key @@ -201,18 +192,18 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { *config.ListenPort = wgconfig.ListenPort // Create and configure the WireGuard interface - err = wgClient.ConfigureDevice(interfaceName, config) + err = s.wgClient.ConfigureDevice(s.interfaceName, config) if err != nil { return fmt.Errorf("failed to configure WireGuard device: %v", err) } // bring up the interface - link, err := netlink.LinkByName(interfaceName) + link, err := netlink.LinkByName(s.interfaceName) if err != nil { return fmt.Errorf("failed to get interface: %v", err) } - if err := netlink.LinkSetMTU(link, mtuInt); err != nil { + if err := netlink.LinkSetMTU(link, s.mtu); err != nil { return fmt.Errorf("failed to set MTU: %v", err) } @@ -224,21 +215,21 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { // logger.Warn("Failed to ensure MSS clamping: %v", err) // } - logger.Info("WireGuard interface %s created and configured", interfaceName) + logger.Info("WireGuard interface %s created and configured", s.interfaceName) return nil } func (s *WireGuardService) createWireGuardInterface() error { wgLink := &netlink.GenericLink{ - LinkAttrs: netlink.LinkAttrs{Name: interfaceName}, + LinkAttrs: netlink.LinkAttrs{Name: s.interfaceName}, LinkType: "wireguard", } return netlink.LinkAdd(wgLink) } func (s *WireGuardService) assignIPAddress(ipAddress string) error { - link, err := netlink.LinkByName(interfaceName) + link, err := netlink.LinkByName(s.interfaceName) if err != nil { return fmt.Errorf("failed to get interface: %v", err) } @@ -253,7 +244,7 @@ func (s *WireGuardService) assignIPAddress(ipAddress string) error { func (s *WireGuardService) ensureWireguardPeers(peers []Peer) error { // get the current peers - device, err := wgClient.Device(interfaceName) + device, err := s.wgClient.Device(s.interfaceName) if err != nil { return fmt.Errorf("failed to get device: %v", err) } @@ -432,7 +423,7 @@ func (s *WireGuardService) addPeer(peer Peer) error { Peers: []wgtypes.PeerConfig{peerConfig}, } - if err := wgClient.ConfigureDevice(interfaceName, config); err != nil { + if err := s.wgClient.ConfigureDevice(s.interfaceName, config); err != nil { return fmt.Errorf("failed to add peer: %v", err) } @@ -479,7 +470,7 @@ func (s *WireGuardService) removePeer(publicKey string) error { Peers: []wgtypes.PeerConfig{peerConfig}, } - if err := wgClient.ConfigureDevice(interfaceName, config); err != nil { + if err := s.wgClient.ConfigureDevice(s.interfaceName, config); err != nil { return fmt.Errorf("failed to remove peer: %v", err) } @@ -500,7 +491,7 @@ func (s *WireGuardService) periodicBandwidthCheck() { } func (s *WireGuardService) calculatePeerBandwidth() ([]PeerBandwidth, error) { - device, err := wgClient.Device(interfaceName) + device, err := s.wgClient.Device(s.interfaceName) if err != nil { return nil, fmt.Errorf("failed to get device: %v", err) } @@ -508,8 +499,8 @@ func (s *WireGuardService) calculatePeerBandwidth() ([]PeerBandwidth, error) { peerBandwidths := []PeerBandwidth{} now := time.Now() - mu.Lock() - defer mu.Unlock() + s.mu.Lock() + defer s.mu.Unlock() for _, peer := range device.Peers { publicKey := peer.PublicKey.String() @@ -520,7 +511,7 @@ func (s *WireGuardService) calculatePeerBandwidth() ([]PeerBandwidth, error) { } var bytesInDiff, bytesOutDiff float64 - lastReading, exists := lastReadings[publicKey] + lastReading, exists := s.lastReadings[publicKey] if exists { timeDiff := currentReading.LastChecked.Sub(lastReading.LastChecked).Seconds() @@ -564,11 +555,11 @@ func (s *WireGuardService) calculatePeerBandwidth() ([]PeerBandwidth, error) { } // Update the last reading - lastReadings[publicKey] = currentReading + s.lastReadings[publicKey] = currentReading } // Clean up old peers - for publicKey := range lastReadings { + for publicKey := range s.lastReadings { found := false for _, peer := range device.Peers { if peer.PublicKey.String() == publicKey { @@ -577,7 +568,7 @@ func (s *WireGuardService) calculatePeerBandwidth() ([]PeerBandwidth, error) { } } if !found { - delete(lastReadings, publicKey) + delete(s.lastReadings, publicKey) } } @@ -590,12 +581,9 @@ func (s *WireGuardService) reportPeerBandwidth() error { return fmt.Errorf("failed to calculate peer bandwidth: %v", err) } - jsonData, err := json.Marshal(bandwidths) - if err != nil { - return fmt.Errorf("failed to marshal bandwidth data: %v", err) - } - - err = s.client.SendMessage("wg/bandwidth", jsonData) + err = s.client.SendMessage("newt/receive-bandwidth", map[string]interface{}{ + "bandwidthData": bandwidths, + }) if err != nil { return fmt.Errorf("failed to send bandwidth data: %v", err) } From bff6707577a830860a10aa174065b00057d909f7 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 21 Feb 2025 16:20:03 -0500 Subject: [PATCH 08/87] Basic create wg seems to be working --- main.go | 2 +- websocket/client.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 1f0b289..8e81054 100644 --- a/main.go +++ b/main.go @@ -291,7 +291,7 @@ func main() { flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)") } if interfaceName == "" { - flag.StringVar(&interfaceName, "interface", "wg0", "Name of the WireGuard interface") + flag.StringVar(&interfaceName, "interface", "wg1", "Name of the WireGuard interface") } if generateAndSaveKeyTo == "" { flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "", "Path to save generated private key") diff --git a/websocket/client.go b/websocket/client.go index 8a7d3f9..08b9167 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -288,6 +288,7 @@ func (c *Client) establishConnection() error { // Add token to query parameters q := u.Query() q.Set("token", token) + q.Set("clientType", "newt") u.RawQuery = q.Encode() // Connect to WebSocket From 18d99de924309bf91ba5c0684fd64463d11cdeeb Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 21 Feb 2025 17:13:00 -0500 Subject: [PATCH 09/87] Handle messages correctly --- wg/wg.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wg/wg.go b/wg/wg.go index 6df69a7..da08f84 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -125,16 +125,21 @@ func (s *WireGuardService) LoadRemoteConfig() error { func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { var config WgConfig + logger.Info("Received WireGuard configuration") + jsonData, err := json.Marshal(msg.Data) if err != nil { logger.Info("Error marshaling data: %v", err) + return } if err := json.Unmarshal(jsonData, &config); err != nil { logger.Info("Error unmarshaling target data: %v", err) + return } s.config = config + logger.Info("Config: %v", s.config) // Ensure the WireGuard interface and peers are configured if err := s.ensureWireguardInterface(config); err != nil { @@ -165,12 +170,12 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { return nil } + logger.Info("Assigning IP address %s to interface %s\n", wgconfig.IpAddress, s.interfaceName) // Assign IP address to the interface err = s.assignIPAddress(wgconfig.IpAddress) if err != nil { logger.Fatal("Failed to assign IP address: %v", err) } - logger.Info("Assigned IP address %s to interface %s\n", wgconfig.IpAddress, s.interfaceName) // Check if the interface already exists _, err = s.wgClient.Device(s.interfaceName) From 0affef401c0a9994d11e83eea7fb2d903ca8c430 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 21 Feb 2025 18:04:36 -0500 Subject: [PATCH 10/87] Properly handle key --- wg/wg.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/wg/wg.go b/wg/wg.go index da08f84..9f962b5 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -16,7 +16,6 @@ import ( ) type WgConfig struct { - PrivateKey string `json:"privateKey"` ListenPort int `json:"listenPort"` IpAddress string `json:"ipAddress"` Peers []Peer `json:"peers"` @@ -125,7 +124,7 @@ func (s *WireGuardService) LoadRemoteConfig() error { func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { var config WgConfig - logger.Info("Received WireGuard configuration") + logger.Info("Received message: %v", msg) jsonData, err := json.Marshal(msg.Data) if err != nil { @@ -137,9 +136,7 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { logger.Info("Error unmarshaling target data: %v", err) return } - s.config = config - logger.Info("Config: %v", s.config) // Ensure the WireGuard interface and peers are configured if err := s.ensureWireguardInterface(config); err != nil { @@ -184,7 +181,7 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { } // Parse the private key - key, err := wgtypes.ParseKey(wgconfig.PrivateKey) + key, err := wgtypes.ParseKey(s.key.String()) if err != nil { return fmt.Errorf("failed to parse private key: %v", err) } From 270ee9cd190d3a16faa28dbbef833a095a8c4b60 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 21 Feb 2025 20:33:31 -0500 Subject: [PATCH 11/87] Fix panic --- wg/wg.go | 1 - 1 file changed, 1 deletion(-) diff --git a/wg/wg.go b/wg/wg.go index 9f962b5..4f388c1 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -99,7 +99,6 @@ func NewWireGuardService(interfaceName string, mtu int, reachableAt string, gene } func (s *WireGuardService) Close() { - s.client.Close() s.wgClient.Close() } From afa93d8a3fbeb2821c56fc06107101703f113b10 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 21 Feb 2025 22:27:24 -0500 Subject: [PATCH 12/87] Add static port and udp hole punch --- main.go | 2 +- wg/wg.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 8e81054..139da58 100644 --- a/main.go +++ b/main.go @@ -347,7 +347,7 @@ func main() { if reachableAt != "" { logger.Info("Sending reachableAt to server: %s", reachableAt) // Create WireGuard service - wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, reachableAt, generateAndSaveKeyTo, client) + wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, reachableAt, generateAndSaveKeyTo, endpoint, id, client) if err != nil { logger.Fatal("Failed to create WireGuard service: %v", err) } diff --git a/wg/wg.go b/wg/wg.go index 4f388c1..9b3a137 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -11,6 +11,7 @@ import ( "github.com/fosrl/newt/logger" "github.com/fosrl/newt/websocket" "github.com/vishvananda/netlink" + "golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) @@ -46,11 +47,31 @@ type WireGuardService struct { config WgConfig key wgtypes.Key reachableAt string + newtId string lastReadings map[string]PeerReading mu sync.Mutex + port uint16 } -func NewWireGuardService(interfaceName string, mtu int, reachableAt string, generateAndSaveKeyTo string, wsClient *websocket.Client) (*WireGuardService, error) { +// Add this type definition +type fixedPortBind struct { + port uint16 + conn.Bind +} + +func (b *fixedPortBind) Open(port uint16) ([]conn.ReceiveFunc, uint16, error) { + // Ignore the requested port and use our fixed port + return b.Bind.Open(b.port) +} + +func NewFixedPortBind(port uint16) conn.Bind { + return &fixedPortBind{ + port: port, + Bind: conn.NewDefaultBind(), + } +} + +func NewWireGuardService(interfaceName string, mtu int, reachableAt string, generateAndSaveKeyTo string, endpoint string, newtId string, wsClient *websocket.Client) (*WireGuardService, error) { wgClient, err := wgctrl.New() if err != nil { return nil, fmt.Errorf("failed to create WireGuard client: %v", err) @@ -87,7 +108,14 @@ func NewWireGuardService(interfaceName string, mtu int, reachableAt string, gene wgClient: wgClient, key: key, reachableAt: reachableAt, + newtId: newtId, lastReadings: make(map[string]PeerReading), + port: 21821, + } + + if err := service.sendUDPHolePunch(endpoint + ":21820"); err != nil { + logger.Error("Failed to send UDP hole punch: %v", err) + // Continue anyway as this is just for NAT traversal } // Register websocket handlers @@ -185,12 +213,13 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { return fmt.Errorf("failed to parse private key: %v", err) } - // Create a new WireGuard configuration config := wgtypes.Config{ PrivateKey: &key, ListenPort: new(int), } - *config.ListenPort = wgconfig.ListenPort + + // Use the service's fixed port instead of the config port + *config.ListenPort = int(s.port) // Create and configure the WireGuard interface err = s.wgClient.ConfigureDevice(s.interfaceName, config) @@ -591,3 +620,40 @@ func (s *WireGuardService) reportPeerBandwidth() error { return nil } + +func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { + // Bind to specific local port + localAddr := &net.UDPAddr{ + Port: int(s.port), + IP: net.IPv4zero, + } + + remoteAddr, err := net.ResolveUDPAddr("udp", serverAddr) + if err != nil { + return fmt.Errorf("failed to resolve UDP address: %v", err) + } + + conn, err := net.ListenUDP("udp", localAddr) + if err != nil { + return fmt.Errorf("failed to bind UDP socket: %v", err) + } + defer conn.Close() + + payload := struct { + NewtID string `json:"newtId"` + }{ + NewtID: s.newtId, + } + + data, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal payload: %v", err) + } + + _, err = conn.WriteToUDP(data, remoteAddr) + if err != nil { + return fmt.Errorf("failed to send UDP packet: %v", err) + } + + return nil +} From 4aa718d55f27e2a9f7394fe3409bf5c44249e408 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 22 Feb 2025 11:21:13 -0500 Subject: [PATCH 13/87] Initial hp working but need to fix port issue --- main.go | 12 ++++++--- wg/wg.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index 139da58..e08eefd 100644 --- a/main.go +++ b/main.go @@ -344,10 +344,16 @@ func main() { var connected bool var wgData WgData - if reachableAt != "" { - logger.Info("Sending reachableAt to server: %s", reachableAt) + if generateAndSaveKeyTo != "" { + var host = endpoint + if strings.HasPrefix(host, "http://") { + host = strings.TrimPrefix(host, "http://") + } else if strings.HasPrefix(host, "https://") { + host = strings.TrimPrefix(host, "https://") + } + // Create WireGuard service - wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, reachableAt, generateAndSaveKeyTo, endpoint, id, client) + wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, reachableAt, generateAndSaveKeyTo, host, id, client) if err != nil { logger.Fatal("Failed to create WireGuard service: %v", err) } diff --git a/wg/wg.go b/wg/wg.go index 9b3a137..58bf02a 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -25,6 +25,7 @@ type WgConfig struct { type Peer struct { PublicKey string `json:"publicKey"` AllowedIPs []string `json:"allowedIps"` + Endpoint string `json:"endpoint"` } type PeerBandwidth struct { @@ -51,6 +52,7 @@ type WireGuardService struct { lastReadings map[string]PeerReading mu sync.Mutex port uint16 + stopHolepunch chan struct{} } // Add this type definition @@ -71,7 +73,35 @@ func NewFixedPortBind(port uint16) conn.Bind { } } -func NewWireGuardService(interfaceName string, mtu int, reachableAt string, generateAndSaveKeyTo string, endpoint string, newtId string, wsClient *websocket.Client) (*WireGuardService, error) { +func FindAvailableUDPPort(minPort, maxPort uint16) (uint16, error) { + if maxPort < minPort { + return 0, fmt.Errorf("invalid port range: min=%d, max=%d", minPort, maxPort) + } + + for port := minPort; port <= maxPort; port++ { + // Create the UDP address to test + addr := &net.UDPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: int(port), + } + + // Attempt to create a UDP listener + conn, err := net.ListenUDP("udp", addr) + if err != nil { + continue // Port is in use or there was an error, try next port + } + + // Close the connection immediately + _ = conn.SetDeadline(time.Now()) + conn.Close() + + return port, nil + } + + return 0, fmt.Errorf("no available UDP ports found in range %d-%d", minPort, maxPort) +} + +func NewWireGuardService(interfaceName string, mtu int, reachableAt string, generateAndSaveKeyTo string, host string, newtId string, wsClient *websocket.Client) (*WireGuardService, error) { wgClient, err := wgctrl.New() if err != nil { return nil, fmt.Errorf("failed to create WireGuard client: %v", err) @@ -101,6 +131,12 @@ func NewWireGuardService(interfaceName string, mtu int, reachableAt string, gene } } + port, err := FindAvailableUDPPort(49152, 65535) + if err != nil { + fmt.Printf("Error finding available port: %v\n", err) + return nil, err + } + service := &WireGuardService{ interfaceName: interfaceName, mtu: mtu, @@ -110,13 +146,12 @@ func NewWireGuardService(interfaceName string, mtu int, reachableAt string, gene reachableAt: reachableAt, newtId: newtId, lastReadings: make(map[string]PeerReading), - port: 21821, + port: port, + stopHolepunch: make(chan struct{}), } - if err := service.sendUDPHolePunch(endpoint + ":21820"); err != nil { - logger.Error("Failed to send UDP hole punch: %v", err) - // Continue anyway as this is just for NAT traversal - } + // start the UDP holepunch + go service.keepSendingUDPHolePunch(host) // Register websocket handlers wsClient.RegisterHandler("newt/wg/receive-config", service.handleConfig) @@ -443,10 +478,18 @@ func (s *WireGuardService) addPeer(peer Peer) error { } allowedIPs = append(allowedIPs, *ipNet) } + // add keep alive using *time.Duration of 1 second + keepalive := time.Second + endpoint, err := net.ResolveUDPAddr("udp", peer.Endpoint) + if err != nil { + return fmt.Errorf("failed to resolve endpoint address: %w", err) + } peerConfig := wgtypes.PeerConfig{ - PublicKey: pubKey, - AllowedIPs: allowedIPs, + PublicKey: pubKey, + AllowedIPs: allowedIPs, + PersistentKeepaliveInterval: &keepalive, + Endpoint: endpoint, } config := wgtypes.Config{ @@ -657,3 +700,20 @@ func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { return nil } + +func (s *WireGuardService) keepSendingUDPHolePunch(host string) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-s.stopHolepunch: + logger.Info("Stopping UDP holepunch") + return + case <-ticker.C: + if err := s.sendUDPHolePunch(host + ":21820"); err != nil { + logger.Error("Failed to send UDP hole punch: %v", err) + } + } + } +} From 8795c57b2e806a2e4e71ea026f920a7e0006b00a Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 22 Feb 2025 12:53:23 -0500 Subject: [PATCH 14/87] HP works! --- go.mod | 3 ++- go.sum | 4 ++++ wg/wg.go | 32 +++++++++++++++++++++++++------- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 08b4716..c6931ef 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,8 @@ require ( github.com/mdlayher/socket v0.5.1 // indirect github.com/vishvananda/netns v0.0.4 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.7.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect diff --git a/go.sum b/go.sum index 2f56ede..a0deda0 100644 --- a/go.sum +++ b/go.sum @@ -20,10 +20,14 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= diff --git a/wg/wg.go b/wg/wg.go index 58bf02a..6883ca9 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -11,6 +11,7 @@ import ( "github.com/fosrl/newt/logger" "github.com/fosrl/newt/websocket" "github.com/vishvananda/netlink" + "golang.org/x/exp/rand" "golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -78,23 +79,31 @@ func FindAvailableUDPPort(minPort, maxPort uint16) (uint16, error) { return 0, fmt.Errorf("invalid port range: min=%d, max=%d", minPort, maxPort) } - for port := minPort; port <= maxPort; port++ { - // Create the UDP address to test + // Create a slice of all ports in the range + portRange := make([]uint16, maxPort-minPort+1) + for i := range portRange { + portRange[i] = minPort + uint16(i) + } + + // Fisher-Yates shuffle to randomize the port order + rand.Seed(uint64(time.Now().UnixNano())) + for i := len(portRange) - 1; i > 0; i-- { + j := rand.Intn(i + 1) + portRange[i], portRange[j] = portRange[j], portRange[i] + } + + // Try each port in the randomized order + for _, port := range portRange { addr := &net.UDPAddr{ IP: net.ParseIP("127.0.0.1"), Port: int(port), } - - // Attempt to create a UDP listener conn, err := net.ListenUDP("udp", addr) if err != nil { continue // Port is in use or there was an error, try next port } - - // Close the connection immediately _ = conn.SetDeadline(time.Now()) conn.Close() - return port, nil } @@ -150,6 +159,10 @@ func NewWireGuardService(interfaceName string, mtu int, reachableAt string, gene stopHolepunch: make(chan struct{}), } + if err := service.sendUDPHolePunch(host + ":21820"); err != nil { + logger.Error("Failed to send UDP hole punch: %v", err) + } + // start the UDP holepunch go service.keepSendingUDPHolePunch(host) @@ -200,6 +213,9 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { } s.config = config + // stop the holepunch + close(s.stopHolepunch) + // Ensure the WireGuard interface and peers are configured if err := s.ensureWireguardInterface(config); err != nil { logger.Error("Failed to ensure WireGuard interface: %v", err) @@ -698,6 +714,8 @@ func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { return fmt.Errorf("failed to send UDP packet: %v", err) } + logger.Info("Sent UDP hole punch to %s", serverAddr) + return nil } From f6429b6eeeb91db5b308600998cfa06560bd9168 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 23 Feb 2025 00:59:51 -0500 Subject: [PATCH 15/87] Basic holepunch working --- go.mod | 1 + go.sum | 14 ++++ network/network.go | 202 +++++++++++++++++++++++++++++++++++++++++++++ nohup.out | 25 ++++++ wg/wg.go | 159 ++++++++++++----------------------- 5 files changed, 295 insertions(+), 106 deletions(-) create mode 100644 network/network.go create mode 100644 nohup.out diff --git a/go.mod b/go.mod index c6931ef..c9d2752 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( require ( github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gopacket v1.1.19 // indirect github.com/josharian/native v1.1.0 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect diff --git a/go.sum b/go.sum index a0deda0..5e6875a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= @@ -18,22 +20,34 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= diff --git a/network/network.go b/network/network.go new file mode 100644 index 0000000..0703e8b --- /dev/null +++ b/network/network.go @@ -0,0 +1,202 @@ +package network + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "log" + "net" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/vishvananda/netlink" + "golang.org/x/net/bpf" + "golang.org/x/net/ipv4" +) + +const ( + udpProtocol = 17 + // EmptyUDPSize is the size of an empty UDP packet + EmptyUDPSize = 28 + timeout = time.Second * 10 +) + +// Server stores data relating to the server +type Server struct { + Hostname string + Addr *net.IPAddr + Port uint16 +} + +// PeerNet stores data about a peer's endpoint +type PeerNet struct { + Resolved bool + IP net.IP + Port uint16 + NewtID string +} + +// GetClientIP gets source ip address that will be used when sending data to dstIP +func GetClientIP(dstIP net.IP) net.IP { + routes, err := netlink.RouteGet(dstIP) + if err != nil { + log.Fatalln("Error getting route:", err) + } + return routes[0].Src +} + +// HostToAddr resolves a hostname, whether DNS or IP to a valid net.IPAddr +func HostToAddr(hostStr string) *net.IPAddr { + remoteAddrs, err := net.LookupHost(hostStr) + if err != nil { + log.Fatalln("Error parsing remote address:", err) + } + + for _, addrStr := range remoteAddrs { + if remoteAddr, err := net.ResolveIPAddr("ip4", addrStr); err == nil { + return remoteAddr + } + } + return nil +} + +// SetupRawConn creates an ipv4 and udp only RawConn and applies packet filtering +func SetupRawConn(server *Server, client *PeerNet) *ipv4.RawConn { + packetConn, err := net.ListenPacket("ip4:udp", client.IP.String()) + if err != nil { + log.Fatalln("Error creating packetConn:", err) + } + + rawConn, err := ipv4.NewRawConn(packetConn) + if err != nil { + log.Fatalln("Error creating rawConn:", err) + } + + ApplyBPF(rawConn, server, client) + + return rawConn +} + +// ApplyBPF constructs a BPF program and applies it to the RawConn +func ApplyBPF(rawConn *ipv4.RawConn, server *Server, client *PeerNet) { + const ipv4HeaderLen = 20 + const srcIPOffset = 12 + const srcPortOffset = ipv4HeaderLen + 0 + const dstPortOffset = ipv4HeaderLen + 2 + + ipArr := []byte(server.Addr.IP.To4()) + ipInt := uint32(ipArr[0])<<(3*8) + uint32(ipArr[1])<<(2*8) + uint32(ipArr[2])<<8 + uint32(ipArr[3]) + + bpfRaw, err := bpf.Assemble([]bpf.Instruction{ + bpf.LoadAbsolute{Off: srcIPOffset, Size: 4}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: ipInt, SkipFalse: 5, SkipTrue: 0}, + + bpf.LoadAbsolute{Off: srcPortOffset, Size: 2}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(server.Port), SkipFalse: 3, SkipTrue: 0}, + + bpf.LoadAbsolute{Off: dstPortOffset, Size: 2}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(client.Port), SkipFalse: 1, SkipTrue: 0}, + + bpf.RetConstant{Val: 1<<(8*4) - 1}, + bpf.RetConstant{Val: 0}, + }) + + if err != nil { + log.Fatalln("Error assembling BPF:", err) + } + + err = rawConn.SetBPF(bpfRaw) + if err != nil { + log.Fatalln("Error setting BPF:", err) + } +} + +// MakePacket constructs a request packet to send to the server +func MakePacket(payload []byte, server *Server, client *PeerNet) []byte { + buf := gopacket.NewSerializeBuffer() + + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + ipHeader := layers.IPv4{ + SrcIP: client.IP, + DstIP: server.Addr.IP, + Version: 4, + TTL: 64, + Protocol: layers.IPProtocolUDP, + } + + udpHeader := layers.UDP{ + SrcPort: layers.UDPPort(client.Port), + DstPort: layers.UDPPort(server.Port), + } + + payloadLayer := gopacket.Payload(payload) + + udpHeader.SetNetworkLayerForChecksum(&ipHeader) + + gopacket.SerializeLayers(buf, opts, &ipHeader, &udpHeader, &payloadLayer) + + return buf.Bytes() +} + +// SendPacket sends packet to the Server +func SendPacket(packet []byte, conn *ipv4.RawConn, server *Server, client *PeerNet) error { + fullPacket := MakePacket(packet, server, client) + _, err := conn.WriteToIP(fullPacket, server.Addr) + return err +} + +// SendDataPacket sends a JSON payload to the Server +func SendDataPacket(data interface{}, conn *ipv4.RawConn, server *Server, client *PeerNet) error { + jsonData, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("failed to marshal payload: %v", err) + } + + return SendPacket(jsonData, conn, server, client) +} + +// RecvPacket receives a UDP packet from server +func RecvPacket(conn *ipv4.RawConn, server *Server, client *PeerNet) ([]byte, int, error) { + err := conn.SetReadDeadline(time.Now().Add(timeout)) + if err != nil { + return nil, 0, err + } + + response := make([]byte, 4096) + n, err := conn.Read(response) + if err != nil { + return nil, n, err + } + return response, n, nil +} + +// RecvDataPacket receives and unmarshals a JSON packet from server +func RecvDataPacket(conn *ipv4.RawConn, server *Server, client *PeerNet) ([]byte, error) { + response, n, err := RecvPacket(conn, server, client) + if err != nil { + return nil, err + } + + // Extract payload from UDP packet + payload := response[EmptyUDPSize:n] + return payload, nil +} + +// ParseResponse takes a response packet and parses it into an IP and port +func ParseResponse(response []byte) (net.IP, uint16) { + ip := net.IP(response[:4]) + port := binary.BigEndian.Uint16(response[4:6]) + return ip, port +} + +func parseForBPF(response []byte) (srcIP net.IP, srcPort uint16, dstPort uint16) { + srcIP = net.IP(response[12:16]) + srcPort = binary.BigEndian.Uint16(response[20:22]) + dstPort = binary.BigEndian.Uint16(response[22:24]) + return +} diff --git a/nohup.out b/nohup.out new file mode 100644 index 0000000..58bc6f3 --- /dev/null +++ b/nohup.out @@ -0,0 +1,25 @@ +INFO: 2025/02/22 23:25:47 Requesting WireGuard configuration from remote server +INFO: 2025/02/22 23:25:47 Sent registration message +INFO: 2025/02/22 23:25:47 Received message: {newt/wg/receive-config map[ipAddress:100.90.128.1/24 listenPort:51822 peers:[]]} +INFO: 2025/02/22 23:25:47 Created WireGuard interface wg1 +INFO: 2025/02/22 23:25:47 Assigning IP address 100.90.128.1/24 to interface wg1 +INFO: 2025/02/22 23:25:47 WireGuard interface wg1 created and configured +INFO: 2025/02/22 23:25:47 Received registration message +INFO: 2025/02/22 23:25:47 Received: {Type:newt/wg/connect Data:map[endpoint:pangolin.fosrl.io:51820 publicKey:tng9Z/BN32flFjqwwT1yAxN/twFkmgbZA+D9N+YqdjM= serverIP:100.89.128.1 targets:map[tcp:[] udp:[]] tunnelIP:100.89.128.4]} +INFO: 2025/02/22 23:25:47 WireGuard device created. Lets ping the server now... +INFO: 2025/02/22 23:25:47 Ping attempt 1 of 5 +INFO: 2025/02/22 23:25:47 Pinging 100.89.128.1 +INFO: 2025/02/22 23:25:47 Ping latency: 9.00105ms +INFO: 2025/02/22 23:25:47 Starting ping check +INFO: 2025/02/22 23:26:48 Peer P9pacnRfUSfvDibaQTdTk59q27eRpgtbMMmMpkNwKl0= removed successfully +INFO: 2025/02/22 23:26:48 Peer NMrcorGgTTi4tAUZ1lLru0qISNrt9D9JdsFGyDYlcSQ= added successfully +INFO: 2025/02/22 23:28:58 Peer NMrcorGgTTi4tAUZ1lLru0qISNrt9D9JdsFGyDYlcSQ= removed successfully +INFO: 2025/02/22 23:28:58 Peer n8ZKTG8vsROL/OiqHYJELU/Rg9XDifz0YjE/lQsL0m0= added successfully +INFO: 2025/02/22 23:33:59 Peer n8ZKTG8vsROL/OiqHYJELU/Rg9XDifz0YjE/lQsL0m0= removed successfully +INFO: 2025/02/22 23:33:59 Peer /i8YTgrLkZh08HKXLXqNFQJsyg1E8I2ELXqF0zuP9D8= added successfully +INFO: 2025/02/22 23:34:06 Peer /i8YTgrLkZh08HKXLXqNFQJsyg1E8I2ELXqF0zuP9D8= removed successfully +INFO: 2025/02/22 23:34:06 Peer 50+RB00sDoSG+KAKzl/baaqPkKGOe7upX7uqRCKqsRo= added successfully +INFO: 2025/02/22 23:35:07 Peer 50+RB00sDoSG+KAKzl/baaqPkKGOe7upX7uqRCKqsRo= removed successfully +INFO: 2025/02/22 23:35:07 Peer Aa2Y2NEmc+SITlT89+fsOeqDkXJVu9RBY14+77TXa3w= added successfully +INFO: 2025/02/23 00:55:55 Peer Aa2Y2NEmc+SITlT89+fsOeqDkXJVu9RBY14+77TXa3w= removed successfully +INFO: 2025/02/23 00:55:55 Peer 2AXNjMQzT7GGvdbIG6MJVFpO3FIzQ+qCqZkdSnBA3DE= added successfully diff --git a/wg/wg.go b/wg/wg.go index 6883ca9..fa6760f 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -5,10 +5,13 @@ import ( "fmt" "net" "os" + "strconv" + "strings" "sync" "time" "github.com/fosrl/newt/logger" + "github.com/fosrl/newt/network" "github.com/fosrl/newt/websocket" "github.com/vishvananda/netlink" "golang.org/x/exp/rand" @@ -214,7 +217,7 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { s.config = config // stop the holepunch - close(s.stopHolepunch) + // close(s.stopHolepunch) // Ensure the WireGuard interface and peers are configured if err := s.ensureWireguardInterface(config); err != nil { @@ -373,94 +376,6 @@ func (s *WireGuardService) ensureWireguardPeers(peers []Peer) error { return nil } -// func (s *WireGuardService) ensureMSSClamping() error { -// // Calculate MSS value (MTU - 40 for IPv4 header (20) and TCP header (20)) -// mssValue := mtuInt - 40 - -// // Rules to be managed - just the chains, we'll construct the full command separately -// chains := []string{"INPUT", "OUTPUT", "FORWARD"} - -// // First, try to delete any existing rules -// for _, chain := range chains { -// deleteCmd := exec.Command("/usr/sbin/iptables", -// "-t", "mangle", -// "-D", chain, -// "-p", "tcp", -// "--tcp-flags", "SYN,RST", "SYN", -// "-j", "TCPMSS", -// "--set-mss", fmt.Sprintf("%d", mssValue)) - -// logger.Info("Attempting to delete existing MSS clamping rule for chain %s", chain) - -// // Try deletion multiple times to handle multiple existing rules -// for i := 0; i < 3; i++ { -// out, err := deleteCmd.CombinedOutput() -// if err != nil { -// // Convert exit status 1 to string for better logging -// if exitErr, ok := err.(*exec.ExitError); ok { -// logger.Debug("Deletion stopped for chain %s: %v (output: %s)", -// chain, exitErr.String(), string(out)) -// } -// break // No more rules to delete -// } -// logger.Info("Deleted MSS clamping rule for chain %s (attempt %d)", chain, i+1) -// } -// } - -// // Then add the new rules -// var errors []error -// for _, chain := range chains { -// addCmd := exec.Command("/usr/sbin/iptables", -// "-t", "mangle", -// "-A", chain, -// "-p", "tcp", -// "--tcp-flags", "SYN,RST", "SYN", -// "-j", "TCPMSS", -// "--set-mss", fmt.Sprintf("%d", mssValue)) - -// logger.Info("Adding MSS clamping rule for chain %s", chain) - -// if out, err := addCmd.CombinedOutput(); err != nil { -// errMsg := fmt.Sprintf("Failed to add MSS clamping rule for chain %s: %v (output: %s)", -// chain, err, string(out)) -// logger.Error(errMsg) -// errors = append(errors, fmt.Errorf(errMsg)) -// continue -// } - -// // Verify the rule was added -// checkCmd := exec.Command("/usr/sbin/iptables", -// "-t", "mangle", -// "-C", chain, -// "-p", "tcp", -// "--tcp-flags", "SYN,RST", "SYN", -// "-j", "TCPMSS", -// "--set-mss", fmt.Sprintf("%d", mssValue)) - -// if out, err := checkCmd.CombinedOutput(); err != nil { -// errMsg := fmt.Sprintf("Rule verification failed for chain %s: %v (output: %s)", -// chain, err, string(out)) -// logger.Error(errMsg) -// errors = append(errors, fmt.Errorf(errMsg)) -// continue -// } - -// logger.Info("Successfully added and verified MSS clamping rule for chain %s", chain) -// } - -// // If we encountered any errors, return them combined -// if len(errors) > 0 { -// var errMsgs []string -// for _, err := range errors { -// errMsgs = append(errMsgs, err.Error()) -// } -// return fmt.Errorf("MSS clamping setup encountered errors:\n%s", -// strings.Join(errMsgs, "\n")) -// } - -// return nil -// } - func (s *WireGuardService) handleAddPeer(msg websocket.WSMessage) { var peer Peer @@ -681,40 +596,72 @@ func (s *WireGuardService) reportPeerBandwidth() error { } func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { - // Bind to specific local port - localAddr := &net.UDPAddr{ - Port: int(s.port), - IP: net.IPv4zero, + // Parse server address + serverSplit := strings.Split(serverAddr, ":") + if len(serverSplit) < 2 { + return fmt.Errorf("invalid server address format, expected hostname:port") } - remoteAddr, err := net.ResolveUDPAddr("udp", serverAddr) + serverHostname := serverSplit[0] + serverPort, err := strconv.ParseUint(serverSplit[1], 10, 16) if err != nil { - return fmt.Errorf("failed to resolve UDP address: %v", err) + return fmt.Errorf("failed to parse server port: %v", err) } - conn, err := net.ListenUDP("udp", localAddr) - if err != nil { - return fmt.Errorf("failed to bind UDP socket: %v", err) + // Resolve server hostname to IP + serverIPAddr := network.HostToAddr(serverHostname) + if serverIPAddr == nil { + return fmt.Errorf("failed to resolve server hostname") } - defer conn.Close() + // Get client IP based on route to server + clientIP := network.GetClientIP(serverIPAddr.IP) + + // Create server and client configs + server := &network.Server{ + Hostname: serverHostname, + Addr: serverIPAddr, + Port: uint16(serverPort), + } + + client := &network.PeerNet{ + IP: clientIP, + Port: s.port, + NewtID: s.newtId, + } + + // Setup raw connection with BPF filtering + rawConn := network.SetupRawConn(server, client) + defer rawConn.Close() + + // Create JSON payload payload := struct { NewtID string `json:"newtId"` }{ NewtID: s.newtId, } - data, err := json.Marshal(payload) - if err != nil { - return fmt.Errorf("failed to marshal payload: %v", err) - } - - _, err = conn.WriteToUDP(data, remoteAddr) + // Send the packet using the raw connection + err = network.SendDataPacket(payload, rawConn, server, client) if err != nil { return fmt.Errorf("failed to send UDP packet: %v", err) } - logger.Info("Sent UDP hole punch to %s", serverAddr) + // logger.Info("Sent UDP hole punch to %s", serverAddr) + + // // Wait for response if needed + // response, err := network.RecvDataPacket(rawConn, server, client) + // if err != nil { + // if err, ok := err.(net.Error); ok && err.Timeout() { + // return fmt.Errorf("connection to %s timed out", serverAddr) + // } + // return fmt.Errorf("error receiving response: %v", err) + // } + + // // Process response if needed + // if len(response) > 0 { + // logger.Info("Received response from server") + // } return nil } From b68502de9e81d754c57eae3d88fe128a93112026 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 23 Feb 2025 16:49:24 -0500 Subject: [PATCH 16/87] Basic relay working! --- .gitignore | 3 ++- main.go | 14 ++++++----- nohup.out | 25 ------------------ wg/wg.go | 74 +++++++++++++++++++++++++----------------------------- 4 files changed, 44 insertions(+), 72 deletions(-) delete mode 100644 nohup.out diff --git a/.gitignore b/.gitignore index 8b1c477..100fc81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ newt .DS_Store -bin/ \ No newline at end of file +bin/ +nohup.out \ No newline at end of file diff --git a/main.go b/main.go index e08eefd..da80c48 100644 --- a/main.go +++ b/main.go @@ -258,7 +258,6 @@ func main() { logLevel string interfaceName string generateAndSaveKeyTo string - reachableAt string ) // if PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET are set as environment variables, they will be used as default values @@ -270,7 +269,6 @@ func main() { logLevel = os.Getenv("LOG_LEVEL") interfaceName = os.Getenv("INTERFACE") generateAndSaveKeyTo = os.Getenv("GENERATE_AND_SAVE_KEY_TO") - reachableAt = os.Getenv("REACHABLE_AT") if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -296,9 +294,6 @@ func main() { if generateAndSaveKeyTo == "" { flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "", "Path to save generated private key") } - if reachableAt == "" { - flag.StringVar(&reachableAt, "reachableAt", "", "Endpoint of the http server to tell remote config about") - } // do a --version check version := flag.Bool("version", false, "Print the version") @@ -353,7 +348,7 @@ func main() { } // Create WireGuard service - wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, reachableAt, generateAndSaveKeyTo, host, id, client) + wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, generateAndSaveKeyTo, host, id, client) if err != nil { logger.Fatal("Failed to create WireGuard service: %v", err) } @@ -469,6 +464,13 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( updateTargets(pm, "add", wgData.TunnelIP, "udp", TargetData{Targets: wgData.Targets.UDP}) } + // first make sure the wpgService has a port + if wgService != nil { + // add a udp proxy for localost and the wgService port + // TODO: make sure this port is not used in a target + pm.AddTarget("udp", wgData.TunnelIP, int(wgService.Port), fmt.Sprintf("localhost:%d", wgService.Port)) + } + err = pm.Start() if err != nil { logger.Error("Failed to start proxy manager: %v", err) diff --git a/nohup.out b/nohup.out deleted file mode 100644 index 58bc6f3..0000000 --- a/nohup.out +++ /dev/null @@ -1,25 +0,0 @@ -INFO: 2025/02/22 23:25:47 Requesting WireGuard configuration from remote server -INFO: 2025/02/22 23:25:47 Sent registration message -INFO: 2025/02/22 23:25:47 Received message: {newt/wg/receive-config map[ipAddress:100.90.128.1/24 listenPort:51822 peers:[]]} -INFO: 2025/02/22 23:25:47 Created WireGuard interface wg1 -INFO: 2025/02/22 23:25:47 Assigning IP address 100.90.128.1/24 to interface wg1 -INFO: 2025/02/22 23:25:47 WireGuard interface wg1 created and configured -INFO: 2025/02/22 23:25:47 Received registration message -INFO: 2025/02/22 23:25:47 Received: {Type:newt/wg/connect Data:map[endpoint:pangolin.fosrl.io:51820 publicKey:tng9Z/BN32flFjqwwT1yAxN/twFkmgbZA+D9N+YqdjM= serverIP:100.89.128.1 targets:map[tcp:[] udp:[]] tunnelIP:100.89.128.4]} -INFO: 2025/02/22 23:25:47 WireGuard device created. Lets ping the server now... -INFO: 2025/02/22 23:25:47 Ping attempt 1 of 5 -INFO: 2025/02/22 23:25:47 Pinging 100.89.128.1 -INFO: 2025/02/22 23:25:47 Ping latency: 9.00105ms -INFO: 2025/02/22 23:25:47 Starting ping check -INFO: 2025/02/22 23:26:48 Peer P9pacnRfUSfvDibaQTdTk59q27eRpgtbMMmMpkNwKl0= removed successfully -INFO: 2025/02/22 23:26:48 Peer NMrcorGgTTi4tAUZ1lLru0qISNrt9D9JdsFGyDYlcSQ= added successfully -INFO: 2025/02/22 23:28:58 Peer NMrcorGgTTi4tAUZ1lLru0qISNrt9D9JdsFGyDYlcSQ= removed successfully -INFO: 2025/02/22 23:28:58 Peer n8ZKTG8vsROL/OiqHYJELU/Rg9XDifz0YjE/lQsL0m0= added successfully -INFO: 2025/02/22 23:33:59 Peer n8ZKTG8vsROL/OiqHYJELU/Rg9XDifz0YjE/lQsL0m0= removed successfully -INFO: 2025/02/22 23:33:59 Peer /i8YTgrLkZh08HKXLXqNFQJsyg1E8I2ELXqF0zuP9D8= added successfully -INFO: 2025/02/22 23:34:06 Peer /i8YTgrLkZh08HKXLXqNFQJsyg1E8I2ELXqF0zuP9D8= removed successfully -INFO: 2025/02/22 23:34:06 Peer 50+RB00sDoSG+KAKzl/baaqPkKGOe7upX7uqRCKqsRo= added successfully -INFO: 2025/02/22 23:35:07 Peer 50+RB00sDoSG+KAKzl/baaqPkKGOe7upX7uqRCKqsRo= removed successfully -INFO: 2025/02/22 23:35:07 Peer Aa2Y2NEmc+SITlT89+fsOeqDkXJVu9RBY14+77TXa3w= added successfully -INFO: 2025/02/23 00:55:55 Peer Aa2Y2NEmc+SITlT89+fsOeqDkXJVu9RBY14+77TXa3w= removed successfully -INFO: 2025/02/23 00:55:55 Peer 2AXNjMQzT7GGvdbIG6MJVFpO3FIzQ+qCqZkdSnBA3DE= added successfully diff --git a/wg/wg.go b/wg/wg.go index fa6760f..f77bdcd 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -51,12 +51,12 @@ type WireGuardService struct { wgClient *wgctrl.Client config WgConfig key wgtypes.Key - reachableAt string newtId string lastReadings map[string]PeerReading mu sync.Mutex - port uint16 + Port uint16 stopHolepunch chan struct{} + host string } // Add this type definition @@ -113,7 +113,7 @@ func FindAvailableUDPPort(minPort, maxPort uint16) (uint16, error) { return 0, fmt.Errorf("no available UDP ports found in range %d-%d", minPort, maxPort) } -func NewWireGuardService(interfaceName string, mtu int, reachableAt string, generateAndSaveKeyTo string, host string, newtId string, wsClient *websocket.Client) (*WireGuardService, error) { +func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo string, host string, newtId string, wsClient *websocket.Client) (*WireGuardService, error) { wgClient, err := wgctrl.New() if err != nil { return nil, fmt.Errorf("failed to create WireGuard client: %v", err) @@ -155,20 +155,13 @@ func NewWireGuardService(interfaceName string, mtu int, reachableAt string, gene client: wsClient, wgClient: wgClient, key: key, - reachableAt: reachableAt, newtId: newtId, lastReadings: make(map[string]PeerReading), - port: port, + Port: port, stopHolepunch: make(chan struct{}), + host: host, } - if err := service.sendUDPHolePunch(host + ":21820"); err != nil { - logger.Error("Failed to send UDP hole punch: %v", err) - } - - // start the UDP holepunch - go service.keepSendingUDPHolePunch(host) - // Register websocket handlers wsClient.RegisterHandler("newt/wg/receive-config", service.handleConfig) wsClient.RegisterHandler("newt/wg/peer/add", service.handleAddPeer) @@ -185,7 +178,6 @@ func (s *WireGuardService) LoadRemoteConfig() error { err := s.client.SendMessage("newt/wg/get-config", map[string]interface{}{ "publicKey": fmt.Sprintf("%s", s.key.PublicKey().String()), - "endpoint": s.reachableAt, }) if err != nil { logger.Error("Failed to send registration message: %v", err) @@ -216,9 +208,6 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { } s.config = config - // stop the holepunch - // close(s.stopHolepunch) - // Ensure the WireGuard interface and peers are configured if err := s.ensureWireguardInterface(config); err != nil { logger.Error("Failed to ensure WireGuard interface: %v", err) @@ -227,6 +216,13 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { if err := s.ensureWireguardPeers(config.Peers); err != nil { logger.Error("Failed to ensure WireGuard peers: %v", err) } + + if err := s.sendUDPHolePunch(s.host + ":21820"); err != nil { + logger.Error("Failed to send UDP hole punch: %v", err) + } + + // start the UDP holepunch + go s.keepSendingUDPHolePunch(s.host) } func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { @@ -245,6 +241,17 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { } } else { logger.Info("WireGuard interface %s already exists\n", s.interfaceName) + + // get the exising wireguard port + device, err := s.wgClient.Device(s.interfaceName) + if err != nil { + return fmt.Errorf("failed to get device: %v", err) + } + + // get the existing port + s.Port = uint16(device.ListenPort) + logger.Info("WireGuard interface %s already exists with port %d\n", s.interfaceName, s.Port) + return nil } @@ -273,7 +280,7 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { } // Use the service's fixed port instead of the config port - *config.ListenPort = int(s.port) + *config.ListenPort = int(s.Port) // Create and configure the WireGuard interface err = s.wgClient.ConfigureDevice(s.interfaceName, config) @@ -390,6 +397,7 @@ func (s *WireGuardService) handleAddPeer(msg websocket.WSMessage) { err = s.addPeer(peer) if err != nil { + logger.Info("Error adding peer: %v", err) return } } @@ -411,16 +419,18 @@ func (s *WireGuardService) addPeer(peer Peer) error { } // add keep alive using *time.Duration of 1 second keepalive := time.Second - endpoint, err := net.ResolveUDPAddr("udp", peer.Endpoint) - if err != nil { - return fmt.Errorf("failed to resolve endpoint address: %w", err) - } + // endpoint, err := net.ResolveUDPAddr("udp", peer.Endpoint) + // if err != nil { + // return fmt.Errorf("failed to resolve endpoint address: %w", err) + // } + + // make the endpoint localhost to test peerConfig := wgtypes.PeerConfig{ PublicKey: pubKey, AllowedIPs: allowedIPs, PersistentKeepaliveInterval: &keepalive, - Endpoint: endpoint, + // Endpoint: endpoint, } config := wgtypes.Config{ @@ -626,7 +636,7 @@ func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { client := &network.PeerNet{ IP: clientIP, - Port: s.port, + Port: s.Port, NewtID: s.newtId, } @@ -647,27 +657,11 @@ func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { return fmt.Errorf("failed to send UDP packet: %v", err) } - // logger.Info("Sent UDP hole punch to %s", serverAddr) - - // // Wait for response if needed - // response, err := network.RecvDataPacket(rawConn, server, client) - // if err != nil { - // if err, ok := err.(net.Error); ok && err.Timeout() { - // return fmt.Errorf("connection to %s timed out", serverAddr) - // } - // return fmt.Errorf("error receiving response: %v", err) - // } - - // // Process response if needed - // if len(response) > 0 { - // logger.Info("Received response from server") - // } - return nil } func (s *WireGuardService) keepSendingUDPHolePunch(host string) { - ticker := time.NewTicker(1 * time.Second) + ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() for { From cd3ec0b259e8635015303b88ddadb745ab481521 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 23 Feb 2025 20:18:25 -0500 Subject: [PATCH 17/87] Support relay switch --- wg/wg.go | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/wg/wg.go b/wg/wg.go index f77bdcd..bcb7cda 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -419,18 +419,29 @@ func (s *WireGuardService) addPeer(peer Peer) error { } // add keep alive using *time.Duration of 1 second keepalive := time.Second - // endpoint, err := net.ResolveUDPAddr("udp", peer.Endpoint) - // if err != nil { - // return fmt.Errorf("failed to resolve endpoint address: %w", err) - // } - // make the endpoint localhost to test + var peerConfig wgtypes.PeerConfig + if peer.Endpoint != "" { + endpoint, err := net.ResolveUDPAddr("udp", peer.Endpoint) + if err != nil { + return fmt.Errorf("failed to resolve endpoint address: %w", err) + } - peerConfig := wgtypes.PeerConfig{ - PublicKey: pubKey, - AllowedIPs: allowedIPs, - PersistentKeepaliveInterval: &keepalive, - // Endpoint: endpoint, + // make the endpoint localhost to test + + peerConfig = wgtypes.PeerConfig{ + PublicKey: pubKey, + AllowedIPs: allowedIPs, + PersistentKeepaliveInterval: &keepalive, + Endpoint: endpoint, + } + } else { + peerConfig = wgtypes.PeerConfig{ + PublicKey: pubKey, + AllowedIPs: allowedIPs, + PersistentKeepaliveInterval: &keepalive, + } + logger.Info("Added peer with no endpoint!") } config := wgtypes.Config{ From 5e673c829b1d88a7350f851445c1fd593980b4d1 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 24 Feb 2025 10:05:35 -0500 Subject: [PATCH 18/87] Clean up when wg is used --- main.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index da80c48..7618d3b 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "net/netip" "os" "os/signal" + "runtime" "strconv" "strings" "syscall" @@ -340,6 +341,12 @@ func main() { var wgData WgData if generateAndSaveKeyTo != "" { + // make sure we are running on linux + if runtime.GOOS != "linux" { + logger.Fatal("Tunnel management is only supported on Linux right now!") + os.Exit(1) + } + var host = endpoint if strings.HasPrefix(host, "http://") { host = strings.TrimPrefix(host, "http://") @@ -569,7 +576,9 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( return err } - wgService.LoadRemoteConfig() + if wgService != nil { + wgService.LoadRemoteConfig() + } logger.Info("Sent registration message") return nil From 067e07929353c70eb4c43a3c596fe52a7ddf7ccd Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 12 Mar 2025 20:37:57 -0400 Subject: [PATCH 19/87] Handle / better --- main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main.go b/main.go index 7618d3b..f585222 100644 --- a/main.go +++ b/main.go @@ -214,6 +214,9 @@ func resolveDomain(domain string) (string, error) { host = strings.TrimPrefix(host, "https://") } + // if there are any trailing slashes, remove them + host = strings.TrimSuffix(host, "/") + // Lookup IP addresses ips, err := net.LookupIP(host) if err != nil { @@ -354,6 +357,8 @@ func main() { host = strings.TrimPrefix(host, "https://") } + host = strings.TrimSuffix(host, "/") + // Create WireGuard service wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, generateAndSaveKeyTo, host, id, client) if err != nil { From f9e52c4d9199b2e07740000fc93d0a17c8bb749f Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 14 Mar 2025 18:49:50 -0400 Subject: [PATCH 20/87] Working on encryption --- main.go | 2 ++ wg/wg.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index afd6412..0522007 100644 --- a/main.go +++ b/main.go @@ -414,6 +414,8 @@ func main() { return } + wgService.SetServerPubKey(wgData.PublicKey) + logger.Info("Received: %+v", msg) tun, tnet, err = netstack.CreateNetTUN( []netip.Addr{netip.MustParseAddr(wgData.TunnelIP)}, diff --git a/wg/wg.go b/wg/wg.go index bcb7cda..a3f7f9e 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -14,6 +14,8 @@ import ( "github.com/fosrl/newt/network" "github.com/fosrl/newt/websocket" "github.com/vishvananda/netlink" + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/curve25519" "golang.org/x/exp/rand" "golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/wgctrl" @@ -57,6 +59,7 @@ type WireGuardService struct { Port uint16 stopHolepunch chan struct{} host string + serverPubKey string } // Add this type definition @@ -174,6 +177,10 @@ func (s *WireGuardService) Close() { s.wgClient.Close() } +func (s *WireGuardService) SetServerPubKey(serverPubKey string) { + s.serverPubKey = serverPubKey +} + func (s *WireGuardService) LoadRemoteConfig() error { err := s.client.SendMessage("newt/wg/get-config", map[string]interface{}{ @@ -662,8 +669,20 @@ func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { NewtID: s.newtId, } - // Send the packet using the raw connection - err = network.SendDataPacket(payload, rawConn, server, client) + // Convert payload to JSON + payloadBytes, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal payload: %v", err) + } + + // Encrypt the payload using the server's WireGuard public key + encryptedPayload, err := s.encryptPayload(payloadBytes) + if err != nil { + return fmt.Errorf("failed to encrypt payload: %v", err) + } + + // Send the encrypted packet using the raw connection + err = network.SendDataPacket(encryptedPayload, rawConn, server, client) if err != nil { return fmt.Errorf("failed to send UDP packet: %v", err) } @@ -671,6 +690,60 @@ func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { return nil } +// Add a new function to encrypt the payload +func (s *WireGuardService) encryptPayload(payload []byte) (interface{}, error) { + // Generate an ephemeral keypair for this message + ephemeralPrivateKey, err := wgtypes.GeneratePrivateKey() + if err != nil { + return nil, fmt.Errorf("failed to generate ephemeral private key: %v", err) + } + ephemeralPublicKey := ephemeralPrivateKey.PublicKey() + + // Parse the server's public key + serverPubKey, err := wgtypes.ParseKey(s.serverPubKey) + if err != nil { + return nil, fmt.Errorf("failed to parse server public key: %v", err) + } + + // Perform Diffie-Hellman key exchange + var serverPubKeyFixed [32]byte + copy(serverPubKeyFixed[:], serverPubKey[:]) + + var ephPrivKeyFixed [32]byte + copy(ephPrivKeyFixed[:], ephemeralPrivateKey[:]) + + var sharedSecret [32]byte + curve25519.ScalarMult(&sharedSecret, &ephPrivKeyFixed, &serverPubKeyFixed) + + // Create an AEAD cipher using the shared secret + aead, err := chacha20poly1305.New(sharedSecret[:]) + if err != nil { + return nil, fmt.Errorf("failed to create AEAD cipher: %v", err) + } + + // Generate a random nonce + nonce := make([]byte, aead.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return nil, fmt.Errorf("failed to generate nonce: %v", err) + } + + // Encrypt the payload + ciphertext := aead.Seal(nil, nonce, payload, nil) + + // Prepare the final encrypted message + encryptedMsg := struct { + EphemeralPublicKey string `json:"ephemeralPublicKey"` + Nonce []byte `json:"nonce"` + Ciphertext []byte `json:"ciphertext"` + }{ + EphemeralPublicKey: ephemeralPublicKey.String(), + Nonce: nonce, + Ciphertext: ciphertext, + } + + return encryptedMsg, nil +} + func (s *WireGuardService) keepSendingUDPHolePunch(host string) { ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() From 1501de691abc6526594485bce04ef0b6e0e117bc Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 15 Mar 2025 21:47:22 -0400 Subject: [PATCH 21/87] Handle encrypted messages --- main.go | 6 ++++++ websocket/client.go | 9 ++++++++- wg/wg.go | 27 +++++++++++++++++++-------- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index 61e7e33..4708f74 100644 --- a/main.go +++ b/main.go @@ -597,6 +597,12 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( return nil }) + client.OnTokenUpdate(func(token string) { + if wgService != nil { + wgService.SetToken(token) + } + }) + // Connect to the WebSocket server if err := client.Connect(); err != nil { logger.Fatal("Failed to connect to server: %v", err) diff --git a/websocket/client.go b/websocket/client.go index 2706eee..98c9388 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -27,7 +27,8 @@ type Client struct { isConnected bool reconnectMux sync.RWMutex - onConnect func() error + onConnect func() error + onTokenUpdate func(token string) } type ClientOption func(*Client) @@ -45,6 +46,10 @@ func (c *Client) OnConnect(callback func() error) { c.onConnect = callback } +func (c *Client) OnTokenUpdate(callback func(token string)) { + c.onTokenUpdate = callback +} + // NewClient creates a new Newt client func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*Client, error) { config := &Config{ @@ -270,6 +275,8 @@ func (c *Client) establishConnection() error { return fmt.Errorf("failed to get token: %w", err) } + c.onTokenUpdate(token) + // Parse the base URL to determine protocol and hostname baseURL, err := url.Parse(c.baseURL) if err != nil { diff --git a/wg/wg.go b/wg/wg.go index a3f7f9e..7330fdf 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -60,6 +60,7 @@ type WireGuardService struct { stopHolepunch chan struct{} host string serverPubKey string + token string } // Add this type definition @@ -181,6 +182,10 @@ func (s *WireGuardService) SetServerPubKey(serverPubKey string) { s.serverPubKey = serverPubKey } +func (s *WireGuardService) SetToken(token string) { + s.token = token +} + func (s *WireGuardService) LoadRemoteConfig() error { err := s.client.SendMessage("newt/wg/get-config", map[string]interface{}{ @@ -624,6 +629,11 @@ func (s *WireGuardService) reportPeerBandwidth() error { } func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { + + if s.serverPubKey == "" || s.token == "" { + return fmt.Errorf("server public key or token is not set") + } + // Parse server address serverSplit := strings.Split(serverAddr, ":") if len(serverSplit) < 2 { @@ -665,8 +675,10 @@ func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { // Create JSON payload payload := struct { NewtID string `json:"newtId"` + Token string `json:"token"` }{ NewtID: s.newtId, + Token: s.token, } // Convert payload to JSON @@ -690,7 +702,6 @@ func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { return nil } -// Add a new function to encrypt the payload func (s *WireGuardService) encryptPayload(payload []byte) (interface{}, error) { // Generate an ephemeral keypair for this message ephemeralPrivateKey, err := wgtypes.GeneratePrivateKey() @@ -705,18 +716,18 @@ func (s *WireGuardService) encryptPayload(payload []byte) (interface{}, error) { return nil, fmt.Errorf("failed to parse server public key: %v", err) } - // Perform Diffie-Hellman key exchange - var serverPubKeyFixed [32]byte - copy(serverPubKeyFixed[:], serverPubKey[:]) - + // Use X25519 for key exchange (replacing deprecated ScalarMult) var ephPrivKeyFixed [32]byte copy(ephPrivKeyFixed[:], ephemeralPrivateKey[:]) - var sharedSecret [32]byte - curve25519.ScalarMult(&sharedSecret, &ephPrivKeyFixed, &serverPubKeyFixed) + // Perform X25519 key exchange + sharedSecret, err := curve25519.X25519(ephPrivKeyFixed[:], serverPubKey[:]) + if err != nil { + return nil, fmt.Errorf("failed to perform X25519 key exchange: %v", err) + } // Create an AEAD cipher using the shared secret - aead, err := chacha20poly1305.New(sharedSecret[:]) + aead, err := chacha20poly1305.New(sharedSecret) if err != nil { return nil, fmt.Errorf("failed to create AEAD cipher: %v", err) } From f08378b67e9fcbcf3623ef51deab04be4f983719 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 25 Mar 2025 20:54:09 -0400 Subject: [PATCH 22/87] Fix segfault if no wgService created --- main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 4708f74..dd5e9c9 100644 --- a/main.go +++ b/main.go @@ -414,7 +414,9 @@ func main() { return } - wgService.SetServerPubKey(wgData.PublicKey) + if wgService != nil { + wgService.SetServerPubKey(wgData.PublicKey) + } logger.Info("Received: %+v", msg) tun, tnet, err = netstack.CreateNetTUN( From 7f9a31ac3e4cca7ab2a28babaadd27e9dc076661 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 26 Mar 2025 10:54:41 -0400 Subject: [PATCH 23/87] Remove listen port - unused --- wg/wg.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wg/wg.go b/wg/wg.go index 7330fdf..9e78624 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -23,9 +23,8 @@ import ( ) type WgConfig struct { - ListenPort int `json:"listenPort"` - IpAddress string `json:"ipAddress"` - Peers []Peer `json:"peers"` + IpAddress string `json:"ipAddress"` + Peers []Peer `json:"peers"` } type Peer struct { From c5978d9c4e66fb711c7e4df4bbe5b025a77de9bf Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 27 Mar 2025 22:12:54 -0400 Subject: [PATCH 24/87] Handle port correctly and delete interface --- main.go | 17 +++++++++++++++-- wg/wg.go | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index dd5e9c9..c7a50d5 100644 --- a/main.go +++ b/main.go @@ -490,7 +490,7 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( if wgService != nil { // add a udp proxy for localost and the wgService port // TODO: make sure this port is not used in a target - pm.AddTarget("udp", wgData.TunnelIP, int(wgService.Port), fmt.Sprintf("localhost:%d", wgService.Port)) + pm.AddTarget("udp", wgData.TunnelIP, int(wgService.Port), fmt.Sprintf("127.0.0.1:%d", wgService.Port)) } err = pm.Start() @@ -616,8 +616,21 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh - // Cleanup dev.Close() + + if wgService != nil { + wgService.Close() + } + + if pm != nil { + pm.Stop() + } + + if client != nil { + client.Close() + } + logger.Info("Exiting...") + os.Exit(0) } func parseTargetData(data interface{}) (TargetData, error) { diff --git a/wg/wg.go b/wg/wg.go index 7330fdf..eb931b9 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -23,9 +23,8 @@ import ( ) type WgConfig struct { - ListenPort int `json:"listenPort"` - IpAddress string `json:"ipAddress"` - Peers []Peer `json:"peers"` + IpAddress string `json:"ipAddress"` + Peers []Peer `json:"peers"` } type Peer struct { @@ -176,6 +175,10 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str func (s *WireGuardService) Close() { s.wgClient.Close() + // Remove the WireGuard interface + if err := s.removeInterface(); err != nil { + logger.Error("Failed to remove WireGuard interface: %v", err) + } } func (s *WireGuardService) SetServerPubKey(serverPubKey string) { @@ -188,8 +191,16 @@ func (s *WireGuardService) SetToken(token string) { func (s *WireGuardService) LoadRemoteConfig() error { - err := s.client.SendMessage("newt/wg/get-config", map[string]interface{}{ + // get the exising wireguard port + device, err := s.wgClient.Device(s.interfaceName) + if err == nil { + s.Port = uint16(device.ListenPort) + logger.Info("WireGuard interface %s already exists with port %d\n", s.interfaceName, s.Port) + } + + err = s.client.SendMessage("newt/wg/get-config", map[string]interface{}{ "publicKey": fmt.Sprintf("%s", s.key.PublicKey().String()), + "port": s.Port, }) if err != nil { logger.Error("Failed to send registration message: %v", err) @@ -771,3 +782,20 @@ func (s *WireGuardService) keepSendingUDPHolePunch(host string) { } } } + +func (s *WireGuardService) removeInterface() error { + // Remove the WireGuard interface + link, err := netlink.LinkByName(s.interfaceName) + if err != nil { + return fmt.Errorf("failed to get interface: %v", err) + } + + err = netlink.LinkDel(link) + if err != nil { + return fmt.Errorf("failed to delete interface: %v", err) + } + + logger.Info("WireGuard interface %s removed successfully", s.interfaceName) + + return nil +} From 09d6829f8b552997af5b3c9f2372a3f4ea160244 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 31 Mar 2025 15:46:01 -0400 Subject: [PATCH 25/87] Add update message --- wg/wg.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/wg/wg.go b/wg/wg.go index 9e78624..4322756 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -169,6 +169,7 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str wsClient.RegisterHandler("newt/wg/receive-config", service.handleConfig) wsClient.RegisterHandler("newt/wg/peer/add", service.handleAddPeer) wsClient.RegisterHandler("newt/wg/peer/remove", service.handleRemovePeer) + wsClient.RegisterHandler("newt/wg/peer/update", service.handleUpdatePeer) return service, nil } @@ -515,6 +516,120 @@ func (s *WireGuardService) removePeer(publicKey string) error { return nil } +func (s *WireGuardService) handleUpdatePeer(msg websocket.WSMessage) { + // Define a struct to match the incoming message structure with optional fields + type UpdatePeerRequest struct { + PublicKey string `json:"publicKey"` + AllowedIPs []string `json:"allowedIps,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + } + + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Info("Error marshaling data: %v", err) + return + } + + var request UpdatePeerRequest + if err := json.Unmarshal(jsonData, &request); err != nil { + logger.Info("Error unmarshaling peer data: %v", err) + return + } + + // First, get the current peer configuration to preserve any unmodified fields + device, err := s.wgClient.Device(s.interfaceName) + if err != nil { + logger.Info("Error getting WireGuard device: %v", err) + return + } + + pubKey, err := wgtypes.ParseKey(request.PublicKey) + if err != nil { + logger.Info("Error parsing public key: %v", err) + return + } + + // Find the existing peer configuration + var currentPeer *wgtypes.Peer + for _, p := range device.Peers { + if p.PublicKey == pubKey { + currentPeer = &p + break + } + } + + if currentPeer == nil { + logger.Info("Peer %s not found, cannot update", request.PublicKey) + return + } + + // Create the update peer config + peerConfig := wgtypes.PeerConfig{ + PublicKey: pubKey, + UpdateOnly: true, + } + + // Keep the default persistent keepalive of 1 second + keepalive := time.Second + peerConfig.PersistentKeepaliveInterval = &keepalive + + // Only update AllowedIPs if provided in the request + if request.AllowedIPs != nil && len(request.AllowedIPs) > 0 { + var allowedIPs []net.IPNet + for _, ipStr := range request.AllowedIPs { + _, ipNet, err := net.ParseCIDR(ipStr) + if err != nil { + logger.Info("Error parsing allowed IP %s: %v", ipStr, err) + return + } + allowedIPs = append(allowedIPs, *ipNet) + } + peerConfig.AllowedIPs = allowedIPs + logger.Info("Updating AllowedIPs for peer %s", request.PublicKey) + } + + // Handle Endpoint field special case + // If Endpoint is included in the request but empty, we want to remove the endpoint + // If Endpoint is not included, we don't modify it + endpointSpecified := false + for key := range msg.Data.(map[string]interface{}) { + if key == "endpoint" { + endpointSpecified = true + break + } + } + + if endpointSpecified { + if request.Endpoint != "" { + // Update to new endpoint + endpoint, err := net.ResolveUDPAddr("udp", request.Endpoint) + if err != nil { + logger.Info("Error resolving endpoint address %s: %v", request.Endpoint, err) + return + } + peerConfig.Endpoint = endpoint + logger.Info("Updating Endpoint for peer %s to %s", request.PublicKey, request.Endpoint) + } else { + // Request contained endpoint field but it was empty/null - remove endpoint + // To remove an endpoint in WireGuard, we set it to nil and specify ReplaceAllowedIPs + peerConfig.Endpoint = nil + logger.Info("Removing Endpoint for peer %s", request.PublicKey) + } + } + + // Apply the configuration update + config := wgtypes.Config{ + Peers: []wgtypes.PeerConfig{peerConfig}, + } + + if err := s.wgClient.ConfigureDevice(s.interfaceName, config); err != nil { + logger.Info("Error updating peer configuration: %v", err) + return + } + + logger.Info("Peer %s updated successfully", request.PublicKey) +} + func (s *WireGuardService) periodicBandwidthCheck() { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() From 65dc81ca8bc6853466744f1ddf4e4d64d282c525 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 31 Mar 2025 18:10:16 -0400 Subject: [PATCH 26/87] Add wgtester --- wgtester/wgtester.go | 347 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 wgtester/wgtester.go diff --git a/wgtester/wgtester.go b/wgtester/wgtester.go new file mode 100644 index 0000000..cefe785 --- /dev/null +++ b/wgtester/wgtester.go @@ -0,0 +1,347 @@ +package wgtester + +import ( + "context" + "encoding/binary" + "log" + "net" + "sync" + "time" +) + +const ( + // Magic bytes to identify our packets + magicHeader uint32 = 0xDEADBEEF + // Request packet type + packetTypeRequest uint8 = 1 + // Response packet type + packetTypeResponse uint8 = 2 + // Packet format: + // - 4 bytes: magic header (0xDEADBEEF) + // - 1 byte: packet type (1 = request, 2 = response) + // - 8 bytes: timestamp (for round-trip timing) + packetSize = 13 +) + +// Server handles listening for connection check requests +type Server struct { + conn *net.UDPConn + listenAddr string + shutdownCh chan struct{} + isRunning bool + runningLock sync.Mutex +} + +// NewServer creates a new connection test server +func NewServer(listenAddr string) *Server { + return &Server{ + listenAddr: listenAddr, + shutdownCh: make(chan struct{}), + } +} + +// Start begins listening for connection test packets +func (s *Server) Start() error { + s.runningLock.Lock() + defer s.runningLock.Unlock() + + if s.isRunning { + return nil + } + + addr, err := net.ResolveUDPAddr("udp", s.listenAddr) + if err != nil { + return err + } + + s.conn, err = net.ListenUDP("udp", addr) + if err != nil { + return err + } + + s.isRunning = true + go s.handleConnections() + + log.Printf("Server listening on %s", s.listenAddr) + return nil +} + +// Stop shuts down the server +func (s *Server) Stop() { + s.runningLock.Lock() + defer s.runningLock.Unlock() + + if !s.isRunning { + return + } + + close(s.shutdownCh) + if s.conn != nil { + s.conn.Close() + } + s.isRunning = false + log.Println("Server stopped") +} + +// handleConnections processes incoming packets +func (s *Server) handleConnections() { + buffer := make([]byte, packetSize) + + for { + select { + case <-s.shutdownCh: + return + default: + // Set read deadline to avoid blocking forever + s.conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + + n, addr, err := s.conn.ReadFromUDP(buffer) + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + // Just a timeout, keep going + continue + } + log.Printf("Error reading from UDP: %v", err) + continue + } + + if n != packetSize { + continue // Ignore malformed packets + } + + // Check magic header + magic := binary.BigEndian.Uint32(buffer[0:4]) + if magic != magicHeader { + continue // Not our packet + } + + // Check packet type + packetType := buffer[4] + if packetType != packetTypeRequest { + continue // Not a request packet + } + + // Keep the timestamp the same (for RTT calculation) + // Just change the packet type to response + buffer[4] = packetTypeResponse + + // Send response + _, err = s.conn.WriteToUDP(buffer, addr) + if err != nil { + log.Printf("Error sending response: %v", err) + } + } + } +} + +// Client handles checking connectivity to a server +type Client struct { + conn *net.UDPConn + serverAddr string + monitorRunning bool + monitorLock sync.Mutex + shutdownCh chan struct{} + packetInterval time.Duration + timeout time.Duration + maxAttempts int +} + +// ConnectionStatus represents the current connection state +type ConnectionStatus struct { + Connected bool + RTT time.Duration +} + +// NewClient creates a new connection test client +func NewClient(serverAddr string) (*Client, error) { + return &Client{ + serverAddr: serverAddr, + shutdownCh: make(chan struct{}), + packetInterval: 2 * time.Second, + timeout: 500 * time.Millisecond, // Timeout for individual packets + maxAttempts: 3, // Default max attempts + }, nil +} + +// SetPacketInterval changes how frequently packets are sent in monitor mode +func (c *Client) SetPacketInterval(interval time.Duration) { + c.packetInterval = interval +} + +// SetTimeout changes the timeout for waiting for responses +func (c *Client) SetTimeout(timeout time.Duration) { + c.timeout = timeout +} + +// SetMaxAttempts changes the maximum number of attempts for TestConnection +func (c *Client) SetMaxAttempts(attempts int) { + c.maxAttempts = attempts +} + +// Close cleans up client resources +func (c *Client) Close() { + c.StopMonitor() + if c.conn != nil { + c.conn.Close() + c.conn = nil + } +} + +// ensureConnection makes sure we have an active UDP connection +func (c *Client) ensureConnection() error { + if c.conn != nil { + return nil + } + + serverAddr, err := net.ResolveUDPAddr("udp", c.serverAddr) + if err != nil { + return err + } + + c.conn, err = net.DialUDP("udp", nil, serverAddr) + if err != nil { + return err + } + + return nil +} + +// TestConnection checks if the connection to the server is working +// Returns true if connected, false otherwise +func (c *Client) TestConnection(ctx context.Context) (bool, time.Duration) { + if err := c.ensureConnection(); err != nil { + return false, 0 + } + + // Prepare packet buffer + packet := make([]byte, packetSize) + binary.BigEndian.PutUint32(packet[0:4], magicHeader) + packet[4] = packetTypeRequest + + // Send multiple attempts as specified + for attempt := 0; attempt < c.maxAttempts; attempt++ { + select { + case <-ctx.Done(): + return false, 0 + default: + // Add current timestamp to packet + timestamp := time.Now().UnixNano() + binary.BigEndian.PutUint64(packet[5:13], uint64(timestamp)) + + // Send the packet + _, err := c.conn.Write(packet) + if err != nil { + log.Printf("Error sending packet: %v", err) + continue + } + + // Set read deadline + c.conn.SetReadDeadline(time.Now().Add(c.timeout)) + + // Wait for response + responseBuffer := make([]byte, packetSize) + n, err := c.conn.Read(responseBuffer) + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + // Timeout, try next attempt + time.Sleep(100 * time.Millisecond) // Brief pause between attempts + continue + } + log.Printf("Error reading response: %v", err) + continue + } + + if n != packetSize { + continue // Malformed packet + } + + // Verify response + magic := binary.BigEndian.Uint32(responseBuffer[0:4]) + packetType := responseBuffer[4] + if magic != magicHeader || packetType != packetTypeResponse { + continue // Not our response + } + + // Extract the original timestamp and calculate RTT + sentTimestamp := int64(binary.BigEndian.Uint64(responseBuffer[5:13])) + rtt := time.Duration(time.Now().UnixNano() - sentTimestamp) + + return true, rtt + } + } + + return false, 0 +} + +// TestConnectionWithTimeout tries to test connection with a timeout +// Returns true if connected, false otherwise +func (c *Client) TestConnectionWithTimeout(timeout time.Duration) (bool, time.Duration) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return c.TestConnection(ctx) +} + +// MonitorCallback is the function type for connection status change callbacks +type MonitorCallback func(status ConnectionStatus) + +// StartMonitor begins monitoring the connection and calls the callback +// when the connection status changes +func (c *Client) StartMonitor(callback MonitorCallback) error { + c.monitorLock.Lock() + defer c.monitorLock.Unlock() + + if c.monitorRunning { + return nil // Already running + } + + if err := c.ensureConnection(); err != nil { + return err + } + + c.monitorRunning = true + c.shutdownCh = make(chan struct{}) + + go func() { + var lastConnected bool + firstRun := true + + ticker := time.NewTicker(c.packetInterval) + defer ticker.Stop() + + for { + select { + case <-c.shutdownCh: + return + case <-ticker.C: + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + connected, rtt := c.TestConnection(ctx) + cancel() + + // Callback if status changed or it's the first check + if connected != lastConnected || firstRun { + callback(ConnectionStatus{ + Connected: connected, + RTT: rtt, + }) + lastConnected = connected + firstRun = false + } + } + } + }() + + return nil +} + +// StopMonitor stops the connection monitoring +func (c *Client) StopMonitor() { + c.monitorLock.Lock() + defer c.monitorLock.Unlock() + + if !c.monitorRunning { + return + } + + close(c.shutdownCh) + c.monitorRunning = false +} From 6b0ca9cab583bd29073e451dfd0636220bded7d8 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 3 Apr 2025 21:59:16 -0400 Subject: [PATCH 27/87] Adjust wgtester to work with bpf --- main.go | 17 +++ network/network.go | 96 ++++++++++++ wgtester/wgtester.go | 351 ++++++++++++------------------------------- 3 files changed, 213 insertions(+), 251 deletions(-) diff --git a/main.go b/main.go index faa80e6..6070b5f 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ import ( "github.com/fosrl/newt/proxy" "github.com/fosrl/newt/websocket" "github.com/fosrl/newt/wg" + "github.com/fosrl/newt/wgtester" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" @@ -442,6 +443,7 @@ func main() { var pm *proxy.ProxyManager var connected bool var wgData WgData + var wgTesterServer *wgtester.Server if generateAndSaveKeyTo != "" { // make sure we are running on linux @@ -465,6 +467,17 @@ func main() { logger.Fatal("Failed to create WireGuard service: %v", err) } defer wgService.Close() + + wgTesterServer = wgtester.NewServer("0.0.0.0", wgService.Port, id) // TODO: maybe make this the same ip of the wg server? + err := wgTesterServer.Start() + if err != nil { + logger.Error("Failed to start WireGuard tester server: %v", err) + } else { + logger.Info("WireGuard connection testing server started on port %d", wgService.Port) + + // Make sure to stop the server on exit + defer wgTesterServer.Stop() + } } client.RegisterHandler("newt/terminate", func(msg websocket.WSMessage) { @@ -711,6 +724,10 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( wgService.Close() } + if wgTesterServer != nil { + wgTesterServer.Stop() + } + if pm != nil { pm.Stop() } diff --git a/network/network.go b/network/network.go index 0703e8b..49192ca 100644 --- a/network/network.go +++ b/network/network.go @@ -200,3 +200,99 @@ func parseForBPF(response []byte) (srcIP net.IP, srcPort uint16, dstPort uint16) dstPort = binary.BigEndian.Uint16(response[22:24]) return } + +// SetupRawConnWithCustomBPF creates an ipv4 and udp RawConn with a custom BPF program +// This allows sharing the port between WireGuard and the WGTester +func SetupRawConnWithCustomBPF(server *Server, client *PeerNet, captureMagicHeader uint32) *ipv4.RawConn { + packetConn, err := net.ListenPacket("ip4:udp", client.IP.String()) + if err != nil { + log.Fatalln("Error creating packetConn:", err) + } + + rawConn, err := ipv4.NewRawConn(packetConn) + if err != nil { + log.Fatalln("Error creating rawConn:", err) + } + + // Apply a BPF that allows capturing both WireGuard and tester packets + ApplyCustomBPF(rawConn, server, client, captureMagicHeader) + + return rawConn +} + +// ApplyCustomBPF constructs a simpler BPF program that should be more compatible +// The previous filter might have been too complex for the kernel to accept +func ApplyCustomBPF(rawConn *ipv4.RawConn, server *Server, client *PeerNet, captureMagicHeader uint32) { + const ipv4HeaderLen = 20 + const udpHeaderLen = 8 + // Magic header would be located after IP + UDP headers + const magicHeaderOffset = ipv4HeaderLen + udpHeaderLen + + // Many BPF implementations have limitations on jump offsets and program complexity + // Let's create a simpler program that just looks for: + // 1. UDP Protocol + // 2. Destination port matching our listening port or source port matching our port + // 3. We'll handle the magic header check in our application code instead + + // This creates a more basic filter that will be accepted by most kernels + bpfRaw, err := bpf.Assemble([]bpf.Instruction{ + // Load IP Protocol field (at offset 9) + bpf.LoadAbsolute{Off: 9, Size: 1}, + + // Is it UDP? (17 is UDP protocol number) + bpf.JumpIf{Cond: bpf.JumpEqual, Val: 17, SkipFalse: 5, SkipTrue: 0}, + + // Load destination port (at IP header + 2) + bpf.LoadAbsolute{Off: ipv4HeaderLen + 2, Size: 2}, + + // Is it our port? + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(client.Port), SkipFalse: 2, SkipTrue: 0}, + + // Accept packet + bpf.RetConstant{Val: 1<<(8*4) - 1}, + + // Not matching destination port, check source port + bpf.LoadAbsolute{Off: ipv4HeaderLen + 0, Size: 2}, + + // Is source port our port? + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(client.Port), SkipFalse: 1, SkipTrue: 0}, + + // Accept packet + bpf.RetConstant{Val: 1<<(8*4) - 1}, + + // Reject packet + bpf.RetConstant{Val: 0}, + }) + + if err != nil { + log.Fatalln("Error assembling BPF:", err) + } + + err = rawConn.SetBPF(bpfRaw) + if err != nil { + log.Fatalln("Error setting BPF:", err) + } +} + +// These helper functions will make it easier to extract information from packets +// ExtractUDPPayload extracts the UDP payload from a raw IP packet +func ExtractUDPPayload(packet []byte) []byte { + if len(packet) < 28 { // IP header (20) + UDP header (8) + return nil + } + return packet[28:] +} + +// ExtractIPAndPorts extracts source/dest IP and ports from a raw IP packet +func ExtractIPAndPorts(packet []byte) (srcIP net.IP, srcPort uint16, dstIP net.IP, dstPort uint16) { + if len(packet) < 28 { + return nil, 0, nil, 0 + } + + srcIP = net.IP(packet[12:16]) + dstIP = net.IP(packet[16:20]) + srcPort = binary.BigEndian.Uint16(packet[20:22]) + dstPort = binary.BigEndian.Uint16(packet[22:24]) + + return +} diff --git a/wgtester/wgtester.go b/wgtester/wgtester.go index cefe785..48119e8 100644 --- a/wgtester/wgtester.go +++ b/wgtester/wgtester.go @@ -1,12 +1,14 @@ package wgtester import ( - "context" "encoding/binary" - "log" "net" "sync" "time" + + "github.com/fosrl/newt/logger" + "github.com/fosrl/newt/network" + "golang.org/x/net/ipv4" ) const ( @@ -23,24 +25,30 @@ const ( packetSize = 13 ) -// Server handles listening for connection check requests +// Server handles listening for connection check requests using raw sockets type Server struct { - conn *net.UDPConn - listenAddr string - shutdownCh chan struct{} - isRunning bool - runningLock sync.Mutex + rawConn *ipv4.RawConn + serverAddr string + serverPort uint16 + shutdownCh chan struct{} + isRunning bool + runningLock sync.Mutex + newtID string + outputPrefix string } -// NewServer creates a new connection test server -func NewServer(listenAddr string) *Server { +// NewServer creates a new connection test server using raw sockets +func NewServer(serverAddr string, serverPort uint16, newtID string) *Server { return &Server{ - listenAddr: listenAddr, - shutdownCh: make(chan struct{}), + serverAddr: serverAddr, + serverPort: serverPort, + shutdownCh: make(chan struct{}), + newtID: newtID, + outputPrefix: "[WGTester] ", } } -// Start begins listening for connection test packets +// Start begins listening for connection test packets using raw sockets func (s *Server) Start() error { s.runningLock.Lock() defer s.runningLock.Unlock() @@ -49,20 +57,30 @@ func (s *Server) Start() error { return nil } - addr, err := net.ResolveUDPAddr("udp", s.listenAddr) - if err != nil { - return err + // Configure server and client for BPF filtering + server := &network.Server{ + Hostname: s.serverAddr, + Addr: network.HostToAddr(s.serverAddr), + Port: s.serverPort, } - s.conn, err = net.ListenUDP("udp", addr) - if err != nil { - return err + clientIP := network.GetClientIP(server.Addr.IP) + + // Use the server port as our client port to match the WireGuard configuration + client := &network.PeerNet{ + IP: clientIP, + Port: s.serverPort, // Use same port as server to share with WireGuard + NewtID: s.newtID, } + // Setup raw connection with custom BPF to filter for our magic header + rawConn := network.SetupRawConnWithCustomBPF(server, client, magicHeader) + s.rawConn = rawConn + s.isRunning = true go s.handleConnections() - log.Printf("Server listening on %s", s.listenAddr) + logger.Info(""+s.outputPrefix+"Server started on %s:%d", s.serverAddr, s.serverPort) return nil } @@ -76,272 +94,103 @@ func (s *Server) Stop() { } close(s.shutdownCh) - if s.conn != nil { - s.conn.Close() + if s.rawConn != nil { + s.rawConn.Close() } s.isRunning = false - log.Println("Server stopped") + logger.Info(s.outputPrefix + "Server stopped") } // handleConnections processes incoming packets func (s *Server) handleConnections() { - buffer := make([]byte, packetSize) - for { select { case <-s.shutdownCh: return default: - // Set read deadline to avoid blocking forever - s.conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + // Read packet with timeout using RawConn + err := s.rawConn.SetReadDeadline(time.Now().Add(1 * time.Second)) + if err != nil { + logger.Error(s.outputPrefix+"Error setting read deadline: %v", err) + continue + } - n, addr, err := s.conn.ReadFromUDP(buffer) + // Create buffer for the entire IP packet + payload := make([]byte, 2000) // Large enough for any UDP packet + + // Read the packet + _, _, _, err = s.rawConn.ReadFrom(payload) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { // Just a timeout, keep going continue } - log.Printf("Error reading from UDP: %v", err) + logger.Error(s.outputPrefix+"Error reading from UDP: %v", err) continue } - if n != packetSize { - continue // Ignore malformed packets + // Extract IP and port information + srcIP, srcPort, _, _ := network.ExtractIPAndPorts(payload) + if srcIP == nil { + continue // Invalid packet + } + + // Extract UDP payload + udpPayload := network.ExtractUDPPayload(payload) + if udpPayload == nil || len(udpPayload) < packetSize { + continue // Too small to be our packet } // Check magic header - magic := binary.BigEndian.Uint32(buffer[0:4]) + magic := binary.BigEndian.Uint32(udpPayload[0:4]) if magic != magicHeader { continue // Not our packet } // Check packet type - packetType := buffer[4] + packetType := udpPayload[4] if packetType != packetTypeRequest { continue // Not a request packet } - // Keep the timestamp the same (for RTT calculation) - // Just change the packet type to response - buffer[4] = packetTypeResponse + // Create response packet + responsePacket := make([]byte, packetSize) + // Copy the same magic header + binary.BigEndian.PutUint32(responsePacket[0:4], magicHeader) + // Change the packet type to response + responsePacket[4] = packetTypeResponse + // Copy the timestamp (for RTT calculation) + if len(udpPayload) >= 13 { + copy(responsePacket[5:13], udpPayload[5:13]) + } - // Send response - _, err = s.conn.WriteToUDP(buffer, addr) + // Use the client's source information to send the response + peerClient := &network.PeerNet{ + IP: s.rawConn.LocalAddr().(*net.IPAddr).IP, + Port: s.serverPort, + NewtID: s.newtID, + } + + // Setup target server from the source of the incoming packet + server := &network.Server{ + Hostname: srcIP.String(), + Addr: &net.IPAddr{IP: srcIP}, + Port: srcPort, + } + + // Log response being sent for debugging + logger.Debug(s.outputPrefix+"Sending response to %s:%d", srcIP.String(), srcPort) + + // Send the response packet + err = network.SendPacket(responsePacket, s.rawConn, server, peerClient) if err != nil { - log.Printf("Error sending response: %v", err) + logger.Error(s.outputPrefix+"Error sending response: %v", err) + } else { + logger.Debug(s.outputPrefix + "Response sent successfully") + } + if err != nil { + logger.Error(s.outputPrefix+"Error sending response: %v", err) } } } } - -// Client handles checking connectivity to a server -type Client struct { - conn *net.UDPConn - serverAddr string - monitorRunning bool - monitorLock sync.Mutex - shutdownCh chan struct{} - packetInterval time.Duration - timeout time.Duration - maxAttempts int -} - -// ConnectionStatus represents the current connection state -type ConnectionStatus struct { - Connected bool - RTT time.Duration -} - -// NewClient creates a new connection test client -func NewClient(serverAddr string) (*Client, error) { - return &Client{ - serverAddr: serverAddr, - shutdownCh: make(chan struct{}), - packetInterval: 2 * time.Second, - timeout: 500 * time.Millisecond, // Timeout for individual packets - maxAttempts: 3, // Default max attempts - }, nil -} - -// SetPacketInterval changes how frequently packets are sent in monitor mode -func (c *Client) SetPacketInterval(interval time.Duration) { - c.packetInterval = interval -} - -// SetTimeout changes the timeout for waiting for responses -func (c *Client) SetTimeout(timeout time.Duration) { - c.timeout = timeout -} - -// SetMaxAttempts changes the maximum number of attempts for TestConnection -func (c *Client) SetMaxAttempts(attempts int) { - c.maxAttempts = attempts -} - -// Close cleans up client resources -func (c *Client) Close() { - c.StopMonitor() - if c.conn != nil { - c.conn.Close() - c.conn = nil - } -} - -// ensureConnection makes sure we have an active UDP connection -func (c *Client) ensureConnection() error { - if c.conn != nil { - return nil - } - - serverAddr, err := net.ResolveUDPAddr("udp", c.serverAddr) - if err != nil { - return err - } - - c.conn, err = net.DialUDP("udp", nil, serverAddr) - if err != nil { - return err - } - - return nil -} - -// TestConnection checks if the connection to the server is working -// Returns true if connected, false otherwise -func (c *Client) TestConnection(ctx context.Context) (bool, time.Duration) { - if err := c.ensureConnection(); err != nil { - return false, 0 - } - - // Prepare packet buffer - packet := make([]byte, packetSize) - binary.BigEndian.PutUint32(packet[0:4], magicHeader) - packet[4] = packetTypeRequest - - // Send multiple attempts as specified - for attempt := 0; attempt < c.maxAttempts; attempt++ { - select { - case <-ctx.Done(): - return false, 0 - default: - // Add current timestamp to packet - timestamp := time.Now().UnixNano() - binary.BigEndian.PutUint64(packet[5:13], uint64(timestamp)) - - // Send the packet - _, err := c.conn.Write(packet) - if err != nil { - log.Printf("Error sending packet: %v", err) - continue - } - - // Set read deadline - c.conn.SetReadDeadline(time.Now().Add(c.timeout)) - - // Wait for response - responseBuffer := make([]byte, packetSize) - n, err := c.conn.Read(responseBuffer) - if err != nil { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { - // Timeout, try next attempt - time.Sleep(100 * time.Millisecond) // Brief pause between attempts - continue - } - log.Printf("Error reading response: %v", err) - continue - } - - if n != packetSize { - continue // Malformed packet - } - - // Verify response - magic := binary.BigEndian.Uint32(responseBuffer[0:4]) - packetType := responseBuffer[4] - if magic != magicHeader || packetType != packetTypeResponse { - continue // Not our response - } - - // Extract the original timestamp and calculate RTT - sentTimestamp := int64(binary.BigEndian.Uint64(responseBuffer[5:13])) - rtt := time.Duration(time.Now().UnixNano() - sentTimestamp) - - return true, rtt - } - } - - return false, 0 -} - -// TestConnectionWithTimeout tries to test connection with a timeout -// Returns true if connected, false otherwise -func (c *Client) TestConnectionWithTimeout(timeout time.Duration) (bool, time.Duration) { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - return c.TestConnection(ctx) -} - -// MonitorCallback is the function type for connection status change callbacks -type MonitorCallback func(status ConnectionStatus) - -// StartMonitor begins monitoring the connection and calls the callback -// when the connection status changes -func (c *Client) StartMonitor(callback MonitorCallback) error { - c.monitorLock.Lock() - defer c.monitorLock.Unlock() - - if c.monitorRunning { - return nil // Already running - } - - if err := c.ensureConnection(); err != nil { - return err - } - - c.monitorRunning = true - c.shutdownCh = make(chan struct{}) - - go func() { - var lastConnected bool - firstRun := true - - ticker := time.NewTicker(c.packetInterval) - defer ticker.Stop() - - for { - select { - case <-c.shutdownCh: - return - case <-ticker.C: - ctx, cancel := context.WithTimeout(context.Background(), c.timeout) - connected, rtt := c.TestConnection(ctx) - cancel() - - // Callback if status changed or it's the first check - if connected != lastConnected || firstRun { - callback(ConnectionStatus{ - Connected: connected, - RTT: rtt, - }) - lastConnected = connected - firstRun = false - } - } - } - }() - - return nil -} - -// StopMonitor stops the connection monitoring -func (c *Client) StopMonitor() { - c.monitorLock.Lock() - defer c.monitorLock.Unlock() - - if !c.monitorRunning { - return - } - - close(c.shutdownCh) - c.monitorRunning = false -} From 0ced66e157cc2b2a0b7ac4f444eb7afe21a76d58 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 11 Apr 2025 20:52:29 -0400 Subject: [PATCH 28/87] Relaying working --- network/network.go | 103 ------------------------------------------- wg/wg.go | 79 +++++++++++++++++++-------------- wgtester/wgtester.go | 102 +++++++++++++++--------------------------- 3 files changed, 82 insertions(+), 202 deletions(-) diff --git a/network/network.go b/network/network.go index 49192ca..e359219 100644 --- a/network/network.go +++ b/network/network.go @@ -193,106 +193,3 @@ func ParseResponse(response []byte) (net.IP, uint16) { port := binary.BigEndian.Uint16(response[4:6]) return ip, port } - -func parseForBPF(response []byte) (srcIP net.IP, srcPort uint16, dstPort uint16) { - srcIP = net.IP(response[12:16]) - srcPort = binary.BigEndian.Uint16(response[20:22]) - dstPort = binary.BigEndian.Uint16(response[22:24]) - return -} - -// SetupRawConnWithCustomBPF creates an ipv4 and udp RawConn with a custom BPF program -// This allows sharing the port between WireGuard and the WGTester -func SetupRawConnWithCustomBPF(server *Server, client *PeerNet, captureMagicHeader uint32) *ipv4.RawConn { - packetConn, err := net.ListenPacket("ip4:udp", client.IP.String()) - if err != nil { - log.Fatalln("Error creating packetConn:", err) - } - - rawConn, err := ipv4.NewRawConn(packetConn) - if err != nil { - log.Fatalln("Error creating rawConn:", err) - } - - // Apply a BPF that allows capturing both WireGuard and tester packets - ApplyCustomBPF(rawConn, server, client, captureMagicHeader) - - return rawConn -} - -// ApplyCustomBPF constructs a simpler BPF program that should be more compatible -// The previous filter might have been too complex for the kernel to accept -func ApplyCustomBPF(rawConn *ipv4.RawConn, server *Server, client *PeerNet, captureMagicHeader uint32) { - const ipv4HeaderLen = 20 - const udpHeaderLen = 8 - // Magic header would be located after IP + UDP headers - const magicHeaderOffset = ipv4HeaderLen + udpHeaderLen - - // Many BPF implementations have limitations on jump offsets and program complexity - // Let's create a simpler program that just looks for: - // 1. UDP Protocol - // 2. Destination port matching our listening port or source port matching our port - // 3. We'll handle the magic header check in our application code instead - - // This creates a more basic filter that will be accepted by most kernels - bpfRaw, err := bpf.Assemble([]bpf.Instruction{ - // Load IP Protocol field (at offset 9) - bpf.LoadAbsolute{Off: 9, Size: 1}, - - // Is it UDP? (17 is UDP protocol number) - bpf.JumpIf{Cond: bpf.JumpEqual, Val: 17, SkipFalse: 5, SkipTrue: 0}, - - // Load destination port (at IP header + 2) - bpf.LoadAbsolute{Off: ipv4HeaderLen + 2, Size: 2}, - - // Is it our port? - bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(client.Port), SkipFalse: 2, SkipTrue: 0}, - - // Accept packet - bpf.RetConstant{Val: 1<<(8*4) - 1}, - - // Not matching destination port, check source port - bpf.LoadAbsolute{Off: ipv4HeaderLen + 0, Size: 2}, - - // Is source port our port? - bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(client.Port), SkipFalse: 1, SkipTrue: 0}, - - // Accept packet - bpf.RetConstant{Val: 1<<(8*4) - 1}, - - // Reject packet - bpf.RetConstant{Val: 0}, - }) - - if err != nil { - log.Fatalln("Error assembling BPF:", err) - } - - err = rawConn.SetBPF(bpfRaw) - if err != nil { - log.Fatalln("Error setting BPF:", err) - } -} - -// These helper functions will make it easier to extract information from packets -// ExtractUDPPayload extracts the UDP payload from a raw IP packet -func ExtractUDPPayload(packet []byte) []byte { - if len(packet) < 28 { // IP header (20) + UDP header (8) - return nil - } - return packet[28:] -} - -// ExtractIPAndPorts extracts source/dest IP and ports from a raw IP packet -func ExtractIPAndPorts(packet []byte) (srcIP net.IP, srcPort uint16, dstIP net.IP, dstPort uint16) { - if len(packet) < 28 { - return nil, 0, nil, 0 - } - - srcIP = net.IP(packet[12:16]) - dstIP = net.IP(packet[16:20]) - srcPort = binary.BigEndian.Uint16(packet[20:22]) - dstPort = binary.BigEndian.Uint16(packet[22:24]) - - return -} diff --git a/wg/wg.go b/wg/wg.go index b879c9c..8095606 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -80,13 +80,20 @@ func NewFixedPortBind(port uint16) conn.Bind { } } +// find an available UDP port in the range [minPort, maxPort] and also the next port for the wgtester func FindAvailableUDPPort(minPort, maxPort uint16) (uint16, error) { if maxPort < minPort { return 0, fmt.Errorf("invalid port range: min=%d, max=%d", minPort, maxPort) } - // Create a slice of all ports in the range - portRange := make([]uint16, maxPort-minPort+1) + // We need to check port+1 as well, so adjust the max port to avoid going out of range + adjustedMaxPort := maxPort - 1 + if adjustedMaxPort < minPort { + return 0, fmt.Errorf("insufficient port range to find consecutive ports: min=%d, max=%d", minPort, maxPort) + } + + // Create a slice of all ports in the range (excluding the last one) + portRange := make([]uint16, adjustedMaxPort-minPort+1) for i := range portRange { portRange[i] = minPort + uint16(i) } @@ -100,20 +107,35 @@ func FindAvailableUDPPort(minPort, maxPort uint16) (uint16, error) { // Try each port in the randomized order for _, port := range portRange { - addr := &net.UDPAddr{ + // Check if port is available + addr1 := &net.UDPAddr{ IP: net.ParseIP("127.0.0.1"), Port: int(port), } - conn, err := net.ListenUDP("udp", addr) - if err != nil { + conn1, err1 := net.ListenUDP("udp", addr1) + if err1 != nil { continue // Port is in use or there was an error, try next port } - _ = conn.SetDeadline(time.Now()) - conn.Close() + + // Check if port+1 is also available + addr2 := &net.UDPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: int(port + 1), + } + conn2, err2 := net.ListenUDP("udp", addr2) + if err2 != nil { + // The next port is not available, so close the first connection and try again + conn1.Close() + continue + } + + // Both ports are available, close connections and return the first port + conn1.Close() + conn2.Close() return port, nil } - return 0, fmt.Errorf("no available UDP ports found in range %d-%d", minPort, maxPort) + return 0, fmt.Errorf("no available consecutive UDP ports found in range %d-%d", minPort, maxPort) } func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo string, host string, newtId string, wsClient *websocket.Client) (*WireGuardService, error) { @@ -408,6 +430,7 @@ func (s *WireGuardService) ensureWireguardPeers(peers []Peer) error { } func (s *WireGuardService) handleAddPeer(msg websocket.WSMessage) { + logger.Info("Received message: %v", msg.Data) var peer Peer jsonData, err := json.Marshal(msg.Data) @@ -451,8 +474,6 @@ func (s *WireGuardService) addPeer(peer Peer) error { return fmt.Errorf("failed to resolve endpoint address: %w", err) } - // make the endpoint localhost to test - peerConfig = wgtypes.PeerConfig{ PublicKey: pubKey, AllowedIPs: allowedIPs, @@ -482,6 +503,7 @@ func (s *WireGuardService) addPeer(peer Peer) error { } func (s *WireGuardService) handleRemovePeer(msg websocket.WSMessage) { + logger.Info("Received message: %v", msg.Data) // parse the publicKey from the message which is json { "publicKey": "asdfasdfl;akjsdf" } type RemoveRequest struct { PublicKey string `json:"publicKey"` @@ -529,38 +551,34 @@ func (s *WireGuardService) removePeer(publicKey string) error { } func (s *WireGuardService) handleUpdatePeer(msg websocket.WSMessage) { + logger.Info("Received message: %v", msg.Data) // Define a struct to match the incoming message structure with optional fields type UpdatePeerRequest struct { PublicKey string `json:"publicKey"` AllowedIPs []string `json:"allowedIps,omitempty"` Endpoint string `json:"endpoint,omitempty"` } - jsonData, err := json.Marshal(msg.Data) if err != nil { logger.Info("Error marshaling data: %v", err) return } - var request UpdatePeerRequest if err := json.Unmarshal(jsonData, &request); err != nil { logger.Info("Error unmarshaling peer data: %v", err) return } - // First, get the current peer configuration to preserve any unmodified fields device, err := s.wgClient.Device(s.interfaceName) if err != nil { logger.Info("Error getting WireGuard device: %v", err) return } - pubKey, err := wgtypes.ParseKey(request.PublicKey) if err != nil { logger.Info("Error parsing public key: %v", err) return } - // Find the existing peer configuration var currentPeer *wgtypes.Peer for _, p := range device.Peers { @@ -569,22 +587,30 @@ func (s *WireGuardService) handleUpdatePeer(msg websocket.WSMessage) { break } } - if currentPeer == nil { logger.Info("Peer %s not found, cannot update", request.PublicKey) return } - // Create the update peer config peerConfig := wgtypes.PeerConfig{ PublicKey: pubKey, UpdateOnly: true, } - // Keep the default persistent keepalive of 1 second keepalive := time.Second peerConfig.PersistentKeepaliveInterval = &keepalive + // Handle Endpoint field special case + // If Endpoint is included in the request but empty, we want to remove the endpoint + // If Endpoint is not included, we don't modify it + endpointSpecified := false + for key := range msg.Data.(map[string]interface{}) { + if key == "endpoint" { + endpointSpecified = true + break + } + } + // Only update AllowedIPs if provided in the request if request.AllowedIPs != nil && len(request.AllowedIPs) > 0 { var allowedIPs []net.IPNet @@ -597,18 +623,10 @@ func (s *WireGuardService) handleUpdatePeer(msg websocket.WSMessage) { allowedIPs = append(allowedIPs, *ipNet) } peerConfig.AllowedIPs = allowedIPs + peerConfig.ReplaceAllowedIPs = true logger.Info("Updating AllowedIPs for peer %s", request.PublicKey) - } - - // Handle Endpoint field special case - // If Endpoint is included in the request but empty, we want to remove the endpoint - // If Endpoint is not included, we don't modify it - endpointSpecified := false - for key := range msg.Data.(map[string]interface{}) { - if key == "endpoint" { - endpointSpecified = true - break - } + } else if endpointSpecified && request.Endpoint == "" { + peerConfig.ReplaceAllowedIPs = false } if endpointSpecified { @@ -623,7 +641,6 @@ func (s *WireGuardService) handleUpdatePeer(msg websocket.WSMessage) { logger.Info("Updating Endpoint for peer %s to %s", request.PublicKey, request.Endpoint) } else { // Request contained endpoint field but it was empty/null - remove endpoint - // To remove an endpoint in WireGuard, we set it to nil and specify ReplaceAllowedIPs peerConfig.Endpoint = nil logger.Info("Removing Endpoint for peer %s", request.PublicKey) } @@ -633,12 +650,10 @@ func (s *WireGuardService) handleUpdatePeer(msg websocket.WSMessage) { config := wgtypes.Config{ Peers: []wgtypes.PeerConfig{peerConfig}, } - if err := s.wgClient.ConfigureDevice(s.interfaceName, config); err != nil { logger.Info("Error updating peer configuration: %v", err) return } - logger.Info("Peer %s updated successfully", request.PublicKey) } diff --git a/wgtester/wgtester.go b/wgtester/wgtester.go index 48119e8..b302fd4 100644 --- a/wgtester/wgtester.go +++ b/wgtester/wgtester.go @@ -2,13 +2,12 @@ package wgtester import ( "encoding/binary" + "fmt" "net" "sync" "time" "github.com/fosrl/newt/logger" - "github.com/fosrl/newt/network" - "golang.org/x/net/ipv4" ) const ( @@ -25,9 +24,9 @@ const ( packetSize = 13 ) -// Server handles listening for connection check requests using raw sockets +// Server handles listening for connection check requests using UDP type Server struct { - rawConn *ipv4.RawConn + conn *net.UDPConn serverAddr string serverPort uint16 shutdownCh chan struct{} @@ -37,18 +36,18 @@ type Server struct { outputPrefix string } -// NewServer creates a new connection test server using raw sockets +// NewServer creates a new connection test server using UDP func NewServer(serverAddr string, serverPort uint16, newtID string) *Server { return &Server{ serverAddr: serverAddr, - serverPort: serverPort, + serverPort: serverPort + 1, // use the next port for the server shutdownCh: make(chan struct{}), newtID: newtID, outputPrefix: "[WGTester] ", } } -// Start begins listening for connection test packets using raw sockets +// Start begins listening for connection test packets using UDP func (s *Server) Start() error { s.runningLock.Lock() defer s.runningLock.Unlock() @@ -57,30 +56,26 @@ func (s *Server) Start() error { return nil } - // Configure server and client for BPF filtering - server := &network.Server{ - Hostname: s.serverAddr, - Addr: network.HostToAddr(s.serverAddr), - Port: s.serverPort, + //create the address to listen on + addr := net.JoinHostPort(s.serverAddr, fmt.Sprintf("%d", s.serverPort)) + + // Create UDP address to listen on + udpAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return err } - clientIP := network.GetClientIP(server.Addr.IP) - - // Use the server port as our client port to match the WireGuard configuration - client := &network.PeerNet{ - IP: clientIP, - Port: s.serverPort, // Use same port as server to share with WireGuard - NewtID: s.newtID, + // Create UDP connection + conn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + return err } - - // Setup raw connection with custom BPF to filter for our magic header - rawConn := network.SetupRawConnWithCustomBPF(server, client, magicHeader) - s.rawConn = rawConn + s.conn = conn s.isRunning = true go s.handleConnections() - logger.Info(""+s.outputPrefix+"Server started on %s:%d", s.serverAddr, s.serverPort) + logger.Info("%sServer started on %s:%d", s.outputPrefix, s.serverAddr, s.serverPort) return nil } @@ -94,8 +89,8 @@ func (s *Server) Stop() { } close(s.shutdownCh) - if s.rawConn != nil { - s.rawConn.Close() + if s.conn != nil { + s.conn.Close() } s.isRunning = false logger.Info(s.outputPrefix + "Server stopped") @@ -103,23 +98,22 @@ func (s *Server) Stop() { // handleConnections processes incoming packets func (s *Server) handleConnections() { + buffer := make([]byte, 2000) // Buffer large enough for any UDP packet + for { select { case <-s.shutdownCh: return default: - // Read packet with timeout using RawConn - err := s.rawConn.SetReadDeadline(time.Now().Add(1 * time.Second)) + // Set read deadline to avoid blocking forever + err := s.conn.SetReadDeadline(time.Now().Add(1 * time.Second)) if err != nil { logger.Error(s.outputPrefix+"Error setting read deadline: %v", err) continue } - // Create buffer for the entire IP packet - payload := make([]byte, 2000) // Large enough for any UDP packet - - // Read the packet - _, _, _, err = s.rawConn.ReadFrom(payload) + // Read from UDP connection + n, addr, err := s.conn.ReadFromUDP(buffer) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { // Just a timeout, keep going @@ -129,26 +123,19 @@ func (s *Server) handleConnections() { continue } - // Extract IP and port information - srcIP, srcPort, _, _ := network.ExtractIPAndPorts(payload) - if srcIP == nil { - continue // Invalid packet - } - - // Extract UDP payload - udpPayload := network.ExtractUDPPayload(payload) - if udpPayload == nil || len(udpPayload) < packetSize { + // Process packet only if it meets minimum size requirements + if n < packetSize { continue // Too small to be our packet } // Check magic header - magic := binary.BigEndian.Uint32(udpPayload[0:4]) + magic := binary.BigEndian.Uint32(buffer[0:4]) if magic != magicHeader { continue // Not our packet } // Check packet type - packetType := udpPayload[4] + packetType := buffer[4] if packetType != packetTypeRequest { continue // Not a request packet } @@ -160,37 +147,18 @@ func (s *Server) handleConnections() { // Change the packet type to response responsePacket[4] = packetTypeResponse // Copy the timestamp (for RTT calculation) - if len(udpPayload) >= 13 { - copy(responsePacket[5:13], udpPayload[5:13]) - } - - // Use the client's source information to send the response - peerClient := &network.PeerNet{ - IP: s.rawConn.LocalAddr().(*net.IPAddr).IP, - Port: s.serverPort, - NewtID: s.newtID, - } - - // Setup target server from the source of the incoming packet - server := &network.Server{ - Hostname: srcIP.String(), - Addr: &net.IPAddr{IP: srcIP}, - Port: srcPort, - } + copy(responsePacket[5:13], buffer[5:13]) // Log response being sent for debugging - logger.Debug(s.outputPrefix+"Sending response to %s:%d", srcIP.String(), srcPort) + logger.Debug(s.outputPrefix+"Sending response to %s", addr.String()) - // Send the response packet - err = network.SendPacket(responsePacket, s.rawConn, server, peerClient) + // Send the response packet directly to the source address + _, err = s.conn.WriteToUDP(responsePacket, addr) if err != nil { logger.Error(s.outputPrefix+"Error sending response: %v", err) } else { logger.Debug(s.outputPrefix + "Response sent successfully") } - if err != nil { - logger.Error(s.outputPrefix+"Error sending response: %v", err) - } } } } From b1edef2407f795c9c6b443be43d6ca11100f94ec Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 12 Apr 2025 17:51:03 -0400 Subject: [PATCH 29/87] Remove redundant log message Former-commit-id: 027d9a059fbb34191b3c5202c03e19ef83293add --- main.go | 2 -- newt_arm64.REMOVED.git-id | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 newt_arm64.REMOVED.git-id diff --git a/main.go b/main.go index 6070b5f..77f1cec 100644 --- a/main.go +++ b/main.go @@ -473,8 +473,6 @@ func main() { if err != nil { logger.Error("Failed to start WireGuard tester server: %v", err) } else { - logger.Info("WireGuard connection testing server started on port %d", wgService.Port) - // Make sure to stop the server on exit defer wgTesterServer.Stop() } diff --git a/newt_arm64.REMOVED.git-id b/newt_arm64.REMOVED.git-id new file mode 100644 index 0000000..67585b0 --- /dev/null +++ b/newt_arm64.REMOVED.git-id @@ -0,0 +1 @@ +e3dab3248cc8b7e23f9a99c817371495f93c1f5d \ No newline at end of file From 0e4b638fb7445f93cd94cb44dac2473c31d3da16 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 13 Apr 2025 21:28:38 -0400 Subject: [PATCH 30/87] Add more sensible controls Former-commit-id: 6a146ed37191df73f630d230a2b2a65070c1b642 --- main.go | 14 ++++++++++---- wg/wg.go | 11 +++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 77f1cec..97c9c33 100644 --- a/main.go +++ b/main.go @@ -358,6 +358,8 @@ var ( updownScript string interfaceName string generateAndSaveKeyTo string + rm bool + acceptClients bool ) func main() { @@ -371,6 +373,8 @@ func main() { updownScript = os.Getenv("UPDOWN_SCRIPT") interfaceName = os.Getenv("INTERFACE") generateAndSaveKeyTo = os.Getenv("GENERATE_AND_SAVE_KEY_TO") + rm = os.Getenv("RM") == "true" + acceptClients = os.Getenv("ACCEPT_CLIENTS") == "true" if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -397,8 +401,10 @@ func main() { flag.StringVar(&interfaceName, "interface", "wg1", "Name of the WireGuard interface") } if generateAndSaveKeyTo == "" { - flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "", "Path to save generated private key") + flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "/tmp/newtkey", "Path to save generated private key") } + flag.BoolVar(&rm, "rm", true, "Remove the WireGuard interface") + flag.BoolVar(&acceptClients, "accept-clients", false, "Accept clients on the WireGuard interface") // do a --version check version := flag.Bool("version", false, "Print the version") @@ -445,7 +451,7 @@ func main() { var wgData WgData var wgTesterServer *wgtester.Server - if generateAndSaveKeyTo != "" { + if acceptClients { // make sure we are running on linux if runtime.GOOS != "linux" { logger.Fatal("Tunnel management is only supported on Linux right now!") @@ -466,7 +472,7 @@ func main() { if err != nil { logger.Fatal("Failed to create WireGuard service: %v", err) } - defer wgService.Close() + defer wgService.Close(rm) wgTesterServer = wgtester.NewServer("0.0.0.0", wgService.Port, id) // TODO: maybe make this the same ip of the wg server? err := wgTesterServer.Start() @@ -719,7 +725,7 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( dev.Close() if wgService != nil { - wgService.Close() + wgService.Close(rm) } if wgTesterServer != nil { diff --git a/wg/wg.go b/wg/wg.go index 8095606..20cb9cd 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -196,11 +196,13 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str return service, nil } -func (s *WireGuardService) Close() { +func (s *WireGuardService) Close(rm bool) { s.wgClient.Close() // Remove the WireGuard interface - if err := s.removeInterface(); err != nil { - logger.Error("Failed to remove WireGuard interface: %v", err) + if rm { + if err := s.removeInterface(); err != nil { + logger.Error("Failed to remove WireGuard interface: %v", err) + } } } @@ -772,7 +774,8 @@ func (s *WireGuardService) reportPeerBandwidth() error { func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error { if s.serverPubKey == "" || s.token == "" { - return fmt.Errorf("server public key or token is not set") + logger.Debug("Server public key or token not set, skipping UDP hole punch") + return nil } // Parse server address From e0a5ea5a4ed04206208088b9051c7537ffd31680 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 22 Apr 2025 22:11:37 -0400 Subject: [PATCH 31/87] Handle order of opertions of hole punch better Former-commit-id: 175718a48e4bbb58b9499d0b872d4fb0c63c8f2b --- main.go | 2 +- wg/wg.go | 103 +++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/main.go b/main.go index 97c9c33..d9a3cef 100644 --- a/main.go +++ b/main.go @@ -403,7 +403,7 @@ func main() { if generateAndSaveKeyTo == "" { flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "/tmp/newtkey", "Path to save generated private key") } - flag.BoolVar(&rm, "rm", true, "Remove the WireGuard interface") + flag.BoolVar(&rm, "rm", false, "Remove the WireGuard interface") flag.BoolVar(&acceptClients, "accept-clients", false, "Accept clients on the WireGuard interface") // do a --version check diff --git a/wg/wg.go b/wg/wg.go index 20cb9cd..6e17880 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -60,6 +60,7 @@ type WireGuardService struct { host string serverPubKey string token string + stopGetConfig chan struct{} } // Add this type definition @@ -168,12 +169,6 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str } } - port, err := FindAvailableUDPPort(49152, 65535) - if err != nil { - fmt.Printf("Error finding available port: %v\n", err) - return nil, err - } - service := &WireGuardService{ interfaceName: interfaceName, mtu: mtu, @@ -181,10 +176,23 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str wgClient: wgClient, key: key, newtId: newtId, - lastReadings: make(map[string]PeerReading), - Port: port, - stopHolepunch: make(chan struct{}), host: host, + lastReadings: make(map[string]PeerReading), + stopHolepunch: make(chan struct{}), + stopGetConfig: make(chan struct{}), + } + + // Get the existing wireguard port (keep this part) + device, err := service.wgClient.Device(service.interfaceName) + if err == nil { + service.Port = uint16(device.ListenPort) + logger.Info("WireGuard interface %s already exists with port %d\n", service.interfaceName, service.Port) + } else { + service.Port, err = FindAvailableUDPPort(49152, 65535) + if err != nil { + fmt.Printf("Error finding available port: %v\n", err) + return nil, err + } } // Register websocket handlers @@ -193,16 +201,35 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str wsClient.RegisterHandler("newt/wg/peer/remove", service.handleRemovePeer) wsClient.RegisterHandler("newt/wg/peer/update", service.handleUpdatePeer) + if err := service.sendUDPHolePunch(service.host + ":21820"); err != nil { + logger.Error("Failed to send UDP hole punch: %v", err) + } + + // start the UDP holepunch + go service.keepSendingUDPHolePunch(service.host) + return service, nil } func (s *WireGuardService) Close(rm bool) { + select { + case <-s.stopGetConfig: + // Already closed, do nothing + default: + close(s.stopGetConfig) + } + s.wgClient.Close() // Remove the WireGuard interface if rm { if err := s.removeInterface(); err != nil { logger.Error("Failed to remove WireGuard interface: %v", err) } + + // Remove the private key file + if err := os.Remove(s.key.String()); err != nil { + logger.Error("Failed to remove private key file: %v", err) + } } } @@ -215,24 +242,15 @@ func (s *WireGuardService) SetToken(token string) { } func (s *WireGuardService) LoadRemoteConfig() error { - - // get the exising wireguard port - device, err := s.wgClient.Device(s.interfaceName) - if err == nil { - s.Port = uint16(device.ListenPort) - logger.Info("WireGuard interface %s already exists with port %d\n", s.interfaceName, s.Port) - } - - err = s.client.SendMessage("newt/wg/get-config", map[string]interface{}{ - "publicKey": fmt.Sprintf("%s", s.key.PublicKey().String()), - "port": s.Port, - }) + // Send the initial message + err := s.sendGetConfigMessage() if err != nil { - logger.Error("Failed to send registration message: %v", err) + logger.Error("Failed to send initial get-config message: %v", err) return err } - logger.Info("Requesting WireGuard configuration from remote server") + // Start goroutine to periodically send the message until config is received + go s.keepSendingGetConfig() go s.periodicBandwidthCheck() @@ -256,6 +274,8 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { } s.config = config + close(s.stopGetConfig) + // Ensure the WireGuard interface and peers are configured if err := s.ensureWireguardInterface(config); err != nil { logger.Error("Failed to ensure WireGuard interface: %v", err) @@ -264,13 +284,6 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) { if err := s.ensureWireguardPeers(config.Peers); err != nil { logger.Error("Failed to ensure WireGuard peers: %v", err) } - - if err := s.sendUDPHolePunch(s.host + ":21820"); err != nil { - logger.Error("Failed to send UDP hole punch: %v", err) - } - - // start the UDP holepunch - go s.keepSendingUDPHolePunch(s.host) } func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { @@ -932,3 +945,33 @@ func (s *WireGuardService) removeInterface() error { return nil } + +func (s *WireGuardService) sendGetConfigMessage() error { + err := s.client.SendMessage("newt/wg/get-config", map[string]interface{}{ + "publicKey": fmt.Sprintf("%s", s.key.PublicKey().String()), + "port": s.Port, + }) + if err != nil { + logger.Error("Failed to send get-config message: %v", err) + return err + } + logger.Info("Requesting WireGuard configuration from remote server") + return nil +} + +func (s *WireGuardService) keepSendingGetConfig() { + ticker := time.NewTicker(3 * time.Second) + defer ticker.Stop() + + for { + select { + case <-s.stopGetConfig: + logger.Info("Stopping get-config messages") + return + case <-ticker.C: + if err := s.sendGetConfigMessage(); err != nil { + logger.Error("Failed to send periodic get-config: %v", err) + } + } + } +} From 311061a7e47e3f40e36caad56bb02f02f6b7ea54 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 13 May 2025 11:41:55 -0400 Subject: [PATCH 32/87] Set to 127 Former-commit-id: 494e30704b4ff5ac32e25fd9fe02054f20f1f1e2 --- wg/wg.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/wg/wg.go b/wg/wg.go index 6e17880..cc86d57 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -655,8 +655,10 @@ func (s *WireGuardService) handleUpdatePeer(msg websocket.WSMessage) { peerConfig.Endpoint = endpoint logger.Info("Updating Endpoint for peer %s to %s", request.PublicKey, request.Endpoint) } else { - // Request contained endpoint field but it was empty/null - remove endpoint - peerConfig.Endpoint = nil + // specify any address to listen for any incoming packets + peerConfig.Endpoint = &net.UDPAddr{ + IP: net.IPv4(127, 0, 0, 1), + } logger.Info("Removing Endpoint for peer %s", request.PublicKey) } } From a4d49761033969cf2288b831bdcc5bbdefc2f420 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Mon, 9 Jun 2025 22:54:10 +0100 Subject: [PATCH 33/87] Update to use docker network checking against newt networking --- docker/client.go | 99 +++++++++++++++++++++++++++++++++++++++++++----- main.go | 57 ++++++++++++++++++---------- 2 files changed, 128 insertions(+), 28 deletions(-) diff --git a/docker/client.go b/docker/client.go index 98936fe..762ce28 100644 --- a/docker/client.go +++ b/docker/client.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "net" + "os" + "strconv" "strings" "time" @@ -67,8 +69,44 @@ func CheckSocket(socketPath string) bool { return true } +// IsWithinNewtNetwork checks if a provided target is within the newt network +func IsWithinNewtNetwork(socketPath string, containerNameAsHostname bool, targetAddress string, targetPort int) (bool, error) { + containers, err := ListContainers(socketPath, containerNameAsHostname) + if err != nil { + return false, fmt.Errorf("failed to list Docker containers: %s", err) + } + + // If we can find the passed hostname/ip in the networks or as the container name, it is valid and can add it + for _, c := range containers { + for _, network := range c.Networks { + //If the container name matches, check the ports being mapped too + if containerNameAsHostname { + if c.Name == targetAddress { + for _, port := range c.Ports { + if port.PublicPort == targetPort || port.PrivatePort == targetPort { + return true, nil + } + } + } + } else { + //If the ip address matches, check the ports being mapped too + if network.IPAddress == targetAddress { + for _, port := range c.Ports { + if port.PublicPort == targetPort || port.PrivatePort == targetPort { + return true, nil + } + } + } + } + } + } + + combinedTargetAddress := targetAddress + ":" + strconv.Itoa(targetPort) + return false, fmt.Errorf("target address not within newt network: %s", combinedTargetAddress) +} + // ListContainers lists all Docker containers with their network information -func ListContainers(socketPath string) ([]Container, error) { +func ListContainers(socketPath string, containerNameAsHostname bool) ([]Container, error) { // Use the provided socket path or default to standard location if socketPath == "" { socketPath = "/var/run/docker.sock" @@ -88,6 +126,12 @@ func ListContainers(socketPath string) ([]Container, error) { } defer cli.Close() + // Get the newt container + newtContainer, err := getNewtContainer(ctx, cli) + if err != nil { + return nil, fmt.Errorf("failed to list containers: %v", err) + } + // List containers containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) if err != nil { @@ -96,6 +140,12 @@ func ListContainers(socketPath string) ([]Container, error) { var dockerContainers []Container for _, c := range containers { + // Get container name (remove leading slash) + name := "" + if len(c.Names) > 0 { + name = strings.TrimPrefix(c.Names[0], "/") + } + // Convert ports var ports []Port for _, port := range c.Ports { @@ -112,29 +162,32 @@ func ListContainers(socketPath string) ([]Container, error) { ports = append(ports, dockerPort) } - // Get container name (remove leading slash) - name := "" - if len(c.Names) > 0 { - name = strings.TrimPrefix(c.Names[0], "/") - } - // Get network information by inspecting the container networks := make(map[string]Network) - // Inspect container to get detailed network information + // Inspect the container to get detailed network information containerInfo, err := cli.ContainerInspect(ctx, c.ID) if err != nil { logger.Debug("Failed to inspect container %s for network info: %v", c.ID[:12], err) // Continue without network info if inspection fails } else { + // Only containers within the newt network will be returned + isInNewtNetwork := false + // Extract network information from inspection if containerInfo.NetworkSettings != nil && containerInfo.NetworkSettings.Networks != nil { for networkName, endpoint := range containerInfo.NetworkSettings.Networks { + // Determine if the current container is in the newt network + for _, newtNetwork := range newtContainer.NetworkSettings.Networks { + if !isInNewtNetwork { + isInNewtNetwork = endpoint.NetworkID == newtNetwork.NetworkID + } + } + dockerNetwork := Network{ NetworkID: endpoint.NetworkID, EndpointID: endpoint.EndpointID, Gateway: endpoint.Gateway, - IPAddress: endpoint.IPAddress, IPPrefixLen: endpoint.IPPrefixLen, IPv6Gateway: endpoint.IPv6Gateway, GlobalIPv6Address: endpoint.GlobalIPv6Address, @@ -143,9 +196,21 @@ func ListContainers(socketPath string) ([]Container, error) { Aliases: endpoint.Aliases, DNSNames: endpoint.DNSNames, } + + // Don't set the IP address if container name is used as hostname + if !containerNameAsHostname { + dockerNetwork.IPAddress = endpoint.IPAddress + } + networks[networkName] = dockerNetwork } } + + // Don't continue returning this container if not in the newt network(s) + if !isInNewtNetwork { + logger.Debug("container not found within the newt network, skipping: %s", name) + continue + } } dockerContainer := Container{ @@ -164,3 +229,19 @@ func ListContainers(socketPath string) ([]Container, error) { return dockerContainers, nil } + +func getNewtContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) { + // Get newt hostname from the os + newtContainerName, err := os.Hostname() + if err != nil { + return nil, fmt.Errorf("failed to find newt hostname: %v", err) + } + + // Get newt container from the docker socket + newtContainer, err := dockerClient.ContainerInspect(dockerContext, newtContainerName) + if err != nil { + return nil, fmt.Errorf("failed to find newt container: %v", err) + } + + return &newtContainer, nil +} \ No newline at end of file diff --git a/main.go b/main.go index fdece97..7cfb002 100644 --- a/main.go +++ b/main.go @@ -341,18 +341,20 @@ func resolveDomain(domain string) (string, error) { } var ( - endpoint string - id string - secret string - mtu string - mtuInt int - dns string - privateKey wgtypes.Key - err error - logLevel string - updownScript string - tlsPrivateKey string - dockerSocket string + endpoint string + id string + secret string + mtu string + mtuInt int + dns string + privateKey wgtypes.Key + err error + logLevel string + updownScript string + tlsPrivateKey string + dockerSocket string + dockerContainerAsHostname string + dockerContainerAsHostnameBool bool ) func main() { @@ -366,6 +368,7 @@ func main() { updownScript = os.Getenv("UPDOWN_SCRIPT") tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") + dockerContainerAsHostname = os.Getenv("DOCKER_CONTAINER_NAME_AS_HOSTNAME") if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -394,6 +397,9 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } + if dockerContainerAsHostname == "" { + flag.StringVar(&dockerContainerAsHostname, "docker-container-name-as-hostname", "false", "Use container name when hostname for networking (true or false)") + } // do a --version check version := flag.Bool("version", false, "Print the version") @@ -418,6 +424,13 @@ func main() { logger.Fatal("Failed to parse MTU: %v", err) } + // pase if to use hostname over ip address for network sent to pangolin + dockerContainerAsHostnameBool, err = strconv.ParseBool(dockerContainerAsHostname) + if err != nil { + logger.Info("Docker use container name cannot be parsed. Defaulting to 'false'") + dockerContainerAsHostnameBool = false + } + privateKey, err = wgtypes.GeneratePrivateKey() if err != nil { logger.Fatal("Failed to generate private key: %v", err) @@ -676,7 +689,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } // List Docker containers - containers, err := docker.ListContainers(dockerSocket) + containers, err := docker.ListContainers(dockerSocket, dockerContainerAsHostnameBool) if err != nil { logger.Error("Failed to list Docker containers: %v", err) return @@ -760,12 +773,14 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } if action == "add" { - target := parts[1] + ":" + parts[2] + targetAddress := parts[1] + targetPort, _ := strconv.Atoi(parts[2]) + combinedAddress := targetAddress + ":" + parts[2] // Call updown script if provided - processedTarget := target + processedTarget := combinedAddress if updownScript != "" { - newTarget, err := executeUpdownScript(action, proto, target) + newTarget, err := executeUpdownScript(action, proto, combinedAddress) if err != nil { logger.Warn("Updown script error: %v", err) } else if newTarget != "" { @@ -783,8 +798,12 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } // Add the new target - pm.AddTarget(proto, tunnelIP, port, processedTarget) - + isWithinNewtNetwork, err := docker.IsWithinNewtNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) + if !isWithinNewtNetwork { + logger.Error("Not adding target: %v", err) + } else { + pm.AddTarget(proto, tunnelIP, port, processedTarget) + } } else if action == "remove" { logger.Info("Removing target with port %d", port) @@ -849,4 +868,4 @@ func executeUpdownScript(action, proto, target string) (string, error) { } return target, nil -} +} \ No newline at end of file From a52260b49d7b10a2656b992c224aed13ae15159e Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Mon, 9 Jun 2025 23:06:29 +0100 Subject: [PATCH 34/87] Add an enforce network validation flag for docker to not break previous functionality --- docker/client.go | 7 +++-- main.go | 74 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/docker/client.go b/docker/client.go index 762ce28..aee7fdf 100644 --- a/docker/client.go +++ b/docker/client.go @@ -71,7 +71,8 @@ func CheckSocket(socketPath string) bool { // IsWithinNewtNetwork checks if a provided target is within the newt network func IsWithinNewtNetwork(socketPath string, containerNameAsHostname bool, targetAddress string, targetPort int) (bool, error) { - containers, err := ListContainers(socketPath, containerNameAsHostname) + // Always enforce network validation + containers, err := ListContainers(socketPath, true, containerNameAsHostname) if err != nil { return false, fmt.Errorf("failed to list Docker containers: %s", err) } @@ -106,7 +107,7 @@ func IsWithinNewtNetwork(socketPath string, containerNameAsHostname bool, target } // ListContainers lists all Docker containers with their network information -func ListContainers(socketPath string, containerNameAsHostname bool) ([]Container, error) { +func ListContainers(socketPath string, enforceNetworkValidation bool, containerNameAsHostname bool) ([]Container, error) { // Use the provided socket path or default to standard location if socketPath == "" { socketPath = "/var/run/docker.sock" @@ -207,7 +208,7 @@ func ListContainers(socketPath string, containerNameAsHostname bool) ([]Containe } // Don't continue returning this container if not in the newt network(s) - if !isInNewtNetwork { + if enforceNetworkValidation && !isInNewtNetwork { logger.Debug("container not found within the newt network, skipping: %s", name) continue } diff --git a/main.go b/main.go index 7cfb002..177f32e 100644 --- a/main.go +++ b/main.go @@ -341,20 +341,22 @@ func resolveDomain(domain string) (string, error) { } var ( - endpoint string - id string - secret string - mtu string - mtuInt int - dns string - privateKey wgtypes.Key - err error - logLevel string - updownScript string - tlsPrivateKey string - dockerSocket string - dockerContainerAsHostname string - dockerContainerAsHostnameBool bool + endpoint string + id string + secret string + mtu string + mtuInt int + dns string + privateKey wgtypes.Key + err error + logLevel string + updownScript string + tlsPrivateKey string + dockerSocket string + dockerContainerAsHostname string + dockerContainerAsHostnameBool bool + dockerEnforceNetworkValidation string + dockerEnforceNetworkValidationBool bool ) func main() { @@ -369,6 +371,7 @@ func main() { tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") dockerContainerAsHostname = os.Getenv("DOCKER_CONTAINER_NAME_AS_HOSTNAME") + dockerEnforceNetworkValidation = os.Getenv("DOCKER_ENFORCE_NETWORK_VALIDATION") if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -400,6 +403,9 @@ func main() { if dockerContainerAsHostname == "" { flag.StringVar(&dockerContainerAsHostname, "docker-container-name-as-hostname", "false", "Use container name when hostname for networking (true or false)") } + if dockerEnforceNetworkValidation == "" { + flag.StringVar(&dockerEnforceNetworkValidation, "docker-enforce-network-validation", "false", "Enforce validation of container on newt network (true or false)") + } // do a --version check version := flag.Bool("version", false, "Print the version") @@ -431,6 +437,13 @@ func main() { dockerContainerAsHostnameBool = false } + // parse if we want to enforce container network validation + dockerEnforceNetworkValidationBool, err = strconv.ParseBool(dockerEnforceNetworkValidation) + if err != nil { + logger.Info("Docker enforce network validation cannot be parsed. Defaulting to 'false'") + dockerEnforceNetworkValidationBool = false + } + privateKey, err = wgtypes.GeneratePrivateKey() if err != nil { logger.Fatal("Failed to generate private key: %v", err) @@ -450,6 +463,25 @@ func main() { logger.Fatal("Failed to create client: %v", err) } + // output env var values if set + logger.Debug("Endpoint: %v", endpoint) + logger.Debug("Log Level: %v", logLevel) + logger.Debug("Docker Container Name as Hostname: %v", dockerContainerAsHostnameBool) + logger.Debug("Docker Network Validation Enabled: %v", dockerEnforceNetworkValidationBool) + logger.Debug("TLS Private Key Set: %v", tlsPrivateKey != "") + if dns != "" { + logger.Debug("Dns: %v", dns) + } + if dockerSocket != "" { + logger.Debug("Docker Socket: %v", dockerSocket) + } + if mtu != "" { + logger.Debug("MTU: %v", mtu) + } + if updownScript != "" { + logger.Debug("Up Down Script: %v", updownScript) + } + // Create TUN device and network stack var tun tun.Device var tnet *netstack.Net @@ -689,7 +721,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } // List Docker containers - containers, err := docker.ListContainers(dockerSocket, dockerContainerAsHostnameBool) + containers, err := docker.ListContainers(dockerSocket, dockerEnforceNetworkValidationBool, dockerContainerAsHostnameBool) if err != nil { logger.Error("Failed to list Docker containers: %v", err) return @@ -798,9 +830,15 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } // Add the new target - isWithinNewtNetwork, err := docker.IsWithinNewtNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) - if !isWithinNewtNetwork { - logger.Error("Not adding target: %v", err) + if dockerEnforceNetworkValidationBool { + logger.Info("Enforcing docker network validation") + + isWithinNewtNetwork, err := docker.IsWithinNewtNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) + if !isWithinNewtNetwork { + logger.Error("Not adding target: %v", err) + } else { + pm.AddTarget(proto, tunnelIP, port, processedTarget) + } } else { pm.AddTarget(proto, tunnelIP, port, processedTarget) } From 4f84f747ee289e655cbbebb736e9ad70931130c0 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 9 Jun 2025 22:24:49 -0400 Subject: [PATCH 35/87] Update go Former-commit-id: ce6d340a8d0726c41078c4c74e1bca146bf16b4b --- go.mod | 16 +++++------ go.sum | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 go.sum diff --git a/go.mod b/go.mod index 5e43fab..a40e52c 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,13 @@ go 1.23.1 toolchain go1.23.2 require ( + github.com/docker/docker v28.2.2+incompatible github.com/google/gopacket v1.1.19 github.com/gorilla/websocket v1.5.3 github.com/vishvananda/netlink v1.3.0 + golang.org/x/crypto v0.39.0 golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa golang.org/x/net v0.41.0 - github.com/docker/docker v28.2.2+incompatible golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 @@ -30,15 +31,11 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/josharian/native v1.1.0 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.1 // indirect - github.com/vishvananda/netns v0.0.4 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.33.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect @@ -46,14 +43,17 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/vishvananda/netns v0.0.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect - golang.org/x/mod v0.12.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.33.0 // indirect golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.13.0 // indirect + golang.org/x/tools v0.30.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect ) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e96d161 --- /dev/null +++ b/go.sum @@ -0,0 +1,91 @@ +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= +github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= +software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= From d0f997a5d87268381426ead0b599b4b9e56ce438 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 9 Jun 2025 22:26:31 -0400 Subject: [PATCH 36/87] Resolve merge issues Former-commit-id: a5f4d5fdf6447359b0328ed2e1a5df0257a3d360 --- main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main.go b/main.go index fc00942..ad41fd1 100644 --- a/main.go +++ b/main.go @@ -356,7 +356,6 @@ var ( privateKey wgtypes.Key err error logLevel string - updownScript string interfaceName string generateAndSaveKeyTo string rm bool @@ -797,7 +796,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // Wait for interrupt signal sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - sigReceived := <-sigCh + <-sigCh dev.Close() From e335bb8a1f3f9744f3856d828060ee073b48ee25 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:57:50 +0100 Subject: [PATCH 37/87] Rename added functions for docker client --- docker/client.go | 46 +++++++++++++++++++++++----------------------- main.go | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docker/client.go b/docker/client.go index aee7fdf..13eabd9 100644 --- a/docker/client.go +++ b/docker/client.go @@ -69,8 +69,8 @@ func CheckSocket(socketPath string) bool { return true } -// IsWithinNewtNetwork checks if a provided target is within the newt network -func IsWithinNewtNetwork(socketPath string, containerNameAsHostname bool, targetAddress string, targetPort int) (bool, error) { +// IsWithinHostNetwork checks if a provided target is within the host container network +func IsWithinHostNetwork(socketPath string, containerNameAsHostname bool, targetAddress string, targetPort int) (bool, error) { // Always enforce network validation containers, err := ListContainers(socketPath, true, containerNameAsHostname) if err != nil { @@ -103,7 +103,7 @@ func IsWithinNewtNetwork(socketPath string, containerNameAsHostname bool, target } combinedTargetAddress := targetAddress + ":" + strconv.Itoa(targetPort) - return false, fmt.Errorf("target address not within newt network: %s", combinedTargetAddress) + return false, fmt.Errorf("target address not within host container network: %s", combinedTargetAddress) } // ListContainers lists all Docker containers with their network information @@ -127,10 +127,10 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN } defer cli.Close() - // Get the newt container - newtContainer, err := getNewtContainer(ctx, cli) + // Get the host container + hostContainer, err := getHostContainer(ctx, cli) if err != nil { - return nil, fmt.Errorf("failed to list containers: %v", err) + return nil, fmt.Errorf("failed to get host container: %v", err) } // List containers @@ -172,16 +172,16 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN logger.Debug("Failed to inspect container %s for network info: %v", c.ID[:12], err) // Continue without network info if inspection fails } else { - // Only containers within the newt network will be returned - isInNewtNetwork := false + // Only containers within the host container network will be returned + isInHostContainerNetwork := false // Extract network information from inspection if containerInfo.NetworkSettings != nil && containerInfo.NetworkSettings.Networks != nil { for networkName, endpoint := range containerInfo.NetworkSettings.Networks { - // Determine if the current container is in the newt network - for _, newtNetwork := range newtContainer.NetworkSettings.Networks { - if !isInNewtNetwork { - isInNewtNetwork = endpoint.NetworkID == newtNetwork.NetworkID + // Determine if the current container is in the host container network + for _, hostContainerNetwork := range hostContainer.NetworkSettings.Networks { + if !isInHostContainerNetwork { + isInHostContainerNetwork = endpoint.NetworkID == hostContainerNetwork.NetworkID } } @@ -207,9 +207,9 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN } } - // Don't continue returning this container if not in the newt network(s) - if enforceNetworkValidation && !isInNewtNetwork { - logger.Debug("container not found within the newt network, skipping: %s", name) + // Don't continue returning this container if not in the host container network(s) + if enforceNetworkValidation && !isInHostContainerNetwork { + logger.Debug("container not found within the host container network, skipping: %s", name) continue } } @@ -231,18 +231,18 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN return dockerContainers, nil } -func getNewtContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) { - // Get newt hostname from the os - newtContainerName, err := os.Hostname() +func getHostContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) { + // Get hostname from the os + containerHostname, err := os.Hostname() if err != nil { - return nil, fmt.Errorf("failed to find newt hostname: %v", err) + return nil, fmt.Errorf("failed to find hostname: %v", err) } - // Get newt container from the docker socket - newtContainer, err := dockerClient.ContainerInspect(dockerContext, newtContainerName) + // Get host container from the docker socket + hostContainer, err := dockerClient.ContainerInspect(dockerContext, containerHostname) if err != nil { - return nil, fmt.Errorf("failed to find newt container: %v", err) + return nil, fmt.Errorf("failed to inspect host container: %v", err) } - return &newtContainer, nil + return &hostContainer, nil } \ No newline at end of file diff --git a/main.go b/main.go index 177f32e..0606dec 100644 --- a/main.go +++ b/main.go @@ -833,7 +833,7 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto if dockerEnforceNetworkValidationBool { logger.Info("Enforcing docker network validation") - isWithinNewtNetwork, err := docker.IsWithinNewtNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) + isWithinNewtNetwork, err := docker.IsWithinHostNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) if !isWithinNewtNetwork { logger.Error("Not adding target: %v", err) } else { From cbbd5b0c76602e375b540a727e373a3265787f14 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:58:37 +0100 Subject: [PATCH 38/87] Add extra pre-condition check for enforcing docker network --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 0606dec..5236a1e 100644 --- a/main.go +++ b/main.go @@ -830,7 +830,7 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } // Add the new target - if dockerEnforceNetworkValidationBool { + if dockerSocket != "" && dockerEnforceNetworkValidationBool { logger.Info("Enforcing docker network validation") isWithinNewtNetwork, err := docker.IsWithinHostNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) From 5476a69963904b3e51da105c2b8d21c902e21632 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:05:41 +0100 Subject: [PATCH 39/87] Log the container name and id --- docker/client.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker/client.go b/docker/client.go index 13eabd9..a407c81 100644 --- a/docker/client.go +++ b/docker/client.go @@ -141,6 +141,9 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN var dockerContainers []Container for _, c := range containers { + // Short ID like docker ps + shortId := c.ID[:12] + // Get container name (remove leading slash) name := "" if len(c.Names) > 0 { @@ -169,7 +172,7 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN // Inspect the container to get detailed network information containerInfo, err := cli.ContainerInspect(ctx, c.ID) if err != nil { - logger.Debug("Failed to inspect container %s for network info: %v", c.ID[:12], err) + logger.Debug("Failed to inspect container %s (%s) for network info: %v", shortId, name, err) // Continue without network info if inspection fails } else { // Only containers within the host container network will be returned @@ -209,13 +212,13 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN // Don't continue returning this container if not in the host container network(s) if enforceNetworkValidation && !isInHostContainerNetwork { - logger.Debug("container not found within the host container network, skipping: %s", name) + logger.Debug("Container not found within the host container network, skipping: %s (%s)", shortId, name) continue } } dockerContainer := Container{ - ID: c.ID[:12], // Show short ID like docker ps + ID: shortId, Name: name, Image: c.Image, State: c.State, From e26552a5d7329695ffb7afb92a7c2409b696ded2 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:25:27 +0100 Subject: [PATCH 40/87] Small refinement to how the docker enforcement setting is applied --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 5236a1e..a548fe4 100644 --- a/main.go +++ b/main.go @@ -401,7 +401,7 @@ func main() { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } if dockerContainerAsHostname == "" { - flag.StringVar(&dockerContainerAsHostname, "docker-container-name-as-hostname", "false", "Use container name when hostname for networking (true or false)") + flag.StringVar(&dockerContainerAsHostname, "docker-container-name-as-hostname", "false", "Use container name as hostname for networking (true or false)") } if dockerEnforceNetworkValidation == "" { flag.StringVar(&dockerEnforceNetworkValidation, "docker-enforce-network-validation", "false", "Enforce validation of container on newt network (true or false)") @@ -830,7 +830,7 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } // Add the new target - if dockerSocket != "" && dockerEnforceNetworkValidationBool { + if dockerEnforceNetworkValidationBool { logger.Info("Enforcing docker network validation") isWithinNewtNetwork, err := docker.IsWithinHostNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) From 5cb86f3e4738f5f467370418c7fb3cbba28cae3b Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:26:06 +0100 Subject: [PATCH 41/87] Update to readme with new configuration settings --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 9cd2768..0867309 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ When Newt receives WireGuard control messages, it will use the information encod - `updown` (optional): A script to be called when targets are added or removed. - `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls) - `docker-socket` (optional): Set the Docker socket to use the container discovery integration +- `docker-container-name-as-hostname` (optional): Use the docker container name as the hostname rather then the IP of the container +- `docker-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process - Example: @@ -86,6 +88,24 @@ You can specify the Docker socket path using the `--docker-socket` CLI argument If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation. +### Docker Container Name as Hostname + +When run as a Docker container, Newt by default will send the IP Address of the container. This feature will make it so you will be able to use the internal Docker DNS resolution, to be able to use the name of the container over the IP address. + +**Configuration:** + +This feature is `false` by default. It can be enabled via setting the `--docker-container-name-as-hostname` CLI argument or by setting the `DOCKER_CONTAINER_NAME_AS_HOSTNAME` environment variable. + +### Docker Enforce Network Validation + +When run as a Docker container, Newt can validate that the target being provided is on the same network as the Newt container and therefore is reachable. Validation will be carried out against either the hostname/IP Address and the Port number to ensure the running container is exposing the ports to Newt. + +**Configuration:** + +Validation is `false` by default. It can be enabled via setting the `--docker-enforce-network-validation` CLI argument or by setting the `DOCKER_ENFORCE_NETWORK_VALIDATION` environment variable. + +If validation is enforced and the Docker socket is enforced and the Docker socket is not available or accessible, Newt will **not** add the target as it cannot be verified. + ### Updown You can pass in a updown script for Newt to call when it is adding or removing a target: From b3c013446276ea884a665767b95fc9179114ba22 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 10 Jun 2025 21:52:33 -0400 Subject: [PATCH 42/87] First pass at pinging Former-commit-id: d0e220511aba2cdd1318385bf071802b0b81dc1d --- main.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index ad41fd1..59fb7c6 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "fmt" "math/rand" "net" + "net/http" "net/netip" "os" "os/exec" @@ -52,6 +53,13 @@ type TargetData struct { Targets []string `json:"targets"` } +// ExitNode represents an exit node with an ID, endpoint, and weight. +type ExitNode struct { + ID string `json:"id"` + Endpoint string `json:"endpoint"` + Weight float64 `json:"weight"` +} + func fixKey(key string) string { // Remove any whitespace key = strings.TrimSpace(key) @@ -363,6 +371,7 @@ var ( updownScript string tlsPrivateKey string dockerSocket string + publicKey wgtypes.Key ) func main() { @@ -623,6 +632,88 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) + client.RegisterHandler("newt/ping/exitNodes", func(msg websocket.WSMessage) { + logger.Info("Received ping message") + + // Parse the incoming list of exit nodes + var exitNodes []ExitNode + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Info("Error marshaling data: %v", err) + return + } + if err := json.Unmarshal(jsonData, &exitNodes); err != nil { + logger.Info("Error unmarshaling exit node data: %v", err) + return + } + if len(exitNodes) == 0 { + logger.Info("No exit nodes provided") + return + } + + type nodeResult struct { + Node ExitNode + Latency time.Duration + Err error + } + + results := make([]nodeResult, len(exitNodes)) + for i, node := range exitNodes { + start := time.Now() + client := &http.Client{ + Timeout: 5 * time.Second, + } + url := node.Endpoint + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + url = "http://" + url + } + if !strings.HasSuffix(url, "/ping") { + url = strings.TrimRight(url, "/") + "/ping" + } + resp, err := client.Get(url) + latency := time.Since(start) + if err != nil { + logger.Warn("Failed to ping exit node %s (%s): %v", node.ID, url, err) + results[i] = nodeResult{Node: node, Latency: latency, Err: err} + continue + } + resp.Body.Close() + results[i] = nodeResult{Node: node, Latency: latency, Err: nil} + logger.Info("Exit node %s latency: %v", node.ID, latency) + } + + // Select the best node based on weighted score (latency * (1/weight)) + var bestNode *ExitNode + var bestScore float64 = 1e12 // large initial value + for _, res := range results { + if res.Err != nil || res.Node.Weight <= 0 { + continue + } + score := float64(res.Latency.Milliseconds()) / res.Node.Weight + logger.Info("Exit node %s score: %.2f (latency: %dms, weight: %.2f)", res.Node.ID, score, res.Latency.Milliseconds(), res.Node.Weight) + if score < bestScore { + bestScore = score + bestNode = &res.Node + } + } + + if bestNode == nil { + logger.Warn("No suitable exit node found") + return + } + + logger.Info("Selected exit node: %s (%s)", bestNode.ID, bestNode.Endpoint) + + err = client.SendMessage("newt/wg/register", map[string]interface{}{ + "publicKey": publicKey.String(), + "exitNode": bestNode.ID, + }) + if err != nil { + logger.Error("Failed to send registration message: %v", err) + return + } + }) + client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) { logger.Info("Received: %+v", msg) @@ -762,15 +853,13 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub }) client.OnConnect(func() error { - publicKey := privateKey.PublicKey() + publicKey = privateKey.PublicKey() logger.Debug("Public key: %s", publicKey) - err := client.SendMessage("newt/wg/register", map[string]interface{}{ - "publicKey": publicKey.String(), - }) + // request from the server the list of nodes to ping at newt/ping/request + err := client.SendMessage("newt/ping/request", map[string]interface{}{}) if err != nil { - logger.Error("Failed to send registration message: %v", err) - return err + logger.Error("Failed to send ping request: %v", err) } if wgService != nil { From 22b2dabdb6a6791add3d8d747eb48322c7bf802d Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 13 Jun 2025 16:15:49 -0400 Subject: [PATCH 43/87] Working on pining Former-commit-id: 8d4d8b91b93e26188d1db2fdfd64929efc313c71 --- main.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 59fb7c6..fef4759 100644 --- a/main.go +++ b/main.go @@ -53,9 +53,13 @@ type TargetData struct { Targets []string `json:"targets"` } +type ExitNodeData struct { + ExitNodes []ExitNode `json:"exitNodes"` +} + // ExitNode represents an exit node with an ID, endpoint, and weight. type ExitNode struct { - ID string `json:"id"` + ID string `json:"exitNodeId"` Endpoint string `json:"endpoint"` Weight float64 `json:"weight"` } @@ -75,7 +79,7 @@ func fixKey(key string) string { } func ping(tnet *netstack.Net, dst string) error { - logger.Info("Pinging %s", dst) + logger.Debug("Pinging %s", dst) socket, err := tnet.Dial("ping4", dst) if err != nil { return fmt.Errorf("failed to create ICMP socket: %w", err) @@ -122,7 +126,7 @@ func ping(tnet *netstack.Net, dst string) error { replyPing.Seq, replyPing.Data, requestPing.Seq, requestPing.Data) } - logger.Info("Ping latency: %v", time.Since(start)) + logger.Debug("Ping latency: %v", time.Since(start)) return nil } @@ -636,16 +640,20 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("Received ping message") // Parse the incoming list of exit nodes - var exitNodes []ExitNode + // Exit nodes is a json + var exitNodeData ExitNodeData + jsonData, err := json.Marshal(msg.Data) if err != nil { logger.Info("Error marshaling data: %v", err) return } - if err := json.Unmarshal(jsonData, &exitNodes); err != nil { + if err := json.Unmarshal(jsonData, &exitNodeData); err != nil { logger.Info("Error unmarshaling exit node data: %v", err) return } + exitNodes := exitNodeData.ExitNodes + if len(exitNodes) == 0 { logger.Info("No exit nodes provided") return @@ -705,8 +713,8 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("Selected exit node: %s (%s)", bestNode.ID, bestNode.Endpoint) err = client.SendMessage("newt/wg/register", map[string]interface{}{ - "publicKey": publicKey.String(), - "exitNode": bestNode.ID, + "publicKey": publicKey.String(), + "exitNodeId": bestNode.ID, }) if err != nil { logger.Error("Failed to send registration message: %v", err) From 6d9160ab5e98b4d82a82a47ecf48a35a23e3145e Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Sat, 14 Jun 2025 01:26:11 +0100 Subject: [PATCH 44/87] Simplified based on PR feedback and support checking use of "bridge" network --- README.md | 15 +++--- docker/client.go | 132 ++++++++++++++++++++++++++--------------------- main.go | 30 +++-------- 3 files changed, 89 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 0867309..5196ac6 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ When Newt receives WireGuard control messages, it will use the information encod - `updown` (optional): A script to be called when targets are added or removed. - `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls) - `docker-socket` (optional): Set the Docker socket to use the container discovery integration -- `docker-container-name-as-hostname` (optional): Use the docker container name as the hostname rather then the IP of the container - `docker-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process - Example: @@ -88,18 +87,20 @@ You can specify the Docker socket path using the `--docker-socket` CLI argument If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation. -### Docker Container Name as Hostname +#### Hostnames vs IPs -When run as a Docker container, Newt by default will send the IP Address of the container. This feature will make it so you will be able to use the internal Docker DNS resolution, to be able to use the name of the container over the IP address. - -**Configuration:** - -This feature is `false` by default. It can be enabled via setting the `--docker-container-name-as-hostname` CLI argument or by setting the `DOCKER_CONTAINER_NAME_AS_HOSTNAME` environment variable. +When the Docker Socket Integration is used, depending on the network which Newt is run with, will determine if the hostname (generally considered the container name) or the IP address of the container is sent to Pangolin. Here are some of the scenarios below to describe what to expect: +- **Running in Network Mode 'host'**: IP addresses will be used +- **Running in Network Mode 'bridge'**: IP addresses will be used +- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default so hostnames will be used +- **Running on docker-compose with defined network**: Will use hostnames ### Docker Enforce Network Validation When run as a Docker container, Newt can validate that the target being provided is on the same network as the Newt container and therefore is reachable. Validation will be carried out against either the hostname/IP Address and the Port number to ensure the running container is exposing the ports to Newt. +It is important to note that if the Newt container is run with a network mode of `host` that this feature will not work. Running in `host` mode causes the container to share its resources with the host machine, therefore making it so the container information cannot be retrieved to be able to carry out required validation + **Configuration:** Validation is `false` by default. It can be enabled via setting the `--docker-enforce-network-validation` CLI argument or by setting the `DOCKER_ENFORCE_NETWORK_VALIDATION` environment variable. diff --git a/docker/client.go b/docker/client.go index a407c81..9fedf52 100644 --- a/docker/client.go +++ b/docker/client.go @@ -10,6 +10,7 @@ import ( "time" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" "github.com/fosrl/newt/logger" ) @@ -70,18 +71,22 @@ func CheckSocket(socketPath string) bool { } // IsWithinHostNetwork checks if a provided target is within the host container network -func IsWithinHostNetwork(socketPath string, containerNameAsHostname bool, targetAddress string, targetPort int) (bool, error) { +func IsWithinHostNetwork(socketPath string, targetAddress string, targetPort int) (bool, error) { // Always enforce network validation - containers, err := ListContainers(socketPath, true, containerNameAsHostname) + containers, err := ListContainers(socketPath, true) if err != nil { - return false, fmt.Errorf("failed to list Docker containers: %s", err) + + return false, err } - // If we can find the passed hostname/ip in the networks or as the container name, it is valid and can add it + // Determine if given an IP address + var parsedTargetAddressIp = net.ParseIP(targetAddress) + + // If we can find the passed hostname/IP address in the networks or as the container name, it is valid and can add it for _, c := range containers { for _, network := range c.Networks { - //If the container name matches, check the ports being mapped too - if containerNameAsHostname { + // If the target address is not an IP address, use the container name + if parsedTargetAddressIp == nil { if c.Name == targetAddress { for _, port := range c.Ports { if port.PublicPort == targetPort || port.PrivatePort == targetPort { @@ -90,7 +95,7 @@ func IsWithinHostNetwork(socketPath string, containerNameAsHostname bool, target } } } else { - //If the ip address matches, check the ports being mapped too + //If the IP address matches, check the ports being mapped too if network.IPAddress == targetAddress { for _, port := range c.Ports { if port.PublicPort == targetPort || port.PrivatePort == targetPort { @@ -107,12 +112,19 @@ func IsWithinHostNetwork(socketPath string, containerNameAsHostname bool, target } // ListContainers lists all Docker containers with their network information -func ListContainers(socketPath string, enforceNetworkValidation bool, containerNameAsHostname bool) ([]Container, error) { +func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Container, error) { // Use the provided socket path or default to standard location if socketPath == "" { socketPath = "/var/run/docker.sock" } + // Used to filter down containers returned to Pangolin + containerFilters := filters.NewArgs() + + // Used to determine if we will send IP addresses or hostnames to Pangolin + useContainerIpAddresses := true + hostContainerId := "" + // Create a new Docker client ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -125,16 +137,34 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN if err != nil { return nil, fmt.Errorf("failed to create Docker client: %v", err) } + defer cli.Close() - // Get the host container hostContainer, err := getHostContainer(ctx, cli) - if err != nil { - return nil, fmt.Errorf("failed to get host container: %v", err) + if enforceNetworkValidation && err != nil { + return nil, fmt.Errorf("network validation enforced, cannot validate due to: %v", err) + } + + // We may not be able to get back host container in scenarios like running the container in network mode 'host' + if hostContainer != nil { + // We can use the host container to filter out the list of returned containers + hostContainerId = hostContainer.ID + + for hostContainerNetworkName := range hostContainer.NetworkSettings.Networks { + // If we're enforcing network validation, we'll filter on the host containers networks + if enforceNetworkValidation { + containerFilters.Add("network", hostContainerNetworkName) + } + + // If the container is on the docker bridge network, we will use IP addresses over hostnames + if useContainerIpAddresses && hostContainerNetworkName != "bridge" { + useContainerIpAddresses = false + } + } } // List containers - containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) + containers, err := cli.ContainerList(ctx, container.ListOptions{All: true, Filters: containerFilters}) if err != nil { return nil, fmt.Errorf("failed to list containers: %v", err) } @@ -144,6 +174,11 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN // Short ID like docker ps shortId := c.ID[:12] + // Skip host container if set + if hostContainerId != "" && c.ID == hostContainerId { + continue + } + // Get container name (remove leading slash) name := "" if len(c.Names) > 0 { @@ -169,51 +204,28 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN // Get network information by inspecting the container networks := make(map[string]Network) - // Inspect the container to get detailed network information - containerInfo, err := cli.ContainerInspect(ctx, c.ID) - if err != nil { - logger.Debug("Failed to inspect container %s (%s) for network info: %v", shortId, name, err) - // Continue without network info if inspection fails - } else { - // Only containers within the host container network will be returned - isInHostContainerNetwork := false - - // Extract network information from inspection - if containerInfo.NetworkSettings != nil && containerInfo.NetworkSettings.Networks != nil { - for networkName, endpoint := range containerInfo.NetworkSettings.Networks { - // Determine if the current container is in the host container network - for _, hostContainerNetwork := range hostContainer.NetworkSettings.Networks { - if !isInHostContainerNetwork { - isInHostContainerNetwork = endpoint.NetworkID == hostContainerNetwork.NetworkID - } - } - - dockerNetwork := Network{ - NetworkID: endpoint.NetworkID, - EndpointID: endpoint.EndpointID, - Gateway: endpoint.Gateway, - IPPrefixLen: endpoint.IPPrefixLen, - IPv6Gateway: endpoint.IPv6Gateway, - GlobalIPv6Address: endpoint.GlobalIPv6Address, - GlobalIPv6PrefixLen: endpoint.GlobalIPv6PrefixLen, - MacAddress: endpoint.MacAddress, - Aliases: endpoint.Aliases, - DNSNames: endpoint.DNSNames, - } - - // Don't set the IP address if container name is used as hostname - if !containerNameAsHostname { - dockerNetwork.IPAddress = endpoint.IPAddress - } - - networks[networkName] = dockerNetwork + // Extract network information from inspection + if c.NetworkSettings != nil && c.NetworkSettings.Networks != nil { + for networkName, endpoint := range c.NetworkSettings.Networks { + dockerNetwork := Network{ + NetworkID: endpoint.NetworkID, + EndpointID: endpoint.EndpointID, + Gateway: endpoint.Gateway, + IPPrefixLen: endpoint.IPPrefixLen, + IPv6Gateway: endpoint.IPv6Gateway, + GlobalIPv6Address: endpoint.GlobalIPv6Address, + GlobalIPv6PrefixLen: endpoint.GlobalIPv6PrefixLen, + MacAddress: endpoint.MacAddress, + Aliases: endpoint.Aliases, + DNSNames: endpoint.DNSNames, } - } - // Don't continue returning this container if not in the host container network(s) - if enforceNetworkValidation && !isInHostContainerNetwork { - logger.Debug("Container not found within the host container network, skipping: %s (%s)", shortId, name) - continue + // Use IPs over hostnames/containers as we're on the bridge network + if useContainerIpAddresses { + dockerNetwork.IPAddress = endpoint.IPAddress + } + + networks[networkName] = dockerNetwork } } @@ -228,23 +240,25 @@ func ListContainers(socketPath string, enforceNetworkValidation bool, containerN Created: c.Created, Networks: networks, } + dockerContainers = append(dockerContainers, dockerContainer) } return dockerContainers, nil } +// getHostContainer gets the current container for the current host if possible func getHostContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) { // Get hostname from the os - containerHostname, err := os.Hostname() + hostContainerName, err := os.Hostname() if err != nil { - return nil, fmt.Errorf("failed to find hostname: %v", err) + return nil, fmt.Errorf("failed to find hostname for container") } // Get host container from the docker socket - hostContainer, err := dockerClient.ContainerInspect(dockerContext, containerHostname) + hostContainer, err := dockerClient.ContainerInspect(dockerContext, hostContainerName) if err != nil { - return nil, fmt.Errorf("failed to inspect host container: %v", err) + return nil, fmt.Errorf("failed to find host container") } return &hostContainer, nil diff --git a/main.go b/main.go index a548fe4..ffa539c 100644 --- a/main.go +++ b/main.go @@ -353,8 +353,6 @@ var ( updownScript string tlsPrivateKey string dockerSocket string - dockerContainerAsHostname string - dockerContainerAsHostnameBool bool dockerEnforceNetworkValidation string dockerEnforceNetworkValidationBool bool ) @@ -370,7 +368,6 @@ func main() { updownScript = os.Getenv("UPDOWN_SCRIPT") tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") - dockerContainerAsHostname = os.Getenv("DOCKER_CONTAINER_NAME_AS_HOSTNAME") dockerEnforceNetworkValidation = os.Getenv("DOCKER_ENFORCE_NETWORK_VALIDATION") if endpoint == "" { @@ -400,9 +397,6 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } - if dockerContainerAsHostname == "" { - flag.StringVar(&dockerContainerAsHostname, "docker-container-name-as-hostname", "false", "Use container name as hostname for networking (true or false)") - } if dockerEnforceNetworkValidation == "" { flag.StringVar(&dockerEnforceNetworkValidation, "docker-enforce-network-validation", "false", "Enforce validation of container on newt network (true or false)") } @@ -412,7 +406,7 @@ func main() { flag.Parse() - newtVersion := "Newt version replaceme" + newtVersion := "Newt version JB wip" if *version { fmt.Println(newtVersion) os.Exit(0) @@ -430,13 +424,6 @@ func main() { logger.Fatal("Failed to parse MTU: %v", err) } - // pase if to use hostname over ip address for network sent to pangolin - dockerContainerAsHostnameBool, err = strconv.ParseBool(dockerContainerAsHostname) - if err != nil { - logger.Info("Docker use container name cannot be parsed. Defaulting to 'false'") - dockerContainerAsHostnameBool = false - } - // parse if we want to enforce container network validation dockerEnforceNetworkValidationBool, err = strconv.ParseBool(dockerEnforceNetworkValidation) if err != nil { @@ -466,7 +453,6 @@ func main() { // output env var values if set logger.Debug("Endpoint: %v", endpoint) logger.Debug("Log Level: %v", logLevel) - logger.Debug("Docker Container Name as Hostname: %v", dockerContainerAsHostnameBool) logger.Debug("Docker Network Validation Enabled: %v", dockerEnforceNetworkValidationBool) logger.Debug("TLS Private Key Set: %v", tlsPrivateKey != "") if dns != "" { @@ -721,7 +707,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } // List Docker containers - containers, err := docker.ListContainers(dockerSocket, dockerEnforceNetworkValidationBool, dockerContainerAsHostnameBool) + containers, err := docker.ListContainers(dockerSocket, dockerEnforceNetworkValidationBool) if err != nil { logger.Error("Failed to list Docker containers: %v", err) return @@ -829,17 +815,17 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } } - // Add the new target + // If docker network validation is enabled if dockerEnforceNetworkValidationBool { - logger.Info("Enforcing docker network validation") - - isWithinNewtNetwork, err := docker.IsWithinHostNetwork(dockerSocket, dockerContainerAsHostnameBool, targetAddress, targetPort) - if !isWithinNewtNetwork { - logger.Error("Not adding target: %v", err) + // If the target address is within the host container network, the target will be added + isWithinHostContainerNetwork, err := docker.IsWithinHostNetwork(dockerSocket, targetAddress, targetPort) + if !isWithinHostContainerNetwork { + logger.Warn("Not adding target address: %v", err) } else { pm.AddTarget(proto, tunnelIP, port, processedTarget) } } else { + // If we're not enforcing network validation, just proceed with adding the target pm.AddTarget(proto, tunnelIP, port, processedTarget) } } else if action == "remove" { From 58f78350724e67c19b58892092a750f9d461fedd Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:22:14 +0100 Subject: [PATCH 45/87] Revise README docs --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5196ac6..b8917b2 100644 --- a/README.md +++ b/README.md @@ -89,23 +89,23 @@ If the Docker socket is not available or accessible, Newt will gracefully disabl #### Hostnames vs IPs -When the Docker Socket Integration is used, depending on the network which Newt is run with, will determine if the hostname (generally considered the container name) or the IP address of the container is sent to Pangolin. Here are some of the scenarios below to describe what to expect: +When the Docker Socket Integration is used, depending on the network which Newt is run with, either the hostname (generally considered the container name) or the IP address of the container will be sent to Pangolin. Here are some of the scenarios where IPs or hostname of the container will be utilised: - **Running in Network Mode 'host'**: IP addresses will be used - **Running in Network Mode 'bridge'**: IP addresses will be used -- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default so hostnames will be used +- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default, will use hostnames - **Running on docker-compose with defined network**: Will use hostnames ### Docker Enforce Network Validation -When run as a Docker container, Newt can validate that the target being provided is on the same network as the Newt container and therefore is reachable. Validation will be carried out against either the hostname/IP Address and the Port number to ensure the running container is exposing the ports to Newt. +When run as a Docker container, Newt can validate that the target being provided is on the same network as the Newt container and only return containers directly accessible by Newt. Validation will be carried out against either the hostname/IP Address and the Port number to ensure the running container is exposing the ports to Newt. -It is important to note that if the Newt container is run with a network mode of `host` that this feature will not work. Running in `host` mode causes the container to share its resources with the host machine, therefore making it so the container information cannot be retrieved to be able to carry out required validation +It is important to note that if the Newt container is run with a network mode of `host` that this feature will not work. Running in `host` mode causes the container to share its resources with the host machine, therefore making it so the specific host container information for Newt cannot be retrieved to be able to carry out network validation. **Configuration:** Validation is `false` by default. It can be enabled via setting the `--docker-enforce-network-validation` CLI argument or by setting the `DOCKER_ENFORCE_NETWORK_VALIDATION` environment variable. -If validation is enforced and the Docker socket is enforced and the Docker socket is not available or accessible, Newt will **not** add the target as it cannot be verified. +If validation is enforced and the Docker socket is available, Newt will **not** add the target as it cannot be verified. A warning will be presented in the Newt logs. ### Updown From 48cb0bf5a74add58cc55e0742b21da8c8bc27bc7 Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:22:56 +0100 Subject: [PATCH 46/87] Minor README update for consistentcy --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b8917b2..c2e6a28 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,8 @@ If the Docker socket is not available or accessible, Newt will gracefully disabl When the Docker Socket Integration is used, depending on the network which Newt is run with, either the hostname (generally considered the container name) or the IP address of the container will be sent to Pangolin. Here are some of the scenarios where IPs or hostname of the container will be utilised: - **Running in Network Mode 'host'**: IP addresses will be used - **Running in Network Mode 'bridge'**: IP addresses will be used -- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default, will use hostnames -- **Running on docker-compose with defined network**: Will use hostnames +- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default, hostnames will be used +- **Running on docker-compose with defined network**: Hostnames will be used ### Docker Enforce Network Validation From 7c971d278ce7c3fe0d96da3c2be9e992f4488e2a Mon Sep 17 00:00:00 2001 From: Jonny Booker <1131478+JonnyBooker@users.noreply.github.com> Date: Mon, 16 Jun 2025 19:38:10 +0100 Subject: [PATCH 47/87] Revert the newt version placeholder text --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index ffa539c..6622999 100644 --- a/main.go +++ b/main.go @@ -406,7 +406,7 @@ func main() { flag.Parse() - newtVersion := "Newt version JB wip" + newtVersion := "Newt version replaceme" if *version { fmt.Println(newtVersion) os.Exit(0) @@ -892,4 +892,4 @@ func executeUpdownScript(action, proto, target string) (string, error) { } return target, nil -} \ No newline at end of file +} From 2eef0e3f33cf9e852cf2a24a00aa3cc8b4f411d7 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 16 Jun 2025 15:54:46 -0400 Subject: [PATCH 48/87] adjust weight calculation Former-commit-id: bbea9a91da53c4b242b5a0c5640fe0268092ed2a --- main.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index fef4759..98ebd52 100644 --- a/main.go +++ b/main.go @@ -690,17 +690,25 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("Exit node %s latency: %v", node.ID, latency) } - // Select the best node based on weighted score (latency * (1/weight)) + // Select the best node based on weighted score + // weight / latency + // choose highest score + // if same score, choose lowest latency var bestNode *ExitNode - var bestScore float64 = 1e12 // large initial value + var bestScore float64 = -1e12 // small value + var bestLatency time.Duration = 1e12 // large value for _, res := range results { if res.Err != nil || res.Node.Weight <= 0 { continue } - score := float64(res.Latency.Milliseconds()) / res.Node.Weight + score := res.Node.Weight / float64(res.Latency.Milliseconds()) logger.Info("Exit node %s score: %.2f (latency: %dms, weight: %.2f)", res.Node.ID, score, res.Latency.Milliseconds(), res.Node.Weight) if score < bestScore { bestScore = score + bestLatency = res.Latency + bestNode = &res.Node + } else if score == bestScore && res.Latency < bestLatency { + bestLatency = res.Latency bestNode = &res.Node } } From eada420dd07483d227ceafb94211bf54d4d2d794 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 16 Jun 2025 22:00:39 -0400 Subject: [PATCH 49/87] add exit node name to logs Former-commit-id: 95d4cb2758f9a11414f05124ca6ad191356792cf --- main.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 98ebd52..4faa1d3 100644 --- a/main.go +++ b/main.go @@ -59,7 +59,8 @@ type ExitNodeData struct { // ExitNode represents an exit node with an ID, endpoint, and weight. type ExitNode struct { - ID string `json:"exitNodeId"` + ID int `json:"exitNodeId"` + Name string `json:"exitNodeName"` Endpoint string `json:"endpoint"` Weight float64 `json:"weight"` } @@ -687,23 +688,23 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } resp.Body.Close() results[i] = nodeResult{Node: node, Latency: latency, Err: nil} - logger.Info("Exit node %s latency: %v", node.ID, latency) + // logger.Info("Exit node %s latency: %v", node.Name, latency) } - // Select the best node based on weighted score + // Select the best node based on weighted score // weight / latency // choose highest score // if same score, choose lowest latency var bestNode *ExitNode - var bestScore float64 = -1e12 // small value + var bestScore float64 = -1e12 // small value var bestLatency time.Duration = 1e12 // large value for _, res := range results { if res.Err != nil || res.Node.Weight <= 0 { continue } - score := res.Node.Weight / float64(res.Latency.Milliseconds()) - logger.Info("Exit node %s score: %.2f (latency: %dms, weight: %.2f)", res.Node.ID, score, res.Latency.Milliseconds(), res.Node.Weight) - if score < bestScore { + score := (res.Node.Weight / float64(res.Latency.Milliseconds())) * 1000 + logger.Info("Exit node %s with score: %.2f (latency: %dms, weight: %.2f)", res.Node.Name, score, res.Latency.Milliseconds(), res.Node.Weight) + if score > bestScore { bestScore = score bestLatency = res.Latency bestNode = &res.Node @@ -718,7 +719,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub return } - logger.Info("Selected exit node: %s (%s)", bestNode.ID, bestNode.Endpoint) + logger.Info("Selected exit node: %s (%s)", bestNode.Name, bestNode.Endpoint) err = client.SendMessage("newt/wg/register", map[string]interface{}{ "publicKey": publicKey.String(), From 7f9b700ec309ee5e26b1fd6fa6695c548861779c Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 17 Jun 2025 10:23:29 -0400 Subject: [PATCH 50/87] Add mtu to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 590a5de..a31e9a1 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ When Newt receives WireGuard control messages, it will use the information encod - `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket. - `id`: Newt ID generated by Pangolin to identify the client. - `secret`: A unique secret (not shared and kept private) used to authenticate the client ID with the websocket in order to receive commands. +- `mtu`: MTU for the internal WG interface. Default: 1280 - `dns`: DNS server to use to resolve the endpoint - `log-level` (optional): The log level to use. Default: INFO - `updown` (optional): A script to be called when targets are added or removed. From f269ba10604612301454aa964486101097ca0aa8 Mon Sep 17 00:00:00 2001 From: Rob Date: Sat, 14 Jun 2025 15:31:16 +0100 Subject: [PATCH 51/87] Update README.md added docker socket mount example Former-commit-id: 5f9c041c6b6552e0b383e01a6929ec43c7b5603d --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9cd2768..b551e80 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,25 @@ Newt can integrate with the Docker socket to provide remote inspection of Docker **Configuration:** -You can specify the Docker socket path using the `--docker-socket` CLI argument or by setting the `DOCKER_SOCKET` environment variable. On most linux systems the socket is `/var/run/docker.sock` +You can specify the Docker socket path using the `--docker-socket` CLI argument or by setting the `DOCKER_SOCKET` environment variable. On most linux systems the socket is `/var/run/docker.sock`. You need to mount the host socket as a volume for the newt container to access it. If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation. +```yaml +services: + newt: + image: fosrl/newt + container_name: newt + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + - PANGOLIN_ENDPOINT=https://example.com + - NEWT_ID=2ix2t8xk22ubpfy + - NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2 + - DOCKER_SOCKET=/var/run/docker.sock +``` + ### Updown You can pass in a updown script for Newt to call when it is adding or removing a target: From 6e1740ff3395fe9cf326f87d1b3a981ca816c26e Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 15 Jun 2025 10:45:18 -0400 Subject: [PATCH 52/87] Tweak to call out when docker deployment Former-commit-id: 50df49e55696d1a829183d36850b35d86667d3d4 --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index b551e80..590a5de 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,7 @@ Newt can integrate with the Docker socket to provide remote inspection of Docker **Configuration:** -You can specify the Docker socket path using the `--docker-socket` CLI argument or by setting the `DOCKER_SOCKET` environment variable. On most linux systems the socket is `/var/run/docker.sock`. You need to mount the host socket as a volume for the newt container to access it. - -If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation. +You can specify the Docker socket path using the `--docker-socket` CLI argument or by setting the `DOCKER_SOCKET` environment variable. On most linux systems the socket is `/var/run/docker.sock`. When deploying newt as a container, you need to mount the host socket as a volume for the newt container to access it. If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation. ```yaml services: From 64b28627fa86bf74615a35168751cdddd213c28b Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 17 Jun 2025 10:23:29 -0400 Subject: [PATCH 53/87] Add mtu to readme Former-commit-id: c82b84194f8f5292c292785048901f1892fc2d5d --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 590a5de..a31e9a1 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ When Newt receives WireGuard control messages, it will use the information encod - `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket. - `id`: Newt ID generated by Pangolin to identify the client. - `secret`: A unique secret (not shared and kept private) used to authenticate the client ID with the websocket in order to receive commands. +- `mtu`: MTU for the internal WG interface. Default: 1280 - `dns`: DNS server to use to resolve the endpoint - `log-level` (optional): The log level to use. Default: INFO - `updown` (optional): A script to be called when targets are added or removed. From d0ef069e9f86104eabe505abcea20be9e684cbb2 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 17 Jun 2025 15:22:35 -0400 Subject: [PATCH 54/87] Print ping latency better and add connect message Former-commit-id: 850c230c4a4e6d71e893b8accbfe43b069c19efd --- main.go | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/main.go b/main.go index 4faa1d3..79471fc 100644 --- a/main.go +++ b/main.go @@ -79,11 +79,11 @@ func fixKey(key string) string { return hex.EncodeToString(decoded) } -func ping(tnet *netstack.Net, dst string) error { +func ping(tnet *netstack.Net, dst string) (time.Duration, error) { logger.Debug("Pinging %s", dst) socket, err := tnet.Dial("ping4", dst) if err != nil { - return fmt.Errorf("failed to create ICMP socket: %w", err) + return 0, fmt.Errorf("failed to create ICMP socket: %w", err) } defer socket.Close() @@ -94,41 +94,42 @@ func ping(tnet *netstack.Net, dst string) error { icmpBytes, err := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil) if err != nil { - return fmt.Errorf("failed to marshal ICMP message: %w", err) + return 0, fmt.Errorf("failed to marshal ICMP message: %w", err) } if err := socket.SetReadDeadline(time.Now().Add(time.Second * 10)); err != nil { - return fmt.Errorf("failed to set read deadline: %w", err) + return 0, fmt.Errorf("failed to set read deadline: %w", err) } start := time.Now() _, err = socket.Write(icmpBytes) if err != nil { - return fmt.Errorf("failed to write ICMP packet: %w", err) + return 0, fmt.Errorf("failed to write ICMP packet: %w", err) } n, err := socket.Read(icmpBytes[:]) if err != nil { - return fmt.Errorf("failed to read ICMP packet: %w", err) + return 0, fmt.Errorf("failed to read ICMP packet: %w", err) } replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n]) if err != nil { - return fmt.Errorf("failed to parse ICMP packet: %w", err) + return 0, fmt.Errorf("failed to parse ICMP packet: %w", err) } replyPing, ok := replyPacket.Body.(*icmp.Echo) if !ok { - return fmt.Errorf("invalid reply type: got %T, want *icmp.Echo", replyPacket.Body) + return 0, fmt.Errorf("invalid reply type: got %T, want *icmp.Echo", replyPacket.Body) } if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq { - return fmt.Errorf("invalid ping reply: got seq=%d data=%q, want seq=%d data=%q", + return 0, fmt.Errorf("invalid ping reply: got seq=%d data=%q, want seq=%d data=%q", replyPing.Seq, replyPing.Data, requestPing.Seq, requestPing.Data) } - logger.Debug("Ping latency: %v", time.Since(start)) - return nil + latency := time.Since(start) + + return latency, nil } func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) { @@ -144,7 +145,7 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) for { select { case <-ticker.C: - err := ping(tnet, serverIP) + _, err := ping(tnet, serverIP) if err != nil { consecutiveFailures++ logger.Warn("Periodic ping failed (%d consecutive failures): %v", @@ -194,7 +195,7 @@ func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websoc select { case <-ticker.C: // Try a ping to see if connection is alive - err := ping(tnet, serverIP) + _, err := ping(tnet, serverIP) if err != nil && !connectionLost { // We just lost connection @@ -236,8 +237,11 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { // First try with the initial parameters logger.Info("Ping attempt %d", attempt) - if err := ping(tnet, dst); err == nil { + if latency, err := ping(tnet, dst); err == nil { // Successful ping + logger.Info("Ping latency: %v", latency) + + logger.Info("Tunnel connection to server established successfully!") return nil } else { logger.Warn("Ping attempt %d failed: %v", attempt, err) @@ -250,7 +254,7 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { for { logger.Info("Ping attempt %d", attempt) - if err := ping(tnet, dst); err != nil { + if latency, err := ping(tnet, dst); err != nil { logger.Warn("Ping attempt %d failed: %v", attempt, err) // Increase delay after certain thresholds but cap it @@ -267,6 +271,8 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { } else { // Successful ping logger.Info("Ping succeeded after %d attempts", attempt) + logger.Info("Ping latency: %v", latency) + logger.Info("Tunnel connection to server established successfully!") return } } From 098f48d54460ada49ca01500e84a06ece9b1c209 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 18 Jun 2025 15:57:15 -0400 Subject: [PATCH 55/87] give preference to previously connected node Former-commit-id: b397016da8a27c9dbed6eeed32fcfc161379b7e5 --- main.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index 79471fc..369e14b 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "encoding/json" "flag" "fmt" + "math" "math/rand" "net" "net/http" @@ -59,10 +60,11 @@ type ExitNodeData struct { // ExitNode represents an exit node with an ID, endpoint, and weight. type ExitNode struct { - ID int `json:"exitNodeId"` - Name string `json:"exitNodeName"` - Endpoint string `json:"endpoint"` - Weight float64 `json:"weight"` + ID int `json:"exitNodeId"` + Name string `json:"exitNodeName"` + Endpoint string `json:"endpoint"` + Weight float64 `json:"weight"` + WasPreviouslyConnected bool `json:"wasPreviouslyConnected"` } func fixKey(key string) string { @@ -697,19 +699,41 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // logger.Info("Exit node %s latency: %v", node.Name, latency) } - // Select the best node based on weighted score - // weight / latency - // choose highest score - // if same score, choose lowest latency + // we will need to tweak these + const ( + latencyPenaltyExponent = 1.5 // make latency matter more + lastNodeScoreBoost = 1.10 // 10% preference for the last used node + scoreTolerancePercent = 5.0 // allow last node if within 5% of best score + ) + var bestNode *ExitNode - var bestScore float64 = -1e12 // small value - var bestLatency time.Duration = 1e12 // large value + var bestScore float64 = -1e12 + var bestLatency time.Duration = 1e12 + + type ExitNodeScore struct { + Node ExitNode + Score float64 + Latency time.Duration + } + var candidateNodes []ExitNodeScore + for _, res := range results { if res.Err != nil || res.Node.Weight <= 0 { continue } - score := (res.Node.Weight / float64(res.Latency.Milliseconds())) * 1000 + + latencyMs := float64(res.Latency.Milliseconds()) + score := res.Node.Weight / math.Pow(latencyMs, latencyPenaltyExponent) + + // slight boost if this is the last used node + if res.Node.WasPreviouslyConnected == true { + score *= lastNodeScoreBoost + } + logger.Info("Exit node %s with score: %.2f (latency: %dms, weight: %.2f)", res.Node.Name, score, res.Latency.Milliseconds(), res.Node.Weight) + + candidateNodes = append(candidateNodes, ExitNodeScore{Node: res.Node, Score: score, Latency: res.Latency}) + if score > bestScore { bestScore = score bestLatency = res.Latency @@ -720,6 +744,17 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } } + // check if last used node is close enough in score + for _, cand := range candidateNodes { + if cand.Node.WasPreviouslyConnected { + if bestScore - cand.Score <= bestScore*(scoreTolerancePercent/100.0) { + logger.Info("Sticking with last used exit node: %s (%s), score close enough to best", cand.Node.Name, cand.Node.Endpoint) + bestNode = &cand.Node + } + break + } + } + if bestNode == nil { logger.Warn("No suitable exit node found") return From dfc5f2b25360f6c9ed4dacd7c165caa827f9515d Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 18 Jun 2025 22:54:13 -0400 Subject: [PATCH 56/87] Update ping check Former-commit-id: 4b64b0460361e40d853d87adbdaaf750ca429e67 --- main.go | 580 +++++++++++++------------------------------------------- util.go | 352 ++++++++++++++++++++++++++++++++++ 2 files changed, 483 insertions(+), 449 deletions(-) create mode 100644 util.go diff --git a/main.go b/main.go index 369e14b..59ad194 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,13 @@ package main import ( - "bytes" - "encoding/base64" - "encoding/hex" "encoding/json" "flag" "fmt" "math" - "math/rand" - "net" "net/http" "net/netip" "os" - "os/exec" "os/signal" "runtime" "strconv" @@ -28,8 +22,6 @@ import ( "github.com/fosrl/newt/wg" "github.com/fosrl/newt/wgtester" - "golang.org/x/net/icmp" - "golang.org/x/net/ipv4" "golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" @@ -60,313 +52,13 @@ type ExitNodeData struct { // ExitNode represents an exit node with an ID, endpoint, and weight. type ExitNode struct { - ID int `json:"exitNodeId"` - Name string `json:"exitNodeName"` - Endpoint string `json:"endpoint"` - Weight float64 `json:"weight"` + ID int `json:"exitNodeId"` + Name string `json:"exitNodeName"` + Endpoint string `json:"endpoint"` + Weight float64 `json:"weight"` WasPreviouslyConnected bool `json:"wasPreviouslyConnected"` } -func fixKey(key string) string { - // Remove any whitespace - key = strings.TrimSpace(key) - - // Decode from base64 - decoded, err := base64.StdEncoding.DecodeString(key) - if err != nil { - logger.Fatal("Error decoding base64: %v", err) - } - - // Convert to hex - return hex.EncodeToString(decoded) -} - -func ping(tnet *netstack.Net, dst string) (time.Duration, error) { - logger.Debug("Pinging %s", dst) - socket, err := tnet.Dial("ping4", dst) - if err != nil { - return 0, fmt.Errorf("failed to create ICMP socket: %w", err) - } - defer socket.Close() - - requestPing := icmp.Echo{ - Seq: rand.Intn(1 << 16), - Data: []byte("gopher burrow"), - } - - icmpBytes, err := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil) - if err != nil { - return 0, fmt.Errorf("failed to marshal ICMP message: %w", err) - } - - if err := socket.SetReadDeadline(time.Now().Add(time.Second * 10)); err != nil { - return 0, fmt.Errorf("failed to set read deadline: %w", err) - } - - start := time.Now() - _, err = socket.Write(icmpBytes) - if err != nil { - return 0, fmt.Errorf("failed to write ICMP packet: %w", err) - } - - n, err := socket.Read(icmpBytes[:]) - if err != nil { - return 0, fmt.Errorf("failed to read ICMP packet: %w", err) - } - - replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n]) - if err != nil { - return 0, fmt.Errorf("failed to parse ICMP packet: %w", err) - } - - replyPing, ok := replyPacket.Body.(*icmp.Echo) - if !ok { - return 0, fmt.Errorf("invalid reply type: got %T, want *icmp.Echo", replyPacket.Body) - } - - if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq { - return 0, fmt.Errorf("invalid ping reply: got seq=%d data=%q, want seq=%d data=%q", - replyPing.Seq, replyPing.Data, requestPing.Seq, requestPing.Data) - } - - latency := time.Since(start) - - return latency, nil -} - -func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) { - initialInterval := 10 * time.Second - maxInterval := 60 * time.Second - currentInterval := initialInterval - consecutiveFailures := 0 - - ticker := time.NewTicker(currentInterval) - defer ticker.Stop() - - go func() { - for { - select { - case <-ticker.C: - _, err := ping(tnet, serverIP) - if err != nil { - consecutiveFailures++ - logger.Warn("Periodic ping failed (%d consecutive failures): %v", - consecutiveFailures, err) - logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?") - - // Increase interval if we have consistent failures, with a maximum cap - if consecutiveFailures >= 3 && currentInterval < maxInterval { - // Increase by 50% each time, up to the maximum - currentInterval = time.Duration(float64(currentInterval) * 1.5) - if currentInterval > maxInterval { - currentInterval = maxInterval - } - ticker.Reset(currentInterval) - logger.Info("Increased ping check interval to %v due to consecutive failures", - currentInterval) - } - } else { - // On success, if we've backed off, gradually return to normal interval - if currentInterval > initialInterval { - currentInterval = time.Duration(float64(currentInterval) * 0.8) - if currentInterval < initialInterval { - currentInterval = initialInterval - } - ticker.Reset(currentInterval) - logger.Info("Decreased ping check interval to %v after successful ping", - currentInterval) - } - consecutiveFailures = 0 - } - case <-stopChan: - logger.Info("Stopping ping check") - return - } - } - }() -} - -// Function to track connection status and trigger reconnection as needed -func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websocket.Client) { - const checkInterval = 30 * time.Second - connectionLost := false - ticker := time.NewTicker(checkInterval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - // Try a ping to see if connection is alive - _, err := ping(tnet, serverIP) - - if err != nil && !connectionLost { - // We just lost connection - connectionLost = true - logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") - - // Notify the user they might need to check their network - logger.Warn("Please check your internet connection and ensure the Pangolin server is online.") - logger.Warn("Newt will continue reconnection attempts automatically when connectivity is restored.") - } else if err == nil && connectionLost { - // Connection has been restored - connectionLost = false - logger.Info("Connection to server restored!") - - // Tell the server we're back - err := client.SendMessage("newt/wg/register", map[string]interface{}{ - "publicKey": privateKey.PublicKey().String(), - }) - - if err != nil { - logger.Error("Failed to send registration message after reconnection: %v", err) - } else { - logger.Info("Successfully re-registered with server after reconnection") - } - } - } - } -} - -func pingWithRetry(tnet *netstack.Net, dst string) error { - const ( - initialMaxAttempts = 15 - initialRetryDelay = 2 * time.Second - maxRetryDelay = 60 * time.Second // Cap the maximum delay - ) - - attempt := 1 - retryDelay := initialRetryDelay - - // First try with the initial parameters - logger.Info("Ping attempt %d", attempt) - if latency, err := ping(tnet, dst); err == nil { - // Successful ping - logger.Info("Ping latency: %v", latency) - - logger.Info("Tunnel connection to server established successfully!") - return nil - } else { - logger.Warn("Ping attempt %d failed: %v", attempt, err) - } - - // Start a goroutine that will attempt pings indefinitely with increasing delays - go func() { - attempt = 2 // Continue from attempt 2 - - for { - logger.Info("Ping attempt %d", attempt) - - if latency, err := ping(tnet, dst); err != nil { - logger.Warn("Ping attempt %d failed: %v", attempt, err) - - // Increase delay after certain thresholds but cap it - if attempt%5 == 0 && retryDelay < maxRetryDelay { - retryDelay = time.Duration(float64(retryDelay) * 1.5) - if retryDelay > maxRetryDelay { - retryDelay = maxRetryDelay - } - logger.Info("Increasing ping retry delay to %v", retryDelay) - } - - time.Sleep(retryDelay) - attempt++ - } else { - // Successful ping - logger.Info("Ping succeeded after %d attempts", attempt) - logger.Info("Ping latency: %v", latency) - logger.Info("Tunnel connection to server established successfully!") - return - } - } - }() - - // Return an error for the first batch of attempts (to maintain compatibility with existing code) - return fmt.Errorf("initial ping attempts failed, continuing in background") -} - -func parseLogLevel(level string) logger.LogLevel { - switch strings.ToUpper(level) { - case "DEBUG": - return logger.DEBUG - case "INFO": - return logger.INFO - case "WARN": - return logger.WARN - case "ERROR": - return logger.ERROR - case "FATAL": - return logger.FATAL - default: - return logger.INFO // default to INFO if invalid level provided - } -} - -func mapToWireGuardLogLevel(level logger.LogLevel) int { - switch level { - case logger.DEBUG: - return device.LogLevelVerbose - // case logger.INFO: - // return device.LogLevel - case logger.WARN: - return device.LogLevelError - case logger.ERROR, logger.FATAL: - return device.LogLevelSilent - default: - return device.LogLevelSilent - } -} - -func resolveDomain(domain string) (string, error) { - // Check if there's a port in the domain - host, port, err := net.SplitHostPort(domain) - if err != nil { - // No port found, use the domain as is - host = domain - port = "" - } - - // Remove any protocol prefix if present - if strings.HasPrefix(host, "http://") { - host = strings.TrimPrefix(host, "http://") - } else if strings.HasPrefix(host, "https://") { - host = strings.TrimPrefix(host, "https://") - } - - // if there are any trailing slashes, remove them - host = strings.TrimSuffix(host, "/") - - // Lookup IP addresses - ips, err := net.LookupIP(host) - if err != nil { - return "", fmt.Errorf("DNS lookup failed: %v", err) - } - - if len(ips) == 0 { - return "", fmt.Errorf("no IP addresses found for domain %s", host) - } - - // Get the first IPv4 address if available - var ipAddr string - for _, ip := range ips { - if ipv4 := ip.To4(); ipv4 != nil { - ipAddr = ipv4.String() - break - } - } - - // If no IPv4 found, use the first IP (might be IPv6) - if ipAddr == "" { - ipAddr = ips[0].String() - } - - // Add port back if it existed - if port != "" { - ipAddr = net.JoinHostPort(ipAddr, port) - } - - return ipAddr, nil -} - var ( endpoint string id string @@ -524,17 +216,6 @@ func main() { } } - client.RegisterHandler("newt/terminate", func(msg websocket.WSMessage) { - logger.Info("Received terminate message") - if pm != nil { - pm.Stop() - } - if dev != nil { - dev.Close() - } - client.Close() - }) - pingStopChan := make(chan struct{}) defer close(pingStopChan) @@ -543,10 +224,32 @@ func main() { logger.Info("Received registration message") if connected { - logger.Info("Already connected! But I will send a ping anyway...") - // Even if pingWithRetry returns an error, it will continue trying in the background - _ = pingWithRetry(tnet, wgData.ServerIP) // Ignoring initial error as pings will continue - return + // Stop proxy manager if running + if pm != nil { + pm.Stop() + pm = nil + } + + // Close WireGuard device if running + if dev != nil { + dev.Close() + dev = nil + } + + // Close TUN/netstack if running + if tnet != nil { + tnet = nil + } + if tun != nil { + tun.Close() + tun = nil + } + + // Stop the ping check + close(pingStopChan) + + // Mark as disconnected + connected = false } jsonData, err := json.Marshal(msg.Data) @@ -612,10 +315,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // as the pings will continue in the background if !connected { logger.Info("Starting ping check") - startPingCheck(tnet, wgData.ServerIP, pingStopChan) - - // Start connection monitoring in a separate goroutine - go monitorConnectionStatus(tnet, wgData.ServerIP, client) + startPingCheck(tnet, wgData.ServerIP, client, pingStopChan) } // Create proxy manager @@ -645,6 +345,39 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) + client.RegisterHandler("newt/wg/terminate", func(msg websocket.WSMessage) { + logger.Info("Received disconnect message") + + // Stop proxy manager if running + if pm != nil { + pm.Stop() + pm = nil + } + + // Close WireGuard device if running + if dev != nil { + dev.Close() + dev = nil + } + + // Close TUN/netstack if running + if tnet != nil { + tnet = nil + } + if tun != nil { + tun.Close() + tun = nil + } + + // Stop the ping check + close(pingStopChan) + + // Mark as disconnected + connected = false + + logger.Info("Tunnel destroyed, ready for reconnection") + }) + client.RegisterHandler("newt/ping/exitNodes", func(msg websocket.WSMessage) { logger.Info("Received ping message") @@ -747,7 +480,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // check if last used node is close enough in score for _, cand := range candidateNodes { if cand.Node.WasPreviouslyConnected { - if bestScore - cand.Score <= bestScore*(scoreTolerancePercent/100.0) { + if bestScore-cand.Score <= bestScore*(scoreTolerancePercent/100.0) { logger.Info("Sticking with last used exit node: %s (%s), score close enough to best", cand.Node.Name, cand.Node.Endpoint) bestNode = &cand.Node } @@ -966,126 +699,75 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub os.Exit(0) } -func parseTargetData(data interface{}) (TargetData, error) { - var targetData TargetData - jsonData, err := json.Marshal(data) - if err != nil { - logger.Info("Error marshaling data: %v", err) - return targetData, err - } +func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Client, stopChan chan struct{}) { + initialInterval := 10 * time.Second + maxInterval := 60 * time.Second + currentInterval := initialInterval + consecutiveFailures := 0 + connectionLost := false + ticker := time.NewTicker(currentInterval) + defer ticker.Stop() - if err := json.Unmarshal(jsonData, &targetData); err != nil { - logger.Info("Error unmarshaling target data: %v", err) - return targetData, err - } - return targetData, nil -} - -func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto string, targetData TargetData) error { - for _, t := range targetData.Targets { - // Split the first number off of the target with : separator and use as the port - parts := strings.Split(t, ":") - if len(parts) != 3 { - logger.Info("Invalid target format: %s", t) - continue - } - - // Get the port as an int - port := 0 - _, err := fmt.Sscanf(parts[0], "%d", &port) - if err != nil { - logger.Info("Invalid port: %s", parts[0]) - continue - } - - if action == "add" { - target := parts[1] + ":" + parts[2] - - // Call updown script if provided - processedTarget := target - if updownScript != "" { - newTarget, err := executeUpdownScript(action, proto, target) + go func() { + for { + select { + case <-ticker.C: + _, err := ping(tnet, serverIP) if err != nil { - logger.Warn("Updown script error: %v", err) - } else if newTarget != "" { - processedTarget = newTarget + consecutiveFailures++ + + // Check if this is the first failure (connection just lost) + if !connectionLost { + connectionLost = true + logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") + logger.Warn("Please check your internet connection and ensure the Pangolin server is online.") + logger.Warn("Newt will continue reconnection attempts automatically when connectivity is restored.") + } + + logger.Warn("Periodic ping failed (%d consecutive failures): %v", + consecutiveFailures, err) + logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?") + + // Increase interval if we have consistent failures, with a maximum cap + if consecutiveFailures >= 5 && currentInterval < maxInterval { + // Increase by 50% each time, up to the maximum + currentInterval = time.Duration(float64(currentInterval) * 1.5) + if currentInterval > maxInterval { + currentInterval = maxInterval + } + ticker.Reset(currentInterval) + logger.Debug("Increased ping check interval to %v due to consecutive failures", + currentInterval) + + // Restart the connection flow + err := client.SendMessage("newt/ping/request", map[string]interface{}{}) + if err != nil { + logger.Error("Failed to send ping request: %v", err) + } + } + } else { + // Check if connection was previously lost and is now restored + if connectionLost { + connectionLost = false + logger.Info("Connection to server restored!") + } + + // On success, if we've backed off, gradually return to normal interval + if currentInterval > initialInterval { + currentInterval = time.Duration(float64(currentInterval) * 0.8) + if currentInterval < initialInterval { + currentInterval = initialInterval + } + ticker.Reset(currentInterval) + logger.Info("Decreased ping check interval to %v after successful ping", + currentInterval) + } + consecutiveFailures = 0 } - } - - // Only remove the specific target if it exists - err := pm.RemoveTarget(proto, tunnelIP, port) - if err != nil { - // Ignore "target not found" errors as this is expected for new targets - if !strings.Contains(err.Error(), "target not found") { - logger.Error("Failed to remove existing target: %v", err) - } - } - - // Add the new target - pm.AddTarget(proto, tunnelIP, port, processedTarget) - - } else if action == "remove" { - logger.Info("Removing target with port %d", port) - - target := parts[1] + ":" + parts[2] - - // Call updown script if provided - if updownScript != "" { - _, err := executeUpdownScript(action, proto, target) - if err != nil { - logger.Warn("Updown script error: %v", err) - } - } - - err := pm.RemoveTarget(proto, tunnelIP, port) - if err != nil { - logger.Error("Failed to remove target: %v", err) - return err + case <-stopChan: + logger.Info("Stopping ping check") + return } } - } - - return nil -} - -func executeUpdownScript(action, proto, target string) (string, error) { - if updownScript == "" { - return target, nil - } - - // Split the updownScript in case it contains spaces (like "/usr/bin/python3 script.py") - parts := strings.Fields(updownScript) - if len(parts) == 0 { - return target, fmt.Errorf("invalid updown script command") - } - - var cmd *exec.Cmd - if len(parts) == 1 { - // If it's a single executable - logger.Info("Executing updown script: %s %s %s %s", updownScript, action, proto, target) - cmd = exec.Command(parts[0], action, proto, target) - } else { - // If it includes interpreter and script - args := append(parts[1:], action, proto, target) - logger.Info("Executing updown script: %s %s %s %s %s", parts[0], strings.Join(parts[1:], " "), action, proto, target) - cmd = exec.Command(parts[0], args...) - } - - output, err := cmd.Output() - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - return "", fmt.Errorf("updown script execution failed (exit code %d): %s", - exitErr.ExitCode(), string(exitErr.Stderr)) - } - return "", fmt.Errorf("updown script execution failed: %v", err) - } - - // If the script returns a new target, use it - newTarget := strings.TrimSpace(string(output)) - if newTarget != "" { - logger.Info("Updown script returned new target: %s", newTarget) - return newTarget, nil - } - - return target, nil + }() } diff --git a/util.go b/util.go new file mode 100644 index 0000000..808ae2b --- /dev/null +++ b/util.go @@ -0,0 +1,352 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "net" + "os/exec" + "strings" + "time" + + "github.com/fosrl/newt/logger" + "github.com/fosrl/newt/proxy" + "golang.org/x/exp/rand" + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun/netstack" +) + +func fixKey(key string) string { + // Remove any whitespace + key = strings.TrimSpace(key) + + // Decode from base64 + decoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + logger.Fatal("Error decoding base64: %v", err) + } + + // Convert to hex + return hex.EncodeToString(decoded) +} + +func ping(tnet *netstack.Net, dst string) (time.Duration, error) { + logger.Debug("Pinging %s", dst) + socket, err := tnet.Dial("ping4", dst) + if err != nil { + return 0, fmt.Errorf("failed to create ICMP socket: %w", err) + } + defer socket.Close() + + requestPing := icmp.Echo{ + Seq: rand.Intn(1 << 16), + Data: []byte("gopher burrow"), + } + + icmpBytes, err := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil) + if err != nil { + return 0, fmt.Errorf("failed to marshal ICMP message: %w", err) + } + + if err := socket.SetReadDeadline(time.Now().Add(time.Second * 10)); err != nil { + return 0, fmt.Errorf("failed to set read deadline: %w", err) + } + + start := time.Now() + _, err = socket.Write(icmpBytes) + if err != nil { + return 0, fmt.Errorf("failed to write ICMP packet: %w", err) + } + + n, err := socket.Read(icmpBytes[:]) + if err != nil { + return 0, fmt.Errorf("failed to read ICMP packet: %w", err) + } + + replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n]) + if err != nil { + return 0, fmt.Errorf("failed to parse ICMP packet: %w", err) + } + + replyPing, ok := replyPacket.Body.(*icmp.Echo) + if !ok { + return 0, fmt.Errorf("invalid reply type: got %T, want *icmp.Echo", replyPacket.Body) + } + + if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq { + return 0, fmt.Errorf("invalid ping reply: got seq=%d data=%q, want seq=%d data=%q", + replyPing.Seq, replyPing.Data, requestPing.Seq, requestPing.Data) + } + + latency := time.Since(start) + + return latency, nil +} + +func pingWithRetry(tnet *netstack.Net, dst string) error { + const ( + initialMaxAttempts = 15 + initialRetryDelay = 2 * time.Second + maxRetryDelay = 60 * time.Second // Cap the maximum delay + ) + + attempt := 1 + retryDelay := initialRetryDelay + + // First try with the initial parameters + logger.Info("Ping attempt %d", attempt) + if latency, err := ping(tnet, dst); err == nil { + // Successful ping + logger.Info("Ping latency: %v", latency) + + logger.Info("Tunnel connection to server established successfully!") + return nil + } else { + logger.Warn("Ping attempt %d failed: %v", attempt, err) + } + + // Start a goroutine that will attempt pings indefinitely with increasing delays + go func() { + attempt = 2 // Continue from attempt 2 + + for { + logger.Info("Ping attempt %d", attempt) + + if latency, err := ping(tnet, dst); err != nil { + logger.Warn("Ping attempt %d failed: %v", attempt, err) + + // Increase delay after certain thresholds but cap it + if attempt%5 == 0 && retryDelay < maxRetryDelay { + retryDelay = time.Duration(float64(retryDelay) * 1.5) + if retryDelay > maxRetryDelay { + retryDelay = maxRetryDelay + } + logger.Info("Increasing ping retry delay to %v", retryDelay) + } + + time.Sleep(retryDelay) + attempt++ + } else { + // Successful ping + logger.Info("Ping succeeded after %d attempts", attempt) + logger.Info("Ping latency: %v", latency) + logger.Info("Tunnel connection to server established successfully!") + return + } + } + }() + + // Return an error for the first batch of attempts (to maintain compatibility with existing code) + return fmt.Errorf("initial ping attempts failed, continuing in background") +} + +func parseLogLevel(level string) logger.LogLevel { + switch strings.ToUpper(level) { + case "DEBUG": + return logger.DEBUG + case "INFO": + return logger.INFO + case "WARN": + return logger.WARN + case "ERROR": + return logger.ERROR + case "FATAL": + return logger.FATAL + default: + return logger.INFO // default to INFO if invalid level provided + } +} + +func mapToWireGuardLogLevel(level logger.LogLevel) int { + switch level { + case logger.DEBUG: + return device.LogLevelVerbose + // case logger.INFO: + // return device.LogLevel + case logger.WARN: + return device.LogLevelError + case logger.ERROR, logger.FATAL: + return device.LogLevelSilent + default: + return device.LogLevelSilent + } +} + +func resolveDomain(domain string) (string, error) { + // Check if there's a port in the domain + host, port, err := net.SplitHostPort(domain) + if err != nil { + // No port found, use the domain as is + host = domain + port = "" + } + + // Remove any protocol prefix if present + if strings.HasPrefix(host, "http://") { + host = strings.TrimPrefix(host, "http://") + } else if strings.HasPrefix(host, "https://") { + host = strings.TrimPrefix(host, "https://") + } + + // if there are any trailing slashes, remove them + host = strings.TrimSuffix(host, "/") + + // Lookup IP addresses + ips, err := net.LookupIP(host) + if err != nil { + return "", fmt.Errorf("DNS lookup failed: %v", err) + } + + if len(ips) == 0 { + return "", fmt.Errorf("no IP addresses found for domain %s", host) + } + + // Get the first IPv4 address if available + var ipAddr string + for _, ip := range ips { + if ipv4 := ip.To4(); ipv4 != nil { + ipAddr = ipv4.String() + break + } + } + + // If no IPv4 found, use the first IP (might be IPv6) + if ipAddr == "" { + ipAddr = ips[0].String() + } + + // Add port back if it existed + if port != "" { + ipAddr = net.JoinHostPort(ipAddr, port) + } + + return ipAddr, nil +} + +func parseTargetData(data interface{}) (TargetData, error) { + var targetData TargetData + jsonData, err := json.Marshal(data) + if err != nil { + logger.Info("Error marshaling data: %v", err) + return targetData, err + } + + if err := json.Unmarshal(jsonData, &targetData); err != nil { + logger.Info("Error unmarshaling target data: %v", err) + return targetData, err + } + return targetData, nil +} + +func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto string, targetData TargetData) error { + for _, t := range targetData.Targets { + // Split the first number off of the target with : separator and use as the port + parts := strings.Split(t, ":") + if len(parts) != 3 { + logger.Info("Invalid target format: %s", t) + continue + } + + // Get the port as an int + port := 0 + _, err := fmt.Sscanf(parts[0], "%d", &port) + if err != nil { + logger.Info("Invalid port: %s", parts[0]) + continue + } + + if action == "add" { + target := parts[1] + ":" + parts[2] + + // Call updown script if provided + processedTarget := target + if updownScript != "" { + newTarget, err := executeUpdownScript(action, proto, target) + if err != nil { + logger.Warn("Updown script error: %v", err) + } else if newTarget != "" { + processedTarget = newTarget + } + } + + // Only remove the specific target if it exists + err := pm.RemoveTarget(proto, tunnelIP, port) + if err != nil { + // Ignore "target not found" errors as this is expected for new targets + if !strings.Contains(err.Error(), "target not found") { + logger.Error("Failed to remove existing target: %v", err) + } + } + + // Add the new target + pm.AddTarget(proto, tunnelIP, port, processedTarget) + + } else if action == "remove" { + logger.Info("Removing target with port %d", port) + + target := parts[1] + ":" + parts[2] + + // Call updown script if provided + if updownScript != "" { + _, err := executeUpdownScript(action, proto, target) + if err != nil { + logger.Warn("Updown script error: %v", err) + } + } + + err := pm.RemoveTarget(proto, tunnelIP, port) + if err != nil { + logger.Error("Failed to remove target: %v", err) + return err + } + } + } + + return nil +} + +func executeUpdownScript(action, proto, target string) (string, error) { + if updownScript == "" { + return target, nil + } + + // Split the updownScript in case it contains spaces (like "/usr/bin/python3 script.py") + parts := strings.Fields(updownScript) + if len(parts) == 0 { + return target, fmt.Errorf("invalid updown script command") + } + + var cmd *exec.Cmd + if len(parts) == 1 { + // If it's a single executable + logger.Info("Executing updown script: %s %s %s %s", updownScript, action, proto, target) + cmd = exec.Command(parts[0], action, proto, target) + } else { + // If it includes interpreter and script + args := append(parts[1:], action, proto, target) + logger.Info("Executing updown script: %s %s %s %s %s", parts[0], strings.Join(parts[1:], " "), action, proto, target) + cmd = exec.Command(parts[0], args...) + } + + output, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return "", fmt.Errorf("updown script execution failed (exit code %d): %s", + exitErr.ExitCode(), string(exitErr.Stderr)) + } + return "", fmt.Errorf("updown script execution failed: %v", err) + } + + // If the script returns a new target, use it + newTarget := strings.TrimSpace(string(output)) + if newTarget != "" { + logger.Info("Updown script returned new target: %s", newTarget) + return newTarget, nil + } + + return target, nil +} From 4338045b78801770ce50d79006c603b4998063b2 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 19 Jun 2025 15:55:47 -0400 Subject: [PATCH 57/87] New tunnel reconnect works Former-commit-id: 1c75eb3bee208a658b3cd03caa8b8956284e6972 --- main.go | 255 ++++++++++++++++++++------------------------ util.go | 8 +- websocket/client.go | 29 ++++- 3 files changed, 151 insertions(+), 141 deletions(-) diff --git a/main.go b/main.go index 59ad194..a646672 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "encoding/json" "flag" "fmt" - "math" "net/http" "net/netip" "os" @@ -59,6 +58,16 @@ type ExitNode struct { WasPreviouslyConnected bool `json:"wasPreviouslyConnected"` } +type ExitNodePingResult struct { + ExitNodeID int `json:"exitNodeId"` + LatencyMs int64 `json:"latencyMs"` + Weight float64 `json:"weight"` + Error string `json:"error,omitempty"` + Name string `json:"exitNodeName"` + Endpoint string `json:"endpoint"` + WasPreviouslyConnected bool `json:"wasPreviouslyConnected"` +} + var ( endpoint string id string @@ -76,7 +85,10 @@ var ( updownScript string tlsPrivateKey string dockerSocket string + pingInterval = 1 * time.Second publicKey wgtypes.Key + pingStopChan chan struct{} + stopFunc func() ) func main() { @@ -94,6 +106,7 @@ func main() { acceptClients = os.Getenv("ACCEPT_CLIENTS") == "true" tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") + pingIntervalStr := os.Getenv("PING_INTERVAL") if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -130,6 +143,17 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } + if pingIntervalStr == "" { + flag.StringVar(&pingIntervalStr, "ping-interval", "1s", "Interval for pinging the server (default 1s)") + } + + if pingIntervalStr != "" { + pingInterval, err = time.ParseDuration(pingIntervalStr) + if err != nil { + fmt.Printf("Invalid PING_INTERVAL value: %s, using default 1 second\n", pingIntervalStr) + pingInterval = 1 * time.Second + } + } // do a --version check version := flag.Bool("version", false, "Print the version") @@ -216,38 +240,41 @@ func main() { } } - pingStopChan := make(chan struct{}) - defer close(pingStopChan) - // Register handlers for different message types client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) { logger.Info("Received registration message") + if stopFunc != nil { + stopFunc() // stop the ws from sending more requests + stopFunc = nil // reset stopFunc to nil to avoid double stopping + } if connected { + if pingStopChan != nil { + // Stop the ping check + close(pingStopChan) + pingStopChan = nil + } + // Stop proxy manager if running if pm != nil { pm.Stop() pm = nil } - // Close WireGuard device if running + // Close WireGuard device first - this will automatically close the TUN device if dev != nil { dev.Close() dev = nil } - // Close TUN/netstack if running + // Clear references but don't manually close since dev.Close() already did it if tnet != nil { tnet = nil } if tun != nil { - tun.Close() - tun = nil + tun = nil // Don't call tun.Close() here since dev.Close() already closed it } - // Stop the ping check - close(pingStopChan) - // Mark as disconnected connected = false } @@ -315,7 +342,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // as the pings will continue in the background if !connected { logger.Info("Starting ping check") - startPingCheck(tnet, wgData.ServerIP, client, pingStopChan) + pingStopChan = startPingCheck(tnet, wgData.ServerIP, client) } // Create proxy manager @@ -348,30 +375,32 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub client.RegisterHandler("newt/wg/terminate", func(msg websocket.WSMessage) { logger.Info("Received disconnect message") + if pingStopChan != nil { + // Stop the ping check + close(pingStopChan) + pingStopChan = nil + } + // Stop proxy manager if running if pm != nil { pm.Stop() pm = nil } - // Close WireGuard device if running + // Close WireGuard device first - this will automatically close the TUN device if dev != nil { dev.Close() dev = nil } - // Close TUN/netstack if running + // Clear references but don't manually close since dev.Close() already did it if tnet != nil { tnet = nil } if tun != nil { - tun.Close() - tun = nil + tun = nil // Don't call tun.Close() here since dev.Close() already closed it } - // Stop the ping check - close(pingStopChan) - // Mark as disconnected connected = false @@ -380,9 +409,12 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub client.RegisterHandler("newt/ping/exitNodes", func(msg websocket.WSMessage) { logger.Info("Received ping message") + if stopFunc != nil { + stopFunc() // stop the ws from sending more requests + stopFunc = nil // reset stopFunc to nil to avoid double stopping + } // Parse the incoming list of exit nodes - // Exit nodes is a json var exitNodeData ExitNodeData jsonData, err := json.Marshal(msg.Data) @@ -408,8 +440,11 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } results := make([]nodeResult, len(exitNodes)) + const pingAttempts = 3 for i, node := range exitNodes { - start := time.Now() + var totalLatency time.Duration + var lastErr error + successes := 0 client := &http.Client{ Timeout: 5 * time.Second, } @@ -420,89 +455,54 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub if !strings.HasSuffix(url, "/ping") { url = strings.TrimRight(url, "/") + "/ping" } - resp, err := client.Get(url) - latency := time.Since(start) - if err != nil { - logger.Warn("Failed to ping exit node %s (%s): %v", node.ID, url, err) - results[i] = nodeResult{Node: node, Latency: latency, Err: err} - continue - } - resp.Body.Close() - results[i] = nodeResult{Node: node, Latency: latency, Err: nil} - // logger.Info("Exit node %s latency: %v", node.Name, latency) - } - - // we will need to tweak these - const ( - latencyPenaltyExponent = 1.5 // make latency matter more - lastNodeScoreBoost = 1.10 // 10% preference for the last used node - scoreTolerancePercent = 5.0 // allow last node if within 5% of best score - ) - - var bestNode *ExitNode - var bestScore float64 = -1e12 - var bestLatency time.Duration = 1e12 - - type ExitNodeScore struct { - Node ExitNode - Score float64 - Latency time.Duration - } - var candidateNodes []ExitNodeScore - - for _, res := range results { - if res.Err != nil || res.Node.Weight <= 0 { - continue - } - - latencyMs := float64(res.Latency.Milliseconds()) - score := res.Node.Weight / math.Pow(latencyMs, latencyPenaltyExponent) - - // slight boost if this is the last used node - if res.Node.WasPreviouslyConnected == true { - score *= lastNodeScoreBoost - } - - logger.Info("Exit node %s with score: %.2f (latency: %dms, weight: %.2f)", res.Node.Name, score, res.Latency.Milliseconds(), res.Node.Weight) - - candidateNodes = append(candidateNodes, ExitNodeScore{Node: res.Node, Score: score, Latency: res.Latency}) - - if score > bestScore { - bestScore = score - bestLatency = res.Latency - bestNode = &res.Node - } else if score == bestScore && res.Latency < bestLatency { - bestLatency = res.Latency - bestNode = &res.Node - } - } - - // check if last used node is close enough in score - for _, cand := range candidateNodes { - if cand.Node.WasPreviouslyConnected { - if bestScore-cand.Score <= bestScore*(scoreTolerancePercent/100.0) { - logger.Info("Sticking with last used exit node: %s (%s), score close enough to best", cand.Node.Name, cand.Node.Endpoint) - bestNode = &cand.Node + for j := 0; j < pingAttempts; j++ { + start := time.Now() + resp, err := client.Get(url) + latency := time.Since(start) + if err != nil { + lastErr = err + logger.Warn("Failed to ping exit node %d (%s) attempt %d: %v", node.ID, url, j+1, err) + continue } - break + resp.Body.Close() + totalLatency += latency + successes++ + } + var avgLatency time.Duration + if successes > 0 { + avgLatency = totalLatency / time.Duration(successes) + } + if successes == 0 { + results[i] = nodeResult{Node: node, Latency: 0, Err: lastErr} + } else { + results[i] = nodeResult{Node: node, Latency: avgLatency, Err: nil} } } - if bestNode == nil { - logger.Warn("No suitable exit node found") - return + // Prepare data to send to the cloud for selection + var pingResults []ExitNodePingResult + for _, res := range results { + errMsg := "" + if res.Err != nil { + errMsg = res.Err.Error() + } + pingResults = append(pingResults, ExitNodePingResult{ + ExitNodeID: res.Node.ID, + LatencyMs: res.Latency.Milliseconds(), + Weight: res.Node.Weight, + Error: errMsg, + Name: res.Node.Name, + Endpoint: res.Node.Endpoint, + WasPreviouslyConnected: res.Node.WasPreviouslyConnected, + }) } - logger.Info("Selected exit node: %s (%s)", bestNode.Name, bestNode.Endpoint) - - err = client.SendMessage("newt/wg/register", map[string]interface{}{ - "publicKey": publicKey.String(), - "exitNodeId": bestNode.ID, - }) - if err != nil { - logger.Error("Failed to send registration message: %v", err) - return - } + // Send the ping results to the cloud for selection + stopFunc = client.SendMessageInterval("newt/wg/register", map[string]interface{}{ + "publicKey": publicKey.String(), + "pingResults": pingResults, + }, 1*time.Second) + logger.Info("Sent exit node ping results to cloud for selection") }) client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) { @@ -648,10 +648,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Debug("Public key: %s", publicKey) // request from the server the list of nodes to ping at newt/ping/request - err := client.SendMessage("newt/ping/request", map[string]interface{}{}) - if err != nil { - logger.Error("Failed to send ping request: %v", err) - } + stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) if wgService != nil { wgService.LoadRemoteConfig() @@ -699,75 +696,59 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub os.Exit(0) } -func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Client, stopChan chan struct{}) { - initialInterval := 10 * time.Second - maxInterval := 60 * time.Second +func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Client) chan struct{} { + initialInterval := pingInterval + maxInterval := 3 * time.Second currentInterval := initialInterval consecutiveFailures := 0 connectionLost := false - ticker := time.NewTicker(currentInterval) - defer ticker.Stop() + + pingStopChan := make(chan struct{}) go func() { + ticker := time.NewTicker(currentInterval) + defer ticker.Stop() for { select { case <-ticker.C: _, err := ping(tnet, serverIP) if err != nil { consecutiveFailures++ - - // Check if this is the first failure (connection just lost) - if !connectionLost { - connectionLost = true - logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") - logger.Warn("Please check your internet connection and ensure the Pangolin server is online.") - logger.Warn("Newt will continue reconnection attempts automatically when connectivity is restored.") - } - - logger.Warn("Periodic ping failed (%d consecutive failures): %v", - consecutiveFailures, err) - logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?") - - // Increase interval if we have consistent failures, with a maximum cap - if consecutiveFailures >= 5 && currentInterval < maxInterval { - // Increase by 50% each time, up to the maximum + logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) + if consecutiveFailures >= 3 && currentInterval < maxInterval { + if !connectionLost { + connectionLost = true + logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") + stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) + } currentInterval = time.Duration(float64(currentInterval) * 1.5) if currentInterval > maxInterval { currentInterval = maxInterval } ticker.Reset(currentInterval) - logger.Debug("Increased ping check interval to %v due to consecutive failures", - currentInterval) - - // Restart the connection flow - err := client.SendMessage("newt/ping/request", map[string]interface{}{}) - if err != nil { - logger.Error("Failed to send ping request: %v", err) - } + logger.Debug("Increased ping check interval to %v due to consecutive failures", currentInterval) } } else { - // Check if connection was previously lost and is now restored if connectionLost { connectionLost = false logger.Info("Connection to server restored!") } - - // On success, if we've backed off, gradually return to normal interval if currentInterval > initialInterval { currentInterval = time.Duration(float64(currentInterval) * 0.8) if currentInterval < initialInterval { currentInterval = initialInterval } ticker.Reset(currentInterval) - logger.Info("Decreased ping check interval to %v after successful ping", - currentInterval) + logger.Info("Decreased ping check interval to %v after successful ping", currentInterval) } consecutiveFailures = 0 } - case <-stopChan: + case <-pingStopChan: logger.Info("Stopping ping check") return } } }() + + return pingStopChan } diff --git a/util.go b/util.go index 808ae2b..43a48bf 100644 --- a/util.go +++ b/util.go @@ -44,7 +44,7 @@ func ping(tnet *netstack.Net, dst string) (time.Duration, error) { requestPing := icmp.Echo{ Seq: rand.Intn(1 << 16), - Data: []byte("gopher burrow"), + Data: []byte("f"), } icmpBytes, err := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil) @@ -52,7 +52,7 @@ func ping(tnet *netstack.Net, dst string) (time.Duration, error) { return 0, fmt.Errorf("failed to marshal ICMP message: %w", err) } - if err := socket.SetReadDeadline(time.Now().Add(time.Second * 10)); err != nil { + if err := socket.SetReadDeadline(time.Now().Add(time.Second * 2)); err != nil { return 0, fmt.Errorf("failed to set read deadline: %w", err) } @@ -84,12 +84,14 @@ func ping(tnet *netstack.Net, dst string) (time.Duration, error) { latency := time.Since(start) + logger.Debug("Ping to %s successful, latency: %v", dst, latency) + return latency, nil } func pingWithRetry(tnet *netstack.Net, dst string) error { const ( - initialMaxAttempts = 15 + initialMaxAttempts = 5 initialRetryDelay = 2 * time.Second maxRetryDelay = 60 * time.Second // Cap the maximum delay ) diff --git a/websocket/client.go b/websocket/client.go index 1d75ea8..6b34627 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -9,11 +9,12 @@ import ( "net/http" "net/url" "os" - "software.sslmate.com/src/go-pkcs12" "strings" "sync" "time" + "software.sslmate.com/src/go-pkcs12" + "github.com/fosrl/newt/logger" "github.com/gorilla/websocket" ) @@ -126,6 +127,32 @@ func (c *Client) SendMessage(messageType string, data interface{}) error { return c.conn.WriteJSON(msg) } +func (c *Client) SendMessageInterval(messageType string, data interface{}, interval time.Duration) (stop func()) { + stopChan := make(chan struct{}) + go func() { + err := c.SendMessage(messageType, data) // Send immediately + if err != nil { + logger.Error("Failed to send initial message: %v", err) + } + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + err = c.SendMessage(messageType, data) + if err != nil { + logger.Error("Failed to send message: %v", err) + } + case <-stopChan: + return + } + } + }() + return func() { + close(stopChan) + } +} + // RegisterHandler registers a handler for a specific message type func (c *Client) RegisterHandler(messageType string, handler MessageHandler) { c.handlersMux.Lock() From 5e7906a354601c792644873a0f7ad6764b069453 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 19 Jun 2025 15:59:21 -0400 Subject: [PATCH 58/87] Reorg and add timeout Former-commit-id: bb1318278a9c85b631f857778415be4a327ae343 --- main.go | 72 +++++++++++---------------------------------------------- util.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 63 deletions(-) diff --git a/main.go b/main.go index a646672..b4025f8 100644 --- a/main.go +++ b/main.go @@ -86,6 +86,7 @@ var ( tlsPrivateKey string dockerSocket string pingInterval = 1 * time.Second + pingTimeout = 2 * time.Second publicKey wgtypes.Key pingStopChan chan struct{} stopFunc func() @@ -107,6 +108,7 @@ func main() { tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") pingIntervalStr := os.Getenv("PING_INTERVAL") + pingTimeoutStr := os.Getenv("PING_TIMEOUT") if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -146,6 +148,9 @@ func main() { if pingIntervalStr == "" { flag.StringVar(&pingIntervalStr, "ping-interval", "1s", "Interval for pinging the server (default 1s)") } + if pingTimeoutStr == "" { + flag.StringVar(&pingTimeoutStr, "ping-timeout", "2s", " Timeout for each ping (default 2s)") + } if pingIntervalStr != "" { pingInterval, err = time.ParseDuration(pingIntervalStr) @@ -155,6 +160,14 @@ func main() { } } + if pingTimeoutStr != "" { + pingTimeout, err = time.ParseDuration(pingTimeoutStr) + if err != nil { + fmt.Printf("Invalid PING_TIMEOUT value: %s, using default 2 seconds\n", pingTimeoutStr) + pingTimeout = 2 * time.Second + } + } + // do a --version check version := flag.Bool("version", false, "Print the version") @@ -336,7 +349,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("WireGuard device created. Lets ping the server now...") // Even if pingWithRetry returns an error, it will continue trying in the background - _ = pingWithRetry(tnet, wgData.ServerIP) + _ = pingWithRetry(tnet, wgData.ServerIP, pingTimeout) // Always mark as connected and start the proxy manager regardless of initial ping result // as the pings will continue in the background @@ -695,60 +708,3 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("Exiting...") os.Exit(0) } - -func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Client) chan struct{} { - initialInterval := pingInterval - maxInterval := 3 * time.Second - currentInterval := initialInterval - consecutiveFailures := 0 - connectionLost := false - - pingStopChan := make(chan struct{}) - - go func() { - ticker := time.NewTicker(currentInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - _, err := ping(tnet, serverIP) - if err != nil { - consecutiveFailures++ - logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) - if consecutiveFailures >= 3 && currentInterval < maxInterval { - if !connectionLost { - connectionLost = true - logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") - stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) - } - currentInterval = time.Duration(float64(currentInterval) * 1.5) - if currentInterval > maxInterval { - currentInterval = maxInterval - } - ticker.Reset(currentInterval) - logger.Debug("Increased ping check interval to %v due to consecutive failures", currentInterval) - } - } else { - if connectionLost { - connectionLost = false - logger.Info("Connection to server restored!") - } - if currentInterval > initialInterval { - currentInterval = time.Duration(float64(currentInterval) * 0.8) - if currentInterval < initialInterval { - currentInterval = initialInterval - } - ticker.Reset(currentInterval) - logger.Info("Decreased ping check interval to %v after successful ping", currentInterval) - } - consecutiveFailures = 0 - } - case <-pingStopChan: - logger.Info("Stopping ping check") - return - } - } - }() - - return pingStopChan -} diff --git a/util.go b/util.go index 43a48bf..a10c94f 100644 --- a/util.go +++ b/util.go @@ -13,6 +13,7 @@ import ( "github.com/fosrl/newt/logger" "github.com/fosrl/newt/proxy" + "github.com/fosrl/newt/websocket" "golang.org/x/exp/rand" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" @@ -34,7 +35,7 @@ func fixKey(key string) string { return hex.EncodeToString(decoded) } -func ping(tnet *netstack.Net, dst string) (time.Duration, error) { +func ping(tnet *netstack.Net, dst string, timeout time.Duration) (time.Duration, error) { logger.Debug("Pinging %s", dst) socket, err := tnet.Dial("ping4", dst) if err != nil { @@ -52,7 +53,7 @@ func ping(tnet *netstack.Net, dst string) (time.Duration, error) { return 0, fmt.Errorf("failed to marshal ICMP message: %w", err) } - if err := socket.SetReadDeadline(time.Now().Add(time.Second * 2)); err != nil { + if err := socket.SetReadDeadline(time.Now().Add(timeout)); err != nil { return 0, fmt.Errorf("failed to set read deadline: %w", err) } @@ -89,7 +90,7 @@ func ping(tnet *netstack.Net, dst string) (time.Duration, error) { return latency, nil } -func pingWithRetry(tnet *netstack.Net, dst string) error { +func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) error { const ( initialMaxAttempts = 5 initialRetryDelay = 2 * time.Second @@ -101,7 +102,7 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { // First try with the initial parameters logger.Info("Ping attempt %d", attempt) - if latency, err := ping(tnet, dst); err == nil { + if latency, err := ping(tnet, dst, timeout); err == nil { // Successful ping logger.Info("Ping latency: %v", latency) @@ -118,7 +119,7 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { for { logger.Info("Ping attempt %d", attempt) - if latency, err := ping(tnet, dst); err != nil { + if latency, err := ping(tnet, dst, timeout); err != nil { logger.Warn("Ping attempt %d failed: %v", attempt, err) // Increase delay after certain thresholds but cap it @@ -146,6 +147,63 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { return fmt.Errorf("initial ping attempts failed, continuing in background") } +func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Client) chan struct{} { + initialInterval := pingInterval + maxInterval := 3 * time.Second + currentInterval := initialInterval + consecutiveFailures := 0 + connectionLost := false + + pingStopChan := make(chan struct{}) + + go func() { + ticker := time.NewTicker(currentInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + _, err := ping(tnet, serverIP, pingTimeout) + if err != nil { + consecutiveFailures++ + logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) + if consecutiveFailures >= 3 && currentInterval < maxInterval { + if !connectionLost { + connectionLost = true + logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") + stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) + } + currentInterval = time.Duration(float64(currentInterval) * 1.5) + if currentInterval > maxInterval { + currentInterval = maxInterval + } + ticker.Reset(currentInterval) + logger.Debug("Increased ping check interval to %v due to consecutive failures", currentInterval) + } + } else { + if connectionLost { + connectionLost = false + logger.Info("Connection to server restored!") + } + if currentInterval > initialInterval { + currentInterval = time.Duration(float64(currentInterval) * 0.8) + if currentInterval < initialInterval { + currentInterval = initialInterval + } + ticker.Reset(currentInterval) + logger.Info("Decreased ping check interval to %v after successful ping", currentInterval) + } + consecutiveFailures = 0 + } + case <-pingStopChan: + logger.Info("Stopping ping check") + return + } + } + }() + + return pingStopChan +} + func parseLogLevel(level string) logger.LogLevel { switch strings.ToUpper(level) { case "DEBUG": From 5abf4611e5507a90fe982ff6183e39fe3a88cafe Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 19 Jun 2025 16:30:31 -0400 Subject: [PATCH 59/87] Faster detection on ws side Former-commit-id: a14f70dbaa192f85134bf1001bfec0d8e0dd43ed --- main.go | 24 ++++++++++--- websocket/client.go | 88 +++++++++++++++++++++++++-------------------- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/main.go b/main.go index b4025f8..f440db8 100644 --- a/main.go +++ b/main.go @@ -204,6 +204,8 @@ func main() { id, // CLI arg takes precedence secret, // CLI arg takes precedence endpoint, + pingInterval, + pingTimeout, opt, ) if err != nil { @@ -660,14 +662,26 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub publicKey = privateKey.PublicKey() logger.Debug("Public key: %s", publicKey) - // request from the server the list of nodes to ping at newt/ping/request - stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) + if !connected { + // request from the server the list of nodes to ping at newt/ping/request + stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) - if wgService != nil { - wgService.LoadRemoteConfig() + // Send registration message to the server for backward compatibility + err := client.SendMessage("newt/wg/register", map[string]interface{}{ + "publicKey": publicKey.String(), + "backwardsCompatible": true, + }) + if err != nil { + logger.Error("Failed to send registration message: %v", err) + return err + } + logger.Info("Sent registration message") + + if wgService != nil { + wgService.LoadRemoteConfig() + } } - logger.Info("Sent registration message") return nil }) diff --git a/websocket/client.go b/websocket/client.go index 6b34627..4bd2c7d 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -29,9 +29,10 @@ type Client struct { reconnectInterval time.Duration isConnected bool reconnectMux sync.RWMutex - - onConnect func() error - onTokenUpdate func(token string) + pingInterval time.Duration + pingTimeout time.Duration + onConnect func() error + onTokenUpdate func(token string) } type ClientOption func(*Client) @@ -60,7 +61,7 @@ func (c *Client) OnTokenUpdate(callback func(token string)) { } // NewClient creates a new Newt client -func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*Client, error) { +func NewClient(newtID, secret string, endpoint string, pingInterval time.Duration, pingTimeout time.Duration, opts ...ClientOption) (*Client, error) { config := &Config{ NewtID: newtID, Secret: secret, @@ -74,16 +75,16 @@ func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*C done: make(chan struct{}), reconnectInterval: 10 * time.Second, isConnected: false, + pingInterval: pingInterval, + pingTimeout: pingTimeout, } // Apply options before loading config - if opts != nil { - for _, opt := range opts { - if opt == nil { - continue - } - opt(client) + for _, opt := range opts { + if opt == nil { + continue } + opt(client) } // Load existing config if available @@ -160,30 +161,6 @@ func (c *Client) RegisterHandler(messageType string, handler MessageHandler) { c.handlers[messageType] = handler } -// readPump pumps messages from the WebSocket connection -func (c *Client) readPump() { - defer c.conn.Close() - - for { - select { - case <-c.done: - return - default: - var msg WSMessage - err := c.conn.ReadJSON(&msg) - if err != nil { - return - } - - c.handlersMux.RLock() - if handler, ok := c.handlers[msg.Type]; ok { - handler(msg) - } - c.handlersMux.RUnlock() - } - } -} - func (c *Client) getToken() (string, error) { // Parse the base URL to ensure we have the correct hostname baseURL, err := url.Parse(c.baseURL) @@ -380,8 +357,8 @@ func (c *Client) establishConnection() error { // Start the ping monitor go c.pingMonitor() - // Start the read pump - go c.readPump() + // Start the read pump with disconnect detection + go c.readPumpWithDisconnectDetection() if c.onConnect != nil { err := c.saveConfig() @@ -396,8 +373,9 @@ func (c *Client) establishConnection() error { return nil } +// pingMonitor sends pings at a short interval and triggers reconnect on failure func (c *Client) pingMonitor() { - ticker := time.NewTicker(30 * time.Second) + ticker := time.NewTicker(c.pingInterval) defer ticker.Stop() for { @@ -405,7 +383,10 @@ func (c *Client) pingMonitor() { case <-c.done: return case <-ticker.C: - if err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil { + if c.conn == nil { + return + } + if err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(c.pingTimeout)); err != nil { logger.Error("Ping failed: %v", err) c.reconnect() return @@ -414,10 +395,41 @@ func (c *Client) pingMonitor() { } } +// readPumpWithDisconnectDetection reads messages and triggers reconnect on error +func (c *Client) readPumpWithDisconnectDetection() { + defer func() { + if c.conn != nil { + c.conn.Close() + } + c.reconnect() + }() + + for { + select { + case <-c.done: + return + default: + var msg WSMessage + err := c.conn.ReadJSON(&msg) + if err != nil { + logger.Error("WebSocket read error: %v", err) + return // triggers reconnect via defer + } + + c.handlersMux.RLock() + if handler, ok := c.handlers[msg.Type]; ok { + handler(msg) + } + c.handlersMux.RUnlock() + } + } +} + func (c *Client) reconnect() { c.setConnected(false) if c.conn != nil { c.conn.Close() + c.conn = nil } go c.connectWithRetry() From 4a97c35b815622f162d0c602c6a18bb864fbde93 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 19 Jun 2025 16:32:07 -0400 Subject: [PATCH 60/87] Change terminate to reconnect Former-commit-id: 37191924ee8bf1cd72e26655f14bbfd4b2ee646e --- main.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index f440db8..9fb5cbe 100644 --- a/main.go +++ b/main.go @@ -387,8 +387,8 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) - client.RegisterHandler("newt/wg/terminate", func(msg websocket.WSMessage) { - logger.Info("Received disconnect message") + client.RegisterHandler("newt/wg/reconnect", func(msg websocket.WSMessage) { + logger.Info("Received reconnect message") if pingStopChan != nil { // Stop the ping check @@ -419,6 +419,15 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // Mark as disconnected connected = false + // start asking for the exit nodes again + if stopFunc != nil { + stopFunc() // stop the ws from sending more requests + stopFunc = nil // reset stopFunc to nil to avoid double stopping + } + + // Request exit nodes from the server + stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) + logger.Info("Tunnel destroyed, ready for reconnection") }) From bc4f453bae72e687c464b48fc568d4855dd489ab Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 19 Jun 2025 16:39:33 -0400 Subject: [PATCH 61/87] Send version Former-commit-id: 6d3938e14e4fa1ed697400120a714161ac9b5311 --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index 9fb5cbe..091406c 100644 --- a/main.go +++ b/main.go @@ -525,6 +525,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub stopFunc = client.SendMessageInterval("newt/wg/register", map[string]interface{}{ "publicKey": publicKey.String(), "pingResults": pingResults, + "newtVersion": newtVersion, }, 1*time.Second) logger.Info("Sent exit node ping results to cloud for selection") }) @@ -678,6 +679,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // Send registration message to the server for backward compatibility err := client.SendMessage("newt/wg/register", map[string]interface{}{ "publicKey": publicKey.String(), + "newtVersion": newtVersion, "backwardsCompatible": true, }) if err != nil { From 761e46d1b1b73206d0f5e8a556fd8535d0fa2d70 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 19 Jun 2025 17:38:21 -0400 Subject: [PATCH 62/87] Failover is working? Former-commit-id: ca3ffa00ed30bc0ed43dedd4490ced06e739306c --- .github/workflows/cicd.yml | 2 +- main.go | 25 ++++-- updates.go | 173 +++++++++++++++++++++++++++++++++++++ websocket/client.go | 16 ++-- 4 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 updates.go diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 1479a55..db55d7c 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -39,7 +39,7 @@ jobs: run: | TAG=${{ env.TAG }} if [ -f main.go ]; then - sed -i 's/Newt version replaceme/Newt version '"$TAG"'/' main.go + sed -i 's/version_replaceme/'"$TAG"'/' main.go echo "Updated main.go with version $TAG" else echo "main.go not found" diff --git a/main.go b/main.go index 091406c..e070a29 100644 --- a/main.go +++ b/main.go @@ -173,18 +173,22 @@ func main() { flag.Parse() - newtVersion := "Newt version replaceme" - if *version { - fmt.Println(newtVersion) - os.Exit(0) - } else { - logger.Info(newtVersion) - } - logger.Init() loggerLevel := parseLogLevel(logLevel) logger.GetLogger().SetLevel(parseLogLevel(logLevel)) + newtVersion := "version_replaceme" + if *version { + fmt.Println("Newt version " + newtVersion) + os.Exit(0) + } else { + logger.Info("Newt version " + newtVersion) + } + + if err := CheckForUpdate("fosrl", "newt", newtVersion); err != nil { + logger.Error("Error checking for updates: %v\n", err) + } + // parse the mtu string into an int mtuInt, err = strconv.Atoi(mtu) if err != nil { @@ -466,6 +470,11 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub results := make([]nodeResult, len(exitNodes)) const pingAttempts = 3 for i, node := range exitNodes { + if connected && node.WasPreviouslyConnected { + logger.Info("Skipping ping for previously connected exit node so we pick another %d (%s)", node.ID, node.Endpoint) + continue + } + var totalLatency time.Duration var lastErr error successes := 0 diff --git a/updates.go b/updates.go new file mode 100644 index 0000000..ef5bbb5 --- /dev/null +++ b/updates.go @@ -0,0 +1,173 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" +) + +// GitHubRelease represents the GitHub API response for a release +type GitHubRelease struct { + TagName string `json:"tag_name"` + Name string `json:"name"` + HTMLURL string `json:"html_url"` +} + +// Version represents a semantic version +type Version struct { + Major int + Minor int + Patch int +} + +// parseVersion parses a semantic version string (e.g., "v1.2.3" or "1.2.3") +func parseVersion(versionStr string) (Version, error) { + // Remove 'v' prefix if present + versionStr = strings.TrimPrefix(versionStr, "v") + + parts := strings.Split(versionStr, ".") + if len(parts) != 3 { + return Version{}, fmt.Errorf("invalid version format: %s", versionStr) + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + return Version{}, fmt.Errorf("invalid major version: %s", parts[0]) + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return Version{}, fmt.Errorf("invalid minor version: %s", parts[1]) + } + + patch, err := strconv.Atoi(parts[2]) + if err != nil { + return Version{}, fmt.Errorf("invalid patch version: %s", parts[2]) + } + + return Version{Major: major, Minor: minor, Patch: patch}, nil +} + +// isNewer returns true if v2 is newer than v1 +func (v1 Version) isNewer(v2 Version) bool { + if v2.Major > v1.Major { + return true + } + if v2.Major < v1.Major { + return false + } + + if v2.Minor > v1.Minor { + return true + } + if v2.Minor < v1.Minor { + return false + } + + return v2.Patch > v1.Patch +} + +// String returns the version as a string +func (v Version) String() string { + return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) +} + +// CheckForUpdate checks GitHub for a newer version and prints an update banner if found +func CheckForUpdate(owner, repo, currentVersion string) error { + if currentVersion == "version_replaceme" { + return nil + } + + // GitHub API URL for latest release + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo) + + // Create HTTP client with timeout + client := &http.Client{ + Timeout: 10 * time.Second, + } + + // Make the request + resp, err := client.Get(url) + if err != nil { + return fmt.Errorf("failed to fetch release info: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("GitHub API returned status: %d", resp.StatusCode) + } + + // Parse the JSON response + var release GitHubRelease + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return fmt.Errorf("failed to parse release info: %w", err) + } + + // Parse current and latest versions + currentVer, err := parseVersion(currentVersion) + if err != nil { + return fmt.Errorf("invalid current version: %w", err) + } + + latestVer, err := parseVersion(release.TagName) + if err != nil { + return fmt.Errorf("invalid latest version: %w", err) + } + + // Check if update is available + if currentVer.isNewer(latestVer) { + printUpdateBanner(currentVer.String(), latestVer.String(), release.HTMLURL) + } + + return nil +} + +// printUpdateBanner prints a colorful update notification banner +func printUpdateBanner(currentVersion, latestVersion, releaseURL string) { + const contentWidth = 70 // width between the border lines + + borderTop := "╔" + strings.Repeat("═", contentWidth) + "╗" + borderMid := "╠" + strings.Repeat("═", contentWidth) + "╣" + borderBot := "╚" + strings.Repeat("═", contentWidth) + "╝" + emptyLine := "║" + strings.Repeat(" ", contentWidth) + "║" + + lines := []string{ + borderTop, + "║" + centerText("UPDATE AVAILABLE", contentWidth) + "║", + borderMid, + emptyLine, + "║ Current Version: " + padRight(currentVersion, contentWidth-19) + "║", + "║ Latest Version: " + padRight(latestVersion, contentWidth-19) + "║", + emptyLine, + "║ A newer version is available! Please update to get the" + padRight("", contentWidth-56) + "║", + "║ latest features, bug fixes, and security improvements." + padRight("", contentWidth-56) + "║", + emptyLine, + "║ Release URL: " + padRight(releaseURL, contentWidth-15) + "║", + emptyLine, + borderBot, + } + + for _, line := range lines { + fmt.Println(line) + } +} + +// padRight pads s with spaces on the right to the given width +func padRight(s string, width int) string { + if len(s) > width { + return s[:width] + } + return s + strings.Repeat(" ", width-len(s)) +} + +// centerText centers s in a field of width w +func centerText(s string, w int) string { + if len(s) >= w { + return s[:w] + } + padding := (w - len(s)) / 2 + return strings.Repeat(" ", padding) + s + strings.Repeat(" ", w-len(s)-padding) +} diff --git a/websocket/client.go b/websocket/client.go index 4bd2c7d..9e11f01 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -33,6 +33,7 @@ type Client struct { pingTimeout time.Duration onConnect func() error onTokenUpdate func(token string) + writeMux sync.Mutex } type ClientOption func(*Client) @@ -73,7 +74,7 @@ func NewClient(newtID, secret string, endpoint string, pingInterval time.Duratio baseURL: endpoint, // default value handlers: make(map[string]MessageHandler), done: make(chan struct{}), - reconnectInterval: 10 * time.Second, + reconnectInterval: 3 * time.Second, isConnected: false, pingInterval: pingInterval, pingTimeout: pingTimeout, @@ -125,6 +126,8 @@ func (c *Client) SendMessage(messageType string, data interface{}) error { Data: data, } + c.writeMux.Lock() + defer c.writeMux.Unlock() return c.conn.WriteJSON(msg) } @@ -220,6 +223,7 @@ func (c *Client) getToken() (string, error) { var tokenResp TokenResponse if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + logger.Error("Failed to decode token check response. Raw response: %s", resp.Body) return "", fmt.Errorf("failed to decode token check response: %w", err) } @@ -268,10 +272,7 @@ func (c *Client) getToken() (string, error) { var tokenResp TokenResponse if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { - // print out the token response for debugging - buf := new(bytes.Buffer) - buf.ReadFrom(resp.Body) - logger.Info("Token response: %s", buf.String()) + logger.Error("Failed to decode token response. Raw response: %s", resp.Body) return "", fmt.Errorf("failed to decode token response: %w", err) } @@ -386,7 +387,10 @@ func (c *Client) pingMonitor() { if c.conn == nil { return } - if err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(c.pingTimeout)); err != nil { + c.writeMux.Lock() + err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(c.pingTimeout)) + c.writeMux.Unlock() + if err != nil { logger.Error("Ping failed: %v", err) c.reconnect() return From da4e572939c82a791920e1aba2f88556aa51f3a4 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 19 Jun 2025 22:58:27 -0400 Subject: [PATCH 63/87] Properly check the http status code now Former-commit-id: 227631665e751d3e0653e0a6e6b8112c12471d49 --- websocket/client.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/websocket/client.go b/websocket/client.go index 9e11f01..a3538c0 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -221,9 +221,14 @@ func (c *Client) getToken() (string, error) { } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + logger.Error("Token check failed with status code: %d", resp.StatusCode) + return "", fmt.Errorf("token check failed with status code: %d", resp.StatusCode) + } + var tokenResp TokenResponse if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { - logger.Error("Failed to decode token check response. Raw response: %s", resp.Body) + logger.Error("Failed to decode token check response.") return "", fmt.Errorf("failed to decode token check response: %w", err) } @@ -270,9 +275,14 @@ func (c *Client) getToken() (string, error) { } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + logger.Error("Failed to get token with status code: %d", resp.StatusCode) + return "", fmt.Errorf("failed to get token with status code: %d", resp.StatusCode) + } + var tokenResp TokenResponse if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { - logger.Error("Failed to decode token response. Raw response: %s", resp.Body) + logger.Error("Failed to decode token response.") return "", fmt.Errorf("failed to decode token response: %w", err) } From 5f14ff3a0744da1773d026b8838d5cb11fe1b0fa Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 22 Jun 2025 11:38:43 -0400 Subject: [PATCH 64/87] Resolve some basic comments --- docker/client.go | 5 ++--- main.go | 8 ++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docker/client.go b/docker/client.go index 9fedf52..6a3bdb7 100644 --- a/docker/client.go +++ b/docker/client.go @@ -75,7 +75,6 @@ func IsWithinHostNetwork(socketPath string, targetAddress string, targetPort int // Always enforce network validation containers, err := ListContainers(socketPath, true) if err != nil { - return false, err } @@ -142,7 +141,7 @@ func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Contain hostContainer, err := getHostContainer(ctx, cli) if enforceNetworkValidation && err != nil { - return nil, fmt.Errorf("network validation enforced, cannot validate due to: %v", err) + return nil, fmt.Errorf("network validation enforced, cannot validate due to: %w", err) } // We may not be able to get back host container in scenarios like running the container in network mode 'host' @@ -262,4 +261,4 @@ func getHostContainer(dockerContext context.Context, dockerClient *client.Client } return &hostContainer, nil -} \ No newline at end of file +} diff --git a/main.go b/main.go index 6622999..e7275ad 100644 --- a/main.go +++ b/main.go @@ -792,7 +792,11 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto if action == "add" { targetAddress := parts[1] - targetPort, _ := strconv.Atoi(parts[2]) + targetPort, err := strconv.Atoi(parts[2]) + if err != nil { + logger.Info("Invalid target port: %s", parts[2]) + continue + } combinedAddress := targetAddress + ":" + parts[2] // Call updown script if provided @@ -807,7 +811,7 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto } // Only remove the specific target if it exists - err := pm.RemoveTarget(proto, tunnelIP, port) + err = pm.RemoveTarget(proto, tunnelIP, port) if err != nil { // Ignore "target not found" errors as this is expected for new targets if !strings.Contains(err.Error(), "target not found") { From 0090f78f1557a7ede1fbcd95f2c256b6895d4189 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 22 Jun 2025 12:10:00 -0400 Subject: [PATCH 65/87] Move updates Former-commit-id: e642983b882e221bee75bb19e1d0d0ceabe5aa7a --- main.go | 3 ++- updates.go => updates/updates.go | 2 +- util.go | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) rename updates.go => updates/updates.go (99%) diff --git a/main.go b/main.go index 2be8a83..a93a30a 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/fosrl/newt/docker" "github.com/fosrl/newt/logger" "github.com/fosrl/newt/proxy" + "github.com/fosrl/newt/updates" "github.com/fosrl/newt/websocket" "github.com/fosrl/newt/wg" "github.com/fosrl/newt/wgtester" @@ -192,7 +193,7 @@ func main() { logger.Info("Newt version " + newtVersion) } - if err := CheckForUpdate("fosrl", "newt", newtVersion); err != nil { + if err := updates.CheckForUpdate("fosrl", "newt", newtVersion); err != nil { logger.Error("Error checking for updates: %v\n", err) } diff --git a/updates.go b/updates/updates.go similarity index 99% rename from updates.go rename to updates/updates.go index ef5bbb5..8d7de5e 100644 --- a/updates.go +++ b/updates/updates.go @@ -1,4 +1,4 @@ -package main +package updates import ( "encoding/json" diff --git a/util.go b/util.go index a10c94f..6f09eb3 100644 --- a/util.go +++ b/util.go @@ -1,4 +1,4 @@ -package main +package util import ( "bytes" @@ -369,7 +369,7 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto return nil } -func executeUpdownScript(action, proto, target string) (string, error) { +func executeUpdownScript(action, proto, target string, updownScript string) (string, error) { if updownScript == "" { return target, nil } From 3fab3e3e2097ce99daaaad82d6f7a6b00a79de15 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 22 Jun 2025 12:50:55 -0400 Subject: [PATCH 66/87] Add way to stop initial ping Former-commit-id: ac67df63fa5a5a403a1a6280211b768fd1ea8a12 --- main.go | 38 +++++++++++++++++++++----------- util.go | 68 ++++++++++++++++++++++++++++++++++----------------------- 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/main.go b/main.go index a93a30a..cd14d06 100644 --- a/main.go +++ b/main.go @@ -292,6 +292,8 @@ func main() { } } + var pingWithRetryStopChan chan struct{} + // Register handlers for different message types client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) { logger.Info("Received registration message") @@ -388,7 +390,12 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("WireGuard device created. Lets ping the server now...") // Even if pingWithRetry returns an error, it will continue trying in the background - _ = pingWithRetry(tnet, wgData.ServerIP, pingTimeout) + if pingWithRetryStopChan != nil { + // Stop the previous pingWithRetry if it exists + close(pingWithRetryStopChan) + pingWithRetryStopChan = nil + } + pingWithRetryStopChan, _ = pingWithRetry(tnet, wgData.ServerIP, pingTimeout) // Always mark as connected and start the proxy manager regardless of initial ping result // as the pings will continue in the background @@ -703,6 +710,10 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub err = client.SendMessage("newt/socket/containers", map[string]interface{}{ "containers": containers, }) + if err != nil { + logger.Error("Failed to send registration message: %v", err) + } + logger.Info("Sent registration message") if err != nil { logger.Error("Failed to send Docker container list: %v", err) } else { @@ -718,23 +729,24 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // request from the server the list of nodes to ping at newt/ping/request stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) - // Send registration message to the server for backward compatibility - err := client.SendMessage("newt/wg/register", map[string]interface{}{ - "publicKey": publicKey.String(), - "newtVersion": newtVersion, - "backwardsCompatible": true, - }) - if err != nil { - logger.Error("Failed to send registration message: %v", err) - return err - } - logger.Info("Sent registration message") - if wgService != nil { wgService.LoadRemoteConfig() } } + // Send registration message to the server for backward compatibility + err := client.SendMessage("newt/wg/register", map[string]interface{}{ + "publicKey": publicKey.String(), + "newtVersion": newtVersion, + "backwardsCompatible": true, + }) + + if err != nil { + logger.Error("Failed to send registration message: %v", err) + return err + } + logger.Info("Sent registration message") + return nil }) diff --git a/util.go b/util.go index 6f09eb3..3016d30 100644 --- a/util.go +++ b/util.go @@ -1,4 +1,4 @@ -package util +package main import ( "bytes" @@ -90,13 +90,14 @@ func ping(tnet *netstack.Net, dst string, timeout time.Duration) (time.Duration, return latency, nil } -func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) error { +func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) (stopChan chan struct{}, err error) { const ( initialMaxAttempts = 5 initialRetryDelay = 2 * time.Second maxRetryDelay = 60 * time.Second // Cap the maximum delay ) + stopChan = make(chan struct{}) attempt := 1 retryDelay := initialRetryDelay @@ -105,9 +106,8 @@ func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) error if latency, err := ping(tnet, dst, timeout); err == nil { // Successful ping logger.Info("Ping latency: %v", latency) - logger.Info("Tunnel connection to server established successfully!") - return nil + return stopChan, nil } else { logger.Warn("Ping attempt %d failed: %v", attempt, err) } @@ -117,34 +117,40 @@ func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) error attempt = 2 // Continue from attempt 2 for { - logger.Info("Ping attempt %d", attempt) - - if latency, err := ping(tnet, dst, timeout); err != nil { - logger.Warn("Ping attempt %d failed: %v", attempt, err) - - // Increase delay after certain thresholds but cap it - if attempt%5 == 0 && retryDelay < maxRetryDelay { - retryDelay = time.Duration(float64(retryDelay) * 1.5) - if retryDelay > maxRetryDelay { - retryDelay = maxRetryDelay - } - logger.Info("Increasing ping retry delay to %v", retryDelay) - } - - time.Sleep(retryDelay) - attempt++ - } else { - // Successful ping - logger.Info("Ping succeeded after %d attempts", attempt) - logger.Info("Ping latency: %v", latency) - logger.Info("Tunnel connection to server established successfully!") + select { + case <-stopChan: + logger.Info("Stopping pingWithRetry goroutine") return + default: + logger.Info("Ping attempt %d", attempt) + + if latency, err := ping(tnet, dst, timeout); err != nil { + logger.Warn("Ping attempt %d failed: %v", attempt, err) + + // Increase delay after certain thresholds but cap it + if attempt%5 == 0 && retryDelay < maxRetryDelay { + retryDelay = time.Duration(float64(retryDelay) * 1.5) + if retryDelay > maxRetryDelay { + retryDelay = maxRetryDelay + } + logger.Info("Increasing ping retry delay to %v", retryDelay) + } + + time.Sleep(retryDelay) + attempt++ + } else { + // Successful ping + logger.Info("Ping succeeded after %d attempts", attempt) + logger.Info("Ping latency: %v", latency) + logger.Info("Tunnel connection to server established successfully!") + return + } } } }() // Return an error for the first batch of attempts (to maintain compatibility with existing code) - return fmt.Errorf("initial ping attempts failed, continuing in background") + return stopChan, fmt.Errorf("initial ping attempts failed, continuing in background") } func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Client) chan struct{} { @@ -171,6 +177,14 @@ func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Clien connectionLost = true logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) + // Send registration message to the server for backward compatibility + err := client.SendMessage("newt/wg/register", map[string]interface{}{ + "publicKey": publicKey.String(), + "backwardsCompatible": true, + }) + if err != nil { + logger.Error("Failed to send registration message: %v", err) + } } currentInterval = time.Duration(float64(currentInterval) * 1.5) if currentInterval > maxInterval { @@ -369,7 +383,7 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto return nil } -func executeUpdownScript(action, proto, target string, updownScript string) (string, error) { +func executeUpdownScript(action, proto, target string) (string, error) { if updownScript == "" { return target, nil } From 5cf684c81a4da26828648d33e9d9eb790162d7be Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 22 Jun 2025 12:54:13 -0400 Subject: [PATCH 67/87] Add log message Former-commit-id: c1a2a3208c88bbe4856864c944ec9bccc29cd0de --- util.go | 1 - 1 file changed, 1 deletion(-) diff --git a/util.go b/util.go index 3016d30..c17bd5a 100644 --- a/util.go +++ b/util.go @@ -119,7 +119,6 @@ func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) (stopC for { select { case <-stopChan: - logger.Info("Stopping pingWithRetry goroutine") return default: logger.Info("Ping attempt %d", attempt) From beeb75655bb50a23634fef19649840fd5a383dc5 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 22 Jun 2025 12:55:13 -0400 Subject: [PATCH 68/87] Change math random Former-commit-id: 63aea704bd28e394d783318212f07ceb4f96c70c --- util.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util.go b/util.go index c17bd5a..fa6a57f 100644 --- a/util.go +++ b/util.go @@ -11,10 +11,11 @@ import ( "strings" "time" + "math/rand" + "github.com/fosrl/newt/logger" "github.com/fosrl/newt/proxy" "github.com/fosrl/newt/websocket" - "golang.org/x/exp/rand" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.zx2c4.com/wireguard/device" From 678d82fa68068e18430c1c294a778c8ca026bf33 Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Wed, 25 Jun 2025 19:30:05 +0200 Subject: [PATCH 69/87] added healthy check in main.go extended the ping check that creates a /tmp/healthy file if ping successfull and removes that file if ping failes 3 times. With this you can add the following to the newt docker compose to do the health check: healthcheck: test: ["CMD-SHELL", "test -f /tmp/healthy"] interval: 30s timeout: 10s retries: 3 --- main.go | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index fdece97..cb4eab4 100644 --- a/main.go +++ b/main.go @@ -127,42 +127,46 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) go func() { for { select { - case <-ticker.C: + case <-ticker.C: err := ping(tnet, serverIP) if err != nil { consecutiveFailures++ - logger.Warn("Periodic ping failed (%d consecutive failures): %v", - consecutiveFailures, err) + logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?") - - // Increase interval if we have consistent failures, with a maximum cap + // delete healthy file if failed 3 times + if consecutiveFailures >= 3 { + _ = os.Remove("/tmp/healthy") + } + // increase interval if it keeps failing if consecutiveFailures >= 3 && currentInterval < maxInterval { - // Increase by 50% each time, up to the maximum currentInterval = time.Duration(float64(currentInterval) * 1.5) if currentInterval > maxInterval { currentInterval = maxInterval } ticker.Reset(currentInterval) - logger.Info("Increased ping check interval to %v due to consecutive failures", - currentInterval) + logger.Info("Increased ping check interval to %v due to consecutive failures", currentInterval) } } else { - // On success, if we've backed off, gradually return to normal interval + // Write a healthy file if ping successfull + err := os.WriteFile("/tmp/healthy", []byte("ok"), 0644) + if err != nil { + logger.Warn("Failed to write health file: %v", err) + } + // Reset interval if we increased it if currentInterval > initialInterval { currentInterval = time.Duration(float64(currentInterval) * 0.8) if currentInterval < initialInterval { currentInterval = initialInterval } ticker.Reset(currentInterval) - logger.Info("Decreased ping check interval to %v after successful ping", - currentInterval) + logger.Info("Decreased ping check interval to %v after successful ping", currentInterval) } consecutiveFailures = 0 } - case <-stopChan: + case <-stopChan: logger.Info("Stopping ping check") return - } + } } }() } From a76e6c9637ddd28b169b2ca898ebe69abb53b7a9 Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Wed, 25 Jun 2025 19:43:27 +0200 Subject: [PATCH 70/87] added healthy check in main.go added healthy check in main.go extended the ping check that creates a /tmp/healthy file if ping successfull and removes that file if ping failes 3 times. With this you can add the following to the newt docker compose to do the health check: healthcheck: test: ["CMD-SHELL", "test -f /tmp/healthy"] interval: 30s timeout: 10s retries: 3 --- main.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index cb4eab4..e8b8ede 100644 --- a/main.go +++ b/main.go @@ -127,7 +127,7 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) go func() { for { select { - case <-ticker.C: + case <-ticker.C: err := ping(tnet, serverIP) if err != nil { consecutiveFailures++ @@ -137,8 +137,9 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) if consecutiveFailures >= 3 { _ = os.Remove("/tmp/healthy") } - // increase interval if it keeps failing + // Increase interval if we have consistent failures, with a maximum cap if consecutiveFailures >= 3 && currentInterval < maxInterval { + // Increase by 50% each time, up to the maximum currentInterval = time.Duration(float64(currentInterval) * 1.5) if currentInterval > maxInterval { currentInterval = maxInterval @@ -152,7 +153,7 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) if err != nil { logger.Warn("Failed to write health file: %v", err) } - // Reset interval if we increased it + // On success, if we've backed off, gradually return to normal interval if currentInterval > initialInterval { currentInterval = time.Duration(float64(currentInterval) * 0.8) if currentInterval < initialInterval { @@ -163,9 +164,9 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) } consecutiveFailures = 0 } - case <-stopChan: + case <-stopChan: logger.Info("Stopping ping check") - return + return } } }() From e357e7befbc9eddfe09e694b1fedc02406504c90 Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Mon, 30 Jun 2025 12:45:42 +0200 Subject: [PATCH 71/87] Update main.go Added cli and env function --- main.go | 403 ++++---------------------------------------------------- 1 file changed, 27 insertions(+), 376 deletions(-) diff --git a/main.go b/main.go index e8b8ede..116e6e5 100644 --- a/main.go +++ b/main.go @@ -50,16 +50,11 @@ type TargetData struct { } func fixKey(key string) string { - // Remove any whitespace key = strings.TrimSpace(key) - - // Decode from base64 decoded, err := base64.StdEncoding.DecodeString(key) if err != nil { logger.Fatal("Error decoding base64: %v", err) } - - // Convert to hex return hex.EncodeToString(decoded) } @@ -115,7 +110,8 @@ func ping(tnet *netstack.Net, dst string) error { return nil } -func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) { +// --- CHANGED: added healthFile as parameter --- +func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}, healthFile string) { initialInterval := 10 * time.Second maxInterval := 60 * time.Second currentInterval := initialInterval @@ -133,13 +129,12 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) consecutiveFailures++ logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?") - // delete healthy file if failed 3 times - if consecutiveFailures >= 3 { - _ = os.Remove("/tmp/healthy") + // --- CHANGED: Only remove file if healthFile is set --- + if consecutiveFailures >= 3 && healthFile != "" { + _ = os.Remove(healthFile) } // Increase interval if we have consistent failures, with a maximum cap if consecutiveFailures >= 3 && currentInterval < maxInterval { - // Increase by 50% each time, up to the maximum currentInterval = time.Duration(float64(currentInterval) * 1.5) if currentInterval > maxInterval { currentInterval = maxInterval @@ -148,10 +143,12 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) logger.Info("Increased ping check interval to %v due to consecutive failures", currentInterval) } } else { - // Write a healthy file if ping successfull - err := os.WriteFile("/tmp/healthy", []byte("ok"), 0644) - if err != nil { - logger.Warn("Failed to write health file: %v", err) + // --- CHANGED: Only write file if healthFile is set --- + if healthFile != "" { + err := os.WriteFile(healthFile, []byte("ok"), 0644) + if err != nil { + logger.Warn("Failed to write health file: %v", err) + } } // On success, if we've backed off, gradually return to normal interval if currentInterval > initialInterval { @@ -166,13 +163,12 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) } case <-stopChan: logger.Info("Stopping ping check") - return - } + return + } } }() } -// Function to track connection status and trigger reconnection as needed func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websocket.Client) { const checkInterval = 30 * time.Second connectionLost := false @@ -182,27 +178,18 @@ func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websoc for { select { case <-ticker.C: - // Try a ping to see if connection is alive err := ping(tnet, serverIP) - if err != nil && !connectionLost { - // We just lost connection connectionLost = true logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") - - // Notify the user they might need to check their network logger.Warn("Please check your internet connection and ensure the Pangolin server is online.") logger.Warn("Newt will continue reconnection attempts automatically when connectivity is restored.") } else if err == nil && connectionLost { - // Connection has been restored connectionLost = false logger.Info("Connection to server restored!") - - // Tell the server we're back err := client.SendMessage("newt/wg/register", map[string]interface{}{ "publicKey": privateKey.PublicKey().String(), }) - if err != nil { logger.Error("Failed to send registration message after reconnection: %v", err) } else { @@ -217,32 +204,25 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { const ( initialMaxAttempts = 15 initialRetryDelay = 2 * time.Second - maxRetryDelay = 60 * time.Second // Cap the maximum delay + maxRetryDelay = 60 * time.Second ) attempt := 1 retryDelay := initialRetryDelay - // First try with the initial parameters logger.Info("Ping attempt %d", attempt) if err := ping(tnet, dst); err == nil { - // Successful ping return nil } else { logger.Warn("Ping attempt %d failed: %v", attempt, err) } - // Start a goroutine that will attempt pings indefinitely with increasing delays go func() { - attempt = 2 // Continue from attempt 2 - + attempt = 2 for { logger.Info("Ping attempt %d", attempt) - if err := ping(tnet, dst); err != nil { logger.Warn("Ping attempt %d failed: %v", attempt, err) - - // Increase delay after certain thresholds but cap it if attempt%5 == 0 && retryDelay < maxRetryDelay { retryDelay = time.Duration(float64(retryDelay) * 1.5) if retryDelay > maxRetryDelay { @@ -250,18 +230,14 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { } logger.Info("Increasing ping retry delay to %v", retryDelay) } - time.Sleep(retryDelay) attempt++ } else { - // Successful ping logger.Info("Ping succeeded after %d attempts", attempt) return } } }() - - // Return an error for the first batch of attempts (to maintain compatibility with existing code) return fmt.Errorf("initial ping attempts failed, continuing in background") } @@ -278,7 +254,7 @@ func parseLogLevel(level string) logger.LogLevel { case "FATAL": return logger.FATAL default: - return logger.INFO // default to INFO if invalid level provided + return logger.INFO } } @@ -286,8 +262,6 @@ func mapToWireGuardLogLevel(level logger.LogLevel) int { switch level { case logger.DEBUG: return device.LogLevelVerbose - // case logger.INFO: - // return device.LogLevel case logger.WARN: return device.LogLevelError case logger.ERROR, logger.FATAL: @@ -298,32 +272,23 @@ func mapToWireGuardLogLevel(level logger.LogLevel) int { } func resolveDomain(domain string) (string, error) { - // Check if there's a port in the domain host, port, err := net.SplitHostPort(domain) if err != nil { - // No port found, use the domain as is host = domain port = "" } - - // Remove any protocol prefix if present if strings.HasPrefix(host, "http://") { host = strings.TrimPrefix(host, "http://") } else if strings.HasPrefix(host, "https://") { host = strings.TrimPrefix(host, "https://") } - - // Lookup IP addresses ips, err := net.LookupIP(host) if err != nil { return "", fmt.Errorf("DNS lookup failed: %v", err) } - if len(ips) == 0 { return "", fmt.Errorf("no IP addresses found for domain %s", host) } - - // Get the first IPv4 address if available var ipAddr string for _, ip := range ips { if ipv4 := ip.To4(); ipv4 != nil { @@ -331,20 +296,16 @@ func resolveDomain(domain string) (string, error) { break } } - - // If no IPv4 found, use the first IP (might be IPv6) if ipAddr == "" { ipAddr = ips[0].String() } - - // Add port back if it existed if port != "" { ipAddr = net.JoinHostPort(ipAddr, port) } - return ipAddr, nil } +// --- ADDED: healthFile variable --- var ( endpoint string id string @@ -358,10 +319,10 @@ var ( updownScript string tlsPrivateKey string dockerSocket string + healthFile string // NEW ) func main() { - // if PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET are set as environment variables, they will be used as default values endpoint = os.Getenv("PANGOLIN_ENDPOINT") id = os.Getenv("NEWT_ID") secret = os.Getenv("NEWT_SECRET") @@ -371,6 +332,7 @@ func main() { updownScript = os.Getenv("UPDOWN_SCRIPT") tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") + healthFile = os.Getenv("HEALTH_FILE") // NEW if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -399,10 +361,12 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } + // --- ADDED: CLI flag for healthFile if not set by env --- + if healthFile == "" { + flag.StringVar(&healthFile, "health-file", "", "Path to health file (if unset, health file won’t be written)") + } - // do a --version check version := flag.Bool("version", false, "Print the version") - flag.Parse() newtVersion := "Newt version replaceme" @@ -417,7 +381,6 @@ func main() { loggerLevel := parseLogLevel(logLevel) logger.GetLogger().SetLevel(parseLogLevel(logLevel)) - // parse the mtu string into an int mtuInt, err = strconv.Atoi(mtu) if err != nil { logger.Fatal("Failed to parse MTU: %v", err) @@ -431,18 +394,13 @@ func main() { if tlsPrivateKey != "" { opt = websocket.WithTLSConfig(tlsPrivateKey) } - // Create a new client client, err := websocket.NewClient( - id, // CLI arg takes precedence - secret, // CLI arg takes precedence - endpoint, - opt, + id, secret, endpoint, opt, ) if err != nil { logger.Fatal("Failed to create client: %v", err) } - // Create TUN device and network stack var tun tun.Device var tnet *netstack.Net var dev *device.Device @@ -464,14 +422,12 @@ func main() { pingStopChan := make(chan struct{}) defer close(pingStopChan) - // Register handlers for different message types client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) { logger.Info("Received registration message") if connected { logger.Info("Already connected! But I will send a ping anyway...") - // Even if pingWithRetry returns an error, it will continue trying in the background - _ = pingWithRetry(tnet, wgData.ServerIP) // Ignoring initial error as pings will continue + _ = pingWithRetry(tnet, wgData.ServerIP) return } @@ -495,7 +451,6 @@ func main() { logger.Error("Failed to create TUN device: %v", err) } - // Create WireGuard device dev = device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger( mapToWireGuardLogLevel(loggerLevel), "wireguard: ", @@ -507,7 +462,6 @@ func main() { return } - // Configure WireGuard config := fmt.Sprintf(`private_key=%s public_key=%s allowed_ip=%s/32 @@ -519,7 +473,6 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Error("Failed to configure WireGuard device: %v", err) } - // Bring up the device err = dev.Up() if err != nil { logger.Error("Failed to bring up WireGuard device: %v", err) @@ -527,29 +480,21 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("WireGuard device created. Lets ping the server now...") - // Even if pingWithRetry returns an error, it will continue trying in the background _ = pingWithRetry(tnet, wgData.ServerIP) - // Always mark as connected and start the proxy manager regardless of initial ping result - // as the pings will continue in the background if !connected { logger.Info("Starting ping check") - startPingCheck(tnet, wgData.ServerIP, pingStopChan) - - // Start connection monitoring in a separate goroutine + // --- CHANGED: Pass healthFile to startPingCheck --- + startPingCheck(tnet, wgData.ServerIP, pingStopChan, healthFile) go monitorConnectionStatus(tnet, wgData.ServerIP, client) } - // Create proxy manager pm = proxy.NewProxyManager(tnet) - connected = true - // add the targets if there are any if len(wgData.Targets.TCP) > 0 { updateTargets(pm, "add", wgData.TunnelIP, "tcp", TargetData{Targets: wgData.Targets.TCP}) } - if len(wgData.Targets.UDP) > 0 { updateTargets(pm, "add", wgData.TunnelIP, "udp", TargetData{Targets: wgData.Targets.UDP}) } @@ -560,298 +505,4 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) - client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) { - logger.Info("Received: %+v", msg) - - // if there is no wgData or pm, we can't add targets - if wgData.TunnelIP == "" || pm == nil { - logger.Info("No tunnel IP or proxy manager available") - return - } - - targetData, err := parseTargetData(msg.Data) - if err != nil { - logger.Info("Error parsing target data: %v", err) - return - } - - if len(targetData.Targets) > 0 { - updateTargets(pm, "add", wgData.TunnelIP, "tcp", targetData) - } - }) - - client.RegisterHandler("newt/udp/add", func(msg websocket.WSMessage) { - logger.Info("Received: %+v", msg) - - // if there is no wgData or pm, we can't add targets - if wgData.TunnelIP == "" || pm == nil { - logger.Info("No tunnel IP or proxy manager available") - return - } - - targetData, err := parseTargetData(msg.Data) - if err != nil { - logger.Info("Error parsing target data: %v", err) - return - } - - if len(targetData.Targets) > 0 { - updateTargets(pm, "add", wgData.TunnelIP, "udp", targetData) - } - }) - - client.RegisterHandler("newt/udp/remove", func(msg websocket.WSMessage) { - logger.Info("Received: %+v", msg) - - // if there is no wgData or pm, we can't add targets - if wgData.TunnelIP == "" || pm == nil { - logger.Info("No tunnel IP or proxy manager available") - return - } - - targetData, err := parseTargetData(msg.Data) - if err != nil { - logger.Info("Error parsing target data: %v", err) - return - } - - if len(targetData.Targets) > 0 { - updateTargets(pm, "remove", wgData.TunnelIP, "udp", targetData) - } - }) - - client.RegisterHandler("newt/tcp/remove", func(msg websocket.WSMessage) { - logger.Info("Received: %+v", msg) - - // if there is no wgData or pm, we can't add targets - if wgData.TunnelIP == "" || pm == nil { - logger.Info("No tunnel IP or proxy manager available") - return - } - - targetData, err := parseTargetData(msg.Data) - if err != nil { - logger.Info("Error parsing target data: %v", err) - return - } - - if len(targetData.Targets) > 0 { - updateTargets(pm, "remove", wgData.TunnelIP, "tcp", targetData) - } - }) - - // Register handler for Docker socket check - client.RegisterHandler("newt/socket/check", func(msg websocket.WSMessage) { - logger.Info("Received Docker socket check request") - - if dockerSocket == "" { - logger.Info("Docker socket path is not set") - err := client.SendMessage("newt/socket/status", map[string]interface{}{ - "available": false, - "socketPath": dockerSocket, - }) - if err != nil { - logger.Error("Failed to send Docker socket check response: %v", err) - } - return - } - - // Check if Docker socket is available - isAvailable := docker.CheckSocket(dockerSocket) - - // Send response back to server - err := client.SendMessage("newt/socket/status", map[string]interface{}{ - "available": isAvailable, - "socketPath": dockerSocket, - }) - if err != nil { - logger.Error("Failed to send Docker socket check response: %v", err) - } else { - logger.Info("Docker socket check response sent: available=%t", isAvailable) - } - }) - - // Register handler for Docker container listing - client.RegisterHandler("newt/socket/fetch", func(msg websocket.WSMessage) { - logger.Info("Received Docker container fetch request") - - if dockerSocket == "" { - logger.Info("Docker socket path is not set") - return - } - - // List Docker containers - containers, err := docker.ListContainers(dockerSocket) - if err != nil { - logger.Error("Failed to list Docker containers: %v", err) - return - } - - // Send container list back to server - err = client.SendMessage("newt/socket/containers", map[string]interface{}{ - "containers": containers, - }) - if err != nil { - logger.Error("Failed to send Docker container list: %v", err) - } else { - logger.Info("Docker container list sent, count: %d", len(containers)) - } - }) - - client.OnConnect(func() error { - publicKey := privateKey.PublicKey() - logger.Debug("Public key: %s", publicKey) - - err := client.SendMessage("newt/wg/register", map[string]interface{}{ - "publicKey": publicKey.String(), - }) - if err != nil { - logger.Error("Failed to send registration message: %v", err) - return err - } - - logger.Info("Sent registration message") - return nil - }) - - // Connect to the WebSocket server - if err := client.Connect(); err != nil { - logger.Fatal("Failed to connect to server: %v", err) - } - defer client.Close() - - // Wait for interrupt signal - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - sigReceived := <-sigCh - - // Cleanup - logger.Info("Received %s signal, stopping", sigReceived.String()) - if dev != nil { - dev.Close() - } -} - -func parseTargetData(data interface{}) (TargetData, error) { - var targetData TargetData - jsonData, err := json.Marshal(data) - if err != nil { - logger.Info("Error marshaling data: %v", err) - return targetData, err - } - - if err := json.Unmarshal(jsonData, &targetData); err != nil { - logger.Info("Error unmarshaling target data: %v", err) - return targetData, err - } - return targetData, nil -} - -func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto string, targetData TargetData) error { - for _, t := range targetData.Targets { - // Split the first number off of the target with : separator and use as the port - parts := strings.Split(t, ":") - if len(parts) != 3 { - logger.Info("Invalid target format: %s", t) - continue - } - - // Get the port as an int - port := 0 - _, err := fmt.Sscanf(parts[0], "%d", &port) - if err != nil { - logger.Info("Invalid port: %s", parts[0]) - continue - } - - if action == "add" { - target := parts[1] + ":" + parts[2] - - // Call updown script if provided - processedTarget := target - if updownScript != "" { - newTarget, err := executeUpdownScript(action, proto, target) - if err != nil { - logger.Warn("Updown script error: %v", err) - } else if newTarget != "" { - processedTarget = newTarget - } - } - - // Only remove the specific target if it exists - err := pm.RemoveTarget(proto, tunnelIP, port) - if err != nil { - // Ignore "target not found" errors as this is expected for new targets - if !strings.Contains(err.Error(), "target not found") { - logger.Error("Failed to remove existing target: %v", err) - } - } - - // Add the new target - pm.AddTarget(proto, tunnelIP, port, processedTarget) - - } else if action == "remove" { - logger.Info("Removing target with port %d", port) - - target := parts[1] + ":" + parts[2] - - // Call updown script if provided - if updownScript != "" { - _, err := executeUpdownScript(action, proto, target) - if err != nil { - logger.Warn("Updown script error: %v", err) - } - } - - err := pm.RemoveTarget(proto, tunnelIP, port) - if err != nil { - logger.Error("Failed to remove target: %v", err) - return err - } - } - } - - return nil -} - -func executeUpdownScript(action, proto, target string) (string, error) { - if updownScript == "" { - return target, nil - } - - // Split the updownScript in case it contains spaces (like "/usr/bin/python3 script.py") - parts := strings.Fields(updownScript) - if len(parts) == 0 { - return target, fmt.Errorf("invalid updown script command") - } - - var cmd *exec.Cmd - if len(parts) == 1 { - // If it's a single executable - logger.Info("Executing updown script: %s %s %s %s", updownScript, action, proto, target) - cmd = exec.Command(parts[0], action, proto, target) - } else { - // If it includes interpreter and script - args := append(parts[1:], action, proto, target) - logger.Info("Executing updown script: %s %s %s %s %s", parts[0], strings.Join(parts[1:], " "), action, proto, target) - cmd = exec.Command(parts[0], args...) - } - - output, err := cmd.Output() - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - return "", fmt.Errorf("updown script execution failed (exit code %d): %s", - exitErr.ExitCode(), string(exitErr.Stderr)) - } - return "", fmt.Errorf("updown script execution failed: %v", err) - } - - // If the script returns a new target, use it - newTarget := strings.TrimSpace(string(output)) - if newTarget != "" { - logger.Info("Updown script returned new target: %s", newTarget) - return newTarget, nil - } - - return target, nil } From 700287163e8a86d01915d202ead49862ab8f023e Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Mon, 30 Jun 2025 12:50:33 +0200 Subject: [PATCH 72/87] Update main.go --- main.go | 295 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) diff --git a/main.go b/main.go index 116e6e5..ef85099 100644 --- a/main.go +++ b/main.go @@ -505,4 +505,299 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) +} + client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) { + logger.Info("Received: %+v", msg) + + // if there is no wgData or pm, we can't add targets + if wgData.TunnelIP == "" || pm == nil { + logger.Info("No tunnel IP or proxy manager available") + return + } + + targetData, err := parseTargetData(msg.Data) + if err != nil { + logger.Info("Error parsing target data: %v", err) + return + } + + if len(targetData.Targets) > 0 { + updateTargets(pm, "add", wgData.TunnelIP, "tcp", targetData) + } + }) + + client.RegisterHandler("newt/udp/add", func(msg websocket.WSMessage) { + logger.Info("Received: %+v", msg) + + // if there is no wgData or pm, we can't add targets + if wgData.TunnelIP == "" || pm == nil { + logger.Info("No tunnel IP or proxy manager available") + return + } + + targetData, err := parseTargetData(msg.Data) + if err != nil { + logger.Info("Error parsing target data: %v", err) + return + } + + if len(targetData.Targets) > 0 { + updateTargets(pm, "add", wgData.TunnelIP, "udp", targetData) + } + }) + + client.RegisterHandler("newt/udp/remove", func(msg websocket.WSMessage) { + logger.Info("Received: %+v", msg) + + // if there is no wgData or pm, we can't add targets + if wgData.TunnelIP == "" || pm == nil { + logger.Info("No tunnel IP or proxy manager available") + return + } + + targetData, err := parseTargetData(msg.Data) + if err != nil { + logger.Info("Error parsing target data: %v", err) + return + } + + if len(targetData.Targets) > 0 { + updateTargets(pm, "remove", wgData.TunnelIP, "udp", targetData) + } + }) + + client.RegisterHandler("newt/tcp/remove", func(msg websocket.WSMessage) { + logger.Info("Received: %+v", msg) + + // if there is no wgData or pm, we can't add targets + if wgData.TunnelIP == "" || pm == nil { + logger.Info("No tunnel IP or proxy manager available") + return + } + + targetData, err := parseTargetData(msg.Data) + if err != nil { + logger.Info("Error parsing target data: %v", err) + return + } + + if len(targetData.Targets) > 0 { + updateTargets(pm, "remove", wgData.TunnelIP, "tcp", targetData) + } + }) + + // Register handler for Docker socket check + client.RegisterHandler("newt/socket/check", func(msg websocket.WSMessage) { + logger.Info("Received Docker socket check request") + + if dockerSocket == "" { + logger.Info("Docker socket path is not set") + err := client.SendMessage("newt/socket/status", map[string]interface{}{ + "available": false, + "socketPath": dockerSocket, + }) + if err != nil { + logger.Error("Failed to send Docker socket check response: %v", err) + } + return + } + + // Check if Docker socket is available + isAvailable := docker.CheckSocket(dockerSocket) + + // Send response back to server + err := client.SendMessage("newt/socket/status", map[string]interface{}{ + "available": isAvailable, + "socketPath": dockerSocket, + }) + if err != nil { + logger.Error("Failed to send Docker socket check response: %v", err) + } else { + logger.Info("Docker socket check response sent: available=%t", isAvailable) + } + }) + + // Register handler for Docker container listing + client.RegisterHandler("newt/socket/fetch", func(msg websocket.WSMessage) { + logger.Info("Received Docker container fetch request") + + if dockerSocket == "" { + logger.Info("Docker socket path is not set") + return + } + + // List Docker containers + containers, err := docker.ListContainers(dockerSocket) + if err != nil { + logger.Error("Failed to list Docker containers: %v", err) + return + } + + // Send container list back to server + err = client.SendMessage("newt/socket/containers", map[string]interface{}{ + "containers": containers, + }) + if err != nil { + logger.Error("Failed to send Docker container list: %v", err) + } else { + logger.Info("Docker container list sent, count: %d", len(containers)) + } + }) + + client.OnConnect(func() error { + publicKey := privateKey.PublicKey() + logger.Debug("Public key: %s", publicKey) + + err := client.SendMessage("newt/wg/register", map[string]interface{}{ + "publicKey": publicKey.String(), + }) + if err != nil { + logger.Error("Failed to send registration message: %v", err) + return err + } + + logger.Info("Sent registration message") + return nil + }) + + // Connect to the WebSocket server + if err := client.Connect(); err != nil { + logger.Fatal("Failed to connect to server: %v", err) + } + defer client.Close() + + // Wait for interrupt signal + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + sigReceived := <-sigCh + + // Cleanup + logger.Info("Received %s signal, stopping", sigReceived.String()) + if dev != nil { + dev.Close() + } +} + +func parseTargetData(data interface{}) (TargetData, error) { + var targetData TargetData + jsonData, err := json.Marshal(data) + if err != nil { + logger.Info("Error marshaling data: %v", err) + return targetData, err + } + + if err := json.Unmarshal(jsonData, &targetData); err != nil { + logger.Info("Error unmarshaling target data: %v", err) + return targetData, err + } + return targetData, nil +} + +func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto string, targetData TargetData) error { + for _, t := range targetData.Targets { + // Split the first number off of the target with : separator and use as the port + parts := strings.Split(t, ":") + if len(parts) != 3 { + logger.Info("Invalid target format: %s", t) + continue + } + + // Get the port as an int + port := 0 + _, err := fmt.Sscanf(parts[0], "%d", &port) + if err != nil { + logger.Info("Invalid port: %s", parts[0]) + continue + } + + if action == "add" { + target := parts[1] + ":" + parts[2] + + // Call updown script if provided + processedTarget := target + if updownScript != "" { + newTarget, err := executeUpdownScript(action, proto, target) + if err != nil { + logger.Warn("Updown script error: %v", err) + } else if newTarget != "" { + processedTarget = newTarget + } + } + + // Only remove the specific target if it exists + err := pm.RemoveTarget(proto, tunnelIP, port) + if err != nil { + // Ignore "target not found" errors as this is expected for new targets + if !strings.Contains(err.Error(), "target not found") { + logger.Error("Failed to remove existing target: %v", err) + } + } + + // Add the new target + pm.AddTarget(proto, tunnelIP, port, processedTarget) + + } else if action == "remove" { + logger.Info("Removing target with port %d", port) + + target := parts[1] + ":" + parts[2] + + // Call updown script if provided + if updownScript != "" { + _, err := executeUpdownScript(action, proto, target) + if err != nil { + logger.Warn("Updown script error: %v", err) + } + } + + err := pm.RemoveTarget(proto, tunnelIP, port) + if err != nil { + logger.Error("Failed to remove target: %v", err) + return err + } + } + } + + return nil +} + +func executeUpdownScript(action, proto, target string) (string, error) { + if updownScript == "" { + return target, nil + } + + // Split the updownScript in case it contains spaces (like "/usr/bin/python3 script.py") + parts := strings.Fields(updownScript) + if len(parts) == 0 { + return target, fmt.Errorf("invalid updown script command") + } + + var cmd *exec.Cmd + if len(parts) == 1 { + // If it's a single executable + logger.Info("Executing updown script: %s %s %s %s", updownScript, action, proto, target) + cmd = exec.Command(parts[0], action, proto, target) + } else { + // If it includes interpreter and script + args := append(parts[1:], action, proto, target) + logger.Info("Executing updown script: %s %s %s %s %s", parts[0], strings.Join(parts[1:], " "), action, proto, target) + cmd = exec.Command(parts[0], args...) + } + + output, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return "", fmt.Errorf("updown script execution failed (exit code %d): %s", + exitErr.ExitCode(), string(exitErr.Stderr)) + } + return "", fmt.Errorf("updown script execution failed: %v", err) + } + + // If the script returns a new target, use it + newTarget := strings.TrimSpace(string(output)) + if newTarget != "" { + logger.Info("Updown script returned new target: %s", newTarget) + return newTarget, nil + } + + return target, nil } From 9db3b78373ed3991eeeadbb980cbf773648d239a Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Mon, 30 Jun 2025 13:03:06 +0200 Subject: [PATCH 73/87] Update main.go fixed some errors, This file should be ok. --- main.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 15 deletions(-) diff --git a/main.go b/main.go index ef85099..238d576 100644 --- a/main.go +++ b/main.go @@ -50,11 +50,16 @@ type TargetData struct { } func fixKey(key string) string { + // Remove any whitespace key = strings.TrimSpace(key) + + // Decode from base64 decoded, err := base64.StdEncoding.DecodeString(key) if err != nil { logger.Fatal("Error decoding base64: %v", err) } + + // Convert to hex return hex.EncodeToString(decoded) } @@ -110,7 +115,6 @@ func ping(tnet *netstack.Net, dst string) error { return nil } -// --- CHANGED: added healthFile as parameter --- func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}, healthFile string) { initialInterval := 10 * time.Second maxInterval := 60 * time.Second @@ -127,23 +131,28 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}, err := ping(tnet, serverIP) if err != nil { consecutiveFailures++ - logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) + logger.Warn("Periodic ping failed (%d consecutive failures): %v", + consecutiveFailures, err) logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?") - // --- CHANGED: Only remove file if healthFile is set --- + + // Only remove file if healthFile is set if consecutiveFailures >= 3 && healthFile != "" { _ = os.Remove(healthFile) } + // Increase interval if we have consistent failures, with a maximum cap if consecutiveFailures >= 3 && currentInterval < maxInterval { + // Increase by 50% each time, up to the maximum currentInterval = time.Duration(float64(currentInterval) * 1.5) if currentInterval > maxInterval { currentInterval = maxInterval } ticker.Reset(currentInterval) - logger.Info("Increased ping check interval to %v due to consecutive failures", currentInterval) + logger.Info("Increased ping check interval to %v due to consecutive failures", + currentInterval) } } else { - // --- CHANGED: Only write file if healthFile is set --- + // Only write file if healthFile is set if healthFile != "" { err := os.WriteFile(healthFile, []byte("ok"), 0644) if err != nil { @@ -157,7 +166,8 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}, currentInterval = initialInterval } ticker.Reset(currentInterval) - logger.Info("Decreased ping check interval to %v after successful ping", currentInterval) + logger.Info("Decreased ping check interval to %v after successful ping", + currentInterval) } consecutiveFailures = 0 } @@ -169,6 +179,7 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}, }() } +// Function to track connection status and trigger reconnection as needed func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websocket.Client) { const checkInterval = 30 * time.Second connectionLost := false @@ -178,18 +189,27 @@ func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websoc for { select { case <-ticker.C: + // Try a ping to see if connection is alive err := ping(tnet, serverIP) + if err != nil && !connectionLost { + // We just lost connection connectionLost = true logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") + + // Notify the user they might need to check their network logger.Warn("Please check your internet connection and ensure the Pangolin server is online.") logger.Warn("Newt will continue reconnection attempts automatically when connectivity is restored.") } else if err == nil && connectionLost { + // Connection has been restored connectionLost = false logger.Info("Connection to server restored!") + + // Tell the server we're back err := client.SendMessage("newt/wg/register", map[string]interface{}{ "publicKey": privateKey.PublicKey().String(), }) + if err != nil { logger.Error("Failed to send registration message after reconnection: %v", err) } else { @@ -204,25 +224,32 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { const ( initialMaxAttempts = 15 initialRetryDelay = 2 * time.Second - maxRetryDelay = 60 * time.Second + maxRetryDelay = 60 * time.Second // Cap the maximum delay ) attempt := 1 retryDelay := initialRetryDelay + // First try with the initial parameters logger.Info("Ping attempt %d", attempt) if err := ping(tnet, dst); err == nil { + // Successful ping return nil } else { logger.Warn("Ping attempt %d failed: %v", attempt, err) } + // Start a goroutine that will attempt pings indefinitely with increasing delays go func() { - attempt = 2 + attempt = 2 // Continue from attempt 2 + for { logger.Info("Ping attempt %d", attempt) + if err := ping(tnet, dst); err != nil { logger.Warn("Ping attempt %d failed: %v", attempt, err) + + // Increase delay after certain thresholds but cap it if attempt%5 == 0 && retryDelay < maxRetryDelay { retryDelay = time.Duration(float64(retryDelay) * 1.5) if retryDelay > maxRetryDelay { @@ -230,14 +257,18 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { } logger.Info("Increasing ping retry delay to %v", retryDelay) } + time.Sleep(retryDelay) attempt++ } else { + // Successful ping logger.Info("Ping succeeded after %d attempts", attempt) return } } }() + + // Return an error for the first batch of attempts (to maintain compatibility with existing code) return fmt.Errorf("initial ping attempts failed, continuing in background") } @@ -254,7 +285,7 @@ func parseLogLevel(level string) logger.LogLevel { case "FATAL": return logger.FATAL default: - return logger.INFO + return logger.INFO // default to INFO if invalid level provided } } @@ -262,6 +293,8 @@ func mapToWireGuardLogLevel(level logger.LogLevel) int { switch level { case logger.DEBUG: return device.LogLevelVerbose + // case logger.INFO: + // return device.LogLevel case logger.WARN: return device.LogLevelError case logger.ERROR, logger.FATAL: @@ -272,23 +305,32 @@ func mapToWireGuardLogLevel(level logger.LogLevel) int { } func resolveDomain(domain string) (string, error) { + // Check if there's a port in the domain host, port, err := net.SplitHostPort(domain) if err != nil { + // No port found, use the domain as is host = domain port = "" } + + // Remove any protocol prefix if present if strings.HasPrefix(host, "http://") { host = strings.TrimPrefix(host, "http://") } else if strings.HasPrefix(host, "https://") { host = strings.TrimPrefix(host, "https://") } + + // Lookup IP addresses ips, err := net.LookupIP(host) if err != nil { return "", fmt.Errorf("DNS lookup failed: %v", err) } + if len(ips) == 0 { return "", fmt.Errorf("no IP addresses found for domain %s", host) } + + // Get the first IPv4 address if available var ipAddr string for _, ip := range ips { if ipv4 := ip.To4(); ipv4 != nil { @@ -296,16 +338,20 @@ func resolveDomain(domain string) (string, error) { break } } + + // If no IPv4 found, use the first IP (might be IPv6) if ipAddr == "" { ipAddr = ips[0].String() } + + // Add port back if it existed if port != "" { ipAddr = net.JoinHostPort(ipAddr, port) } + return ipAddr, nil } -// --- ADDED: healthFile variable --- var ( endpoint string id string @@ -323,6 +369,7 @@ var ( ) func main() { + // if PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET are set as environment variables, they will be used as default values endpoint = os.Getenv("PANGOLIN_ENDPOINT") id = os.Getenv("NEWT_ID") secret = os.Getenv("NEWT_SECRET") @@ -361,12 +408,14 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } - // --- ADDED: CLI flag for healthFile if not set by env --- + // CLI flag for healthFile if not set by env if healthFile == "" { flag.StringVar(&healthFile, "health-file", "", "Path to health file (if unset, health file won’t be written)") } + // do a --version check version := flag.Bool("version", false, "Print the version") + flag.Parse() newtVersion := "Newt version replaceme" @@ -381,6 +430,7 @@ func main() { loggerLevel := parseLogLevel(logLevel) logger.GetLogger().SetLevel(parseLogLevel(logLevel)) + // parse the mtu string into an int mtuInt, err = strconv.Atoi(mtu) if err != nil { logger.Fatal("Failed to parse MTU: %v", err) @@ -394,13 +444,18 @@ func main() { if tlsPrivateKey != "" { opt = websocket.WithTLSConfig(tlsPrivateKey) } + // Create a new client client, err := websocket.NewClient( - id, secret, endpoint, opt, + id, // CLI arg takes precedence + secret, // CLI arg takes precedence + endpoint, + opt, ) if err != nil { logger.Fatal("Failed to create client: %v", err) } + // Create TUN device and network stack var tun tun.Device var tnet *netstack.Net var dev *device.Device @@ -422,12 +477,14 @@ func main() { pingStopChan := make(chan struct{}) defer close(pingStopChan) + // Register handlers for different message types client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) { logger.Info("Received registration message") if connected { logger.Info("Already connected! But I will send a ping anyway...") - _ = pingWithRetry(tnet, wgData.ServerIP) + // Even if pingWithRetry returns an error, it will continue trying in the background + _ = pingWithRetry(tnet, wgData.ServerIP) // Ignoring initial error as pings will continue return } @@ -451,6 +508,7 @@ func main() { logger.Error("Failed to create TUN device: %v", err) } + // Create WireGuard device dev = device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger( mapToWireGuardLogLevel(loggerLevel), "wireguard: ", @@ -462,6 +520,7 @@ func main() { return } + // Configure WireGuard config := fmt.Sprintf(`private_key=%s public_key=%s allowed_ip=%s/32 @@ -473,6 +532,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Error("Failed to configure WireGuard device: %v", err) } + // Bring up the device err = dev.Up() if err != nil { logger.Error("Failed to bring up WireGuard device: %v", err) @@ -480,21 +540,29 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("WireGuard device created. Lets ping the server now...") + // Even if pingWithRetry returns an error, it will continue trying in the background _ = pingWithRetry(tnet, wgData.ServerIP) + // Always mark as connected and start the proxy manager regardless of initial ping result + // as the pings will continue in the background if !connected { logger.Info("Starting ping check") - // --- CHANGED: Pass healthFile to startPingCheck --- startPingCheck(tnet, wgData.ServerIP, pingStopChan, healthFile) + + // Start connection monitoring in a separate goroutine go monitorConnectionStatus(tnet, wgData.ServerIP, client) } + // Create proxy manager pm = proxy.NewProxyManager(tnet) + connected = true + // add the targets if there are any if len(wgData.Targets.TCP) > 0 { updateTargets(pm, "add", wgData.TunnelIP, "tcp", TargetData{Targets: wgData.Targets.TCP}) } + if len(wgData.Targets.UDP) > 0 { updateTargets(pm, "add", wgData.TunnelIP, "udp", TargetData{Targets: wgData.Targets.UDP}) } @@ -505,7 +573,6 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) -} client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) { logger.Info("Received: %+v", msg) From 071a51afbc26da00fd310155650195dcb0637ace Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Mon, 30 Jun 2025 13:09:11 +0200 Subject: [PATCH 74/87] Update main.go synced with dev --- main.go | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index 238d576..d1b3c3f 100644 --- a/main.go +++ b/main.go @@ -353,19 +353,21 @@ func resolveDomain(domain string) (string, error) { } var ( - endpoint string - id string - secret string - mtu string - mtuInt int - dns string - privateKey wgtypes.Key - err error - logLevel string - updownScript string - tlsPrivateKey string - dockerSocket string - healthFile string // NEW + endpoint string + id string + secret string + mtu string + mtuInt int + dns string + privateKey wgtypes.Key + err error + logLevel string + updownScript string + tlsPrivateKey string + dockerSocket string + dockerEnforceNetworkValidation string + dockerEnforceNetworkValidationBool bool + healthFile string // NEW ) func main() { @@ -379,6 +381,7 @@ func main() { updownScript = os.Getenv("UPDOWN_SCRIPT") tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") + dockerEnforceNetworkValidation = os.Getenv("DOCKER_ENFORCE_NETWORK_VALIDATION") healthFile = os.Getenv("HEALTH_FILE") // NEW if endpoint == "" { @@ -408,6 +411,9 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } + if dockerEnforceNetworkValidation == "" { + flag.StringVar(&dockerEnforceNetworkValidation, "docker-enforce-network-validation", "false", "Enforce validation of container on newt network (true or false)") + } // CLI flag for healthFile if not set by env if healthFile == "" { flag.StringVar(&healthFile, "health-file", "", "Path to health file (if unset, health file won’t be written)") From f4ed9b4d49bebd8a60ceb0068d7e5b959deb3a0a Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 30 Jun 2025 09:04:58 -0700 Subject: [PATCH 75/87] Remove binary mistake Former-commit-id: a39d0567257e9f0c3d4efb0c83467a82be775b06 --- .gitignore | 1 + newt_arm64.REMOVED.git-id | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 newt_arm64.REMOVED.git-id diff --git a/.gitignore b/.gitignore index bd45f8f..d14efa9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ nohup.out .idea *.iml certs/ +newt_arm64 diff --git a/newt_arm64.REMOVED.git-id b/newt_arm64.REMOVED.git-id deleted file mode 100644 index 67585b0..0000000 --- a/newt_arm64.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -e3dab3248cc8b7e23f9a99c817371495f93c1f5d \ No newline at end of file From 3d7e0c3abea097097c69f7ee8fecb46d4a55b356 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 30 Jun 2025 09:49:27 -0700 Subject: [PATCH 76/87] Comment out WIP clients flags Former-commit-id: ec8fc2043815dd295ea1f4c7146cc8a47bee6bad --- main.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index 20a7b04..0a4fdf3 100644 --- a/main.go +++ b/main.go @@ -105,10 +105,10 @@ func main() { dns = os.Getenv("DNS") logLevel = os.Getenv("LOG_LEVEL") updownScript = os.Getenv("UPDOWN_SCRIPT") - interfaceName = os.Getenv("INTERFACE") - generateAndSaveKeyTo = os.Getenv("GENERATE_AND_SAVE_KEY_TO") - rm = os.Getenv("RM") == "true" - acceptClients = os.Getenv("ACCEPT_CLIENTS") == "true" + // interfaceName = os.Getenv("INTERFACE") + // generateAndSaveKeyTo = os.Getenv("GENERATE_AND_SAVE_KEY_TO") + // rm = os.Getenv("RM") == "true" + // acceptClients = os.Getenv("ACCEPT_CLIENTS") == "true" tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") pingIntervalStr := os.Getenv("PING_INTERVAL") @@ -137,14 +137,14 @@ func main() { if updownScript == "" { flag.StringVar(&updownScript, "updown", "", "Path to updown script to be called when targets are added or removed") } - if interfaceName == "" { - flag.StringVar(&interfaceName, "interface", "wg1", "Name of the WireGuard interface") - } - if generateAndSaveKeyTo == "" { - flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "/tmp/newtkey", "Path to save generated private key") - } - flag.BoolVar(&rm, "rm", false, "Remove the WireGuard interface") - flag.BoolVar(&acceptClients, "accept-clients", false, "Accept clients on the WireGuard interface") + // if interfaceName == "" { + // flag.StringVar(&interfaceName, "interface", "wg1", "Name of the WireGuard interface") + // } + // if generateAndSaveKeyTo == "" { + // flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "/tmp/newtkey", "Path to save generated private key") + // } + // flag.BoolVar(&rm, "rm", false, "Remove the WireGuard interface") + // flag.BoolVar(&acceptClients, "accept-clients", false, "Accept clients on the WireGuard interface") if tlsPrivateKey == "" { flag.StringVar(&tlsPrivateKey, "tls-client-cert", "", "Path to client certificate used for mTLS") } From 38ea265bb7ce6c177cbec677d178e33ed411d2d4 Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Tue, 1 Jul 2025 17:38:54 +0200 Subject: [PATCH 77/87] Update go.mod Former-commit-id: a896291831150be4a9dd09c82d423cbc3a4cdbbb --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 7e2fa2d..c2a2171 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa golang.org/x/net v0.41.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20231211153847-12269c276173 gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 software.sslmate.com/src/go-pkcs12 v0.5.0 ) From 2b01c03a80673f4233b1078f0ac780315835bc0d Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 3 Jul 2025 09:05:09 -0700 Subject: [PATCH 78/87] Working packages? Former-commit-id: c2a326c70aa570496232d8f05129a9f5813dc740 --- go.mod | 4 ++-- go.sum | 67 ++++++++++++++++++++++++++++++---------------------------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index c2a2171..b1dcba5 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,14 @@ go 1.23.1 toolchain go1.23.2 require ( - github.com/google/gopacket v1.1.19 github.com/docker/docker v28.3.0+incompatible + github.com/google/gopacket v1.1.19 github.com/gorilla/websocket v1.5.3 github.com/vishvananda/netlink v1.3.0 golang.org/x/crypto v0.39.0 golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa golang.org/x/net v0.41.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - golang.zx2c4.com/wireguard/wgctrl v0.0.0-20231211153847-12269c276173 gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 software.sslmate.com/src/go-pkcs12 v0.5.0 ) @@ -56,4 +55,5 @@ require ( golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.30.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect ) diff --git a/go.sum b/go.sum index 8113aea..8025d04 100644 --- a/go.sum +++ b/go.sum @@ -1,91 +1,94 @@ -github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ= github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= -github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= -golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= -gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= -software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= From d1397c30fe926a84e7aee95418d6a554d80067d1 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 3 Jul 2025 09:06:26 -0700 Subject: [PATCH 79/87] Fix missing netstack pack Former-commit-id: a88d25f36986a476888388f9ec0e4648ed80edb3 --- go.mod | 7 ++--- go.sum | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index b1dcba5..07a3ce0 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,9 @@ require ( golang.org/x/crypto v0.39.0 golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa golang.org/x/net v0.41.0 - golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 + golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 + gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c software.sslmate.com/src/go-pkcs12 v0.5.0 ) @@ -21,7 +22,6 @@ require ( github.com/Microsoft/go-winio v0.6.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.4.0 // indirect @@ -55,5 +55,4 @@ require ( golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.30.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 // indirect ) diff --git a/go.sum b/go.sum index 8025d04..2b3bcca 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,47 @@ +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ= github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -26,33 +52,68 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= +go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -74,21 +135,41 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= -golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= +golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= +golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= -gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= +gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= +software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= From 465bf1bd37602389a322272414d6853f24606b56 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 3 Jul 2025 10:09:55 -0700 Subject: [PATCH 80/87] Make linux clients build correctly Former-commit-id: 61a9097bafc9ebf1b8f00d97c503d9b6228ff52a --- linux.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 58 ++++------------------------------------ stub.go | 32 +++++++++++++++++++++++ wg/wg.go | 2 ++ 4 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 linux.go create mode 100644 stub.go diff --git a/linux.go b/linux.go new file mode 100644 index 0000000..076e2f1 --- /dev/null +++ b/linux.go @@ -0,0 +1,80 @@ +//go:build linux + +package main + +import ( + "fmt" + "strings" + + "github.com/fosrl/newt/logger" + "github.com/fosrl/newt/proxy" + "github.com/fosrl/newt/websocket" + "github.com/fosrl/newt/wg" + "github.com/fosrl/newt/wgtester" +) + +var wgService *wg.WireGuardService +var wgTesterServer *wgtester.Server + +func setupClients(client *websocket.Client) { + var host = endpoint + if strings.HasPrefix(host, "http://") { + host = strings.TrimPrefix(host, "http://") + } else if strings.HasPrefix(host, "https://") { + host = strings.TrimPrefix(host, "https://") + } + + host = strings.TrimSuffix(host, "/") + + // Create WireGuard service + wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, generateAndSaveKeyTo, host, id, client) + if err != nil { + logger.Fatal("Failed to create WireGuard service: %v", err) + } + defer wgService.Close(rm) + + wgTesterServer = wgtester.NewServer("0.0.0.0", wgService.Port, id) // TODO: maybe make this the same ip of the wg server? + err := wgTesterServer.Start() + if err != nil { + logger.Error("Failed to start WireGuard tester server: %v", err) + } else { + // Make sure to stop the server on exit + defer wgTesterServer.Stop() + } + + client.OnTokenUpdate(func(token string) { + wgService.SetToken(token) + }) +} + +func closeClients() { + if wgService != nil { + wgService.Close(rm) + wgService = nil + } + + if wgTesterServer != nil { + wgTesterServer.Stop() + wgTesterServer = nil + } +} + +func clientsHandleNewtConnection(publicKey string) { + if wgService != nil { + wgService.SetServerPubKey(publicKey) + } else { + logger.Error("WireGuard service is not initialized, cannot set server public key") + } +} + +func clientsOnConnect() { + if wgService != nil { + wgService.LoadRemoteConfig() + } +} + +func clientsAddProxyTarget(pm *proxy.ProxyManager, tunnelIp string) { + // add a udp proxy for localost and the wgService port + // TODO: make sure this port is not used in a target + pm.AddTarget("udp", tunnelIp, int(wgService.Port), fmt.Sprintf("127.0.0.1:%d", wgService.Port)) +} diff --git a/main.go b/main.go index 0a4fdf3..9b517c7 100644 --- a/main.go +++ b/main.go @@ -19,8 +19,6 @@ import ( "github.com/fosrl/newt/proxy" "github.com/fosrl/newt/updates" "github.com/fosrl/newt/websocket" - "github.com/fosrl/newt/wg" - "github.com/fosrl/newt/wgtester" "golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/device" @@ -255,14 +253,12 @@ func main() { } // Create TUN device and network stack - var wgService *wg.WireGuardService var tun tun.Device var tnet *netstack.Net var dev *device.Device var pm *proxy.ProxyManager var connected bool var wgData WgData - var wgTesterServer *wgtester.Server if acceptClients { // make sure we are running on linux @@ -271,30 +267,7 @@ func main() { os.Exit(1) } - var host = endpoint - if strings.HasPrefix(host, "http://") { - host = strings.TrimPrefix(host, "http://") - } else if strings.HasPrefix(host, "https://") { - host = strings.TrimPrefix(host, "https://") - } - - host = strings.TrimSuffix(host, "/") - - // Create WireGuard service - wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, generateAndSaveKeyTo, host, id, client) - if err != nil { - logger.Fatal("Failed to create WireGuard service: %v", err) - } - defer wgService.Close(rm) - - wgTesterServer = wgtester.NewServer("0.0.0.0", wgService.Port, id) // TODO: maybe make this the same ip of the wg server? - err := wgTesterServer.Start() - if err != nil { - logger.Error("Failed to start WireGuard tester server: %v", err) - } else { - // Make sure to stop the server on exit - defer wgTesterServer.Stop() - } + setupClients(client) } var pingWithRetryStopChan chan struct{} @@ -349,9 +322,7 @@ func main() { return } - if wgService != nil { - wgService.SetServerPubKey(wgData.PublicKey) - } + clientsHandleNewtConnection(wgData.PublicKey) logger.Info("Received: %+v", msg) tun, tnet, err = netstack.CreateNetTUN( @@ -423,12 +394,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub updateTargets(pm, "add", wgData.TunnelIP, "udp", TargetData{Targets: wgData.Targets.UDP}) } - // first make sure the wpgService has a port - if wgService != nil { - // add a udp proxy for localost and the wgService port - // TODO: make sure this port is not used in a target - pm.AddTarget("udp", wgData.TunnelIP, int(wgService.Port), fmt.Sprintf("127.0.0.1:%d", wgService.Port)) - } + clientsAddProxyTarget(pm, wgData.TunnelIP) err = pm.Start() if err != nil { @@ -734,9 +700,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // request from the server the list of nodes to ping at newt/ping/request stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) - if wgService != nil { - wgService.LoadRemoteConfig() - } + clientsOnConnect() } // Send registration message to the server for backward compatibility @@ -755,12 +719,6 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub return nil }) - client.OnTokenUpdate(func(token string) { - if wgService != nil { - wgService.SetToken(token) - } - }) - // Connect to the WebSocket server if err := client.Connect(); err != nil { logger.Fatal("Failed to connect to server: %v", err) @@ -774,13 +732,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub dev.Close() - if wgService != nil { - wgService.Close(rm) - } - - if wgTesterServer != nil { - wgTesterServer.Stop() - } + closeClients() if pm != nil { pm.Stop() diff --git a/stub.go b/stub.go new file mode 100644 index 0000000..e2360ff --- /dev/null +++ b/stub.go @@ -0,0 +1,32 @@ +//go:build !linux + +package main + +import ( + "github.com/fosrl/newt/proxy" + "github.com/fosrl/newt/websocket" +) + +func setupClients(client *websocket.Client) { + return // This function is not implemented for non-Linux systems. +} + +func closeClients() { + // This function is not implemented for non-Linux systems. + return +} + +func clientsHandleNewtConnection(publicKey string) { + // This function is not implemented for non-Linux systems. + return +} + +func clientsOnConnect() { + // This function is not implemented for non-Linux systems. + return +} + +func clientsAddProxyTarget(pm *proxy.ProxyManager, tunnelIp string) { + // This function is not implemented for non-Linux systems. + return +} diff --git a/wg/wg.go b/wg/wg.go index cc86d57..1c378ea 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -1,3 +1,5 @@ +//go:build linux + package wg import ( From 5ce4455b99a5b50722998d65ffb087678478c52d Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 3 Jul 2025 20:17:21 -0700 Subject: [PATCH 81/87] Fix nil issues Former-commit-id: b7d4ea0c84fbd82a1a8c80af7119d6d14db48e48 --- linux.go | 15 ++++++----- main.go | 6 +++-- websocket/client.go | 64 +++++---------------------------------------- websocket/config.go | 3 --- websocket/types.go | 1 - 5 files changed, 20 insertions(+), 69 deletions(-) diff --git a/linux.go b/linux.go index 076e2f1..790f634 100644 --- a/linux.go +++ b/linux.go @@ -60,20 +60,23 @@ func closeClients() { } func clientsHandleNewtConnection(publicKey string) { - if wgService != nil { - wgService.SetServerPubKey(publicKey) - } else { - logger.Error("WireGuard service is not initialized, cannot set server public key") + if wgService == nil { + return } + wgService.SetServerPubKey(publicKey) } func clientsOnConnect() { - if wgService != nil { - wgService.LoadRemoteConfig() + if wgService == nil { + return } + wgService.LoadRemoteConfig() } func clientsAddProxyTarget(pm *proxy.ProxyManager, tunnelIp string) { + if wgService == nil { + return + } // add a udp proxy for localost and the wgService port // TODO: make sure this port is not used in a target pm.AddTarget("udp", tunnelIp, int(wgService.Port), fmt.Sprintf("127.0.0.1:%d", wgService.Port)) diff --git a/main.go b/main.go index 9b517c7..ed545c5 100644 --- a/main.go +++ b/main.go @@ -547,7 +547,8 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub "pingResults": pingResults, "newtVersion": newtVersion, }, 1*time.Second) - logger.Info("Sent exit node ping results to cloud for selection") + + logger.Debug("Sent exit node ping results to cloud for selection: pingResults=%+v", pingResults) }) client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) { @@ -684,7 +685,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub if err != nil { logger.Error("Failed to send registration message: %v", err) } - logger.Info("Sent registration message") + if err != nil { logger.Error("Failed to send Docker container list: %v", err) } else { @@ -714,6 +715,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Error("Failed to send registration message: %v", err) return err } + logger.Info("Sent registration message") return nil diff --git a/websocket/client.go b/websocket/client.go index a3538c0..d32698f 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -126,6 +126,8 @@ func (c *Client) SendMessage(messageType string, data interface{}) error { Data: data, } + logger.Debug("Sending message: %s, data: %+v", messageType, data) + c.writeMux.Lock() defer c.writeMux.Unlock() return c.conn.WriteJSON(msg) @@ -182,62 +184,6 @@ func (c *Client) getToken() (string, error) { } } - // If we already have a token, try to use it - if c.config.Token != "" { - tokenCheckData := map[string]interface{}{ - "newtId": c.config.NewtID, - "secret": c.config.Secret, - "token": c.config.Token, - } - jsonData, err := json.Marshal(tokenCheckData) - if err != nil { - return "", fmt.Errorf("failed to marshal token check data: %w", err) - } - - // Create a new request - req, err := http.NewRequest( - "POST", - baseEndpoint+"/api/v1/auth/newt/get-token", - bytes.NewBuffer(jsonData), - ) - if err != nil { - return "", fmt.Errorf("failed to create request: %w", err) - } - - // Set headers - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-CSRF-Token", "x-csrf-protection") - - // Make the request - client := &http.Client{} - if tlsConfig != nil { - client.Transport = &http.Transport{ - TLSClientConfig: tlsConfig, - } - } - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("failed to check token validity: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - logger.Error("Token check failed with status code: %d", resp.StatusCode) - return "", fmt.Errorf("token check failed with status code: %d", resp.StatusCode) - } - - var tokenResp TokenResponse - if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { - logger.Error("Failed to decode token check response.") - return "", fmt.Errorf("failed to decode token check response: %w", err) - } - - // If token is still valid, return it - if tokenResp.Success && tokenResp.Message == "Token session already valid" { - return c.config.Token, nil - } - } - // Get a new token tokenData := map[string]interface{}{ "newtId": c.config.NewtID, @@ -294,6 +240,8 @@ func (c *Client) getToken() (string, error) { return "", fmt.Errorf("received empty token from server") } + logger.Debug("Received token: %s", tokenResp.Data.Token) + return tokenResp.Data.Token, nil } @@ -321,7 +269,9 @@ func (c *Client) establishConnection() error { return fmt.Errorf("failed to get token: %w", err) } - c.onTokenUpdate(token) + if c.onTokenUpdate != nil { + c.onTokenUpdate(token) + } // Parse the base URL to determine protocol and hostname baseURL, err := url.Parse(c.baseURL) diff --git a/websocket/config.go b/websocket/config.go index e2b0055..fe11c5a 100644 --- a/websocket/config.go +++ b/websocket/config.go @@ -48,9 +48,6 @@ func (c *Client) loadConfig() error { if c.config.NewtID == "" { c.config.NewtID = config.NewtID } - if c.config.Token == "" { - c.config.Token = config.Token - } if c.config.Secret == "" { c.config.Secret = config.Secret } diff --git a/websocket/types.go b/websocket/types.go index 0ea24fc..54d33f1 100644 --- a/websocket/types.go +++ b/websocket/types.go @@ -3,7 +3,6 @@ package websocket type Config struct { NewtID string `json:"newtId"` Secret string `json:"secret"` - Token string `json:"token"` Endpoint string `json:"endpoint"` TlsClientCert string `json:"tlsClientCert"` } From 0d4a5cfc171fac4ddbbac3c3052d1241686a4fee Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 5 Jul 2025 18:14:33 -0700 Subject: [PATCH 82/87] Remove dup code Former-commit-id: 07bd283604ac9af096a14484ea7e65bdf8167899 --- main.go | 97 +++++++++++++++++++++++++++------------------------------ 1 file changed, 46 insertions(+), 51 deletions(-) diff --git a/main.go b/main.go index ed545c5..ca86508 100644 --- a/main.go +++ b/main.go @@ -272,6 +272,35 @@ func main() { var pingWithRetryStopChan chan struct{} + closeWgTunnel := func() { + if pingStopChan != nil { + // Stop the ping check + close(pingStopChan) + pingStopChan = nil + } + + // Stop proxy manager if running + if pm != nil { + pm.Stop() + pm = nil + } + + // Close WireGuard device first - this will automatically close the TUN device + if dev != nil { + dev.Close() + dev = nil + } + + // Clear references but don't manually close since dev.Close() already did it + if tnet != nil { + tnet = nil + } + if tun != nil { + tun = nil // Don't call tun.Close() here since dev.Close() already closed it + } + + } + // Register handlers for different message types client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) { logger.Info("Received registration message") @@ -281,33 +310,10 @@ func main() { } if connected { - if pingStopChan != nil { - // Stop the ping check - close(pingStopChan) - pingStopChan = nil - } - - // Stop proxy manager if running - if pm != nil { - pm.Stop() - pm = nil - } - - // Close WireGuard device first - this will automatically close the TUN device - if dev != nil { - dev.Close() - dev = nil - } - - // Clear references but don't manually close since dev.Close() already did it - if tnet != nil { - tnet = nil - } - if tun != nil { - tun = nil // Don't call tun.Close() here since dev.Close() already closed it - } - // Mark as disconnected + + closeWgTunnel() + connected = false } @@ -405,31 +411,8 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub client.RegisterHandler("newt/wg/reconnect", func(msg websocket.WSMessage) { logger.Info("Received reconnect message") - if pingStopChan != nil { - // Stop the ping check - close(pingStopChan) - pingStopChan = nil - } - - // Stop proxy manager if running - if pm != nil { - pm.Stop() - pm = nil - } - - // Close WireGuard device first - this will automatically close the TUN device - if dev != nil { - dev.Close() - dev = nil - } - - // Clear references but don't manually close since dev.Close() already did it - if tnet != nil { - tnet = nil - } - if tun != nil { - tun = nil // Don't call tun.Close() here since dev.Close() already closed it - } + // Close the WireGuard device and TUN + closeWgTunnel() // Mark as disconnected connected = false @@ -446,6 +429,18 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("Tunnel destroyed, ready for reconnection") }) + client.RegisterHandler("newt/wg/terminate", func(msg websocket.WSMessage) { + logger.Info("Received termination message") + + // Close the WireGuard device and TUN + closeWgTunnel() + + // Mark as disconnected + connected = false + + logger.Info("Tunnel destroyed, ready for reconnection") + }) + client.RegisterHandler("newt/ping/exitNodes", func(msg websocket.WSMessage) { logger.Info("Received ping message") if stopFunc != nil { From 0d060f4529da608abf43aafab455adfbc85fd6e1 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 8 Jul 2025 08:48:27 -0700 Subject: [PATCH 83/87] Adjust logging Former-commit-id: e4bdbbec7c58916561f64da19fdce79d0144d136 --- main.go | 24 ++++++++++++++++-------- proxy/manager.go | 3 ++- util.go | 10 +++++----- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/main.go b/main.go index ca86508..613ca23 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "encoding/json" "flag" "fmt" + "net" "net/http" "net/netip" "os" @@ -86,8 +87,8 @@ var ( dockerSocket string dockerEnforceNetworkValidation string dockerEnforceNetworkValidationBool bool - pingInterval = 1 * time.Second - pingTimeout = 2 * time.Second + pingInterval = 2 * time.Second + pingTimeout = 3 * time.Second publicKey wgtypes.Key pingStopChan chan struct{} stopFunc func() @@ -330,7 +331,7 @@ func main() { clientsHandleNewtConnection(wgData.PublicKey) - logger.Info("Received: %+v", msg) + logger.Debug("Received: %+v", msg) tun, tnet, err = netstack.CreateNetTUN( []netip.Addr{netip.MustParseAddr(wgData.TunnelIP)}, []netip.Addr{netip.MustParseAddr(dns)}, @@ -345,6 +346,14 @@ func main() { "wireguard: ", )) + host, _, err := net.SplitHostPort(wgData.Endpoint) + if err != nil { + logger.Error("Failed to split endpoint: %v", err) + return + } + + logger.Info("Connecting to endpoint: %s", host) + endpoint, err := resolveDomain(wgData.Endpoint) if err != nil { logger.Error("Failed to resolve endpoint: %v", err) @@ -369,7 +378,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Error("Failed to bring up WireGuard device: %v", err) } - logger.Info("WireGuard device created. Lets ping the server now...") + logger.Debug("WireGuard device created. Lets ping the server now...") // Even if pingWithRetry returns an error, it will continue trying in the background if pingWithRetryStopChan != nil { @@ -382,7 +391,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // Always mark as connected and start the proxy manager regardless of initial ping result // as the pings will continue in the background if !connected { - logger.Info("Starting ping check") + logger.Debug("Starting ping check") pingStopChan = startPingCheck(tnet, wgData.ServerIP, client) } @@ -417,7 +426,6 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // Mark as disconnected connected = false - // start asking for the exit nodes again if stopFunc != nil { stopFunc() // stop the ws from sending more requests stopFunc = nil // reset stopFunc to nil to avoid double stopping @@ -438,7 +446,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // Mark as disconnected connected = false - logger.Info("Tunnel destroyed, ready for reconnection") + logger.Info("Tunnel destroyed") }) client.RegisterHandler("newt/ping/exitNodes", func(msg websocket.WSMessage) { @@ -547,7 +555,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub }) client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) { - logger.Info("Received: %+v", msg) + logger.Debug("Received: %+v", msg) // if there is no wgData or pm, we can't add targets if wgData.TunnelIP == "" || pm == nil { diff --git a/proxy/manager.go b/proxy/manager.go index 0792acb..35d023a 100644 --- a/proxy/manager.go +++ b/proxy/manager.go @@ -213,7 +213,8 @@ func (pm *ProxyManager) startTarget(proto, listenIP string, port int, targetAddr return fmt.Errorf("unsupported protocol: %s", proto) } - logger.Info("Started %s proxy from %s:%d to %s", proto, listenIP, port, targetAddr) + logger.Info("Started %s proxy to %s", proto, targetAddr) + logger.Debug("Started %s proxy from %s:%d to %s", proto, listenIP, port, targetAddr) return nil } diff --git a/util.go b/util.go index c0f634c..9bdab59 100644 --- a/util.go +++ b/util.go @@ -112,10 +112,10 @@ func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) (stopC retryDelay := initialRetryDelay // First try with the initial parameters - logger.Info("Ping attempt %d", attempt) + logger.Debug("Ping attempt %d", attempt) if latency, err := ping(tnet, dst, timeout); err == nil { // Successful ping - logger.Info("Ping latency: %v", latency) + logger.Debug("Ping latency: %v", latency) logger.Info("Tunnel connection to server established successfully!") if healthFile != "" { err := os.WriteFile(healthFile, []byte("ok"), 0644) @@ -137,7 +137,7 @@ func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) (stopC case <-stopChan: return default: - logger.Info("Ping attempt %d", attempt) + logger.Debug("Ping attempt %d", attempt) if latency, err := ping(tnet, dst, timeout); err != nil { logger.Warn("Ping attempt %d failed: %v", attempt, err) @@ -155,8 +155,8 @@ func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) (stopC attempt++ } else { // Successful ping - logger.Info("Ping succeeded after %d attempts", attempt) - logger.Info("Ping latency: %v", latency) + logger.Debug("Ping succeeded after %d attempts", attempt) + logger.Debug("Ping latency: %v", latency) logger.Info("Tunnel connection to server established successfully!") if healthFile != "" { err := os.WriteFile(healthFile, []byte("ok"), 0644) From c21e29ffd105b909646a72e98ad7128e6b55c4d7 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 8 Jul 2025 08:48:39 -0700 Subject: [PATCH 84/87] Fix disconnect errors about closed connection Former-commit-id: 221d5862fb72ca30b6ebbf06530477d4dcd9f520 --- websocket/client.go | 72 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/websocket/client.go b/websocket/client.go index d32698f..98f07e6 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -102,16 +102,31 @@ func (c *Client) Connect() error { return nil } -// Close closes the WebSocket connection +// Close closes the WebSocket connection gracefully func (c *Client) Close() error { - close(c.done) - if c.conn != nil { - return c.conn.Close() + // Signal shutdown to all goroutines first + select { + case <-c.done: + // Already closed + return nil + default: + close(c.done) } - // stop the ping monitor + // Set connection status to false c.setConnected(false) + // Close the WebSocket connection gracefully + if c.conn != nil { + // Send close message + c.writeMux.Lock() + c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + c.writeMux.Unlock() + + // Close the connection + return c.conn.Close() + } + return nil } @@ -351,9 +366,16 @@ func (c *Client) pingMonitor() { err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(c.pingTimeout)) c.writeMux.Unlock() if err != nil { - logger.Error("Ping failed: %v", err) - c.reconnect() - return + // Check if we're shutting down before logging error and reconnecting + select { + case <-c.done: + // Expected during shutdown + return + default: + logger.Error("Ping failed: %v", err) + c.reconnect() + return + } } } } @@ -365,7 +387,14 @@ func (c *Client) readPumpWithDisconnectDetection() { if c.conn != nil { c.conn.Close() } - c.reconnect() + // Only attempt reconnect if we're not shutting down + select { + case <-c.done: + // Shutting down, don't reconnect + return + default: + c.reconnect() + } }() for { @@ -376,8 +405,21 @@ func (c *Client) readPumpWithDisconnectDetection() { var msg WSMessage err := c.conn.ReadJSON(&msg) if err != nil { - logger.Error("WebSocket read error: %v", err) - return // triggers reconnect via defer + // Check if we're shutting down before logging error + select { + case <-c.done: + // Expected during shutdown, don't log as error + logger.Debug("WebSocket connection closed during shutdown") + return + default: + // Unexpected error during normal operation + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNormalClosure) { + logger.Error("WebSocket read error: %v", err) + } else { + logger.Debug("WebSocket connection closed: %v", err) + } + return // triggers reconnect via defer + } } c.handlersMux.RLock() @@ -396,7 +438,13 @@ func (c *Client) reconnect() { c.conn = nil } - go c.connectWithRetry() + // Only reconnect if we're not shutting down + select { + case <-c.done: + return + default: + go c.connectWithRetry() + } } func (c *Client) setConnected(status bool) { From 8d61b9d628720dad549bcf7ecd9343f1bfdd631b Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 8 Jul 2025 17:18:43 -0700 Subject: [PATCH 85/87] Adjust logging Former-commit-id: 53397663ef02003927574fc8b1cef8d7c809eb83 --- main.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 613ca23..8af6403 100644 --- a/main.go +++ b/main.go @@ -699,11 +699,12 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub client.OnConnect(func() error { publicKey = privateKey.PublicKey() logger.Debug("Public key: %s", publicKey) + logger.Info("Websocket connected") if !connected { // request from the server the list of nodes to ping at newt/ping/request stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) - + logger.Info("Requesting exit nodes from server") clientsOnConnect() } @@ -719,8 +720,6 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub return err } - logger.Info("Sent registration message") - return nil }) From 44d6b6060c8566a3912ce8a1f7c2ec1e29467646 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 10 Jul 2025 17:17:12 -0700 Subject: [PATCH 86/87] Get newt script Former-commit-id: 2d9b761de98c56392a7d5dc98b95817f896ed0a6 --- get-newt.sh | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++++ util.go | 6 +- 2 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 get-newt.sh diff --git a/get-newt.sh b/get-newt.sh new file mode 100644 index 0000000..af4c248 --- /dev/null +++ b/get-newt.sh @@ -0,0 +1,235 @@ +#!/bin/bash + +# Get Newt - Cross-platform installation script +# Usage: curl -fsSL https://raw.githubusercontent.com/your-repo/get-newt.sh | bash + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# GitHub repository info +REPO="fosrl/newt" +GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest" + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to get latest version from GitHub API +get_latest_version() { + local latest_info + + if command -v curl >/dev/null 2>&1; then + latest_info=$(curl -fsSL "$GITHUB_API_URL" 2>/dev/null) + elif command -v wget >/dev/null 2>&1; then + latest_info=$(wget -qO- "$GITHUB_API_URL" 2>/dev/null) + else + print_error "Neither curl nor wget is available. Please install one of them." >&2 + exit 1 + fi + + if [ -z "$latest_info" ]; then + print_error "Failed to fetch latest version information" >&2 + exit 1 + fi + + # Extract version from JSON response (works without jq) + local version=$(echo "$latest_info" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') + + if [ -z "$version" ]; then + print_error "Could not parse version from GitHub API response" >&2 + exit 1 + fi + + # Remove 'v' prefix if present + version=$(echo "$version" | sed 's/^v//') + + echo "$version" +} + +# Detect OS and architecture +detect_platform() { + local os arch + + # Detect OS + case "$(uname -s)" in + Linux*) os="linux" ;; + Darwin*) os="darwin" ;; + MINGW*|MSYS*|CYGWIN*) os="windows" ;; + FreeBSD*) os="freebsd" ;; + *) + print_error "Unsupported operating system: $(uname -s)" + exit 1 + ;; + esac + + # Detect architecture + case "$(uname -m)" in + x86_64|amd64) arch="amd64" ;; + arm64|aarch64) arch="arm64" ;; + armv7l|armv6l) + if [ "$os" = "linux" ]; then + if [ "$(uname -m)" = "armv6l" ]; then + arch="arm32v6" + else + arch="arm32" + fi + else + arch="arm64" # Default for non-Linux ARM + fi + ;; + riscv64) + if [ "$os" = "linux" ]; then + arch="riscv64" + else + print_error "RISC-V architecture only supported on Linux" + exit 1 + fi + ;; + *) + print_error "Unsupported architecture: $(uname -m)" + exit 1 + ;; + esac + + echo "${os}_${arch}" +} + +# Get installation directory +get_install_dir() { + if [ "$OS" = "windows" ]; then + echo "$HOME/bin" + else + # Try to use a directory in PATH, fallback to ~/.local/bin + if echo "$PATH" | grep -q "/usr/local/bin"; then + if [ -w "/usr/local/bin" ] 2>/dev/null; then + echo "/usr/local/bin" + else + echo "$HOME/.local/bin" + fi + else + echo "$HOME/.local/bin" + fi + fi +} + +# Download and install newt +install_newt() { + local platform="$1" + local install_dir="$2" + local binary_name="newt_${platform}" + local exe_suffix="" + + # Add .exe suffix for Windows + if [[ "$platform" == *"windows"* ]]; then + binary_name="${binary_name}.exe" + exe_suffix=".exe" + fi + + local download_url="${BASE_URL}/${binary_name}" + local temp_file="/tmp/newt${exe_suffix}" + local final_path="${install_dir}/newt${exe_suffix}" + + print_status "Downloading newt from ${download_url}" + + # Download the binary + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$download_url" -o "$temp_file" + elif command -v wget >/dev/null 2>&1; then + wget -q "$download_url" -O "$temp_file" + else + print_error "Neither curl nor wget is available. Please install one of them." + exit 1 + fi + + # Create install directory if it doesn't exist + mkdir -p "$install_dir" + + # Move binary to install directory + mv "$temp_file" "$final_path" + + # Make executable (not needed on Windows, but doesn't hurt) + chmod +x "$final_path" + + print_status "newt installed to ${final_path}" + + # Check if install directory is in PATH + if ! echo "$PATH" | grep -q "$install_dir"; then + print_warning "Install directory ${install_dir} is not in your PATH." + print_warning "Add it to your PATH by adding this line to your shell profile:" + print_warning " export PATH=\"${install_dir}:\$PATH\"" + fi +} + +# Verify installation +verify_installation() { + local install_dir="$1" + local exe_suffix="" + + if [[ "$PLATFORM" == *"windows"* ]]; then + exe_suffix=".exe" + fi + + local newt_path="${install_dir}/newt${exe_suffix}" + + if [ -f "$newt_path" ] && [ -x "$newt_path" ]; then + print_status "Installation successful!" + print_status "newt version: $("$newt_path" --version 2>/dev/null || echo "unknown")" + return 0 + else + print_error "Installation failed. Binary not found or not executable." + return 1 + fi +} + +# Main installation process +main() { + print_status "Installing latest version of newt..." + + # Get latest version + print_status "Fetching latest version from GitHub..." + VERSION=$(get_latest_version) + print_status "Latest version: v${VERSION}" + + # Set base URL with the fetched version + BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" + + # Detect platform + PLATFORM=$(detect_platform) + print_status "Detected platform: ${PLATFORM}" + + # Get install directory + INSTALL_DIR=$(get_install_dir) + print_status "Install directory: ${INSTALL_DIR}" + + # Install newt + install_newt "$PLATFORM" "$INSTALL_DIR" + + # Verify installation + if verify_installation "$INSTALL_DIR"; then + print_status "newt is ready to use!" + if [[ "$PLATFORM" == *"windows"* ]]; then + print_status "Run 'newt --help' to get started" + else + print_status "Run 'newt --help' to get started" + fi + else + exit 1 + fi +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/util.go b/util.go index 9bdab59..967f12e 100644 --- a/util.go +++ b/util.go @@ -192,7 +192,11 @@ func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Clien _, err := ping(tnet, serverIP, pingTimeout) if err != nil { consecutiveFailures++ - logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) + if consecutiveFailures == 1 { + logger.Debug("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) + } else { + logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) + } if consecutiveFailures >= 3 && currentInterval < maxInterval { if !connectionLost { connectionLost = true From 51a03a49f991da10ed8c49442a11e21d1d83640e Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 10 Jul 2025 17:23:26 -0700 Subject: [PATCH 87/87] Update link Former-commit-id: b1cfd3ba026c0561813194956b5c16c6dd0ca374 --- get-newt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/get-newt.sh b/get-newt.sh index af4c248..5541707 100644 --- a/get-newt.sh +++ b/get-newt.sh @@ -1,7 +1,7 @@ #!/bin/bash # Get Newt - Cross-platform installation script -# Usage: curl -fsSL https://raw.githubusercontent.com/your-repo/get-newt.sh | bash +# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/newt/get-newt.sh | bash set -e