Compare commits

..

6 Commits

Author SHA1 Message Date
dependabot[bot]
436d8d1bca chore(nix): fix hash for updated go dependencies 2026-03-20 09:45:39 +00:00
dependabot[bot]
2eeb14bb3e chore(deps): bump docker/library/golang in the minor-updates group
Bumps the minor-updates group with 1 update: docker/library/golang.


Updates `docker/library/golang` from 1.25-alpine to 1.26-alpine

---
updated-dependencies:
- dependency-name: docker/library/golang
  dependency-version: 1.26-alpine
  dependency-type: direct:production
  dependency-group: minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 09:44:19 +00:00
Owen Schwartz
b398f531f0 Merge pull request #279 from fosrl/dev
1.10.3
2026-03-16 16:47:39 -07:00
Owen
ef03b4566d Allow passing public dns into resolve 2026-03-12 16:41:41 -07:00
Owen
44ca592a5c Set newt version in dockerfile 2026-03-08 11:28:56 -07:00
Owen
e1edbcea07 Make sure to set version and fix prepare issue 2026-03-07 12:34:55 -08:00
9 changed files with 162 additions and 64 deletions

View File

@@ -1,5 +1,5 @@
# FROM golang:1.25-alpine AS builder # FROM golang:1.25-alpine AS builder
FROM public.ecr.aws/docker/library/golang:1.25-alpine AS builder FROM public.ecr.aws/docker/library/golang:1.26-alpine AS builder
# Install git and ca-certificates # Install git and ca-certificates
RUN apk --no-cache add ca-certificates git tzdata RUN apk --no-cache add ca-certificates git tzdata

View File

@@ -161,9 +161,8 @@ func NewWireGuardService(interfaceName string, port uint16, mtu int, host string
useNativeInterface: useNativeInterface, useNativeInterface: useNativeInterface,
} }
// Create the holepunch manager with ResolveDomain function // Create the holepunch manager
// We'll need to pass a domain resolver function service.holePunchManager = holepunch.NewManager(sharedBind, newtId, "newt", key.PublicKey().String(), nil)
service.holePunchManager = holepunch.NewManager(sharedBind, newtId, "newt", key.PublicKey().String())
// Register websocket handlers // Register websocket handlers
wsClient.RegisterHandler("newt/wg/receive-config", service.handleConfig) wsClient.RegisterHandler("newt/wg/receive-config", service.handleConfig)

View File

@@ -35,7 +35,7 @@
inherit version; inherit version;
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.; src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
vendorHash = "sha256-kmQM8Yy5TuOiNpMpUme/2gfE+vrhUK+0AphN+p71wGs="; vendorHash = "sha256-vy6Dqjek7pLdASbCrM9snq5Dt9lbwNJ0AuQboy1JWNQ=";
nativeInstallCheckInputs = [ pkgs.versionCheckHook ]; nativeInstallCheckInputs = [ pkgs.versionCheckHook ];

View File

@@ -5,9 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"net/http" "net/http"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -367,12 +365,11 @@ func (m *Monitor) performHealthCheck(target *Target) {
target.LastCheck = time.Now() target.LastCheck = time.Now()
target.LastError = "" target.LastError = ""
// Build URL (use net.JoinHostPort to properly handle IPv6 addresses with ports) // Build URL
host := target.Config.Hostname url := fmt.Sprintf("%s://%s", target.Config.Scheme, target.Config.Hostname)
if target.Config.Port > 0 { if target.Config.Port > 0 {
host = net.JoinHostPort(target.Config.Hostname, strconv.Itoa(target.Config.Port)) url = fmt.Sprintf("%s:%d", url, target.Config.Port)
} }
url := fmt.Sprintf("%s://%s", target.Config.Scheme, host)
if target.Config.Path != "" { if target.Config.Path != "" {
if !strings.HasPrefix(target.Config.Path, "/") { if !strings.HasPrefix(target.Config.Path, "/") {
url += "/" url += "/"

View File

@@ -37,6 +37,7 @@ type Manager struct {
clientType string clientType string
exitNodes map[string]ExitNode // key is endpoint exitNodes map[string]ExitNode // key is endpoint
updateChan chan struct{} // signals the goroutine to refresh exit nodes updateChan chan struct{} // signals the goroutine to refresh exit nodes
publicDNS []string
sendHolepunchInterval time.Duration sendHolepunchInterval time.Duration
sendHolepunchIntervalMin time.Duration sendHolepunchIntervalMin time.Duration
@@ -49,12 +50,13 @@ const defaultSendHolepunchIntervalMax = 60 * time.Second
const defaultSendHolepunchIntervalMin = 1 * time.Second const defaultSendHolepunchIntervalMin = 1 * time.Second
// NewManager creates a new hole punch manager // NewManager creates a new hole punch manager
func NewManager(sharedBind *bind.SharedBind, ID string, clientType string, publicKey string) *Manager { func NewManager(sharedBind *bind.SharedBind, ID string, clientType string, publicKey string, publicDNS []string) *Manager {
return &Manager{ return &Manager{
sharedBind: sharedBind, sharedBind: sharedBind,
ID: ID, ID: ID,
clientType: clientType, clientType: clientType,
publicKey: publicKey, publicKey: publicKey,
publicDNS: publicDNS,
exitNodes: make(map[string]ExitNode), exitNodes: make(map[string]ExitNode),
sendHolepunchInterval: defaultSendHolepunchIntervalMin, sendHolepunchInterval: defaultSendHolepunchIntervalMin,
sendHolepunchIntervalMin: defaultSendHolepunchIntervalMin, sendHolepunchIntervalMin: defaultSendHolepunchIntervalMin,
@@ -281,7 +283,13 @@ func (m *Manager) TriggerHolePunch() error {
// Send hole punch to all exit nodes // Send hole punch to all exit nodes
successCount := 0 successCount := 0
for _, exitNode := range currentExitNodes { for _, exitNode := range currentExitNodes {
host, err := util.ResolveDomain(exitNode.Endpoint) var host string
var err error
if len(m.publicDNS) > 0 {
host, err = util.ResolveDomainUpstream(exitNode.Endpoint, m.publicDNS)
} else {
host, err = util.ResolveDomain(exitNode.Endpoint)
}
if err != nil { if err != nil {
logger.Warn("Failed to resolve endpoint %s: %v", exitNode.Endpoint, err) logger.Warn("Failed to resolve endpoint %s: %v", exitNode.Endpoint, err)
continue continue
@@ -392,7 +400,13 @@ func (m *Manager) runMultipleExitNodes() {
var resolvedNodes []resolvedExitNode var resolvedNodes []resolvedExitNode
for _, exitNode := range currentExitNodes { for _, exitNode := range currentExitNodes {
host, err := util.ResolveDomain(exitNode.Endpoint) var host string
var err error
if len(m.publicDNS) > 0 {
host, err = util.ResolveDomainUpstream(exitNode.Endpoint, m.publicDNS)
} else {
host, err = util.ResolveDomain(exitNode.Endpoint)
}
if err != nil { if err != nil {
logger.Warn("Failed to resolve endpoint %s: %v", exitNode.Endpoint, err) logger.Warn("Failed to resolve endpoint %s: %v", exitNode.Endpoint, err)
continue continue

View File

@@ -50,6 +50,7 @@ type cachedAddr struct {
// HolepunchTester monitors holepunch connectivity using magic packets // HolepunchTester monitors holepunch connectivity using magic packets
type HolepunchTester struct { type HolepunchTester struct {
sharedBind *bind.SharedBind sharedBind *bind.SharedBind
publicDNS []string
mu sync.RWMutex mu sync.RWMutex
running bool running bool
stopChan chan struct{} stopChan chan struct{}
@@ -84,9 +85,10 @@ type pendingRequest struct {
} }
// NewHolepunchTester creates a new holepunch tester using the given SharedBind // NewHolepunchTester creates a new holepunch tester using the given SharedBind
func NewHolepunchTester(sharedBind *bind.SharedBind) *HolepunchTester { func NewHolepunchTester(sharedBind *bind.SharedBind, publicDNS []string) *HolepunchTester {
return &HolepunchTester{ return &HolepunchTester{
sharedBind: sharedBind, sharedBind: sharedBind,
publicDNS: publicDNS,
addrCache: make(map[string]*cachedAddr), addrCache: make(map[string]*cachedAddr),
addrCacheTTL: 5 * time.Minute, // Cache addresses for 5 minutes addrCacheTTL: 5 * time.Minute, // Cache addresses for 5 minutes
} }
@@ -169,7 +171,13 @@ func (t *HolepunchTester) resolveEndpoint(endpoint string) (*net.UDPAddr, error)
} }
// Resolve the endpoint // Resolve the endpoint
host, err := util.ResolveDomain(endpoint) var host string
var err error
if len(t.publicDNS) > 0 {
host, err = util.ResolveDomainUpstream(endpoint, t.publicDNS)
} else {
host, err = util.ResolveDomain(endpoint)
}
if err != nil { if err != nil {
host = endpoint host = endpoint
} }

19
main.go
View File

@@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/http/pprof"
"net/netip" "net/netip"
"os" "os"
"os/signal" "os/signal"
@@ -148,7 +147,6 @@ var (
adminAddr string adminAddr string
region string region string
metricsAsyncBytes bool metricsAsyncBytes bool
pprofEnabled bool
blueprintFile string blueprintFile string
noCloud bool noCloud bool
@@ -227,7 +225,6 @@ func runNewtMain(ctx context.Context) {
adminAddrEnv := os.Getenv("NEWT_ADMIN_ADDR") adminAddrEnv := os.Getenv("NEWT_ADMIN_ADDR")
regionEnv := os.Getenv("NEWT_REGION") regionEnv := os.Getenv("NEWT_REGION")
asyncBytesEnv := os.Getenv("NEWT_METRICS_ASYNC_BYTES") asyncBytesEnv := os.Getenv("NEWT_METRICS_ASYNC_BYTES")
pprofEnabledEnv := os.Getenv("NEWT_PPROF_ENABLED")
disableClientsEnv := os.Getenv("DISABLE_CLIENTS") disableClientsEnv := os.Getenv("DISABLE_CLIENTS")
disableClients = disableClientsEnv == "true" disableClients = disableClientsEnv == "true"
@@ -393,14 +390,6 @@ func runNewtMain(ctx context.Context) {
metricsAsyncBytes = v metricsAsyncBytes = v
} }
} }
// pprof debug endpoint toggle
if pprofEnabledEnv == "" {
flag.BoolVar(&pprofEnabled, "pprof", false, "Enable pprof debug endpoints on admin server")
} else {
if v, err := strconv.ParseBool(pprofEnabledEnv); err == nil {
pprofEnabled = v
}
}
// Optional region flag (resource attribute) // Optional region flag (resource attribute)
if regionEnv == "" { if regionEnv == "" {
flag.StringVar(&region, "region", "", "Optional region resource attribute (also NEWT_REGION)") flag.StringVar(&region, "region", "", "Optional region resource attribute (also NEWT_REGION)")
@@ -496,14 +485,6 @@ func runNewtMain(ctx context.Context) {
if tel.PrometheusHandler != nil { if tel.PrometheusHandler != nil {
mux.Handle("/metrics", tel.PrometheusHandler) mux.Handle("/metrics", tel.PrometheusHandler)
} }
if pprofEnabled {
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
logger.Info("pprof debugging enabled on %s/debug/pprof/", tcfg.AdminAddr)
}
admin := &http.Server{ admin := &http.Server{
Addr: tcfg.AdminAddr, Addr: tcfg.AdminAddr,
Handler: otelhttp.NewHandler(mux, "newt-admin"), Handler: otelhttp.NewHandler(mux, "newt-admin"),

View File

@@ -21,10 +21,7 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
) )
const ( const errUnsupportedProtoFmt = "unsupported protocol: %s"
errUnsupportedProtoFmt = "unsupported protocol: %s"
maxUDPPacketSize = 65507
)
// Target represents a proxy target with its address and port // Target represents a proxy target with its address and port
type Target struct { type Target struct {
@@ -108,10 +105,14 @@ func classifyProxyError(err error) string {
if errors.Is(err, net.ErrClosed) { if errors.Is(err, net.ErrClosed) {
return "closed" return "closed"
} }
var ne net.Error if ne, ok := err.(net.Error); ok {
if errors.As(err, &ne) && ne.Timeout() { if ne.Timeout() {
return "timeout" return "timeout"
} }
if ne.Temporary() {
return "temporary"
}
}
msg := strings.ToLower(err.Error()) msg := strings.ToLower(err.Error())
switch { switch {
case strings.Contains(msg, "refused"): case strings.Contains(msg, "refused"):
@@ -436,6 +437,14 @@ func (pm *ProxyManager) Stop() error {
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...) 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 // Give active connections a chance to close gracefully
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@@ -489,7 +498,7 @@ func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string)
if !pm.running { if !pm.running {
return return
} }
if errors.Is(err, net.ErrClosed) { if ne, ok := err.(net.Error); ok && !ne.Temporary() {
logger.Info("TCP listener closed, stopping proxy handler for %v", listener.Addr()) logger.Info("TCP listener closed, stopping proxy handler for %v", listener.Addr())
return return
} }
@@ -555,7 +564,7 @@ func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string)
} }
func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) { func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
buffer := make([]byte, maxUDPPacketSize) // Max UDP packet size buffer := make([]byte, 65507) // Max UDP packet size
clientConns := make(map[string]*net.UDPConn) clientConns := make(map[string]*net.UDPConn)
var clientsMutex sync.RWMutex var clientsMutex sync.RWMutex
@@ -574,7 +583,7 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
} }
// Check for connection closed conditions // Check for connection closed conditions
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") {
logger.Info("UDP connection closed, stopping proxy handler") logger.Info("UDP connection closed, stopping proxy handler")
// Clean up existing client connections // Clean up existing client connections
@@ -653,14 +662,10 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
telemetry.IncProxyConnectionEvent(context.Background(), tunnelID, "udp", telemetry.ProxyConnectionClosed) telemetry.IncProxyConnectionEvent(context.Background(), tunnelID, "udp", telemetry.ProxyConnectionClosed)
}() }()
buffer := make([]byte, maxUDPPacketSize) buffer := make([]byte, 65507)
for { for {
n, _, err := targetConn.ReadFromUDP(buffer) n, _, err := targetConn.ReadFromUDP(buffer)
if err != nil { if err != nil {
// Connection closed is normal during cleanup
if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) {
return // defer will handle cleanup, result stays "success"
}
logger.Error("Error reading from target: %v", err) logger.Error("Error reading from target: %v", err)
result = "failure" result = "failure"
return // defer will handle cleanup return // defer will handle cleanup

View File

@@ -1,6 +1,7 @@
package util package util
import ( import (
"context"
"encoding/base64" "encoding/base64"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
@@ -14,6 +15,99 @@ import (
"golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/device"
) )
func ResolveDomainUpstream(domain string, publicDNS []string) (string, error) {
// trim whitespace
domain = strings.TrimSpace(domain)
// Remove any protocol prefix if present (do this first, before splitting host/port)
domain = strings.TrimPrefix(domain, "http://")
domain = strings.TrimPrefix(domain, "https://")
// if there are any trailing slashes, remove them
domain = strings.TrimSuffix(domain, "/")
// 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 = ""
}
// Check if host is already an IP address (IPv4 or IPv6)
// For IPv6, the host from SplitHostPort will already have brackets stripped
// but if there was no port, we need to handle bracketed IPv6 addresses
cleanHost := strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
if ip := net.ParseIP(cleanHost); ip != nil {
// It's already an IP address, no need to resolve
ipAddr := ip.String()
if port != "" {
return net.JoinHostPort(ipAddr, port), nil
}
return ipAddr, nil
}
// Lookup IP addresses using the upstream DNS servers if provided
var ips []net.IP
if len(publicDNS) > 0 {
var lastErr error
for _, server := range publicDNS {
// Ensure the upstream DNS address has a port
dnsAddr := server
if _, _, err := net.SplitHostPort(dnsAddr); err != nil {
// No port specified, default to 53
dnsAddr = net.JoinHostPort(server, "53")
}
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, "udp", dnsAddr)
},
}
ips, lastErr = resolver.LookupIP(context.Background(), "ip", host)
if lastErr == nil {
break
}
}
if lastErr != nil {
return "", fmt.Errorf("DNS lookup failed using all upstream servers: %v", lastErr)
}
} else {
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 ResolveDomain(domain string) (string, error) { func ResolveDomain(domain string) (string, error) {
// trim whitespace // trim whitespace
domain = strings.TrimSpace(domain) domain = strings.TrimSpace(domain)