diff --git a/.github/DISCUSSION_TEMPLATE/feature-requests.yml b/.github/DISCUSSION_TEMPLATE/feature-requests.yml new file mode 100644 index 0000000..03b580c --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/feature-requests.yml @@ -0,0 +1,47 @@ +body: + - type: textarea + attributes: + label: Summary + description: A clear and concise summary of the requested feature. + validations: + required: true + + - type: textarea + attributes: + label: Motivation + description: | + Why is this feature important? + Explain the problem this feature would solve or what use case it would enable. + validations: + required: true + + - type: textarea + attributes: + label: Proposed Solution + description: | + How would you like to see this feature implemented? + Provide as much detail as possible about the desired behavior, configuration, or changes. + validations: + required: true + + - type: textarea + attributes: + label: Alternatives Considered + description: Describe any alternative solutions or workarounds you've thought about. + validations: + required: false + + - type: textarea + attributes: + label: Additional Context + description: Add any other context, mockups, or screenshots about the feature request here. + validations: + required: false + + - type: markdown + attributes: + value: | + Before submitting, please: + - Check if there is an existing issue for this feature. + - Clearly explain the benefit and use case. + - Be as specific as possible to help contributors evaluate and implement. diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml new file mode 100644 index 0000000..41dbe7b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -0,0 +1,51 @@ +name: Bug Report +description: Create a bug report +labels: [] +body: + - type: textarea + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + attributes: + label: Environment + description: Please fill out the relevant details below for your environment. + value: | + - OS Type & Version: (e.g., Ubuntu 22.04) + - Pangolin Version: + - Gerbil Version: + - Traefik Version: + - Newt Version: + - Olm Version: (if applicable) + validations: + required: true + + - type: textarea + attributes: + label: To Reproduce + description: | + Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. + + If using code blocks, make sure syntax highlighting is correct and double-check that the rendered preview is not broken. + validations: + required: true + + - type: textarea + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: markdown + attributes: + value: | + Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. + + - type: markdown + attributes: + value: | + Contributors should be able to follow the steps provided in order to reproduce the bug. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a3739c4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Need help or have questions? + url: https://github.com/orgs/fosrl/discussions + about: Ask questions, get help, and discuss with other community members + - name: Request a Feature + url: https://github.com/orgs/fosrl/discussions/new?category=feature-requests + about: Feature requests should be opened as discussions so others can upvote and comment diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 7c463f5..f51541d 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -34,7 +34,7 @@ jobs: run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: 1.25 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8fba9ae..643628b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: 1.25 diff --git a/README.md b/README.md index 6ed8a8a..118b7a4 100644 --- a/README.md +++ b/README.md @@ -36,57 +36,61 @@ When Newt receives WireGuard control messages, it will use the information encod ## CLI Args -- `id`: Newt ID generated by Pangolin to identify the client. -- `secret`: A unique secret (not shared and kept private) used to authenticate the client ID with the websocket in order to receive commands. -- `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket. +- `id`: Newt ID generated by Pangolin to identify the client. +- `secret`: A unique secret (not shared and kept private) used to authenticate the client ID with the websocket in order to receive commands. +- `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket. -- `mtu` (optional): MTU for the internal WG interface. Default: 1280 -- `dns` (optional): DNS server to use to resolve the endpoint. Default: 9.9.9.9 -- `log-level` (optional): The log level to use (DEBUG, INFO, WARN, ERROR, FATAL). Default: INFO -- `enforce-hc-cert` (optional): Enforce certificate validation for health checks. Default: false (accepts any cert) -- `docker-socket` (optional): Set the Docker socket to use the container discovery integration -- `ping-interval` (optional): Interval for pinging the server. Default: 3s -- `ping-timeout` (optional): Timeout for each ping. Default: 5s -- `updown` (optional): A script to be called when targets are added or removed. -- `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls) -- `tls-client-cert` (optional): Path to client certificate (PEM format, optional if using PKCS12). See [mTLS](#mtls) -- `tls-client-key` (optional): Path to private key for mTLS (PEM format, optional if using PKCS12) -- `tls-ca-cert` (optional): Path to CA certificate to verify server (PEM format, optional if using PKCS12) -- `docker-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process. Default: false -- `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 -- `accept-clients` (optional): Enable WireGuard server mode to accept incoming newt client connections. Default: false - - `generateAndSaveKeyTo` (optional): Path to save generated private key - - `native` (optional): Use native WireGuard interface when accepting clients (requires WireGuard kernel module and Linux, must run as root). Default: false (uses userspace netstack) - - `interface` (optional): Name of the WireGuard interface. Default: newt - - `keep-interface` (optional): Keep the WireGuard interface. Default: false +- `mtu` (optional): MTU for the internal WG interface. Default: 1280 +- `dns` (optional): DNS server to use to resolve the endpoint. Default: 9.9.9.9 +- `log-level` (optional): The log level to use (DEBUG, INFO, WARN, ERROR, FATAL). Default: INFO +- `enforce-hc-cert` (optional): Enforce certificate validation for health checks. Default: false (accepts any cert) +- `docker-socket` (optional): Set the Docker socket to use the container discovery integration +- `ping-interval` (optional): Interval for pinging the server. Default: 3s +- `ping-timeout` (optional): Timeout for each ping. Default: 5s +- `updown` (optional): A script to be called when targets are added or removed. +- `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls) +- `tls-client-cert` (optional): Path to client certificate (PEM format, optional if using PKCS12). See [mTLS](#mtls) +- `tls-client-key` (optional): Path to private key for mTLS (PEM format, optional if using PKCS12) +- `tls-ca-cert` (optional): Path to CA certificate to verify server (PEM format, optional if using PKCS12) +- `docker-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process. Default: false +- `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 +- `accept-clients` (optional): Enable WireGuard server mode to accept incoming newt client connections. Default: false + - `generateAndSaveKeyTo` (optional): Path to save generated private key + - `native` (optional): Use native WireGuard interface when accepting clients (requires WireGuard kernel module and Linux, must run as root). Default: false (uses userspace netstack) + - `interface` (optional): Name of the WireGuard interface. Default: newt + - `keep-interface` (optional): Keep the WireGuard interface. Default: false +- `blueprint-file` (optional): Path to blueprint file to define Pangolin resources and configurations. +- `no-cloud` (optional): Don't fail over to the cloud when using managed nodes in Pangolin Cloud. Default: false ## Environment Variables All CLI arguments can be set using environment variables as an alternative to command line flags. Environment variables are particularly useful when running Newt in containerized environments. -- `PANGOLIN_ENDPOINT`: Endpoint of your pangolin server (equivalent to `--endpoint`) -- `NEWT_ID`: Newt ID generated by Pangolin (equivalent to `--id`) -- `NEWT_SECRET`: Newt secret for authentication (equivalent to `--secret`) -- `MTU`: MTU for the internal WG interface. Default: 1280 (equivalent to `--mtu`) -- `DNS`: DNS server to use to resolve the endpoint. Default: 9.9.9.9 (equivalent to `--dns`) -- `LOG_LEVEL`: Log level (DEBUG, INFO, WARN, ERROR, FATAL). Default: INFO (equivalent to `--log-level`) -- `DOCKER_SOCKET`: Path to Docker socket for container discovery (equivalent to `--docker-socket`) -- `PING_INTERVAL`: Interval for pinging the server. Default: 3s (equivalent to `--ping-interval`) -- `PING_TIMEOUT`: Timeout for each ping. Default: 5s (equivalent to `--ping-timeout`) -- `UPDOWN_SCRIPT`: Path to updown script for target add/remove events (equivalent to `--updown`) -- `TLS_CLIENT_CERT`: Path to client certificate for mTLS (equivalent to `--tls-client-cert`) -- `TLS_CLIENT_CERT`: Path to client certificate for mTLS (equivalent to `--tls-client-cert`) -- `TLS_CLIENT_KEY`: Path to private key for mTLS (equivalent to `--tls-client-key`) -- `TLS_CA_CERT`: Path to CA certificate to verify server (equivalent to `--tls-ca-cert`) -- `DOCKER_ENFORCE_NETWORK_VALIDATION`: Validate container targets are on same network. Default: false (equivalent to `--docker-enforce-network-validation`) -- `ENFORCE_HC_CERT`: Enforce certificate validation for health checks. Default: false (equivalent to `--enforce-hc-cert`) -- `HEALTH_FILE`: Path to health file for connection monitoring (equivalent to `--health-file`) -- `ACCEPT_CLIENTS`: Enable WireGuard server mode. Default: false (equivalent to `--accept-clients`) -- `GENERATE_AND_SAVE_KEY_TO`: Path to save generated private key (equivalent to `--generateAndSaveKeyTo`) -- `USE_NATIVE_INTERFACE`: Use native WireGuard interface (Linux only). Default: false (equivalent to `--native`) -- `INTERFACE`: Name of the WireGuard interface. Default: newt (equivalent to `--interface`) -- `KEEP_INTERFACE`: Keep the WireGuard interface after shutdown. Default: false (equivalent to `--keep-interface`) -- `CONFIG_FILE`: Load the config json from this file instead of in the home folder. +- `PANGOLIN_ENDPOINT`: Endpoint of your pangolin server (equivalent to `--endpoint`) +- `NEWT_ID`: Newt ID generated by Pangolin (equivalent to `--id`) +- `NEWT_SECRET`: Newt secret for authentication (equivalent to `--secret`) +- `MTU`: MTU for the internal WG interface. Default: 1280 (equivalent to `--mtu`) +- `DNS`: DNS server to use to resolve the endpoint. Default: 9.9.9.9 (equivalent to `--dns`) +- `LOG_LEVEL`: Log level (DEBUG, INFO, WARN, ERROR, FATAL). Default: INFO (equivalent to `--log-level`) +- `DOCKER_SOCKET`: Path to Docker socket for container discovery (equivalent to `--docker-socket`) +- `PING_INTERVAL`: Interval for pinging the server. Default: 3s (equivalent to `--ping-interval`) +- `PING_TIMEOUT`: Timeout for each ping. Default: 5s (equivalent to `--ping-timeout`) +- `UPDOWN_SCRIPT`: Path to updown script for target add/remove events (equivalent to `--updown`) +- `TLS_CLIENT_CERT`: Path to client certificate for mTLS (equivalent to `--tls-client-cert`) +- `TLS_CLIENT_CERT`: Path to client certificate for mTLS (equivalent to `--tls-client-cert`) +- `TLS_CLIENT_KEY`: Path to private key for mTLS (equivalent to `--tls-client-key`) +- `TLS_CA_CERT`: Path to CA certificate to verify server (equivalent to `--tls-ca-cert`) +- `DOCKER_ENFORCE_NETWORK_VALIDATION`: Validate container targets are on same network. Default: false (equivalent to `--docker-enforce-network-validation`) +- `ENFORCE_HC_CERT`: Enforce certificate validation for health checks. Default: false (equivalent to `--enforce-hc-cert`) +- `HEALTH_FILE`: Path to health file for connection monitoring (equivalent to `--health-file`) +- `ACCEPT_CLIENTS`: Enable WireGuard server mode. Default: false (equivalent to `--accept-clients`) +- `GENERATE_AND_SAVE_KEY_TO`: Path to save generated private key (equivalent to `--generateAndSaveKeyTo`) +- `USE_NATIVE_INTERFACE`: Use native WireGuard interface (Linux only). Default: false (equivalent to `--native`) +- `INTERFACE`: Name of the WireGuard interface. Default: newt (equivalent to `--interface`) +- `KEEP_INTERFACE`: Keep the WireGuard interface after shutdown. Default: false (equivalent to `--keep-interface`) +- `CONFIG_FILE`: Load the config json from this file instead of in the home folder. +- `BLUEPRINT_FILE`: Path to blueprint file to define Pangolin resources and configurations. (equivalent to `--blueprint-file`) +- `NO_CLOUD`: Don't fail over to the cloud when using managed nodes in Pangolin Cloud. Default: false (equivalent to `--no-cloud`) ## Loading secrets from files diff --git a/blueprint.yaml b/blueprint.yaml new file mode 100644 index 0000000..5c979d1 --- /dev/null +++ b/blueprint.yaml @@ -0,0 +1,37 @@ +resources: + resource-nice-id: + name: this is my resource + protocol: http + full-domain: level1.test3.example.com + host-header: example.com + tls-server-name: example.com + auth: + pincode: 123456 + password: sadfasdfadsf + sso-enabled: true + sso-roles: + - Member + sso-users: + - owen@fossorial.io + whitelist-users: + - owen@fossorial.io + targets: + # - site: glossy-plains-viscacha-rat + - hostname: localhost + method: http + port: 8000 + healthcheck: + port: 8000 + hostname: localhost + # - site: glossy-plains-viscacha-rat + - hostname: localhost + method: http + port: 8001 + resource-nice-id2: + name: this is other resource + protocol: tcp + proxy-port: 3000 + targets: + # - site: glossy-plains-viscacha-rat + - hostname: localhost + port: 3000 \ No newline at end of file diff --git a/docker/client.go b/docker/client.go index 2a42023..281c594 100644 --- a/docker/client.go +++ b/docker/client.go @@ -10,6 +10,7 @@ import ( "time" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" "github.com/fosrl/newt/logger" @@ -321,3 +322,128 @@ func getHostContainer(dockerContext context.Context, dockerClient *client.Client return &hostContainer, nil } + +// EventCallback defines the function signature for handling Docker events +type EventCallback func(containers []Container) + +// EventMonitor handles Docker event monitoring +type EventMonitor struct { + client *client.Client + ctx context.Context + cancel context.CancelFunc + callback EventCallback + socketPath string + enforceNetworkValidation bool +} + +// NewEventMonitor creates a new Docker event monitor +func NewEventMonitor(socketPath string, enforceNetworkValidation bool, callback EventCallback) (*EventMonitor, error) { + if socketPath == "" { + socketPath = "unix:///var/run/docker.sock" + } + + if !strings.Contains(socketPath, "://") { + socketPath = "unix://" + socketPath + } + + cli, err := client.NewClientWithOpts( + client.WithHost(socketPath), + client.WithAPIVersionNegotiation(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create Docker client: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + + return &EventMonitor{ + client: cli, + ctx: ctx, + cancel: cancel, + callback: callback, + socketPath: socketPath, + enforceNetworkValidation: enforceNetworkValidation, + }, nil +} + +// Start begins monitoring Docker events +func (em *EventMonitor) Start() error { + logger.Debug("Starting Docker event monitoring") + + // Filter for container events we care about + eventFilters := filters.NewArgs() + eventFilters.Add("type", "container") + // eventFilters.Add("event", "create") + eventFilters.Add("event", "start") + eventFilters.Add("event", "stop") + // eventFilters.Add("event", "destroy") + // eventFilters.Add("event", "die") + // eventFilters.Add("event", "pause") + // eventFilters.Add("event", "unpause") + + // Start listening for events + eventCh, errCh := em.client.Events(em.ctx, events.ListOptions{ + Filters: eventFilters, + }) + + go func() { + defer func() { + if err := em.client.Close(); err != nil { + logger.Error("Error closing Docker client: %v", err) + } + }() + + for { + select { + case event := <-eventCh: + logger.Debug("Docker event received: %s %s for container %s", event.Action, event.Type, event.Actor.ID[:12]) + + // Fetch updated container list and trigger callback + go em.handleEvent(event) + + case err := <-errCh: + if err != nil && err != context.Canceled { + logger.Error("Docker event stream error: %v", err) + // Try to reconnect after a brief delay + time.Sleep(5 * time.Second) + if em.ctx.Err() == nil { + logger.Info("Attempting to reconnect to Docker event stream") + eventCh, errCh = em.client.Events(em.ctx, events.ListOptions{ + Filters: eventFilters, + }) + } + } + return + + case <-em.ctx.Done(): + logger.Info("Docker event monitoring stopped") + return + } + } + }() + + return nil +} + +// handleEvent processes a Docker event and triggers the callback with updated container list +func (em *EventMonitor) handleEvent(event events.Message) { + // Add a small delay to ensure Docker has fully processed the event + time.Sleep(100 * time.Millisecond) + + containers, err := ListContainers(em.socketPath, em.enforceNetworkValidation) + if err != nil { + logger.Error("Failed to list containers after Docker event %s: %v", event.Action, err) + return + } + + logger.Debug("Triggering callback with %d containers after Docker event %s", len(containers), event.Action) + em.callback(containers) +} + +// Stop stops the event monitoring +func (em *EventMonitor) Stop() { + logger.Info("Stopping Docker event monitoring") + if em.cancel != nil { + em.cancel() + } +} diff --git a/go.mod b/go.mod index 79a7b41..7c92e23 100644 --- a/go.mod +++ b/go.mod @@ -19,9 +19,11 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.0 golang.org/x/crypto v0.43.0 golang.org/x/net v0.45.0 + golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 - google.golang.org/grpc v1.76.0 + gopkg.in/yaml.v3 v3.0.1 + google.golang.org/grpc v1.76.0 gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c software.sslmate.com/src/go-pkcs12 v0.6.0 ) @@ -39,7 +41,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/btree v1.1.2 // indirect + github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect @@ -71,8 +73,12 @@ require ( golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.7.0 // indirect + golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.37.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect diff --git a/go.sum b/go.sum index 6c8c7e3..30ccea6 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 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/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.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= @@ -33,8 +35,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= @@ -145,6 +147,8 @@ golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPI golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= 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.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= @@ -161,11 +165,12 @@ golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -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/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= @@ -183,6 +188,7 @@ google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/healthcheck/healthcheck.go b/healthcheck/healthcheck.go index 9cce0f9..49ac718 100644 --- a/healthcheck/healthcheck.go +++ b/healthcheck/healthcheck.go @@ -76,7 +76,7 @@ type Monitor struct { // NewMonitor creates a new health check monitor func NewMonitor(callback StatusChangeCallback, enforceCert bool) *Monitor { - logger.Info("Creating new health check monitor with certificate enforcement: %t", enforceCert) + logger.Debug("Creating new health check monitor with certificate enforcement: %t", enforceCert) // Configure TLS settings based on certificate enforcement transport := &http.Transport{ diff --git a/key b/key new file mode 100644 index 0000000..62c22b9 --- /dev/null +++ b/key @@ -0,0 +1 @@ +oBvcoMJZXGzTZ4X+aNSCCQIjroREFBeRCs+a328xWGA= \ No newline at end of file diff --git a/main.go b/main.go index 83f7524..90b687b 100644 --- a/main.go +++ b/main.go @@ -79,6 +79,11 @@ type ExitNodePingResult struct { WasPreviouslyConnected bool `json:"wasPreviouslyConnected"` } +type BlueprintResult struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + // Custom flag type for multiple CA files type stringSlice []string @@ -137,6 +142,8 @@ var ( adminAddr string region string metricsAsyncBytes bool + blueprintFile string + noCloud bool // New mTLS configuration variables tlsClientCert string @@ -175,10 +182,12 @@ func main() { asyncBytesEnv := os.Getenv("NEWT_METRICS_ASYNC_BYTES") keepInterface = keepInterfaceEnv == "true" + acceptClientsEnv := os.Getenv("ACCEPT_CLIENTS") acceptClients = acceptClientsEnv == "true" + useNativeInterfaceEnv := os.Getenv("USE_NATIVE_INTERFACE") useNativeInterface = useNativeInterfaceEnv == "true" + enforceHealthcheckCertEnv := os.Getenv("ENFORCE_HC_CERT") enforceHealthcheckCert = enforceHealthcheckCertEnv == "true" - dockerSocket = os.Getenv("DOCKER_SOCKET") pingIntervalStr := os.Getenv("PING_INTERVAL") pingTimeoutStr := os.Getenv("PING_TIMEOUT") @@ -202,9 +211,12 @@ func main() { // Legacy PKCS12 support (deprecated) tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT_PKCS12") // Keep backward compatibility with old environment variable name - if tlsPrivateKey == "" { + if tlsPrivateKey == "" && tlsClientKey == "" && len(tlsClientCAs) == 0 { tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") } + blueprintFile = os.Getenv("BLUEPRINT_FILE") + noCloudEnv := os.Getenv("NO_CLOUD") + noCloud = noCloudEnv == "true" if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -304,6 +316,12 @@ func main() { if healthFile == "" { flag.StringVar(&healthFile, "health-file", "", "Path to health file (if unset, health file won't be written)") } + if blueprintFile == "" { + flag.StringVar(&blueprintFile, "blueprint-file", "", "Path to blueprint file (if unset, no blueprint will be applied)") + } + if noCloudEnv == "" { + flag.BoolVar(&noCloud, "no-cloud", false, "Disable cloud failover") + } // Metrics/observability flags (mirror ENV if unset) if metricsEnabledEnv == "" { @@ -520,6 +538,7 @@ func main() { var pm *proxy.ProxyManager var connected bool var wgData WgData + var dockerEventMonitor *docker.EventMonitor if acceptClients { setupClients(client) @@ -585,7 +604,7 @@ func main() { // Register handlers for different message types client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) { - logger.Info("Received registration message") + logger.Debug("Received registration message") regResult := "success" defer func() { telemetry.IncSiteRegistration(ctx, regResult) @@ -693,7 +712,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Warn("Initial reliable ping failed, but continuing: %v", err) regResult = "failure" } else { - logger.Info("Initial connection test successful") + logger.Debug("Initial connection test successful") } pingWithRetryStopChan, _ = pingWithRetry(tnet, wgData.ServerIP, pingTimeout) @@ -735,7 +754,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub if err := healthMonitor.AddTargets(wgData.HealthCheckTargets); err != nil { logger.Error("Failed to bulk add health check targets: %v", err) } else { - logger.Info("Successfully added %d health check targets", len(wgData.HealthCheckTargets)) + logger.Debug("Successfully added %d health check targets", len(wgData.HealthCheckTargets)) } err = pm.Start() @@ -768,7 +787,9 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } // Request exit nodes from the server - stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) + stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{ + "noCloud": noCloud, + }, 3*time.Second) logger.Info("Tunnel destroyed, ready for reconnection") }) @@ -794,7 +815,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub }) client.RegisterHandler("newt/ping/exitNodes", func(msg websocket.WSMessage) { - logger.Info("Received ping message") + logger.Debug("Received ping message") if stopFunc != nil { stopFunc() // stop the ws from sending more requests stopFunc = nil // reset stopFunc to nil to avoid double stopping @@ -1085,7 +1106,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub if err != nil { logger.Error("Failed to send Docker socket check response: %v", err) } else { - logger.Info("Docker socket check response sent: available=%t", isAvailable) + logger.Debug("Docker socket check response sent: available=%t", isAvailable) } }) @@ -1116,7 +1137,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub if err != nil { logger.Error("Failed to send Docker container list: %v", err) } else { - logger.Info("Docker container list sent, count: %d", len(containers)) + logger.Debug("Docker container list sent, count: %d", len(containers)) } }) @@ -1232,7 +1253,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub if err := healthMonitor.AddTargets(config.Targets); err != nil { logger.Error("Failed to add health check targets: %v", err) } else { - logger.Info("Added %d health check targets", len(config.Targets)) + logger.Debug("Added %d health check targets", len(config.Targets)) } logger.Debug("Health check targets added: %+v", config.Targets) @@ -1340,6 +1361,29 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) + // Register handler for getting health check status + client.RegisterHandler("newt/blueprint/results", func(msg websocket.WSMessage) { + logger.Debug("Received blueprint results message") + + var blueprintResult BlueprintResult + + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Info("Error marshaling data: %v", err) + return + } + if err := json.Unmarshal(jsonData, &blueprintResult); err != nil { + logger.Info("Error unmarshaling config results data: %v", err) + return + } + + if blueprintResult.Success { + logger.Debug("Blueprint applied successfully!") + } else { + logger.Warn("Blueprint application failed: %s", blueprintResult.Message) + } + }) + client.OnConnect(func() error { publicKey = privateKey.PublicKey() logger.Debug("Public key: %s", publicKey) @@ -1350,9 +1394,11 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub if stopFunc != nil { stopFunc() } - // request from the server the list of nodes to ping at newt/ping/request - stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second) - logger.Info("Requesting exit nodes from server") + // request from the server the list of nodes to ping + stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{ + "noCloud": noCloud, + }, 3*time.Second) + logger.Debug("Requesting exit nodes from server") clientsOnConnect() } @@ -1363,6 +1409,8 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub "backwardsCompatible": true, }) + sendBlueprint(client) + if err != nil { logger.Error("Failed to send registration message: %v", err) return err @@ -1377,6 +1425,34 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } defer client.Close() + // Initialize Docker event monitoring if Docker socket is available and monitoring is enabled + if dockerSocket != "" { + logger.Debug("Initializing Docker event monitoring") + dockerEventMonitor, err = docker.NewEventMonitor(dockerSocket, dockerEnforceNetworkValidationBool, func(containers []docker.Container) { + // Send updated container list via websocket when Docker events occur + logger.Debug("Docker event detected, sending updated container list (%d containers)", len(containers)) + err := client.SendMessage("newt/socket/containers", map[string]interface{}{ + "containers": containers, + }) + if err != nil { + logger.Error("Failed to send updated container list after Docker event: %v", err) + } else { + logger.Debug("Updated container list sent successfully") + } + }) + + if err != nil { + logger.Error("Failed to create Docker event monitor: %v", err) + } else { + err = dockerEventMonitor.Start() + if err != nil { + logger.Error("Failed to start Docker event monitoring: %v", err) + } else { + logger.Debug("Docker event monitoring started successfully") + } + } + } + // Wait for interrupt signal sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) @@ -1385,6 +1461,10 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub // Close clients first (including WGTester) closeClients() + if dockerEventMonitor != nil { + dockerEventMonitor.Stop() + } + if healthMonitor != nil { healthMonitor.Stop() } diff --git a/util.go b/util.go index 9f0d268..dc48f19 100644 --- a/util.go +++ b/util.go @@ -23,6 +23,7 @@ import ( "golang.org/x/net/ipv4" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun/netstack" + "gopkg.in/yaml.v3" ) const msgHealthFileWriteFailed = "Failed to write health file: %v" @@ -574,3 +575,47 @@ func executeUpdownScript(action, proto, target string) (string, error) { return target, nil } + +func sendBlueprint(client *websocket.Client) error { + if blueprintFile == "" { + return nil + } + // try to read the blueprint file + blueprintData, err := os.ReadFile(blueprintFile) + if err != nil { + logger.Error("Failed to read blueprint file: %v", err) + } else { + // first we should convert the yaml to json and error if the yaml is bad + var yamlObj interface{} + var blueprintJsonData string + + err = yaml.Unmarshal(blueprintData, &yamlObj) + if err != nil { + logger.Error("Failed to parse blueprint YAML: %v", err) + } else { + // convert to json + jsonBytes, err := json.Marshal(yamlObj) + if err != nil { + logger.Error("Failed to convert blueprint to JSON: %v", err) + } else { + blueprintJsonData = string(jsonBytes) + logger.Debug("Converted blueprint to JSON: %s", blueprintJsonData) + } + } + + // if we have valid json data, we can send it to the server + if blueprintJsonData == "" { + logger.Error("No valid blueprint JSON data to send to server") + return nil + } + + logger.Info("Sending blueprint to server for application") + + // send the blueprint data to the server + err = client.SendMessage("newt/blueprint/apply", map[string]interface{}{ + "blueprint": blueprintJsonData, + }) + } + + return nil +} diff --git a/websocket/client.go b/websocket/client.go index 8af3be9..a3ba757 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -45,6 +45,7 @@ type Client struct { tlsConfig TLSConfig metricsCtxMu sync.RWMutex metricsCtx context.Context + configNeedsSave bool // Flag to track if config needs to be saved } type ClientOption func(*Client) diff --git a/websocket/config.go b/websocket/config.go index 6803e81..72c9164 100644 --- a/websocket/config.go +++ b/websocket/config.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" "runtime" + + "github.com/fosrl/newt/logger" ) func getConfigPath(clientType string) string { @@ -33,14 +35,25 @@ func getConfigPath(clientType string) string { } func (c *Client) loadConfig() error { + originalConfig := *c.config // Store original config to detect changes + configPath := getConfigPath(c.clientType) + if c.config.ID != "" && c.config.Secret != "" && c.config.Endpoint != "" { + logger.Debug("Config already provided, skipping loading from file") + // Check if config file exists, if not, we should save it + if _, err := os.Stat(configPath); os.IsNotExist(err) { + logger.Info("Config file does not exist at %s, will create it", configPath) + c.configNeedsSave = true + } return nil } - configPath := getConfigPath(c.clientType) + logger.Info("Loading config from: %s", configPath) data, err := os.ReadFile(configPath) if err != nil { if os.IsNotExist(err) { + logger.Info("Config file does not exist at %s, will create it with provided values", configPath) + c.configNeedsSave = true return nil } return err @@ -51,6 +64,12 @@ func (c *Client) loadConfig() error { return err } + // Track what was loaded from file vs provided by CLI + fileHadID := c.config.ID == "" + fileHadSecret := c.config.Secret == "" + fileHadCert := c.config.TlsClientCert == "" + fileHadEndpoint := c.config.Endpoint == "" + if c.config.ID == "" { c.config.ID = config.ID } @@ -65,14 +84,37 @@ func (c *Client) loadConfig() error { c.baseURL = config.Endpoint } + // Check if CLI args provided values that override file values + if (!fileHadID && originalConfig.ID != "") || + (!fileHadSecret && originalConfig.Secret != "") || + (!fileHadCert && originalConfig.TlsClientCert != "") || + (!fileHadEndpoint && originalConfig.Endpoint != "") { + logger.Info("CLI arguments provided, config will be updated") + c.configNeedsSave = true + } + + logger.Debug("Loaded config from %s", configPath) + logger.Debug("Config: %+v", c.config) + return nil } func (c *Client) saveConfig() error { + if !c.configNeedsSave { + logger.Debug("Config has not changed, skipping save") + return nil + } + configPath := getConfigPath(c.clientType) data, err := json.MarshalIndent(c.config, "", " ") if err != nil { return err } - return os.WriteFile(configPath, data, 0644) + + logger.Info("Saving config to: %s", configPath) + err = os.WriteFile(configPath, data, 0644) + if err == nil { + c.configNeedsSave = false // Reset flag after successful save + } + return err } diff --git a/wg/wg.go b/wg/wg.go index 0ab1919..4b9e7f7 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -156,6 +156,7 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str } var key wgtypes.Key + var port uint16 // if generateAndSaveKeyTo is provided, generate a private key and save it to the file. if the file already exists, load the key from the file key, err = wgtypes.GeneratePrivateKey() if err != nil { @@ -181,40 +182,43 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str } } - service := &WireGuardService{ - interfaceName: interfaceName, - mtu: mtu, - client: wsClient, - wgClient: wgClient, - key: key, - keyFilePath: generateAndSaveKeyTo, - newtId: newtId, - host: host, - lastReadings: make(map[string]PeerReading), - stopHolepunch: make(chan struct{}), - } - - // Get the existing wireguard port (keep this part) - device, err := service.wgClient.Device(service.interfaceName) + // Get the existing wireguard port + device, err := wgClient.Device(interfaceName) if err == nil { - service.Port = uint16(device.ListenPort) - if service.Port != 0 { - logger.Info("WireGuard interface %s already exists with port %d\n", service.interfaceName, service.Port) + port = uint16(device.ListenPort) + // also set the private key to the existing key + key = device.PrivateKey + if port != 0 { + logger.Info("WireGuard interface %s already exists with port %d\n", interfaceName, port) } else { - service.Port, err = FindAvailableUDPPort(49152, 65535) + port, err = FindAvailableUDPPort(49152, 65535) if err != nil { fmt.Printf("Error finding available port: %v\n", err) return nil, err } } } else { - service.Port, err = FindAvailableUDPPort(49152, 65535) + port, err = FindAvailableUDPPort(49152, 65535) if err != nil { fmt.Printf("Error finding available port: %v\n", err) return nil, err } } + service := &WireGuardService{ + interfaceName: interfaceName, + mtu: mtu, + client: wsClient, + wgClient: wgClient, + key: key, + Port: port, + keyFilePath: generateAndSaveKeyTo, + newtId: newtId, + host: host, + lastReadings: make(map[string]PeerReading), + stopHolepunch: make(chan struct{}), + } + // Register websocket handlers wsClient.RegisterHandler("newt/wg/receive-config", service.handleConfig) wsClient.RegisterHandler("newt/wg/peer/add", service.handleAddPeer) @@ -979,22 +983,30 @@ func (s *WireGuardService) encryptPayload(payload []byte) (interface{}, error) { } func (s *WireGuardService) keepSendingUDPHolePunch(host string) { + logger.Info("Starting UDP hole punch routine to %s:21820", host) + // send initial hole punch if err := s.sendUDPHolePunch(host + ":21820"); err != nil { - logger.Error("Failed to send initial UDP hole punch: %v", err) + logger.Debug("Failed to send initial UDP hole punch: %v", err) } ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() + timeout := time.NewTimer(15 * time.Second) + defer timeout.Stop() + for { select { case <-s.stopHolepunch: logger.Info("Stopping UDP holepunch") return + case <-timeout.C: + logger.Info("UDP holepunch routine timed out after 15 seconds") + return case <-ticker.C: if err := s.sendUDPHolePunch(host + ":21820"); err != nil { - logger.Error("Failed to send UDP hole punch: %v", err) + logger.Debug("Failed to send UDP hole punch: %v", err) } } } diff --git a/wgnetstack/wgnetstack.go b/wgnetstack/wgnetstack.go index dd7d493..664d1f0 100644 --- a/wgnetstack/wgnetstack.go +++ b/wgnetstack/wgnetstack.go @@ -190,6 +190,13 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str // Load or generate private key if generateAndSaveKeyTo != "" { if _, err := os.Stat(generateAndSaveKeyTo); os.IsNotExist(err) { + // File doesn't exist, save the generated key + err = os.WriteFile(generateAndSaveKeyTo, []byte(key.String()), 0600) + if err != nil { + return nil, fmt.Errorf("failed to save private key: %v", err) + } + } else { + // File exists, read the existing key keyData, err := os.ReadFile(generateAndSaveKeyTo) if err != nil { return nil, fmt.Errorf("failed to read private key: %v", err) @@ -198,11 +205,6 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str if err != nil { return nil, fmt.Errorf("failed to parse private key: %v", err) } - } else { - err = os.WriteFile(generateAndSaveKeyTo, []byte(key.String()), 0600) - if err != nil { - return nil, fmt.Errorf("failed to save private key: %v", err) - } } } @@ -1083,11 +1085,17 @@ func (s *WireGuardService) keepSendingUDPHolePunch(host string) { ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() + timeout := time.NewTimer(15 * time.Second) + defer timeout.Stop() + for { select { case <-s.stopHolepunch: logger.Info("Stopping UDP holepunch") return + case <-timeout.C: + logger.Info("UDP holepunch routine timed out after 15 seconds") + return case <-ticker.C: if err := s.sendUDPHolePunch(host + ":21820"); err != nil { logger.Debug("Failed to send UDP hole punch: %v", err)