diff --git a/Dockerfile b/Dockerfile index b25ebe3f..c54568ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24.4-alpine AS builder +FROM golang:1.24.5-alpine AS builder # Set the working directory inside the container WORKDIR /app diff --git a/README.md b/README.md index 9d880967..dd776f7e 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ When Newt receives WireGuard control messages, it will use the information encod - `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-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process +- `health-file` (optional): Check if connection to WG server (pangolin) is ok. creates a file if ok, removes it if not ok. Can be used with docker healtcheck to restart newt - Example: @@ -61,7 +62,8 @@ services: environment: - PANGOLIN_ENDPOINT=https://example.com - NEWT_ID=2ix2t8xk22ubpfy - - NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2 + - NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2 + - HEALTH_FILE=/tmp/healthy ``` You can also pass the CLI args to the container: @@ -76,6 +78,7 @@ services: - --id 31frd0uzbjvp721 - --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 - --endpoint https://example.com + - --health-file /tmp/healthy ``` ### Docker Socket Integration diff --git a/docker/client.go b/docker/client.go index 6a3bdb74..226e0995 100644 --- a/docker/client.go +++ b/docker/client.go @@ -26,6 +26,8 @@ type Container struct { Labels map[string]string `json:"labels"` Created int64 `json:"created"` Networks map[string]Network `json:"networks"` + Hostname string `json:"hostname"` // added to use hostname if available instead of network address + } // Port represents a port mapping for a Docker container @@ -173,6 +175,14 @@ func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Contain // Short ID like docker ps shortId := c.ID[:12] + // Inspect container to get hostname + hostname := "" + containerInfo, err := cli.ContainerInspect(ctx, c.ID) + if err == nil && containerInfo.Config != nil { + hostname = containerInfo.Config.Hostname + } + + // Skip host container if set if hostContainerId != "" && c.ID == hostContainerId { continue @@ -238,6 +248,7 @@ func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Contain Labels: c.Labels, Created: c.Created, Networks: networks, + Hostname: hostname, // added } dockerContainers = append(dockerContainers, dockerContainer) diff --git a/get-newt.sh b/get-newt.sh new file mode 100644 index 00000000..d57f69a2 --- /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/fosrl/newt/refs/heads/main/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/go.mod b/go.mod index 7e2fa2d6..739d4fdf 100644 --- a/go.mod +++ b/go.mod @@ -5,16 +5,16 @@ go 1.23.1 toolchain go1.23.2 require ( + github.com/docker/docker v28.3.2+incompatible github.com/google/gopacket v1.1.19 - github.com/docker/docker v28.3.0+incompatible github.com/gorilla/websocket v1.5.3 - github.com/vishvananda/netlink v1.3.0 - golang.org/x/crypto v0.39.0 + github.com/vishvananda/netlink v1.3.1 + golang.org/x/crypto v0.40.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.org/x/net v0.42.0 + 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-20230927004350-cbd86285d259 + gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c software.sslmate.com/src/go-pkcs12 v0.5.0 ) @@ -22,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 @@ -43,7 +42,7 @@ 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 + github.com/vishvananda/netns v0.0.5 // 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 @@ -52,7 +51,7 @@ require ( go.opentelemetry.io/otel/trace v1.36.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/sys v0.34.0 // indirect 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 diff --git a/go.sum b/go.sum index 8113aeaf..33ff3b15 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,29 @@ +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/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA= +github.com/docker/docker v28.3.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.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= @@ -27,20 +36,33 @@ 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= +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/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= @@ -48,44 +70,106 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw 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= +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.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/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= -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= +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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= 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 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= -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= diff --git a/linux.go b/linux.go new file mode 100644 index 00000000..790f6349 --- /dev/null +++ b/linux.go @@ -0,0 +1,83 @@ +//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 { + return + } + wgService.SetServerPubKey(publicKey) +} + +func clientsOnConnect() { + 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 0a4fdf39..b84ae8b0 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "encoding/json" "flag" "fmt" + "net" "net/http" "net/netip" "os" @@ -19,8 +20,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" @@ -88,8 +87,8 @@ var ( dockerSocket string dockerEnforceNetworkValidation string dockerEnforceNetworkValidationBool bool - pingInterval = 1 * time.Second - pingTimeout = 2 * time.Second + pingInterval time.Duration + pingTimeout time.Duration publicKey wgtypes.Key pingStopChan chan struct{} stopFunc func() @@ -152,26 +151,30 @@ func main() { 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)") + flag.StringVar(&pingIntervalStr, "ping-interval", "3s", "Interval for pinging the server (default 3s)") } if pingTimeoutStr == "" { - flag.StringVar(&pingTimeoutStr, "ping-timeout", "2s", " Timeout for each ping (default 2s)") + flag.StringVar(&pingTimeoutStr, "ping-timeout", "5s", " Timeout for each ping (default 3s)") } 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 + fmt.Printf("Invalid PING_INTERVAL value: %s, using default 3 seconds\n", pingIntervalStr) + pingInterval = 3 * time.Second } + } else { + pingInterval = 3 * time.Second } 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 + fmt.Printf("Invalid PING_TIMEOUT value: %s, using default 5 seconds\n", pingTimeoutStr) + pingTimeout = 5 * time.Second } + } else { + pingTimeout = 5 * time.Second } if dockerEnforceNetworkValidation == "" { @@ -255,14 +258,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,33 +272,39 @@ 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://") + setupClients(client) + } + + var pingWithRetryStopChan chan struct{} + + closeWgTunnel := func() { + if pingStopChan != nil { + // Stop the ping check + close(pingStopChan) + pingStopChan = nil } - host = strings.TrimSuffix(host, "/") + // Stop proxy manager if running + if pm != nil { + pm.Stop() + pm = nil + } - // 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) + // Close WireGuard device first - this will automatically close the TUN device + if dev != nil { + dev.Close() + dev = nil } - 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() + // 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 } - } - var pingWithRetryStopChan chan struct{} + } // Register handlers for different message types client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) { @@ -308,33 +315,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 - } + // Mark as disconnected - // 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 - } + closeWgTunnel() - // Mark as disconnected connected = false } @@ -349,11 +333,9 @@ func main() { return } - if wgService != nil { - wgService.SetServerPubKey(wgData.PublicKey) - } + 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)}, @@ -368,6 +350,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) @@ -392,7 +382,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 { @@ -400,12 +390,21 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub close(pingWithRetryStopChan) pingWithRetryStopChan = nil } + // Use reliable ping for initial connection test + logger.Debug("Testing initial connection with reliable ping...") + _, err = reliablePing(tnet, wgData.ServerIP, pingTimeout, 5) + if err != nil { + logger.Warn("Initial reliable ping failed, but continuing: %v", err) + } else { + logger.Info("Initial connection test successful!") + } + 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 if !connected { - logger.Info("Starting ping check") + logger.Debug("Starting ping check") pingStopChan = startPingCheck(tnet, wgData.ServerIP, client) } @@ -423,12 +422,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 { @@ -439,36 +433,12 @@ 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 - // 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 @@ -480,6 +450,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") + }) + client.RegisterHandler("newt/ping/exitNodes", func(msg websocket.WSMessage) { logger.Info("Received ping message") if stopFunc != nil { @@ -581,11 +563,12 @@ 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) { - 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 { @@ -718,7 +701,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 { @@ -729,14 +712,13 @@ 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) - - if wgService != nil { - wgService.LoadRemoteConfig() - } + logger.Info("Requesting exit nodes from server") + clientsOnConnect() } // Send registration message to the server for backward compatibility @@ -750,17 +732,10 @@ 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 }) - 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 +749,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/proxy/manager.go b/proxy/manager.go index 0792acbb..35d023a3 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/stub.go b/stub.go new file mode 100644 index 00000000..e2360ff2 --- /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/util.go b/util.go index c0f634cf..7d6da4ff 100644 --- a/util.go +++ b/util.go @@ -45,9 +45,17 @@ func ping(tnet *netstack.Net, dst string, timeout time.Duration) (time.Duration, } defer socket.Close() + // Set socket buffer sizes to handle high bandwidth scenarios + if tcpConn, ok := socket.(interface{ SetReadBuffer(int) error }); ok { + tcpConn.SetReadBuffer(64 * 1024) + } + if tcpConn, ok := socket.(interface{ SetWriteBuffer(int) error }); ok { + tcpConn.SetWriteBuffer(64 * 1024) + } + requestPing := icmp.Echo{ Seq: rand.Intn(1 << 16), - Data: []byte("f"), + Data: []byte("newtping"), } icmpBytes, err := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil) @@ -65,12 +73,14 @@ func ping(tnet *netstack.Net, dst string, timeout time.Duration) (time.Duration, return 0, fmt.Errorf("failed to write ICMP packet: %w", err) } - n, err := socket.Read(icmpBytes[:]) + // Use larger buffer for reading to handle potential network congestion + readBuffer := make([]byte, 1500) + n, err := socket.Read(readBuffer) if err != nil { return 0, fmt.Errorf("failed to read ICMP packet: %w", err) } - replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n]) + replyPacket, err := icmp.ParseMessage(1, readBuffer[:n]) if err != nil { return 0, fmt.Errorf("failed to parse ICMP packet: %w", err) } @@ -92,6 +102,51 @@ func ping(tnet *netstack.Net, dst string, timeout time.Duration) (time.Duration, return latency, nil } +// reliablePing performs multiple ping attempts with adaptive timeout +func reliablePing(tnet *netstack.Net, dst string, baseTimeout time.Duration, maxAttempts int) (time.Duration, error) { + var lastErr error + var totalLatency time.Duration + successCount := 0 + + for attempt := 1; attempt <= maxAttempts; attempt++ { + // Adaptive timeout: increase timeout for later attempts + timeout := baseTimeout + time.Duration(attempt-1)*500*time.Millisecond + + // Add jitter to prevent thundering herd + jitter := time.Duration(rand.Intn(100)) * time.Millisecond + timeout += jitter + + latency, err := ping(tnet, dst, timeout) + if err != nil { + lastErr = err + logger.Debug("Ping attempt %d/%d failed: %v", attempt, maxAttempts, err) + + // Brief pause between attempts with exponential backoff + if attempt < maxAttempts { + backoff := time.Duration(attempt) * 50 * time.Millisecond + time.Sleep(backoff) + } + continue + } + + totalLatency += latency + successCount++ + + // If we get at least one success, we can return early for health checks + if successCount > 0 { + avgLatency := totalLatency / time.Duration(successCount) + logger.Debug("Reliable ping succeeded after %d attempts, avg latency: %v", attempt, avgLatency) + return avgLatency, nil + } + } + + if successCount == 0 { + return 0, fmt.Errorf("all %d ping attempts failed, last error: %v", maxAttempts, lastErr) + } + + return totalLatency / time.Duration(successCount), nil +} + func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) (stopChan chan struct{}, err error) { if healthFile != "" { @@ -112,10 +167,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 +192,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 +210,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) @@ -175,12 +230,14 @@ func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) (stopC } func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Client) chan struct{} { - initialInterval := pingInterval - maxInterval := 3 * time.Second - currentInterval := initialInterval + maxInterval := 6 * time.Second + currentInterval := pingInterval consecutiveFailures := 0 connectionLost := false + // Track recent latencies for adaptive timeout calculation + recentLatencies := make([]time.Duration, 0, 10) + pingStopChan := make(chan struct{}) go func() { @@ -189,14 +246,52 @@ func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Clien for { select { case <-ticker.C: - _, err := ping(tnet, serverIP, pingTimeout) + // Calculate adaptive timeout based on recent latencies + adaptiveTimeout := pingTimeout + if len(recentLatencies) > 0 { + var sum time.Duration + for _, lat := range recentLatencies { + sum += lat + } + avgLatency := sum / time.Duration(len(recentLatencies)) + // Use 3x average latency as timeout, with minimum of pingTimeout + adaptiveTimeout = avgLatency * 3 + if adaptiveTimeout < pingTimeout { + adaptiveTimeout = pingTimeout + } + if adaptiveTimeout > 15*time.Second { + adaptiveTimeout = 15 * time.Second + } + } + + // Use reliable ping with multiple attempts + maxAttempts := 2 + if consecutiveFailures > 4 { + maxAttempts = 4 // More attempts when connection is unstable + } + + latency, err := reliablePing(tnet, serverIP, adaptiveTimeout, maxAttempts) if err != nil { consecutiveFailures++ - logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) - if consecutiveFailures >= 3 && currentInterval < maxInterval { + + // Track recent latencies (add a high value for failures) + recentLatencies = append(recentLatencies, adaptiveTimeout) + if len(recentLatencies) > 10 { + recentLatencies = recentLatencies[1:] + } + + if consecutiveFailures < 2 { + logger.Debug("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) + } else { + logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) + } + + // More lenient threshold for declaring connection lost under load + failureThreshold := 4 + if consecutiveFailures >= failureThreshold && currentInterval < maxInterval { if !connectionLost { connectionLost = true - logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") + logger.Warn("Connection to server lost after %d failures. Continuous reconnection attempts will be made.", consecutiveFailures) 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{}{ @@ -213,7 +308,7 @@ func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Clien } } } - currentInterval = time.Duration(float64(currentInterval) * 1.5) + currentInterval = time.Duration(float64(currentInterval) * 1.3) // Slower increase if currentInterval > maxInterval { currentInterval = maxInterval } @@ -221,9 +316,15 @@ func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Clien logger.Debug("Increased ping check interval to %v due to consecutive failures", currentInterval) } } else { + // Track recent latencies + recentLatencies = append(recentLatencies, latency) + if len(recentLatencies) > 10 { + recentLatencies = recentLatencies[1:] + } + if connectionLost { connectionLost = false - logger.Info("Connection to server restored!") + logger.Info("Connection to server restored after %d failures!", consecutiveFailures) if healthFile != "" { err := os.WriteFile(healthFile, []byte("ok"), 0644) if err != nil { @@ -231,13 +332,13 @@ func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Clien } } } - if currentInterval > initialInterval { - currentInterval = time.Duration(float64(currentInterval) * 0.8) - if currentInterval < initialInterval { - currentInterval = initialInterval + if currentInterval > pingInterval { + currentInterval = time.Duration(float64(currentInterval) * 0.9) // Slower decrease + if currentInterval < pingInterval { + currentInterval = pingInterval } ticker.Reset(currentInterval) - logger.Info("Decreased ping check interval to %v after successful ping", currentInterval) + logger.Debug("Decreased ping check interval to %v after successful ping", currentInterval) } consecutiveFailures = 0 } diff --git a/websocket/client.go b/websocket/client.go index a3538c0f..98f07e6c 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 } @@ -126,6 +141,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 +199,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 +255,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 +284,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) @@ -401,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 + } } } } @@ -415,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 { @@ -426,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() @@ -446,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) { diff --git a/websocket/config.go b/websocket/config.go index e2b0055a..fe11c5a7 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 0ea24fca..54d33f11 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"` } diff --git a/wg/wg.go b/wg/wg.go index cc86d570..1c378ead 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -1,3 +1,5 @@ +//go:build linux + package wg import (