From 533886f2e41b45a9f8ba02ad500efd2fb7a26871 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jan 2025 07:41:56 -0500 Subject: [PATCH 1/8] Standarize makefile release --- .gitignore | 1 + Makefile | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index e057418..8b1c477 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ newt .DS_Store +bin/ \ No newline at end of file diff --git a/Makefile b/Makefile index 0f0aa39..b895de1 100644 --- a/Makefile +++ b/Makefile @@ -13,14 +13,14 @@ test: local: CGO_ENABLED=0 go build -o newt -all_arches: - CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o newt_linux_arm64 - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o newt_linux_amd64 - CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o newt_darwin_arm64 - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o newt_darwin_amd64 - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o newt_windows_amd64.exe - CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o newt_freebsd_amd64 - CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o newt_freebsd_arm64 +release: + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/newt_linux_arm64 + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/newt_linux_amd64 + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/newt_darwin_arm64 + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/newt_darwin_amd64 + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o newt_windows_amd64.bin/exe + CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/newt_freebsd_amd64 + CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/newt_freebsd_arm64 clean: rm newt From 759780508a3179e87b0cde5be3302f5569d03c85 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 19 Jan 2025 22:46:00 -0500 Subject: [PATCH 2/8] Resolve TCP hanging but port is in use issue --- go.mod | 1 + go.sum | 2 ++ proxy/manager.go | 87 +++++++++++++++++++++++------------------------- proxy/types.go | 21 ++++++------ 4 files changed, 56 insertions(+), 55 deletions(-) diff --git a/go.mod b/go.mod index 66a9c2a..2cc0c19 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/google/btree v1.1.2 // indirect github.com/gorilla/websocket v1.5.3 // indirect golang.org/x/crypto v0.28.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/time v0.7.0 // indirect diff --git a/go.sum b/go.sum index 571b2d6..d95ab3a 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +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/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= diff --git a/proxy/manager.go b/proxy/manager.go index 6aa3128..ae89b3e 100644 --- a/proxy/manager.go +++ b/proxy/manager.go @@ -9,6 +9,7 @@ import ( "time" "github.com/fosrl/newt/logger" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" "golang.zx2c4.com/wireguard/tun/netstack" ) @@ -19,13 +20,12 @@ func NewProxyManager(tnet *netstack.Net) *ProxyManager { } } -func (pm *ProxyManager) AddTarget(protocol, listen string, port int, target string) { +func (pm *ProxyManager) AddTarget(protocol, listen string, port int, target string) error { pm.Lock() defer pm.Unlock() logger.Info("Adding target: %s://%s:%d -> %s", protocol, listen, port, target) - - newTarget := ProxyTarget{ + newTarget := &ProxyTarget{ Protocol: protocol, Listen: listen, Port: port, @@ -35,6 +35,7 @@ func (pm *ProxyManager) AddTarget(protocol, listen string, port int, target stri } pm.targets = append(pm.targets, newTarget) + return nil } func (pm *ProxyManager) RemoveTarget(protocol, listen string, port int) error { @@ -54,31 +55,21 @@ func (pm *ProxyManager) RemoveTarget(protocol, listen string, port int) error { // Signal the serving goroutine to stop select { case <-target.cancel: - // Channel is already closed, no need to close it again + // Channel is already closed default: close(target.cancel) } - // Close the appropriate listener/connection based on protocol + // Close the listener/connection target.Lock() switch protocol { case "tcp": if target.listener != nil { - select { - case <-target.cancel: - // Listener was already closed by Stop() - default: - target.listener.Close() - } + target.listener.Close() } case "udp": if target.udpConn != nil { - select { - case <-target.cancel: - // Connection was already closed by Stop() - default: - target.udpConn.Close() - } + target.udpConn.Close() } } target.Unlock() @@ -86,7 +77,6 @@ func (pm *ProxyManager) RemoveTarget(protocol, listen string, port int) error { // Wait for the target to fully stop <-target.done - // Remove the target from the slice pm.targets = append(pm.targets[:i], pm.targets[i+1:]...) return nil } @@ -99,9 +89,7 @@ func (pm *ProxyManager) Start() error { pm.RLock() defer pm.RUnlock() - for i := range pm.targets { - target := &pm.targets[i] - + for _, target := range pm.targets { target.Lock() // If target is already running, skip it if target.listener != nil || target.udpConn != nil { @@ -110,7 +98,6 @@ func (pm *ProxyManager) Start() error { } // Mark the target as starting by creating a nil listener/connection - // This prevents other goroutines from trying to start it if strings.ToLower(target.Protocol) == "tcp" { target.listener = nil } else { @@ -135,10 +122,11 @@ func (pm *ProxyManager) Stop() error { defer pm.Unlock() var wg sync.WaitGroup - for i := range pm.targets { - target := &pm.targets[i] + for _, target := range pm.targets { wg.Add(1) - go func(t *ProxyTarget) { + // Create a new variable in the loop to avoid closure issues + t := target // Take a local copy + go func() { defer wg.Done() close(t.cancel) t.Lock() @@ -151,7 +139,7 @@ func (pm *ProxyManager) Stop() error { t.Unlock() // Wait for the target to fully stop <-t.done - }(target) + }() } wg.Wait() return nil @@ -220,32 +208,41 @@ func (pm *ProxyManager) handleTCPConnection(clientConn net.Conn, target string, } defer serverConn.Close() - var wg sync.WaitGroup - wg.Add(2) + // Create error channels for both copy operations + errc1 := make(chan error, 1) + errc2 := make(chan error, 1) - // Client -> Server + // Copy from client to server go func() { - defer wg.Done() - select { - case <-done: - return - default: - io.Copy(serverConn, clientConn) - } + _, err := io.Copy(serverConn, clientConn) + errc1 <- err }() - // Server -> Client + // Copy from server to client go func() { - defer wg.Done() - select { - case <-done: - return - default: - io.Copy(clientConn, serverConn) - } + _, err := io.Copy(clientConn, serverConn) + errc2 <- err }() - wg.Wait() + // Wait for either copy to finish or done signal + select { + case <-done: + // Gracefully close connections without type assertions + if closer, ok := clientConn.(interface{ CloseRead() error }); ok { + closer.CloseRead() + } + if closer, ok := serverConn.(*gonet.TCPConn); ok { + closer.CloseRead() + } + case err := <-errc1: + if err != nil { + logger.Info("Error copying client->server: %v", err) + } + case err := <-errc2: + if err != nil { + logger.Info("Error copying server->client: %v", err) + } + } } func (pm *ProxyManager) serveUDP(target *ProxyTarget) { diff --git a/proxy/types.go b/proxy/types.go index 10bd046..f189596 100644 --- a/proxy/types.go +++ b/proxy/types.go @@ -9,19 +9,20 @@ import ( ) type ProxyTarget struct { - Protocol string - Listen string - Port int - Target string - cancel chan struct{} // Channel to signal shutdown - done chan struct{} // Channel to signal completion - listener net.Listener // For TCP - udpConn net.PacketConn // For UDP - sync.Mutex // Protect access to connection + Protocol string + Listen string + Port int + Target string + cancel chan struct{} // Channel to signal shutdown + done chan struct{} // Channel to signal completion + listener net.Listener // For TCP + udpConn net.PacketConn // For UDP + sync.Mutex // Protect access to connection + activeConns sync.Map } type ProxyManager struct { - targets []ProxyTarget + targets []*ProxyTarget tnet *netstack.Net log *log.Logger sync.RWMutex // Protect access to targets slice From 3a6365782281ac2e4af1ad537442d8f78d046ccf Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 20 Jan 2025 21:11:06 -0500 Subject: [PATCH 3/8] Rewrite proxy manager --- proxy/manager.go | 502 ++++++++++++++++++++++++----------------------- proxy/types.go | 29 --- 2 files changed, 253 insertions(+), 278 deletions(-) delete mode 100644 proxy/types.go diff --git a/proxy/manager.go b/proxy/manager.go index ae89b3e..92218fa 100644 --- a/proxy/manager.go +++ b/proxy/manager.go @@ -4,328 +4,332 @@ import ( "fmt" "io" "net" - "strings" "sync" "time" "github.com/fosrl/newt/logger" - "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" - "golang.zx2c4.com/wireguard/tun/netstack" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" ) +// Target represents a proxy target with its address and port +type Target struct { + Address string + Port int +} + +// ProxyManager handles the creation and management of proxy connections +type ProxyManager struct { + tnet *netstack.Net + tcpTargets map[string]map[int]string // map[listenIP]map[port]targetAddress + udpTargets map[string]map[int]string + listeners []*gonet.TCPListener + udpConns []*gonet.UDPConn + running bool + mutex sync.RWMutex +} + +// NewProxyManager creates a new proxy manager instance func NewProxyManager(tnet *netstack.Net) *ProxyManager { return &ProxyManager{ - tnet: tnet, + tnet: tnet, + tcpTargets: make(map[string]map[int]string), + udpTargets: make(map[string]map[int]string), + listeners: make([]*gonet.TCPListener, 0), + udpConns: make([]*gonet.UDPConn, 0), } } -func (pm *ProxyManager) AddTarget(protocol, listen string, port int, target string) error { - pm.Lock() - defer pm.Unlock() +// AddTarget adds a new target for proxying +func (pm *ProxyManager) AddTarget(proto, listenIP string, port int, targetAddr string) error { + pm.mutex.Lock() + defer pm.mutex.Unlock() - logger.Info("Adding target: %s://%s:%d -> %s", protocol, listen, port, target) - newTarget := &ProxyTarget{ - Protocol: protocol, - Listen: listen, - Port: port, - Target: target, - cancel: make(chan struct{}), - done: make(chan struct{}), + switch proto { + case "tcp": + if pm.tcpTargets[listenIP] == nil { + pm.tcpTargets[listenIP] = make(map[int]string) + } + pm.tcpTargets[listenIP][port] = targetAddr + case "udp": + if pm.udpTargets[listenIP] == nil { + pm.udpTargets[listenIP] = make(map[int]string) + } + pm.udpTargets[listenIP][port] = targetAddr + default: + return fmt.Errorf("unsupported protocol: %s", proto) } - pm.targets = append(pm.targets, newTarget) + if pm.running { + return pm.startTarget(proto, listenIP, port, targetAddr) + } else { + logger.Info("Not adding target because not running") + } return nil } -func (pm *ProxyManager) RemoveTarget(protocol, listen string, port int) error { - pm.Lock() - defer pm.Unlock() +func (pm *ProxyManager) RemoveTarget(proto, listenIP string, port int) error { + pm.mutex.Lock() + defer pm.mutex.Unlock() - protocol = strings.ToLower(protocol) - if protocol != "tcp" && protocol != "udp" { - return fmt.Errorf("unsupported protocol: %s", protocol) - } - - for i, target := range pm.targets { - if target.Listen == listen && - target.Port == port && - strings.ToLower(target.Protocol) == protocol { - - // Signal the serving goroutine to stop - select { - case <-target.cancel: - // Channel is already closed - default: - close(target.cancel) - } - - // Close the listener/connection - target.Lock() - switch protocol { - case "tcp": - if target.listener != nil { - target.listener.Close() - } - case "udp": - if target.udpConn != nil { - target.udpConn.Close() + switch proto { + case "tcp": + if targets, ok := pm.tcpTargets[listenIP]; ok { + delete(targets, port) + // Remove and close the corresponding TCP listener + for i, listener := range pm.listeners { + if addr, ok := listener.Addr().(*net.TCPAddr); ok && addr.Port == port { + listener.Close() + time.Sleep(50 * time.Millisecond) + // Remove from slice + pm.listeners = append(pm.listeners[:i], pm.listeners[i+1:]...) + break } } - target.Unlock() - - // Wait for the target to fully stop - <-target.done - - pm.targets = append(pm.targets[:i], pm.targets[i+1:]...) - return nil + } else { + return fmt.Errorf("target not found: %s:%d", listenIP, port) } + case "udp": + if targets, ok := pm.udpTargets[listenIP]; ok { + delete(targets, port) + // Remove and close the corresponding UDP connection + for i, conn := range pm.udpConns { + if addr, ok := conn.LocalAddr().(*net.UDPAddr); ok && addr.Port == port { + conn.Close() + time.Sleep(50 * time.Millisecond) + // Remove from slice + pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...) + break + } + } + } else { + return fmt.Errorf("target not found: %s:%d", listenIP, port) + } + default: + return fmt.Errorf("unsupported protocol: %s", proto) } - - return fmt.Errorf("target not found for %s %s:%d", protocol, listen, port) + return nil } +// Start begins listening for all configured proxy targets func (pm *ProxyManager) Start() error { - pm.RLock() - defer pm.RUnlock() + pm.mutex.Lock() + defer pm.mutex.Unlock() - for _, target := range pm.targets { - target.Lock() - // If target is already running, skip it - if target.listener != nil || target.udpConn != nil { - target.Unlock() - continue - } + if pm.running { + return nil + } - // Mark the target as starting by creating a nil listener/connection - if strings.ToLower(target.Protocol) == "tcp" { - target.listener = nil - } else { - target.udpConn = nil - } - target.Unlock() - - switch strings.ToLower(target.Protocol) { - case "tcp": - go pm.serveTCP(target) - case "udp": - go pm.serveUDP(target) - default: - return fmt.Errorf("unsupported protocol: %s", target.Protocol) + // Start TCP targets + for listenIP, targets := range pm.tcpTargets { + for port, targetAddr := range targets { + if err := pm.startTarget("tcp", listenIP, port, targetAddr); err != nil { + return fmt.Errorf("failed to start TCP target: %v", err) + } } } + + // Start UDP targets + for listenIP, targets := range pm.udpTargets { + for port, targetAddr := range targets { + if err := pm.startTarget("udp", listenIP, port, targetAddr); err != nil { + return fmt.Errorf("failed to start UDP target: %v", err) + } + } + } + + pm.running = true return nil } func (pm *ProxyManager) Stop() error { - pm.Lock() - defer pm.Unlock() + pm.mutex.Lock() + defer pm.mutex.Unlock() - var wg sync.WaitGroup - for _, target := range pm.targets { - wg.Add(1) - // Create a new variable in the loop to avoid closure issues - t := target // Take a local copy - go func() { - defer wg.Done() - close(t.cancel) - t.Lock() - if t.listener != nil { - t.listener.Close() - } - if t.udpConn != nil { - t.udpConn.Close() - } - t.Unlock() - // Wait for the target to fully stop - <-t.done - }() + if !pm.running { + return nil } - wg.Wait() + + // Set running to false first to signal handlers to stop + pm.running = false + + // Close TCP listeners + for i := len(pm.listeners) - 1; i >= 0; i-- { + listener := pm.listeners[i] + if err := listener.Close(); err != nil { + logger.Error("Error closing TCP listener: %v", err) + } + // Remove from slice + pm.listeners = append(pm.listeners[:i], pm.listeners[i+1:]...) + } + + // Close UDP connections + for i := len(pm.udpConns) - 1; i >= 0; i-- { + conn := pm.udpConns[i] + if err := conn.Close(); err != nil { + logger.Error("Error closing UDP connection: %v", err) + } + // Remove from slice + pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...) + } + + // Clear the target maps + for k := range pm.tcpTargets { + delete(pm.tcpTargets, k) + } + for k := range pm.udpTargets { + delete(pm.udpTargets, k) + } + + // Give active connections a chance to close gracefully + time.Sleep(100 * time.Millisecond) + return nil } -func (pm *ProxyManager) serveTCP(target *ProxyTarget) { - defer close(target.done) // Signal that this target is fully stopped +func (pm *ProxyManager) startTarget(proto, listenIP string, port int, targetAddr string) error { + switch proto { + case "tcp": + listener, err := pm.tnet.ListenTCP(&net.TCPAddr{Port: port}) + if err != nil { + return fmt.Errorf("failed to create TCP listener: %v", err) + } - listener, err := pm.tnet.ListenTCP(&net.TCPAddr{ - IP: net.ParseIP(target.Listen), - Port: target.Port, - }) - if err != nil { - logger.Info("Failed to start TCP listener for %s:%d: %v", target.Listen, target.Port, err) - return + pm.listeners = append(pm.listeners, listener) + go pm.handleTCPProxy(listener, targetAddr) + + case "udp": + addr := &net.UDPAddr{Port: port} + conn, err := pm.tnet.ListenUDP(addr) + if err != nil { + return fmt.Errorf("failed to create UDP listener: %v", err) + } + + pm.udpConns = append(pm.udpConns, conn) + go pm.handleUDPProxy(conn, targetAddr) + + default: + return fmt.Errorf("unsupported protocol: %s", proto) } - target.Lock() - target.listener = listener - target.Unlock() + logger.Info("Started %s proxy from %s:%d to %s", proto, listenIP, port, targetAddr) - defer listener.Close() - logger.Info("TCP proxy listening on %s", listener.Addr()) - - var activeConns sync.WaitGroup - acceptDone := make(chan struct{}) - - // Goroutine to handle shutdown signal - go func() { - <-target.cancel - close(acceptDone) - listener.Close() - }() + return nil +} +func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string) { for { conn, err := listener.Accept() if err != nil { - select { - case <-target.cancel: - // Wait for active connections to finish - activeConns.Wait() + // Check if we're shutting down or the listener was closed + if !pm.running { return - default: - logger.Info("Failed to accept TCP connection: %v", err) - // Don't return here, try to accept new connections - time.Sleep(time.Second) - continue } + + // Check for specific network errors that indicate the listener is closed + if ne, ok := err.(net.Error); ok && !ne.Temporary() { + logger.Info("TCP listener closed, stopping proxy handler for %v", listener.Addr()) + return + } + + logger.Error("Error accepting TCP connection: %v", err) + // Don't hammer the CPU if we hit a temporary error + time.Sleep(100 * time.Millisecond) + continue } - activeConns.Add(1) go func() { - defer activeConns.Done() - pm.handleTCPConnection(conn, target.Target, acceptDone) + target, err := net.Dial("tcp", targetAddr) + if err != nil { + logger.Error("Error connecting to target: %v", err) + conn.Close() + return + } + + // Create a WaitGroup to ensure both copy operations complete + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + io.Copy(target, conn) + target.Close() + }() + + go func() { + defer wg.Done() + io.Copy(conn, target) + conn.Close() + }() + + // Wait for both copies to complete + wg.Wait() }() } } -func (pm *ProxyManager) handleTCPConnection(clientConn net.Conn, target string, done chan struct{}) { - defer clientConn.Close() - - serverConn, err := net.Dial("tcp", target) - if err != nil { - logger.Info("Failed to connect to target %s: %v", target, err) - return - } - defer serverConn.Close() - - // Create error channels for both copy operations - errc1 := make(chan error, 1) - errc2 := make(chan error, 1) - - // Copy from client to server - go func() { - _, err := io.Copy(serverConn, clientConn) - errc1 <- err - }() - - // Copy from server to client - go func() { - _, err := io.Copy(clientConn, serverConn) - errc2 <- err - }() - - // Wait for either copy to finish or done signal - select { - case <-done: - // Gracefully close connections without type assertions - if closer, ok := clientConn.(interface{ CloseRead() error }); ok { - closer.CloseRead() - } - if closer, ok := serverConn.(*gonet.TCPConn); ok { - closer.CloseRead() - } - case err := <-errc1: - if err != nil { - logger.Info("Error copying client->server: %v", err) - } - case err := <-errc2: - if err != nil { - logger.Info("Error copying server->client: %v", err) - } - } -} - -func (pm *ProxyManager) serveUDP(target *ProxyTarget) { - defer close(target.done) // Signal that this target is fully stopped - - addr := &net.UDPAddr{ - IP: net.ParseIP(target.Listen), - Port: target.Port, - } - - conn, err := pm.tnet.ListenUDP(addr) - if err != nil { - logger.Info("Failed to start UDP listener for %s:%d: %v", target.Listen, target.Port, err) - return - } - - target.Lock() - target.udpConn = conn - target.Unlock() - - defer conn.Close() - logger.Info("UDP proxy listening on %s", conn.LocalAddr()) - - buffer := make([]byte, 65535) - var activeConns sync.WaitGroup +func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) { + buffer := make([]byte, 65507) // Max UDP packet size + clientConns := make(map[string]*net.UDPConn) + var clientsMutex sync.RWMutex for { - select { - case <-target.cancel: - activeConns.Wait() // Wait for all active UDP handlers to complete - return - default: - n, remoteAddr, err := conn.ReadFrom(buffer) - if err != nil { - select { - case <-target.cancel: - activeConns.Wait() - return - default: - logger.Info("Failed to read UDP packet: %v", err) - continue - } + n, remoteAddr, err := conn.ReadFrom(buffer) + if err != nil { + if !pm.running { + return } + logger.Error("Error reading UDP packet: %v", err) + continue + } - targetAddr, err := net.ResolveUDPAddr("udp", target.Target) + clientKey := remoteAddr.String() + clientsMutex.RLock() + targetConn, exists := clientConns[clientKey] + clientsMutex.RUnlock() + + if !exists { + targetUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr) if err != nil { - logger.Info("Failed to resolve target address %s: %v", target.Target, err) + logger.Error("Error resolving target address: %v", err) continue } - activeConns.Add(1) - go func(data []byte, remote net.Addr) { - defer activeConns.Done() - targetConn, err := net.DialUDP("udp", nil, targetAddr) - if err != nil { - logger.Info("Failed to connect to target %s: %v", target.Target, err) - return - } - defer targetConn.Close() + targetConn, err = net.DialUDP("udp", nil, targetUDPAddr) + if err != nil { + logger.Error("Error connecting to target: %v", err) + continue + } - select { - case <-target.cancel: - return - default: - _, err = targetConn.Write(data) + clientsMutex.Lock() + clientConns[clientKey] = targetConn + clientsMutex.Unlock() + + go func() { + buffer := make([]byte, 65507) + for { + n, _, err := targetConn.ReadFromUDP(buffer) if err != nil { - logger.Info("Failed to write to target: %v", err) + logger.Error("Error reading from target: %v", err) return } - response := make([]byte, 65535) - n, err := targetConn.Read(response) + _, err = conn.WriteTo(buffer[:n], remoteAddr) if err != nil { - logger.Info("Failed to read response from target: %v", err) + logger.Error("Error writing to client: %v", err) return } - - _, err = conn.WriteTo(response[:n], remote) - if err != nil { - logger.Info("Failed to write response to client: %v", err) - } } - }(buffer[:n], remoteAddr) + }() + } + + _, err = targetConn.Write(buffer[:n]) + if err != nil { + logger.Error("Error writing to target: %v", err) + targetConn.Close() + clientsMutex.Lock() + delete(clientConns, clientKey) + clientsMutex.Unlock() } } } diff --git a/proxy/types.go b/proxy/types.go deleted file mode 100644 index f189596..0000000 --- a/proxy/types.go +++ /dev/null @@ -1,29 +0,0 @@ -package proxy - -import ( - "log" - "net" - "sync" - - "golang.zx2c4.com/wireguard/tun/netstack" -) - -type ProxyTarget struct { - Protocol string - Listen string - Port int - Target string - cancel chan struct{} // Channel to signal shutdown - done chan struct{} // Channel to signal completion - listener net.Listener // For TCP - udpConn net.PacketConn // For UDP - sync.Mutex // Protect access to connection - activeConns sync.Map -} - -type ProxyManager struct { - targets []*ProxyTarget - tnet *netstack.Net - log *log.Logger - sync.RWMutex // Protect access to targets slice -} From f7a705e6f8b1e2acb8ae97c04fb476ddecd8d440 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 20 Jan 2025 21:13:09 -0500 Subject: [PATCH 4/8] Remove starts --- main.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/main.go b/main.go index 515077c..bff80ac 100644 --- a/main.go +++ b/main.go @@ -455,11 +455,6 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( if len(targetData.Targets) > 0 { updateTargets(pm, "add", wgData.TunnelIP, "tcp", targetData) } - - err = pm.Start() - if err != nil { - logger.Error("Failed to start proxy manager: %v", err) - } }) client.RegisterHandler("newt/udp/add", func(msg websocket.WSMessage) { @@ -480,11 +475,6 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( if len(targetData.Targets) > 0 { updateTargets(pm, "add", wgData.TunnelIP, "udp", targetData) } - - err = pm.Start() - if err != nil { - logger.Error("Failed to start proxy manager: %v", err) - } }) client.RegisterHandler("newt/udp/remove", func(msg websocket.WSMessage) { From 868bb55f87ed38b35a1edfeb59c1afcb030dccea Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 20 Jan 2025 21:40:55 -0500 Subject: [PATCH 5/8] Fix windows build in release --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b895de1..5666dd9 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ release: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/newt_linux_amd64 CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/newt_darwin_arm64 CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/newt_darwin_amd64 - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o newt_windows_amd64.bin/exe + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/newt_windows_amd64.exe CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/newt_freebsd_amd64 CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/newt_freebsd_arm64 From 0c5c59cf00156a95f3c68f31a21bac438916331c Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Mon, 27 Jan 2025 21:28:22 -0500 Subject: [PATCH 6/8] Fix removing udp sockets --- proxy/manager.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/proxy/manager.go b/proxy/manager.go index 92218fa..b6c521b 100644 --- a/proxy/manager.go +++ b/proxy/manager.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net" + "strings" "sync" "time" @@ -279,6 +280,22 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) { if !pm.running { return } + + // Check for connection closed conditions + if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") { + logger.Info("UDP connection closed, stopping proxy handler") + + // Clean up existing client connections + clientsMutex.Lock() + for _, targetConn := range clientConns { + targetConn.Close() + } + clientConns = nil + clientsMutex.Unlock() + + return + } + logger.Error("Error reading UDP packet: %v", err) continue } From f8dccbec808722216db2f97402dd79b76306ae57 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 29 Jan 2025 22:15:28 -0500 Subject: [PATCH 7/8] Fix save config --- main.go | 5 ----- websocket/client.go | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index bff80ac..c475448 100644 --- a/main.go +++ b/main.go @@ -289,11 +289,6 @@ func main() { loggerLevel := parseLogLevel(logLevel) logger.GetLogger().SetLevel(parseLogLevel(logLevel)) - // Validate required fields - if endpoint == "" || id == "" || secret == "" { - logger.Fatal("endpoint, id, and secret are required either via CLI flags or environment variables") - } - // parse the mtu string into an int mtuInt, err = strconv.Atoi(mtu) if err != nil { diff --git a/websocket/client.go b/websocket/client.go index 879a109..8a7d3f9 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -305,6 +305,10 @@ func (c *Client) establishConnection() error { go c.readPump() if c.onConnect != nil { + err := c.saveConfig() + if err != nil { + logger.Error("Failed to save config: %v", err) + } if err := c.onConnect(); err != nil { logger.Error("OnConnect callback failed: %v", err) } From 4e50819785ba74850d35269f5a660a323faa8777 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Wed, 29 Jan 2025 22:19:18 -0500 Subject: [PATCH 8/8] Add --version check --- main.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/main.go b/main.go index c475448..786ecbd 100644 --- a/main.go +++ b/main.go @@ -283,8 +283,17 @@ func main() { if logLevel == "" { flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)") } + + // do a --version check + version := flag.Bool("version", false, "Print the version") + flag.Parse() + if *version { + fmt.Println("Newt version replaceme") + os.Exit(0) + } + logger.Init() loggerLevel := parseLogLevel(logLevel) logger.GetLogger().SetLevel(parseLogLevel(logLevel))