Compare commits

..

620 Commits

Author SHA1 Message Date
Owen Schwartz
0da8a29a61 Merge pull request #346 from fosrl/dev
Fix redirect
2026-05-08 11:06:31 -07:00
Owen
a1218ab67a Bump version 2026-05-08 11:05:24 -07:00
Owen
bb84762b16 Merge branch 'main' into dev 2026-05-08 11:05:07 -07:00
Owen
86155072de Fix the redirect 2026-05-08 11:03:00 -07:00
Owen Schwartz
4a9a4c4eec Merge pull request #345 from LaurenceJJones/investigate/https-permanent-redirect-loop
fix(http): populate Request.TLS for private HTTPS via httpConnCtx
2026-05-08 09:48:22 -07:00
Owen Schwartz
21c744fe84 Merge pull request #344 from LaurenceJJones/investigate/private-http-redirect-500
fix(http): Set host header based on in
2026-05-08 09:47:09 -07:00
Laurence
146e7835eb fix(http): populate Request.TLS for private HTTPS via httpConnCtx
net/http only sets Request.TLS for *tls.Conn or conns implementing ConnectionState(). Our listener wrapped tls.Server in httpConnCtx with an embedded net.Conn, so TLS was never surfaced and r.TLS stayed nil. That triggered the HTTP→HTTPS permanent redirect on every request for HTTPS rules.

Add ConnectionState() on httpConnCtx delegating to the underlying TLS conn.
Add tests for TLS forwarding and plain TCP.
2026-05-08 15:17:31 +01:00
Laurence
6aa94c0c2a fix(http): Set host header based on in
fix https://github.com/fosrl/pangolin/issues/2952 issue by setting the incoming host header to the outgoing one by the reverse proxy, this was the default behaviour when using single proxy but now since we use more features it now rewrites the host header
2026-05-08 13:45:50 +01:00
Owen Schwartz
542c70b326 Merge pull request #342 from fosrl/dev
1.12.4
2026-05-07 17:41:03 -07:00
Owen
663e98af60 Retry interval while we are disconnected 2026-05-07 17:27:01 -07:00
Owen
901ec71baf Increase max attempts 2026-05-07 17:25:13 -07:00
Owen
9bc0204f57 Merge branch 'main' into dev 2026-05-07 17:24:34 -07:00
Daniel Snider
1e77b09e3b fix(ping): decouple data-plane recovery trigger from backoff ramp
The trigger condition that decides whether to fire the data-plane
recovery flow in startPingCheck was AND-ed with `currentInterval <
maxInterval`. That clause was meant to throttle the *backoff ramp*
(don't widen the interval past 6s), but it also gated the recovery
trigger itself — a conflation that became invisibly load-bearing
once commit 8161fa6 (March 2026) bumped the default pingInterval
from 3s to 15s while leaving maxInterval at 6s. Under the new
defaults `currentInterval` starts at 15s and `15 < 6` is permanently
false, so the recovery branch never executed. Pings just kept
failing and the failure counter climbed forever, with no
"Connection to server lost" log line and no newt/ping/request
emitted on the websocket. Real-world recovery only happened when
the underlying network came back fast enough that a periodic ping
naturally succeeded again — which doesn't happen if the WireGuard
state on either end has rotated, so users were left stuck until
they restarted newt.

This is the proximate cause of the user reports in
fosrl/newt#284 (and dups #310, fosrl/pangolin#1004). Logs in
those issues all show ping-failure counters growing without ever
emitting "Connection to server lost", which is exactly the
fingerprint of this gate being false.

The fix is to extract the trigger decision into shouldFireRecovery
and remove currentInterval from it. Backoff is now computed in a
separate `if` in the caller, still gated by `currentInterval <
maxInterval` so the ramp is a no-op under default settings (which
is the existing behaviour, just no longer entangled with the
recovery trigger). Fixing the backoff ramp itself — making it
useful when pingInterval >= maxInterval — is a follow-up: the
priority is restoring recovery, not improving the dampening
schedule.

The new shouldFireRecovery helper is unit-tested. Its signature
intentionally omits currentInterval, so a future refactor that
re-introduces the interval-dependent gate would need to change
the function signature, which makes the historical bug harder
to reintroduce silently.
2026-05-07 16:57:31 -07:00
Owen
74fd3f3aa3 Bump version 2026-05-07 16:24:30 -07:00
Owen
e8dc19a62b Attempt to fix nix issue 2026-05-07 16:23:59 -07:00
Owen
9ff32b8a8b Fix not logging when rewriting nat 2026-05-07 16:16:47 -07:00
dependabot[bot]
9edaac9c11 chore(deps): bump aquasecurity/trivy-action from 0.35.0 to 0.36.0
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.35.0 to 0.36.0.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](57a97c7e78...ed142fd067)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 18:11:26 -07:00
dependabot[bot]
ced87b1d5e chore(nix): fix hash for updated go dependencies 2026-05-05 18:11:21 -07:00
dependabot[bot]
3aaebe64fb chore(deps): bump the prod-minor-updates group across 1 directory with 4 updates
Bumps the prod-minor-updates group with 3 updates in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/net](https://github.com/golang/net) and [google.golang.org/grpc](https://github.com/grpc/grpc-go).


Updates `golang.org/x/crypto` from 0.49.0 to 0.50.0
- [Commits](https://github.com/golang/crypto/compare/v0.49.0...v0.50.0)

Updates `golang.org/x/net` from 0.52.0 to 0.53.0
- [Commits](https://github.com/golang/net/compare/v0.52.0...v0.53.0)

Updates `golang.org/x/sys` from 0.42.0 to 0.43.0
- [Commits](https://github.com/golang/sys/compare/v0.42.0...v0.43.0)

Updates `google.golang.org/grpc` from 1.80.0 to 1.81.0
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.80.0...v1.81.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/sys
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: google.golang.org/grpc
  dependency-version: 1.81.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 18:11:21 -07:00
Owen
27f7ca6bb9 Try to fix failover not working 2026-05-05 11:40:39 -07:00
Owen
5090907307 Update status code 2026-04-30 15:55:52 -07:00
Owen
a6533b3fa0 Fix incorrect redirect logic 2026-04-29 21:11:07 -07:00
Owen Schwartz
57aa2e2e2c Merge pull request #336 from fosrl/dev
1.12.3
2026-04-29 16:02:49 -07:00
Owen Schwartz
5724c516dc Merge pull request #334 from LaurenceJJones/private-http-websocket
enhance(http): Support websocket upgrades
2026-04-29 15:58:30 -07:00
Owen
b33c3b8849 Add some test scripts for ws and move to testing/ 2026-04-29 15:57:31 -07:00
Laurence
8e19e475bf Support websocket upgrades in private HTTP proxy
Preserve optional ResponseWriter interfaces through statusCapture so httputil.ReverseProxy can hijack upgraded websocket connections. Add a regression test covering websocket traffic through the HTTP handler path.
2026-04-29 07:12:35 +01:00
Owen Schwartz
9e92c42876 Merge pull request #333 from fosrl/dev
Dont block tcp for http unless there are targets
2026-04-28 14:51:01 -07:00
Owen
66c72bbe2e Dont block tcp for http unless there are targets 2026-04-28 14:29:55 -07:00
Owen Schwartz
ffd26f9a6d Merge pull request #331 from fosrl/dev
Follow redirects by default for backward compat
2026-04-28 10:13:49 -07:00
Owen
7610aa40bf Follow redirects by default for backward compat
Fixes #330
2026-04-28 10:10:28 -07:00
Owen Schwartz
bf33a66043 Merge pull request #328 from fosrl/dev
Quiet message
2026-04-27 20:11:01 -07:00
Owen
23caf57bf4 Quiet message 2026-04-27 20:10:35 -07:00
Owen Schwartz
df3aa60cf5 Merge pull request #327 from fosrl/dev
1.12.0
2026-04-27 20:08:45 -07:00
Owen
5c43db466a Fix crashing when removing hc 2026-04-27 15:03:36 -07:00
Owen Schwartz
cc663f1636 Merge pull request #323 from fosrl/dev
1.12.0-rc.1
2026-04-24 13:42:38 -07:00
Owen
1a67ff30c2 Hard code the ifconfig path 2026-04-24 10:39:44 -07:00
Owen
bfd61ca511 Fix transport issue 2026-04-22 21:36:16 -07:00
Owen
294f99e024 Try to add redirect 2026-04-22 20:12:51 -07:00
Owen Schwartz
af2ecf486a Merge pull request #322 from fosrl/dev
Revert nix in cicd
2026-04-22 11:40:45 -07:00
Owen
efd6743ce4 Revert nix version in cicd 2026-04-22 11:40:12 -07:00
Owen Schwartz
a0d2bb999a Merge pull request #321 from fosrl/dev
1.12.0-rc.0
2026-04-22 11:35:31 -07:00
Owen
5d889fbc09 Merge branch 'main' into dev 2026-04-22 11:34:40 -07:00
Owen
1a7cf06ff8 Merge branch 'fix-nix' into dev 2026-04-22 11:31:58 -07:00
Owen
35a334c842 Merge branch 'http-ha' into dev 2026-04-21 15:07:05 -07:00
Owen
c8e5112a2a Merge branch 'alerting-rules' into dev 2026-04-21 15:06:50 -07:00
Owen
8bfb4659c0 Remove hc id 2026-04-20 21:52:21 -07:00
Owen
309f9caad2 Fix nil pointer 2026-04-20 15:05:07 -07:00
Owen
26de268466 Add x-forwarded-for 2026-04-20 15:04:59 -07:00
Owen
0f927a37ab Find old bins and support freebsd 2026-04-16 21:47:48 -07:00
Owen
e8961c5de5 Use follow redirects bool 2026-04-15 21:36:40 -07:00
Owen
9bb8eaeadb Updating with new methods 2026-04-15 21:01:04 -07:00
Owen Schwartz
d3d10d02e8 Merge pull request #317 from fosrl/fix-nix
fix nix
2026-04-14 14:24:26 -07:00
Owen
d133d69cb9 Update nix version in cicd 2026-04-14 14:22:52 -07:00
Owen
50be4f617e Update version 2026-04-14 14:22:48 -07:00
Owen
be1cd190e7 Merge branch 'main' into dev 2026-04-14 14:17:42 -07:00
Owen
5c9d13bcca Add ldflags version to local 2026-04-13 17:00:06 -07:00
Owen Schwartz
dc2e23380a Merge pull request #306 from LaurenceJJones/investigate/heap-leak-udp-proxy
fix(proxy): reclaim idle UDP flows and make timeout configurable
2026-04-13 10:27:37 -07:00
Marc Schäfer
3d2b73d417 Merge pull request #303 from fosrl/dependabot/go_modules/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp-1.43.0
chore(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.38.0 to 1.43.0
2026-04-12 22:49:53 +02:00
Owen
12776d65c1 Add logging 2026-04-11 21:56:28 -07:00
Laurence
0569525743 Merge remote-tracking branch 'upstream/dev' into investigate/heap-leak-udp-proxy
Made-with: Cursor

# Conflicts:
#	proxy/manager.go
2026-04-10 13:36:13 +01:00
Owen
342af9e42d Switch to scheme 2026-04-09 17:21:36 -04:00
Owen
092535441e Pass the new data down from the websocket 2026-04-09 16:13:19 -04:00
Owen
5848c8d4b4 Adjust to use data saved inside of the subnet rule 2026-04-09 16:04:11 -04:00
Owen Schwartz
6becf0f719 Merge pull request #277 from LaurenceJJones/refactor/proxy-udp-buffer-pool
perf(proxy): add sync.Pool for UDP buffers
2026-04-09 13:09:06 -04:00
Owen
47c646bc33 Basic http is working 2026-04-09 11:43:26 -04:00
Laurence
4d8d00241d perf(proxy): add sync.Pool for UDP buffers
- Add udpBufferPool for reusable 65507-byte UDP packet buffers
- Add getUDPBuffer() and putUDPBuffer() helper functions
- Clear buffer contents before returning to pool to prevent data leakage
- Apply pooling to both main handler buffer and per-client goroutine buffers
- Reduces GC pressure from frequent large allocations during UDP proxying

Made-with: Cursor
2026-04-09 15:59:03 +01:00
Laurence
31f899588f fix(proxy): reclaim idle UDP flows and make timeout configurable 2026-04-09 15:45:55 +01:00
dependabot[bot]
0104fb9b2d chore(nix): fix hash for updated go dependencies 2026-04-09 02:01:28 +00:00
dependabot[bot]
6dd9c4b0d1 chore(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go) from 1.38.0 to 1.43.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.43.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
  dependency-version: 1.43.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 02:00:10 +00:00
Owen Schwartz
e4daa620c7 Merge pull request #299 from fosrl/dependabot/go_modules/prod-minor-updates-7fd0df0afe
chore(deps): bump the prod-minor-updates group with 2 updates
2026-04-08 21:58:53 -04:00
Owen Schwartz
7e1e3408d5 Merge pull request #302 from LaurenceJJones/fix/config-file-provision-save
fix: allow empty config file bootstrap before provisioning
2026-04-08 21:58:07 -04:00
Laurence
d7c3c38d24 fix: allow empty config file bootstrap before provisioning
Treat an empty CONFIG_FILE as initial state instead of failing JSON parse, so provisioning can proceed and credentials can be saved. Ref: fosrl/pangolin#2812
2026-04-08 14:13:13 +01:00
Owen
27e471942e Add CODEOWNERS 2026-04-07 11:34:18 -04:00
dependabot[bot]
f5f2ba38d7 chore(nix): fix hash for updated go dependencies 2026-04-07 09:47:01 +00:00
dependabot[bot]
8cf3942366 chore(deps): bump the prod-minor-updates group with 2 updates
Bumps the prod-minor-updates group with 2 updates: [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) and [go.opentelemetry.io/contrib/instrumentation/runtime](https://github.com/open-telemetry/opentelemetry-go-contrib).


Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.67.0 to 0.68.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.67.0...zpages/v0.68.0)

Updates `go.opentelemetry.io/contrib/instrumentation/runtime` from 0.67.0 to 0.68.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.67.0...zpages/v0.68.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
  dependency-version: 0.68.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/contrib/instrumentation/runtime
  dependency-version: 0.68.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-07 09:45:12 +00:00
Marc Schäfer
f7bb240c74 Merge pull request #293 from fosrl/dependabot/github_actions/actions/attest-build-provenance-4.1.0
chore(deps): bump actions/attest-build-provenance from 3.2.0 to 4.1.0
2026-04-06 17:28:26 +02:00
Marc Schäfer
cbd17ff249 Merge pull request #294 from fosrl/dependabot/github_actions/sigstore/cosign-installer-4.1.1
chore(deps): bump sigstore/cosign-installer from 4.0.0 to 4.1.1
2026-04-06 17:28:05 +02:00
Marc Schäfer
b7f2445cfd Merge pull request #295 from fosrl/dependabot/github_actions/docker/login-action-4.1.0
chore(deps): bump docker/login-action from 4.0.0 to 4.1.0
2026-04-06 17:27:44 +02:00
Marc Schäfer
88d954fc64 Merge pull request #296 from fosrl/dependabot/github_actions/softprops/action-gh-release-2.6.1
chore(deps): bump softprops/action-gh-release from 2.4.2 to 2.6.1
2026-04-06 17:27:25 +02:00
Marc Schäfer
9b2d1f2a10 Merge pull request #297 from fosrl/dependabot/github_actions/docker/setup-qemu-action-4.0.0
chore(deps): bump docker/setup-qemu-action from 3.7.0 to 4.0.0
2026-04-06 17:27:07 +02:00
Marc Schäfer
caa5a6a476 Merge pull request #298 from fosrl/dependabot/go_modules/prod-minor-updates-497a73c3c2
chore(deps): bump the prod-minor-updates group with 13 updates
2026-04-06 17:23:16 +02:00
dependabot[bot]
74183952fb chore(nix): fix hash for updated go dependencies 2026-04-06 10:12:08 +00:00
dependabot[bot]
05fc12f66e chore(deps): bump the prod-minor-updates group with 13 updates
Bumps the prod-minor-updates group with 13 updates:

| Package | From | To |
| --- | --- | --- |
| [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.66.0` | `0.67.0` |
| [go.opentelemetry.io/contrib/instrumentation/runtime](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.66.0` | `0.67.0` |
| [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) | `1.41.0` | `1.42.0` |
| [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc](https://github.com/open-telemetry/opentelemetry-go) | `1.41.0` | `1.43.0` |
| [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://github.com/open-telemetry/opentelemetry-go) | `1.41.0` | `1.43.0` |
| [go.opentelemetry.io/otel/exporters/prometheus](https://github.com/open-telemetry/opentelemetry-go) | `0.63.0` | `0.65.0` |
| [go.opentelemetry.io/otel/metric](https://github.com/open-telemetry/opentelemetry-go) | `1.41.0` | `1.43.0` |
| [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) | `1.41.0` | `1.43.0` |
| [go.opentelemetry.io/otel/sdk/metric](https://github.com/open-telemetry/opentelemetry-go) | `1.41.0` | `1.43.0` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.48.0` | `0.49.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.51.0` | `0.52.0` |
| [golang.org/x/sys](https://github.com/golang/sys) | `0.41.0` | `0.42.0` |
| [google.golang.org/grpc](https://github.com/grpc/grpc-go) | `1.79.3` | `1.80.0` |


Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.66.0 to 0.67.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.66.0...zpages/v0.67.0)

Updates `go.opentelemetry.io/contrib/instrumentation/runtime` from 0.66.0 to 0.67.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.66.0...zpages/v0.67.0)

Updates `go.opentelemetry.io/otel` from 1.41.0 to 1.42.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.42.0)

Updates `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc` from 1.41.0 to 1.43.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.43.0)

Updates `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc` from 1.41.0 to 1.43.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.43.0)

Updates `go.opentelemetry.io/otel/exporters/prometheus` from 0.63.0 to 0.65.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/exporters/prometheus/v0.63.0...exporters/prometheus/v0.65.0)

Updates `go.opentelemetry.io/otel/metric` from 1.41.0 to 1.43.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.43.0)

Updates `go.opentelemetry.io/otel/sdk` from 1.41.0 to 1.43.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.43.0)

Updates `go.opentelemetry.io/otel/sdk/metric` from 1.41.0 to 1.43.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.43.0)

Updates `golang.org/x/crypto` from 0.48.0 to 0.49.0
- [Commits](https://github.com/golang/crypto/compare/v0.48.0...v0.49.0)

Updates `golang.org/x/net` from 0.51.0 to 0.52.0
- [Commits](https://github.com/golang/net/compare/v0.51.0...v0.52.0)

Updates `golang.org/x/sys` from 0.41.0 to 0.42.0
- [Commits](https://github.com/golang/sys/compare/v0.41.0...v0.42.0)

Updates `google.golang.org/grpc` from 1.79.3 to 1.80.0
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.79.3...v1.80.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
  dependency-version: 0.67.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/contrib/instrumentation/runtime
  dependency-version: 0.67.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
  dependency-version: 1.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
  dependency-version: 1.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/exporters/prometheus
  dependency-version: 0.65.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/metric
  dependency-version: 1.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/sdk/metric
  dependency-version: 1.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/crypto
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/sys
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: google.golang.org/grpc
  dependency-version: 1.80.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 10:10:38 +00:00
dependabot[bot]
56cc225bd3 chore(deps): bump docker/setup-qemu-action from 3.7.0 to 4.0.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](c7c5346462...ce360397dd)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 10:10:31 +00:00
dependabot[bot]
fee7fbe20a chore(deps): bump softprops/action-gh-release from 2.4.2 to 2.6.1
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.4.2 to 2.6.1.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](5be0e66d93...153bb8e044)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 10:10:27 +00:00
dependabot[bot]
bc6661faa5 chore(deps): bump docker/login-action from 4.0.0 to 4.1.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](b45d80f862...4907a6ddec)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 10:10:21 +00:00
dependabot[bot]
db6cabc6d7 chore(deps): bump sigstore/cosign-installer from 4.0.0 to 4.1.1
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 4.0.0 to 4.1.1.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](faadad0cce...cad07c2e89)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 10:10:17 +00:00
dependabot[bot]
3f32c9e8ef chore(deps): bump actions/attest-build-provenance from 3.2.0 to 4.1.0
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3.2.0 to 4.1.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](96278af6ca...a2bbfa2537)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 10:10:11 +00:00
Owen Schwartz
cd4782265a Merge pull request #290 from fosrl/dev
1.11.0
2026-04-03 17:37:05 -04:00
Owen
184bfb12d6 Delete bad bp 2026-04-03 17:36:48 -04:00
Owen
2e02c9b7a9 Remove files 2026-04-03 16:49:09 -04:00
Owen Schwartz
5c329be1f3 Merge pull request #286 from fosrl/dependabot/go_modules/prod-patch-updates-a06038febc
chore(deps): bump github.com/gaissmai/bart from 0.26.0 to 0.26.1 in the prod-patch-updates group across 1 directory
2026-04-03 16:47:50 -04:00
Marc Schäfer
732e788c66 Merge pull request #261 from fosrl/dependabot/github_actions/actions/stale-10.2.0
chore(deps): bump actions/stale from 10.1.1 to 10.2.0
2026-04-03 14:36:14 +02:00
Marc Schäfer
aa42b3623d Merge pull request #262 from fosrl/dependabot/github_actions/docker/setup-buildx-action-4.0.0
chore(deps): bump docker/setup-buildx-action from 3.12.0 to 4.0.0
2026-04-03 14:35:45 +02:00
Marc Schäfer
4f42560e26 Merge pull request #263 from fosrl/dependabot/github_actions/aquasecurity/trivy-action-0.35.0
chore(deps): bump aquasecurity/trivy-action from 0.34.2 to 0.35.0
2026-04-03 14:33:52 +02:00
dependabot[bot]
c2187de482 chore(deps): bump docker/setup-buildx-action from 3.12.0 to 4.0.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.12.0 to 4.0.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](8d2750c68a...4d04d5d948)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-03 12:33:48 +00:00
dependabot[bot]
5ced7d6909 chore(deps): bump actions/stale from 10.1.1 to 10.2.0
Bumps [actions/stale](https://github.com/actions/stale) from 10.1.1 to 10.2.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](997185467f...b5d41d4e1d)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-03 12:33:07 +00:00
Marc Schäfer
4e6e79ad21 Merge pull request #264 from fosrl/dependabot/github_actions/docker/login-action-4.0.0
chore(deps): bump docker/login-action from 3.7.0 to 4.0.0
2026-04-03 14:32:59 +02:00
Marc Schäfer
abe6e2e400 Merge branch 'main' into dependabot/github_actions/aquasecurity/trivy-action-0.35.0 2026-04-03 14:32:11 +02:00
Marc Schäfer
f432a17c16 Merge pull request #265 from fosrl/dependabot/github_actions/docker/build-push-action-7.0.0
chore(deps): bump docker/build-push-action from 6.19.2 to 7.0.0
2026-04-03 14:31:45 +02:00
Marc Schäfer
6f96169ff1 Merge branch 'main' into dependabot/github_actions/docker/login-action-4.0.0 2026-04-03 14:31:00 +02:00
Marc Schäfer
575942c4be Merge branch 'main' into dependabot/github_actions/docker/build-push-action-7.0.0 2026-04-03 14:29:10 +02:00
dependabot[bot]
16864fc1d7 chore(nix): fix hash for updated go dependencies 2026-04-03 12:28:34 +00:00
dependabot[bot]
f925c681d2 chore(deps): bump github.com/gaissmai/bart
Bumps the prod-patch-updates group with 1 update in the / directory: [github.com/gaissmai/bart](https://github.com/gaissmai/bart).


Updates `github.com/gaissmai/bart` from 0.26.0 to 0.26.1
- [Release notes](https://github.com/gaissmai/bart/releases)
- [Commits](https://github.com/gaissmai/bart/compare/v0.26.0...v0.26.1)

---
updated-dependencies:
- dependency-name: github.com/gaissmai/bart
  dependency-version: 0.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-03 12:27:19 +00:00
Marc Schäfer
e01b0ae9c7 Merge pull request #281 from fosrl/dependabot/go_modules/google.golang.org/grpc-1.79.3
chore(deps): bump google.golang.org/grpc from 1.79.1 to 1.79.3
2026-04-03 14:26:08 +02:00
Owen
f4d071fe27 Add provisioning blueprint file 2026-04-02 21:39:59 -04:00
Owen
8d82460a76 Send health checks to the server on reconnect 2026-03-31 17:06:07 -07:00
Owen
5208117c56 Add name to provisioning 2026-03-30 17:18:22 -07:00
Owen
381f5a619c Merge branch 'main' into logging-provision 2026-03-29 21:19:53 -07:00
Owen Schwartz
b6f13a1b55 Merge pull request #285 from fosrl/dev
1.10.4
2026-03-29 12:25:10 -07:00
Owen
cdaf4f7898 Add chain id to ping 2026-03-29 12:00:17 -07:00
Owen
d4a5ac8682 Merge branch 'main' into dev 2026-03-29 11:40:34 -07:00
Owen
1057013b50 Add chainId based dedup 2026-03-27 11:55:34 -07:00
Owen
fc4b375bf1 Allow blueprint interpolation for env vars 2026-03-26 20:05:04 -07:00
Owen
baca04ee58 Add --config-file 2026-03-26 17:31:04 -07:00
Owen
b43572dd8d Provisioning key working 2026-03-26 17:23:19 -07:00
Owen
69019d5655 Process log to form sessions 2026-03-24 17:26:44 -07:00
Owen
0f57985b6f Saving and sending access logs pass 1 2026-03-23 16:39:01 -07:00
dependabot[bot]
212bdf765a chore(nix): fix hash for updated go dependencies 2026-03-19 02:16:03 +00:00
dependabot[bot]
b045a0f5d4 chore(deps): bump google.golang.org/grpc from 1.79.1 to 1.79.3
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.79.1 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.79.1...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-19 02:14:44 +00:00
Owen Schwartz
a2683eb385 Merge pull request #274 from LaurenceJJones/refactor/proxy-cleanup-basics
refactor(proxy): cleanup basics - constants, remove dead code, fix de…
2026-03-18 15:39:43 -07:00
Owen Schwartz
d3722c2519 Merge pull request #280 from LaurenceJJones/fix/healthcheck-ipv6
fix(healthcheck): Support ipv6 healthchecks
2026-03-18 15:38:15 -07:00
Laurence
8fda35db4f fix(healthcheck): Support ipv6 healthchecks
Currently we are doing fmt.sprintf on hostname and port which will not properly handle ipv6 addresses, instead of changing pangolin to send bracketed address a simply net.join can do this for us since we dont need to parse a formatted string
2026-03-18 13:37:31 +00:00
Owen Schwartz
de4353f2e6 Merge pull request #269 from LaurenceJJones/feature/pprof-endpoint
feat(admin): Add pprof endpoints
2026-03-17 11:42:08 -07:00
Owen Schwartz
b398f531f0 Merge pull request #279 from fosrl/dev
1.10.3
2026-03-16 16:47:39 -07:00
Owen
8161fa6626 Bump ping interval up 2026-03-16 14:33:40 -07:00
Owen
24dfb3a8a2 Remove redundant info 2026-03-16 13:50:45 -07:00
Laurence
13448f76aa refactor(proxy): cleanup basics - constants, remove dead code, fix deprecated calls
- Add maxUDPPacketSize constant to replace magic number 65507
- Remove commented-out code in Stop()
- Replace deprecated ne.Temporary() with errors.Is(err, net.ErrClosed)
- Use errors.As instead of type assertion for net.Error
- Use errors.Is for closed connection checks instead of string matching
- Handle closed connection gracefully when reading from UDP target
2026-03-16 14:11:14 +00:00
Owen
d4ebb3e2af Send disconnecting message 2026-03-15 17:42:03 -07:00
Owen
bf029b7bb2 Clean up to match olm 2026-03-14 11:57:37 -07:00
Owen
745d2dbc7e Merge branch 'dev' into msg-opt 2026-03-13 17:10:49 -07:00
Owen
c7b01288e0 Clean up previous logging 2026-03-13 11:45:36 -07:00
Owen
539e595c48 Add optional compression 2026-03-12 17:49:05 -07:00
Owen
ef03b4566d Allow passing public dns into resolve 2026-03-12 16:41:41 -07:00
Laurence
836144aebf feat(admin): Add pprof endpoints
To aid us in debugging user issues with memory or leaks we need to be able for the user to configure pprof, wait and then provide us the output files to see where memory/leaks occur in actual runtimes
2026-03-12 09:22:50 +00:00
Owen
a1df3d7ff0 Merge branch 'dev' of github.com:fosrl/newt into dev 2026-03-11 17:28:16 -07:00
Laurence
d68a13ea1f feat(installer): prefer /usr/local/bin and improve POSIX compatibility
- Always install to /usr/local/bin instead of ~/.local/bin
  - Use sudo automatically when write access is needed
  - Replace bash-specific syntax with POSIX equivalents:
    - Change shebang from #!/bin/bash to #!/bin/sh
    - Replace [[ == *pattern* ]] with case statements
    - Replace echo -e with printf for colored output
  - Script now works with dash, ash, busybox sh, and bash
2026-03-10 10:01:28 -07:00
dependabot[bot]
d7741df514 chore(nix): fix hash for updated go dependencies 2026-03-09 10:29:50 +00:00
dependabot[bot]
8e188933a2 chore(nix): fix hash for updated go dependencies 2026-03-09 10:29:45 +00:00
dependabot[bot]
a13c7c6e65 chore(nix): fix hash for updated go dependencies 2026-03-09 10:29:43 +00:00
dependabot[bot]
bc44ca1aba chore(deps): bump docker/build-push-action from 6.19.2 to 7.0.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](10e90e3645...d08e5c354a)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 10:28:02 +00:00
dependabot[bot]
a76089db98 chore(deps): bump docker/login-action from 3.7.0 to 4.0.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](c94ce9fb46...b45d80f862)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 10:27:59 +00:00
dependabot[bot]
627ec2fdbc chore(deps): bump aquasecurity/trivy-action from 0.34.2 to 0.35.0
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.34.2 to 0.35.0.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](97e0b3872f...57a97c7e78)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 10:27:55 +00:00
Owen
44ca592a5c Set newt version in dockerfile 2026-03-08 11:28:56 -07:00
Owen
accac75a53 Set newt version in dockerfile 2026-03-08 11:26:35 -07:00
Laurence
768415f90b Parse target strings with IPv6 support and strict validation
Add parseTargetString() for listenPort:host:targetPort using net.SplitHostPort/JoinHostPort. Replace manual split in updateTargets; fix err shadowing on remove. Validate listen port 1–65535 and reject empty host/port; use %w for errors. Add tests for IPv4, IPv6, hostnames, and invalid cases.
2026-03-07 21:32:36 -08:00
Owen
e1edbcea07 Make sure to set version and fix prepare issue 2026-03-07 12:34:55 -08:00
Owen
da9825d030 Merge branch 'main' into dev 2026-03-07 12:34:45 -08:00
Owen
afdb1fc977 Make sure to set version and fix prepare issue 2026-03-07 12:32:49 -08:00
Owen
392e4c83bf Make sure to skip prepare 2026-03-07 10:37:44 -08:00
Owen
1bd1133ac2 Make sure to skip prepare 2026-03-07 10:36:18 -08:00
Owen
a85454e770 Build full arn 2026-03-07 10:20:18 -08:00
Owen
fac0f5b197 Build full arn 2026-03-07 10:17:14 -08:00
Marc Schäfer
068145c539 fix(ci): Refactor CI/CD workflow for AWS and image management
Updated CI/CD workflow to improve AWS role handling and image tagging.
2026-03-07 10:07:55 -08:00
Marc Schäfer
91a035f4ab fix(ci): Use AWS SelfHosted runner to fix pull and install request limit 2026-03-07 10:07:55 -08:00
Owen
e68b65683f Temp lets ignore the sync messages 2026-03-06 15:14:48 -08:00
Owen
7d6825132b Merge branch 'dev' into msg-opt 2026-03-03 16:56:41 -08:00
Owen Schwartz
beaf386615 Merge pull request #250 from fosrl/dev
1.10.2
2026-03-03 16:49:18 -08:00
Owen
e474866f84 Fix icmp when ports disabled
Fixes #247
2026-03-03 16:38:11 -08:00
Owen
7920295b8c Fix --port 2026-03-03 16:27:57 -08:00
Owen
6371e980d2 Update the get all rules 2026-03-03 16:11:32 -08:00
Owen Schwartz
bf7882eacc Merge pull request #249 from fosrl/dependabot/go_modules/prod-minor-updates-26e8ddd78e
chore(deps): bump the prod-minor-updates group with 3 updates
2026-03-03 16:10:38 -08:00
dependabot[bot]
86a7d79092 chore(nix): fix hash for updated go dependencies 2026-03-03 09:46:50 +00:00
dependabot[bot]
1c22555bd4 chore(deps): bump the prod-minor-updates group with 3 updates
Bumps the prod-minor-updates group with 3 updates: [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib), [go.opentelemetry.io/contrib/instrumentation/runtime](https://github.com/open-telemetry/opentelemetry-go-contrib) and [golang.org/x/net](https://github.com/golang/net).


Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.65.0 to 0.66.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.65.0...zpages/v0.66.0)

Updates `go.opentelemetry.io/contrib/instrumentation/runtime` from 0.65.0 to 0.66.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.65.0...zpages/v0.66.0)

Updates `golang.org/x/net` from 0.50.0 to 0.51.0
- [Commits](https://github.com/golang/net/compare/v0.50.0...v0.51.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
  dependency-version: 0.66.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/contrib/instrumentation/runtime
  dependency-version: 0.66.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.51.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 09:45:25 +00:00
Owen Schwartz
30ade8102d Merge pull request #233 from fosrl/dependabot/go_modules/prod-minor-updates-85791fb009
chore(deps): bump the prod-minor-updates group across 1 directory with 13 updates
2026-03-02 21:02:09 -08:00
Owen
15ea631b96 Mutex on handlers, slight change to ping message and handler 2026-03-02 20:56:36 -08:00
Owen
4e854b5f96 Working on message versioning 2026-03-02 20:56:18 -08:00
Owen
287eef0f44 Add version and send it down 2026-03-02 18:27:26 -08:00
Owen
f982e6b629 Merge branch 'dev' into msg-opt 2026-03-02 18:13:55 -08:00
Owen
6c6ba45024 Merge branch 'LaurenceJJones-optimize-reverse-nat-lookup' into dev 2026-03-02 18:13:12 -08:00
Owen
3f84354e7f Merge branch 'optimize-reverse-nat-lookup' of github.com:LaurenceJJones/newt into LaurenceJJones-optimize-reverse-nat-lookup 2026-03-02 18:13:05 -08:00
Owen
039ae07b7b Support prefixes sent from server 2026-03-02 18:11:20 -08:00
Owen Schwartz
ec399e9d4d Merge pull request #204 from LaurenceJJones/optimize-subnet-lookup-bart
perf(netstack2): optimize subnet rule matching with BART
2026-03-02 18:10:01 -08:00
Marc Schäfer
19f143fc6a Merge pull request #227 from fosrl/dependabot/github_actions/actions/setup-go-6.2.0
chore(deps): bump actions/setup-go from 6.1.0 to 6.2.0
2026-03-03 00:51:39 +01:00
Marc Schäfer
6d5e099681 Merge pull request #230 from fosrl/dependabot/github_actions/actions/cache-5.0.3
chore(deps): bump actions/cache from 5.0.1 to 5.0.3
2026-03-03 00:51:13 +01:00
Marc Schäfer
d6ea37f45c Merge pull request #231 from fosrl/dependabot/github_actions/actions/attest-build-provenance-3.2.0
chore(deps): bump actions/attest-build-provenance from 3.1.0 to 3.2.0
2026-03-03 00:50:44 +01:00
Marc Schäfer
de3d358fe9 Merge pull request #232 from fosrl/dependabot/github_actions/docker/login-action-3.7.0
chore(deps): bump docker/login-action from 3.6.0 to 3.7.0
2026-03-03 00:50:15 +01:00
Marc Schäfer
4bcd2ffc59 Merge pull request #235 from fosrl/dependabot/github_actions/docker/build-push-action-6.19.2
chore(deps): bump docker/build-push-action from 6.18.0 to 6.19.2
2026-03-03 00:49:45 +01:00
dependabot[bot]
06e175ac9a chore(nix): fix hash for updated go dependencies 2026-03-02 19:55:17 +00:00
dependabot[bot]
6e79eef224 chore(deps): bump the prod-minor-updates group across 1 directory with 13 updates
Bumps the prod-minor-updates group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.64.0` | `0.65.0` |
| [go.opentelemetry.io/contrib/instrumentation/runtime](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.64.0` | `0.65.0` |
| [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc](https://github.com/open-telemetry/opentelemetry-go) | `1.39.0` | `1.40.0` |
| [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://github.com/open-telemetry/opentelemetry-go) | `1.39.0` | `1.40.0` |
| [go.opentelemetry.io/otel/exporters/prometheus](https://github.com/open-telemetry/opentelemetry-go) | `0.61.0` | `0.62.0` |



Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.64.0 to 0.65.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.64.0...zpages/v0.65.0)

Updates `go.opentelemetry.io/contrib/instrumentation/runtime` from 0.64.0 to 0.65.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.64.0...zpages/v0.65.0)

Updates `go.opentelemetry.io/otel` from 1.39.0 to 1.40.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.40.0)

Updates `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc` from 1.39.0 to 1.40.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.40.0)

Updates `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc` from 1.39.0 to 1.40.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.40.0)

Updates `go.opentelemetry.io/otel/exporters/prometheus` from 0.61.0 to 0.62.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/exporters/prometheus/v0.61.0...exporters/prometheus/v0.62.0)

Updates `go.opentelemetry.io/otel/metric` from 1.39.0 to 1.40.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.40.0)

Updates `go.opentelemetry.io/otel/sdk` from 1.39.0 to 1.40.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.40.0)

Updates `go.opentelemetry.io/otel/sdk/metric` from 1.39.0 to 1.40.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.40.0)

Updates `golang.org/x/crypto` from 0.46.0 to 0.47.0
- [Commits](https://github.com/golang/crypto/compare/v0.46.0...v0.47.0)

Updates `golang.org/x/net` from 0.48.0 to 0.49.0
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

Updates `golang.org/x/sys` from 0.39.0 to 0.40.0
- [Commits](https://github.com/golang/sys/compare/v0.39.0...v0.40.0)

Updates `google.golang.org/grpc` from 1.77.0 to 1.78.0
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.77.0...v1.78.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
  dependency-version: 0.65.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/contrib/instrumentation/runtime
  dependency-version: 0.65.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
  dependency-version: 1.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
  dependency-version: 1.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/exporters/prometheus
  dependency-version: 0.62.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/metric
  dependency-version: 1.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/sdk/metric
  dependency-version: 1.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/crypto
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/sys
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: google.golang.org/grpc
  dependency-version: 1.78.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 19:53:59 +00:00
Marc Schäfer
23dc230654 Merge pull request #248 from fosrl/dependabot/go_modules/go.opentelemetry.io/otel/sdk-1.40.0
chore(deps): bump go.opentelemetry.io/otel/sdk from 1.39.0 to 1.40.0
2026-03-02 20:50:35 +01:00
dependabot[bot]
9b8bc7b66e chore(nix): fix hash for updated go dependencies 2026-03-01 21:07:12 +00:00
dependabot[bot]
c5ae4a8f11 chore(deps): bump go.opentelemetry.io/otel/sdk from 1.39.0 to 1.40.0
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.39.0 to 1.40.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.40.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.40.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 21:05:54 +00:00
Owen
b2600b0dab Pull from ecr 2026-02-26 21:48:11 -08:00
Owen Schwartz
1656141599 Merge pull request #245 from fosrl/dev
generate random user password
2026-02-26 20:55:37 -08:00
miloschwartz
1bf89a2cc9 generate random user password 2026-02-26 10:30:24 -08:00
Owen
555e1ddc7c Update iss 2026-02-25 14:57:32 -08:00
Owen
514c94519e Fix port parsing
Fixes #243
2026-02-25 11:52:15 -08:00
Owen Schwartz
1a3eaedfa5 Merge pull request #242 from fosrl/dev
1.10.0
2026-02-22 16:36:25 -08:00
Owen
01e2ba31b7 Merge branch 'main' into dev 2026-02-22 16:36:15 -08:00
Laurence
9738565a3a fix: address code review issues for BART subnet lookup
- Fix prefix canonicalization: use Masked() to handle host bits correctly
  (e.g., 10.0.0.5/24 and 10.0.0.0/24 are now treated as equal)
- Fix empty trie cleanup: use BART's Size() method to check if trie is empty
  instead of relying on rules slice length, preventing stale entries
- Fix go.mod: move BART from indirect to direct dependencies

These fixes ensure proper bookkeeping and prevent memory leaks from
empty tries hanging around after rule removal.
2026-02-22 14:22:23 +00:00
Laurence
c42a606bbd perf: optimize subnet rule matching with BART
Replace O(n) map-based subnet rule matching with BART (Binary Aggregated Range Tree) using Supernets() for O(log n) prefix matching.

Performance improvements:
- 1.3x faster for large rule sets (1000+ rules)
- 39x faster for no-match cases (critical for firewall/security)
- 1.9x faster for adding rules
- Better scaling characteristics

Trade-offs:
- Small rule sets (10-100): 1.2-1.4x slower for matches (20-30ns overhead)
- Large rule sets (1000+): 1.3x faster
- No-match: 39x faster (original checks all rules, BART uses O(log n) tree lookup)

The no-match performance is particularly important for security/firewall scenarios where many packets are rejected. BART can determine 'no match' in ~7 tree operations vs checking all 100+ rules.

Dependencies:
- Added: github.com/gaissmai/bart v0.26.0

Files:
- netstack2/subnet_lookup.go: New BART-based implementation
- netstack2/proxy.go: Removed old map-based implementation, updated to use BART
2026-02-22 14:22:20 +00:00
Laurence Jones
5977667291 Merge branch 'fosrl:main' into optimize-reverse-nat-lookup 2026-02-22 12:09:01 +00:00
miloschwartz
556be90b7e support sudo configuration and daemon mode 2026-02-20 20:42:42 -08:00
Owen
5d04be92f7 Allow sudo passwordless 2026-02-17 22:36:28 -08:00
miloschwartz
b7af49d759 fix flag 2026-02-17 22:10:01 -08:00
Owen
00a5fa1f37 Add daemon into newt 2026-02-17 22:10:01 -08:00
miloschwartz
d256d6c746 remove defaults 2026-02-17 22:10:01 -08:00
miloschwartz
2cc957d55f add auth daemon 2026-02-17 22:10:01 -08:00
Owen
d98eaa88b3 Add round trip tracking for any message 2026-02-17 22:10:01 -08:00
Owen
5b884042cd Add basic newt command relay to auth daemon 2026-02-17 22:10:01 -08:00
Owen
2265b61381 Remove legacy ssh 2026-02-17 22:10:01 -08:00
miloschwartz
60dac98514 fix flag 2026-02-17 21:01:10 -08:00
Owen
759e4c5bac Add daemon into newt 2026-02-17 14:44:28 -08:00
miloschwartz
8609be130e remove defaults 2026-02-16 20:50:13 -08:00
miloschwartz
e06b8de0a7 add auth daemon 2026-02-16 20:36:13 -08:00
Owen
0af6fb8fef Add round trip tracking for any message 2026-02-16 20:29:19 -08:00
Owen
9526768dfe Add basic newt command relay to auth daemon 2026-02-16 20:04:33 -08:00
Owen
051ab6ca9d Remove legacy ssh 2026-02-16 17:55:17 -08:00
Owen
50fbfdc262 Update example domain 2026-02-16 17:54:19 -08:00
dependabot[bot]
5fb60baa14 chore(deps): bump docker/build-push-action from 6.18.0 to 6.19.2
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.18.0 to 6.19.2.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](263435318d...10e90e3645)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.19.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 10:42:19 +00:00
dependabot[bot]
ea2e166973 chore(deps): bump docker/login-action from 3.6.0 to 3.7.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](5e57cd1181...c94ce9fb46)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 10:54:56 +00:00
dependabot[bot]
a8549f32e9 chore(deps): bump actions/attest-build-provenance from 3.1.0 to 3.2.0
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3.1.0 to 3.2.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](00014ed6ed...96278af6ca)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 10:54:51 +00:00
dependabot[bot]
ad6bbd47ad chore(deps): bump actions/cache from 5.0.1 to 5.0.3
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.1 to 5.0.3.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](9255dc7a25...cdf6c1fa76)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 10:54:45 +00:00
dependabot[bot]
f6b7aaedfd chore(deps): bump actions/setup-go from 6.1.0 to 6.2.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4dc6199c7b...7a3fe6cf4c)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 10:50:02 +00:00
Owen
2055b773fd Merge branch 'main' of github.com:fosrl/newt into dev 2026-01-21 15:59:03 -08:00
Owen
1c9c98e2f6 Show download script to update 2026-01-19 21:25:28 -08:00
dependabot[bot]
9c57677493 chore(nix): fix hash for updated go dependencies 2026-01-19 17:33:19 -08:00
dependabot[bot]
ff825a51dd Bump the prod-minor-updates group across 1 directory with 14 updates
Bumps the prod-minor-updates group with 8 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.63.0` | `0.64.0` |
| [go.opentelemetry.io/contrib/instrumentation/runtime](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.63.0` | `0.64.0` |
| [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc](https://github.com/open-telemetry/opentelemetry-go) | `1.38.0` | `1.39.0` |
| [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://github.com/open-telemetry/opentelemetry-go) | `1.38.0` | `1.39.0` |
| [go.opentelemetry.io/otel/exporters/prometheus](https://github.com/open-telemetry/opentelemetry-go) | `0.60.0` | `0.61.0` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.45.0` | `0.46.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.47.0` | `0.48.0` |
| software.sslmate.com/src/go-pkcs12 | `0.6.0` | `0.7.0` |



Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.63.0 to 0.64.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.63.0...zpages/v0.64.0)

Updates `go.opentelemetry.io/contrib/instrumentation/runtime` from 0.63.0 to 0.64.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.63.0...zpages/v0.64.0)

Updates `go.opentelemetry.io/otel` from 1.38.0 to 1.39.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.39.0)

Updates `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc` from 1.38.0 to 1.39.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.39.0)

Updates `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc` from 1.38.0 to 1.39.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.39.0)

Updates `go.opentelemetry.io/otel/exporters/prometheus` from 0.60.0 to 0.61.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/exporters/prometheus/v0.60.0...exporters/prometheus/v0.61.0)

Updates `go.opentelemetry.io/otel/metric` from 1.38.0 to 1.39.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.39.0)

Updates `go.opentelemetry.io/otel/sdk` from 1.38.0 to 1.39.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.39.0)

Updates `go.opentelemetry.io/otel/sdk/metric` from 1.38.0 to 1.39.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.39.0)

Updates `golang.org/x/crypto` from 0.45.0 to 0.46.0
- [Commits](https://github.com/golang/crypto/compare/v0.45.0...v0.46.0)

Updates `golang.org/x/net` from 0.47.0 to 0.48.0
- [Commits](https://github.com/golang/net/compare/v0.47.0...v0.48.0)

Updates `golang.org/x/sys` from 0.38.0 to 0.39.0
- [Commits](https://github.com/golang/sys/compare/v0.38.0...v0.39.0)

Updates `google.golang.org/grpc` from 1.76.0 to 1.77.0
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.76.0...v1.77.0)

Updates `software.sslmate.com/src/go-pkcs12` from 0.6.0 to 0.7.0

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
  dependency-version: 0.64.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/contrib/instrumentation/runtime
  dependency-version: 0.64.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
  dependency-version: 1.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
  dependency-version: 1.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/exporters/prometheus
  dependency-version: 0.61.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/metric
  dependency-version: 1.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: go.opentelemetry.io/otel/sdk/metric
  dependency-version: 1.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/crypto
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/sys
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: google.golang.org/grpc
  dependency-version: 1.77.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: software.sslmate.com/src/go-pkcs12
  dependency-version: 0.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:33:19 -08:00
dependabot[bot]
cdfc5733f0 Bump docker/setup-buildx-action from 3.11.1 to 3.12.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.11.1 to 3.12.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](e468171a9d...8d2750c68a)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 3.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:33:02 -08:00
dependabot[bot]
cadbb50bdf Bump actions/attest-build-provenance from 3.0.0 to 3.1.0
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](977bb373ed...00014ed6ed)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:32:56 -08:00
dependabot[bot]
4ac33c824b Bump actions/cache from 4.3.0 to 5.0.1
Bumps [actions/cache](https://github.com/actions/cache) from 4.3.0 to 5.0.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](0057852bfa...9255dc7a25)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:32:46 -08:00
dependabot[bot]
d91228f636 chore(deps): bump actions/checkout from 5.0.0 to 6.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](08c6903cd8...8e8c483db8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:32:25 -08:00
dependabot[bot]
6c3b85bb9a chore(deps): bump docker/metadata-action from 5.9.0 to 5.10.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.9.0 to 5.10.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](318604b99e...c299e40c65)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 5.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:32:18 -08:00
Owen
77d99f1722 Add stale bot 2026-01-19 17:11:48 -08:00
Owen
43e1341352 Disable metrics by default 2026-01-18 15:20:13 -08:00
Owen
daa1a90e05 Dont block waiting for a rebind signal 2026-01-18 11:36:42 -08:00
Owen
3739c237c7 Handle rebind in the polling function 2026-01-18 11:36:30 -08:00
Owen
ddde1758e5 Try to close the socket first 2026-01-17 17:35:10 -08:00
Owen
dca29781f3 Rebind in shared bind 2026-01-17 17:06:01 -08:00
Owen
91bfd69179 Filter out no bandwidth peers 2026-01-16 17:54:05 -08:00
Owen
060d876429 Allow updating the intervals 2026-01-14 17:09:27 -08:00
Owen
69952efe89 Fix bug where not all routes are added 2026-01-12 16:01:15 -08:00
Owen
66949ca047 Merge branch 'mobile' of github.com:fosrl/newt into mobile 2026-01-12 14:22:01 -08:00
Owen
8c12db6dff Try to improve cpu usage 2026-01-12 14:21:05 -08:00
Owen
b84d465763 Add noop for android ios 2026-01-12 12:31:38 -08:00
miloschwartz
a62567997d quiet and logs and fix ios errors 2026-01-01 17:29:02 -05:00
Owen
9bb4bbccb8 Fix incrementor not updating; restrict routes to darwin 2025-12-31 15:58:04 -05:00
Owen
c3fad797e5 Handle android and ios in routes 2025-12-31 15:43:16 -05:00
Owen
0168b4796e Add mobile subs for permission 2025-12-30 10:31:35 -05:00
Owen
6c05d76c88 Merge branch 'main' into dev 2025-12-24 15:18:11 -05:00
Owen
a701add824 Reuse http client for each target
Fixes #220
2025-12-24 10:58:46 -05:00
Owen
d754cea397 Dont run on v tags 2025-12-23 17:54:31 -05:00
Owen
31d52ad3ff Quiet up HandleIncomingPacket 2025-12-23 10:29:15 -05:00
Owen
e1ee4dc8f2 Fix latest tag 2025-12-22 21:32:47 -05:00
Varun Narravula
f9b6f36b4f ci: update nix go vendor hash if needed for dependabot PRs 2025-12-22 19:43:48 -05:00
Varun Narravula
0e961761b8 chore: add direnv and nix result dirs to gitignore 2025-12-22 19:43:48 -05:00
Varun Narravula
baf1b9b972 ci: build nix package when go.mod is changed 2025-12-22 19:43:48 -05:00
Varun Narravula
f078136b5a fix(nix): disable tests, set meta.mainProgram for package 2025-12-22 19:43:48 -05:00
Varun Narravula
ca341a8bb0 chore(nix): sync version number with latest version 2025-12-22 19:43:48 -05:00
Owen
80ae03997a Merge branch 'dev' 2025-12-22 16:15:41 -05:00
Owen
5c94789d9a Quiet up logs 2025-12-22 14:31:44 -05:00
Owen
6c65cc8e5e Fix makefile cicd binaries 2025-12-21 21:34:56 -05:00
Owen
a21a8e90fa Add back release and binaries 2025-12-21 21:01:04 -05:00
Owen
3d5335f2cb Add back release and binaries 2025-12-21 21:00:45 -05:00
Owen Schwartz
94788edce3 Merge pull request #214 from fosrl/dev
1.8.0-rc.0
2025-12-21 20:59:32 -05:00
Owen
2bbe037544 Merge branch 'main' into dev 2025-12-21 20:57:45 -05:00
Owen
9b015e9f7c Tie siteIds to exit node 2025-12-19 10:54:21 -05:00
Owen
3305f711b9 Prevent sigsegv with bad address
Fixes #210
Fixes #201
2025-12-18 10:29:37 -05:00
Owen
ff7fe1275b Take 21820 from config 2025-12-16 18:35:25 -05:00
Owen
1cbf41e094 Take 21820 from config 2025-12-16 18:33:05 -05:00
Owen Schwartz
9bc35433ef Merge pull request #208 from fosrl/icmp2
Support ICMP test requests for clients
2025-12-16 17:19:22 -05:00
Owen
b8349aab4e Install iputils not ping 2025-12-16 17:16:58 -05:00
Owen
3f29a553ae Merge branch 'dev' into icmp2 2025-12-16 17:15:15 -05:00
Owen
745045f619 Merge branch 'main' into dev 2025-12-16 17:15:06 -05:00
Owen
3783a12055 Add fallback to non privileged ping 2025-12-16 17:05:36 -05:00
Owen
a9b84c8c09 Disabling icmp ping 2025-12-16 16:30:14 -05:00
Owen
5c5ef4c7e6 Merge branch 'dev' into icmp2 2025-12-16 13:48:00 -05:00
Owen
6e9249e664 Add disable icmp 2025-12-16 13:47:45 -05:00
Owen
55be2a52a5 Handle reply correctly 2025-12-16 12:23:12 -05:00
Owen
058330d41b Icmp2 2025-12-16 12:05:59 -05:00
Laurence
1cf75b00ff perf: optimize reverse NAT lookup with O(1) map instead of O(n) iteration
Replace O(n) linear search through NAT table with O(1) reverse lookup map
for reply packet NAT translation.

Changes:
- Add reverseConnKey type for reverse NAT lookups
- Add reverseNatTable map to ProxyHandler for O(1) lookups
- Populate both forward and reverse maps when creating NAT entries
- Replace iteration-based reverse lookup with direct map access

Performance:
- O(n) → O(1) complexity for reverse NAT lookups
- Eliminates lock-held iteration on every reply packet
- Removes string comparisons from hot path
- Expected 10-50x improvement for reverse NAT lookups

This addresses Critical #1 from performance analysis where reply path
was walking the entire NAT table to find original mapping.
2025-12-16 08:16:37 +00:00
Owen Schwartz
5e7b970115 Merge pull request #203 from fosrl/port-firewall
Port firewalling for Private Resources
2025-12-15 22:16:02 -05:00
Owen
dc180abba9 Add test udp server and client 2025-12-15 22:11:57 -05:00
Owen
004bb9b12d Allow proto restriction 2025-12-15 18:37:34 -05:00
Owen
0637360b31 Fix healthcheck interval not resetting
Ref PAN-158
2025-12-15 12:10:47 -05:00
Owen Schwartz
d5e0771094 Merge pull request #200 from water-sucks/parallelize-makefile
feat(build): parallelize go-build-release and github actions with matrix
2025-12-13 09:51:59 -05:00
Varun Narravula
1dcb68d694 feat(ci): use matrix for building all makefile targets in parallel 2025-12-12 15:51:53 -08:00
Varun Narravula
865ac4b682 feat(build): double-tag docker release builds with "latest" and tag name 2025-12-12 15:51:20 -08:00
Varun Narravula
de5627b0b7 feat(build): parallelize go-build-release using separate arch rules 2025-12-12 15:51:15 -08:00
Owen
44470abd54 Print version before otel 2025-12-12 14:32:12 -05:00
Owen
4bb0537c39 Remove accidental file 2025-12-11 23:27:13 -05:00
Owen
92fb96f9bd Fix test 2025-12-11 23:24:14 -05:00
Owen Schwartz
b68b7fe49d Merge pull request #199 from water-sucks/update-nix-hash
fix(nix): use correct hash for vendored deps
2025-12-11 23:21:57 -05:00
Varun Narravula
1da424bb20 feat(nix): sync version number 2025-12-11 17:53:07 -08:00
Varun Narravula
22e5104a41 fix(nix): use correct hash for vendored deps 2025-12-11 17:52:52 -08:00
Owen
b96adeaa5b Make sure to process version first 2025-12-11 19:35:08 -05:00
Owen
533e0b9ca7 Update cicd 2025-12-11 16:40:50 -05:00
Owen
bd86abe8d5 Make cicd create draft 2025-12-11 16:09:24 -05:00
Owen
d978b27ebc Merge branch 'dev' 2025-12-11 16:01:59 -05:00
Owen
cdfcf49d89 Fix host header not working in health checks 2025-12-11 14:20:52 -05:00
Owen
2fb4bf09ea Update iss 2025-12-11 12:29:49 -05:00
Owen
dddae547f5 Merge branch 'windows' into dev 2025-12-11 12:22:03 -05:00
Owen
73a14f5fa1 Adjust debug function 2025-12-11 12:21:54 -05:00
Owen
67d5217379 Add iss file 2025-12-11 11:57:06 -05:00
Owen
9f1f1328f6 Update readme 2025-12-10 16:24:22 -05:00
Owen
30da7eaa8b Kind of working 2025-12-10 15:32:49 -05:00
Owen
0fca3457c3 Rename logs, optional port 2025-12-10 14:07:53 -05:00
Owen Schwartz
1271e8235e Merge pull request #197 from water-sucks/fix-nix-flake
fix(nix): use new version number, update deps hash
2025-12-09 18:22:57 -05:00
Varun Narravula
24c6edf3e0 fix(nix): sync release version 2025-12-09 15:16:46 -08:00
Varun Narravula
1875c987fe fix(nix): update go deps hash 2025-12-09 15:16:37 -08:00
Owen
7cb1f7e2c2 Working on ipv6 stuff 2025-12-09 17:10:38 -05:00
Owen Schwartz
3f4f4fa15c Merge pull request #196 from marcschaeferger/release/1.6.1
Fix Isssue 194 failed go test before 1.7.0-rc1
2025-12-09 17:09:47 -05:00
Marc Schäfer
bf33a3d81f testdata: add expected telemetry metrics for connection attempts and events 2025-12-09 22:51:48 +01:00
Owen
21ffc0ff4b Fix formatting? 2025-12-08 15:45:29 -05:00
Owen
13de05eec6 Revert some to secret 2025-12-08 15:40:52 -05:00
Owen
0e76b77adc Set username explicitly 2025-12-08 15:38:56 -05:00
Owen
c604f46065 Remove bad test 2025-12-08 14:18:40 -05:00
Owen
f02e29f4dd Update to secrets 2025-12-08 14:12:03 -05:00
Owen
6d79856895 Merge branch 'dev' 2025-12-08 12:17:29 -05:00
Owen
bbece243dd Make ipc cross platform 2025-12-08 12:17:11 -05:00
Owen Schwartz
6948066ae4 Merge pull request #192 from fosrl/dev
Add robust client connectivity support
2025-12-08 12:05:03 -05:00
Owen
3bcafbf07a Handle server version and prevent backward issues with clients 2025-12-08 11:48:14 -05:00
Owen
87e2eb33db Update readme 2025-12-07 21:31:28 -05:00
Owen
5ce3f4502d Fix adding new exit nodes to hp not sending interval 2025-12-07 12:05:39 -05:00
Owen
e5e733123b Merge branch 'main' into dev 2025-12-06 21:09:48 -05:00
Owen Schwartz
f417ee32fb Merge pull request #186 from fosrl/dependabot/docker/minor-updates-60be0b6e22
Bump alpine from 3.22 to 3.23 in the minor-updates group
2025-12-06 12:00:18 -05:00
Owen Schwartz
37c96d0b3e Merge pull request #182 from fosrl/dependabot/github_actions/actions/setup-go-6.1.0
Bump actions/setup-go from 6.0.0 to 6.1.0
2025-12-06 12:00:11 -05:00
Owen Schwartz
78dc39e153 Merge pull request #181 from fosrl/dependabot/github_actions/github/codeql-action-4.31.5
Bump github/codeql-action from 4.31.0 to 4.31.5
2025-12-06 12:00:05 -05:00
Owen Schwartz
71485743ad Merge pull request #175 from fosrl/dependabot/github_actions/docker/setup-qemu-action-3.7.0
Bump docker/setup-qemu-action from 3.6.0 to 3.7.0
2025-12-06 11:59:57 -05:00
Owen Schwartz
458912e5be Merge pull request #174 from fosrl/dependabot/github_actions/docker/metadata-action-5.9.0
Bump docker/metadata-action from 5.8.0 to 5.9.0
2025-12-06 11:59:51 -05:00
Owen Schwartz
2bc91d6c68 Merge pull request #172 from fosrl/dependabot/github_actions/softprops/action-gh-release-2.4.2
Bump softprops/action-gh-release from 2.4.1 to 2.4.2
2025-12-06 11:59:46 -05:00
Owen Schwartz
95c3efc365 Merge pull request #170 from fosrl/dependabot/go_modules/prod-patch-updates-e67280ac4a
Bump github.com/docker/docker from 28.5.1+incompatible to 28.5.2+incompatible in the prod-patch-updates group
2025-12-06 11:59:40 -05:00
Owen
72a9e111dc Localhost working - is this the best way to do it? 2025-12-05 16:33:43 -05:00
dependabot[bot]
3c86edf0d5 Bump github.com/docker/docker in the prod-patch-updates group
Bumps the prod-patch-updates group with 1 update: [github.com/docker/docker](https://github.com/docker/docker).


Updates `github.com/docker/docker` from 28.5.1+incompatible to 28.5.2+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.5.1...v28.5.2)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.5.2+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 09:16:49 +00:00
dependabot[bot]
32b1b817ac Bump alpine from 3.22 to 3.23 in the minor-updates group
Bumps the minor-updates group with 1 update: alpine.


Updates `alpine` from 3.22 to 3.23

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3.23'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 09:16:47 +00:00
Owen
02949be245 Support connection testing in native 2025-12-04 21:48:32 -05:00
Owen
6d51cbf0c0 Check permissions 2025-12-04 21:39:32 -05:00
Owen
4dbf200cca Change DNS lookup to conntrack 2025-12-04 20:13:48 -05:00
Owen
d8b4fb4acb Change to disable clients 2025-12-04 20:13:35 -05:00
Owen Schwartz
ac691517ae Merge pull request #185 from water-sucks/add-tls-server-name-to-healthchecks
feat(healthcheck): add TLS SNI header to request when needed
2025-12-04 14:41:16 -05:00
Varun Narravula
8a45f6fd63 feat(healthcheck): add TLS SNI header to request when needed
Add the Server Name Indication (SNI) field to healthcheck requests, if
present in the target config.

SNI handling is already present for proxying resources, but this has
not been implemented for healthcheck requests yet until this commit.

In order to facilitate this, this commit moves the client instantiation
to when the healthcheck is performed, rather than as a part of the
monitor init.
2025-12-04 14:27:04 -05:00
Owen Schwartz
7f650bbfdf Merge pull request #184 from water-sucks/nix-fixes-this
fix(nix): resolve issues and revamp the flake
2025-12-04 10:23:20 -05:00
Varun Narravula
15b40b0f24 chore(nix): sync to latest version number for newt package 2025-12-03 23:36:33 -05:00
Varun Narravula
e27e6fbce8 feat(nix): disable cgo for the newt package 2025-12-03 23:36:33 -05:00
Varun Narravula
f9fb13a0d7 chore(nix): add water-sucks to maintainers list 2025-12-03 23:36:33 -05:00
Varun Narravula
8db50d94c0 refactor(nix): remove with keyword antipattern 2025-12-03 23:36:33 -05:00
Varun Narravula
09568c1aaf fix(nix): use correct hash for vendored deps 2025-12-03 23:36:26 -05:00
Varun Narravula
c7d656214f fix(nix): replace version string sub with ldflags, use gitignore on src 2025-12-03 23:33:47 -05:00
Varun Narravula
d981a82b1c chore(nix): use nixpkgs-unstable branch and update flake inputs 2025-12-03 23:33:47 -05:00
Owen
5dd5a56379 Add caching to the dns requests - is this good enough? 2025-12-03 22:00:23 -05:00
Owen
8c4d6e2e0a Working on more hp 2025-12-03 20:49:46 -05:00
Owen
284f1ce627 Also close the clients 2025-12-02 11:17:34 -05:00
Owen
cd466ac43f Fix some ipv4 in v6 issues 2025-12-01 17:54:38 -05:00
Owen
2256d1f041 Holepunch tester working? 2025-12-01 17:44:33 -05:00
Owen
40ca839771 Handle hp and other stuff 2025-12-01 16:20:30 -05:00
Owen
01ec6a0ce0 Handle holepunches better 2025-12-01 13:54:14 -05:00
Owen
d04f6cf702 Dont throw errors on cleanup 2025-11-30 19:45:25 -05:00
Owen
cdaff27964 Speed much better! 2025-11-30 11:24:50 -05:00
Owen
de96be810b Working but no wgtester? - revert if bad 2025-11-29 17:38:34 -05:00
Owen Schwartz
ba43083f04 Merge pull request #179 from fosrl/dependabot/go_modules/golang.org/x/crypto-0.45.0
Bump golang.org/x/crypto from 0.43.0 to 0.45.0
2025-11-29 13:03:33 -05:00
Owen
5196effdb8 Kind of working - revert if not 2025-11-26 17:57:27 -05:00
Owen
d6edd6ca01 Make hp regular 2025-11-26 17:39:10 -05:00
Owen
1b1323b553 Move network to newt - handle --native mode 2025-11-26 15:06:16 -05:00
Owen
bb95d10e86 Rewriting desitnation works 2025-11-26 14:28:51 -05:00
Owen
da04746781 Add rewriteTo 2025-11-25 11:29:41 -05:00
dependabot[bot]
a38e0b3e98 Bump actions/setup-go from 6.0.0 to 6.1.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4469467582...4dc6199c7b)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 10:00:04 +00:00
dependabot[bot]
6ced7b5af0 Bump github/codeql-action from 4.31.0 to 4.31.5
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.0 to 4.31.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](4e94bd11f7...fdbfb4d275)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 09:59:58 +00:00
Owen
61b9615aea Add utility functions 2025-11-23 17:07:40 -05:00
dependabot[bot]
39f5782583 Bump golang.org/x/crypto from 0.43.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.43.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.43.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 02:47:14 +00:00
Owen
025c94e586 Export wireguard logger 2025-11-18 14:53:12 -05:00
Owen
75e666c396 Update logger to take in when initing 2025-11-17 21:49:07 -05:00
Owen
82a999eb87 Fix resolve 2025-11-17 18:07:36 -05:00
Owen
921e72f628 Update clients 2025-11-17 15:55:24 -05:00
Owen
46b33fdca6 Remove native and add util 2025-11-17 15:32:22 -05:00
Owen
9caa9fa31e Make logger extensible 2025-11-17 13:49:43 -05:00
Owen
dbbea6b34c Shift things around - remove native 2025-11-17 13:39:32 -05:00
Owen
491180c6a1 Remove proxy manager and break out subnet proxy 2025-11-15 21:46:32 -05:00
Owen
f49a276259 Centralize some functions 2025-11-15 16:32:02 -05:00
Owen
c71c6e0b1a Update to use new packages 2025-11-15 16:14:40 -05:00
Owen
972c9a9760 UDP WORKING! 2025-11-14 15:30:26 -05:00
Owen
8f7ee2a8dc TCP WORKING! 2025-11-14 15:23:20 -05:00
Owen
a737c3e8de REmove readme 2025-11-10 21:37:03 -05:00
Owen
1ba10c1b68 Experiment 2025-11-10 21:33:31 -05:00
dependabot[bot]
b1f2fe8283 Bump docker/setup-qemu-action from 3.6.0 to 3.7.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](29109295f8...c7c5346462)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 11:52:43 +00:00
dependabot[bot]
a1fdb06add Bump docker/metadata-action from 5.8.0 to 5.9.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.8.0 to 5.9.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](c1e51972af...318604b99e)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 5.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 11:52:40 +00:00
dependabot[bot]
25d5fab02b Bump softprops/action-gh-release from 2.4.1 to 2.4.2
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.4.1 to 2.4.2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](6da8fa9354...5be0e66d93)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 11:52:31 +00:00
Owen
2c8755f346 Using 2 nics not working 2025-11-05 21:46:29 -08:00
Owen
348cac66c8 Bring in netstack locally 2025-11-05 13:39:54 -08:00
Owen
6226a262d6 Merge branch 'main' into dev 2025-11-04 16:50:11 -08:00
Owen
5b70feb6a5 Merge branch 'main' into dev 2025-11-04 16:50:01 -08:00
Owen Schwartz
0ec18d6655 Merge pull request #169 from marcschaeferger/gh-action
Fix Github CICD Action and add Improvements
2025-10-28 21:11:56 -07:00
Marc Schäfer
7d60240572 testdata: add expected telemetry metrics for connection attempts and events 2025-10-28 23:17:05 +01:00
Marc Schäfer
ee3e7d1442 Added Improvements for CICD Action 2025-10-28 23:14:40 +01:00
Owen
527321a415 Update cicd 2025-10-27 21:26:14 -07:00
Owen
ff07692248 Merge branch 'main' into dev 2025-10-27 21:25:20 -07:00
Owen
8d3ae5afd7 Add doc for SKIP_TLS_VERIFY 2025-10-27 21:25:12 -07:00
Owen
ed99dce7e0 Add doc for SKIP_TLS_VERIFY 2025-10-27 21:24:22 -07:00
Owen Schwartz
f1e07272bd Merge pull request #166 from marcschaeferger/gh-action
Adding GHCR to CI/CD Release Workflow & further improvements
2025-10-20 17:21:05 -07:00
Marc Schäfer
a1a3d63fcf ci(actions): change runner from ubuntu-latest to amd64-runner for CI/CD workflows 2025-10-21 02:17:49 +02:00
Marc Schäfer
2a273dc435 ci(actions): add GHCR mirroring and cosign signing for Docker images
- mirror images from Docker Hub to GHCR using skopeo (preserves multi-arch manifests)
- login to GHCR via docker/login-action for signing/pushing
- install cosign and perform dual signing: keyless (OIDC) + key-based; verify signatures
- add required permissions for id-token/packages and reference necessary secrets
2025-10-21 00:22:32 +02:00
Marc Schäfer
ec05686523 ci(actions): pin action versions to commit SHAs for security
- Pin actions/checkout to SHA for v5.0.0
- Pin docker/setup-qemu-action to SHA for v3.6.0
- Pin docker/setup-buildx-action to SHA for v3.11.1
- Pin docker/login-action to SHA for v3.6.0
- Pin actions/setup-go to SHA for v6.0.0
- Pin actions/upload-artifact to SHA for v4.6.2
2025-10-21 00:21:28 +02:00
Owen Schwartz
915e7e44d1 Merge pull request #165 from marcschaeferger/ghcr
feat(actions): Sync Images from Docker to GHCR
2025-10-20 12:32:41 -07:00
Marc Schäfer
a729b91ac3 feat(actions): Sync Images from Docker to GHCR 2025-10-20 21:30:31 +02:00
Owen
ddc37658df Update domain 2025-10-19 15:12:15 -07:00
Owen
7c780f7a4f Merge branch 'dev' of github.com:fosrl/newt into dev 2025-10-16 21:09:41 -07:00
Owen
6b1c1ed077 Merge branch 'main' of github.com:fosrl/newt 2025-10-16 21:06:33 -07:00
Owen Schwartz
7a07437b22 Merge pull request #162 from marcschaeferger/otel
Adding OpenTelemetry Metrics and Tracing
2025-10-16 20:48:37 -07:00
Owen
d63d8d6f5e Add log message that the server is on 2025-10-16 20:42:02 -07:00
Owen
bda1d04f67 Add documentation for cli and reporg 2025-10-16 20:39:41 -07:00
Owen
7f8ee37c7f Update runner 2025-10-16 17:51:25 -07:00
Marc Schäfer
6d2073a478 Remove Coolify Code 2025-10-11 18:46:02 +02:00
Owen Schwartz
6048f244f1 Merge pull request #158 from fosrl/dependabot/go_modules/prod-patch-updates-46361b25de
Bump github.com/docker/docker from 28.5.0+incompatible to 28.5.1+incompatible in the prod-patch-updates group
2025-10-11 09:41:30 -07:00
Owen Schwartz
9fec22a53b Merge pull request #159 from fosrl/dependabot/go_modules/prod-minor-updates-a55d2abe4a
Bump the prod-minor-updates group with 2 updates
2025-10-11 09:41:20 -07:00
Marc Schäfer
c086e69dd0 Adding OpenTelemetry Metrics and Tracing 2025-10-11 18:19:51 +02:00
dependabot[bot]
c729ab5fc6 Bump the prod-minor-updates group with 2 updates
Bumps the prod-minor-updates group with 2 updates: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/crypto` from 0.42.0 to 0.43.0
- [Commits](https://github.com/golang/crypto/compare/v0.42.0...v0.43.0)

Updates `golang.org/x/net` from 0.45.0 to 0.46.0
- [Commits](https://github.com/golang/net/compare/v0.45.0...v0.46.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-09 09:34:56 +00:00
dependabot[bot]
552617cbb5 Bump github.com/docker/docker in the prod-patch-updates group
Bumps the prod-patch-updates group with 1 update: [github.com/docker/docker](https://github.com/docker/docker).


Updates `github.com/docker/docker` from 28.5.0+incompatible to 28.5.1+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.5.0...v28.5.1)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.5.1+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-09 09:34:49 +00:00
Owen Schwartz
b383cec0b0 Merge pull request #157 from fosrl/dev
No cloud, config file overwriting, hp
2025-10-08 17:42:45 -07:00
Owen Schwartz
fb110ba2a1 Merge pull request #156 from fosrl/dependabot/go_modules/prod-minor-updates-51461da29c
Bump the prod-minor-updates group across 1 directory with 2 updates
2025-10-08 17:40:23 -07:00
dependabot[bot]
f287888480 Bump the prod-minor-updates group across 1 directory with 2 updates
Bumps the prod-minor-updates group with 2 updates in the / directory: [github.com/docker/docker](https://github.com/docker/docker) and [golang.org/x/net](https://github.com/golang/net).


Updates `github.com/docker/docker` from 28.4.0+incompatible to 28.5.0+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.4.0...v28.5.0)

Updates `golang.org/x/net` from 0.44.0 to 0.45.0
- [Commits](https://github.com/golang/net/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.5.0+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 09:23:09 +00:00
Owen
348b8f6b94 Try to fix overwriting config file 2025-10-01 10:31:14 -07:00
miloschwartz
71c5bf7e65 update template 2025-09-29 16:38:49 -07:00
Owen
dda0b414cc Add timeouts to hp 2025-09-29 14:55:26 -07:00
Owen
8f224e2a45 Add no cloud option 2025-09-29 12:25:07 -07:00
Owen Schwartz
90243cd6c6 Merge pull request #148 from fosrl/dependabot/go_modules/github.com/docker/docker-28.4.0incompatible
Bump github.com/docker/docker from 28.3.3+incompatible to 28.4.0+incompatible
2025-09-28 17:58:58 -07:00
Owen Schwartz
9b79af10ed Merge pull request #153 from fosrl/dev
Dev
2025-09-28 17:58:38 -07:00
Owen
31b1ffcbe9 Merge branch 'dev' into docker-events 2025-09-28 17:44:09 -07:00
dependabot[bot]
f1c4e1db71 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.3.3+incompatible to 28.4.0+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.3.3...v28.4.0)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.4.0+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-28 23:31:05 +00:00
Owen
72a61d0933 Merge branch 'main' into dev 2025-09-28 16:27:05 -07:00
Owen
e489a2cc66 Merge branch 'main' of github.com:fosrl/newt 2025-09-28 16:26:58 -07:00
Owen
4e648af8e9 Pick up the existing interface private key 2025-09-28 16:26:36 -07:00
Owen
5d891225de Fix generateAndSaveKeyTo 2025-09-28 11:28:31 -07:00
Owen Schwartz
9864965381 Merge pull request #152 from didotb/didotb-docs-blueprint-file
docs: Add blueprint-file as a new cli arg and env var
2025-09-25 18:08:50 -07:00
Owen
75f6362a90 Add logging to config 2025-09-25 17:18:28 -07:00
Andrew Barrientos
30907188fb docs: Add new cli arg and env var
Include blueprint-file as an option in the cli arguments and environment variable
2025-09-26 06:46:32 +08:00
Owen Schwartz
5f11df8df2 Merge pull request #147 from marcschaeferger/Dependency-Update-09-25
Golang Dependency Update 09-2025
2025-09-21 20:10:13 -04:00
Owen Schwartz
7eea6dd335 Merge pull request #146 from marcschaeferger/github-actions
fix(gh-actions): Workflow does not contain permissions
2025-09-21 20:09:35 -04:00
Marc Schäfer
9dc5a3d91c fix(deps): add missing gopkg.in/yaml.v3 v3.0.1 back 2025-09-22 00:40:18 +02:00
Marc Schäfer
1881309148 chore(deps): update golang.org/x/crypto to v0.42.0, golang.org/x/net to v0.44.0, and golang.org/x/sys to v0.36.0 2025-09-22 00:30:33 +02:00
Marc Schäfer
aff928e60f fix(gh-actions): Workflow does not contain permissions 2025-09-22 00:22:42 +02:00
Owen
f6e7bfe8ea Watching socket and quiteting some events 2025-09-21 11:32:47 -04:00
Owen
60873f0a4f React to docker events 2025-09-21 11:19:52 -04:00
Owen Schwartz
50bb81981b Merge pull request #132 from fosrl/dependabot/github_actions/actions/setup-go-6
Bump actions/setup-go from 5 to 6
2025-09-20 11:43:42 -04:00
Owen Schwartz
4ced99fa3f Merge pull request #143 from rgutmen/mlts-pkcs12-compatibility
Mlts pkcs12 compatibility
2025-09-20 11:43:24 -04:00
rgutmen
9bd96ac540 Support TLS_CLIENT_CERT, TLS_CLIENT_KEY and TLS_CA_CERT in Docker Compose 2025-09-20 09:15:58 +01:00
Owen Schwartz
c673743692 Merge pull request #142 from marcschaeferger/main
Add Badges to README.md
2025-09-19 11:55:03 -04:00
Marc Schäfer
a08a3b9665 feat(Docs): Add License Badge and PkgGo Badge 2025-09-19 16:34:44 +02:00
Marc Schäfer
0fc13be413 feat(Docs): Addding GoReport Badge 2025-09-19 16:25:04 +02:00
Owen
92cedd00b3 Quiet up the logs 2025-09-15 10:58:40 -07:00
Owen
8b0cc36554 Add blueprint yaml sending 2025-09-08 15:25:05 -07:00
dependabot[bot]
ba9ca9f097 Bump actions/setup-go from 5 to 6
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 10:14:37 +00:00
Owen
8b4a88937c Merge branch 'main' into dev 2025-09-06 17:38:46 -07:00
Owen Schwartz
58412a7a61 Merge pull request #129 from l3pr-org/main
Implement more privacy-respecting DNS service
2025-09-04 10:39:33 -07:00
Stanley Wisnioski
2675b812aa Update README.md
Updated README.md to reflect change of default DNS server from Google to Quad9.
2025-09-04 10:03:58 -04:00
Stanley Wisnioski
217a9346c6 Change DNS Server in clients.go
Changed DNS server from Google (8.8.8.8) to Quad9 (9.9.9.9)
2025-09-04 10:00:48 -04:00
Stanley Wisnioski
eda8073bce Change DNS Server
Changed DNS server from Google (8.8.8.8) to Quad9 (9.9.9.9)
2025-09-04 09:58:43 -04:00
Owen
2969f9d2d6 Ensure backward compatability with --docker-socket 2025-09-02 14:08:24 -07:00
Owen
07b7025a24 Ensure backward compatability with --docker-socket 2025-09-02 13:56:18 -07:00
Owen
502ebfc362 Make sure to call stop function inside of clients 2025-09-01 15:45:23 -07:00
Owen
288413fd15 Limit the amount of times the send message sends
Fixes #115
2025-09-01 11:53:46 -07:00
Owen
0ba44206b1 Print the body for debug 2025-09-01 11:51:23 -07:00
Owen
3f8dcd8f22 Update docs with enforce-hc-cert 2025-09-01 10:59:54 -07:00
Owen
c5c0143013 Allow health check to http self signed by default
Fixes #122
2025-09-01 10:56:08 -07:00
Owen
87ac5c97e3 Merge branch 'main' of github.com:fosrl/newt 2025-08-30 18:07:22 -07:00
Owen
e2238c3cc8 Merge branch 'Pallavikumarimdb-feat/Split-mTLS-client-and-CA-certificates' 2025-08-30 18:07:07 -07:00
Owen
58a67328d3 Merge branch 'feat/Split-mTLS-client-and-CA-certificates' of github.com:Pallavikumarimdb/newt into Pallavikumarimdb-feat/Split-mTLS-client-and-CA-certificates 2025-08-30 18:06:18 -07:00
Owen Schwartz
002fdc4d3f Merge pull request #97 from Nemental/feat/docker-socket-protocol
feat: docker socket protocol
2025-08-30 16:53:21 -07:00
Owen Schwartz
9a1fa2c19f Merge pull request #117 from fosrl/dependabot/github_actions/docker/setup-buildx-action-3
Bump docker/setup-buildx-action from 2 to 3
2025-08-30 16:52:06 -07:00
Owen Schwartz
a6797172ef Merge pull request #118 from fosrl/dependabot/github_actions/actions/setup-go-5
Bump actions/setup-go from 4 to 5
2025-08-30 16:51:59 -07:00
Owen Schwartz
d373de7fa1 Merge pull request #119 from fosrl/dependabot/github_actions/docker/login-action-3
Bump docker/login-action from 2 to 3
2025-08-30 16:51:52 -07:00
Owen Schwartz
f876bad632 Merge pull request #120 from fosrl/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 3 to 5
2025-08-30 16:51:45 -07:00
dependabot[bot]
54b096e6a7 Bump actions/checkout from 3 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-30 22:26:53 +00:00
dependabot[bot]
10720afd31 Bump docker/login-action from 2 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-30 22:26:50 +00:00
dependabot[bot]
0b37f20d5d Bump actions/setup-go from 4 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-30 22:26:47 +00:00
dependabot[bot]
aa6e54f383 Bump docker/setup-buildx-action from 2 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-30 22:26:43 +00:00
Owen Schwartz
30f8eb9785 Merge pull request #116 from Lokowitz/update-version
Update version
2025-08-30 15:26:09 -07:00
Marvin
e765d9c774 Update go.mod 2025-08-28 17:34:34 +02:00
Marvin
3ae4ac23ef Update test.yml 2025-08-28 17:33:59 +02:00
Marvin
6a98b90b01 Update cicd.yml 2025-08-28 17:33:39 +02:00
Marvin
e0ce9d4e48 Update dependabot.yml 2025-08-28 17:33:04 +02:00
Marvin
5914c9ed33 Update .go-version 2025-08-28 17:32:27 +02:00
Owen Schwartz
109bda961f Merge pull request #103 from fosrl/dependabot/go_modules/prod-minor-updates-50897cc7ef
Bump the prod-minor-updates group with 2 updates
2025-08-27 11:02:27 -07:00
Owen Schwartz
c2a93134b1 Merge pull request #106 from fosrl/dependabot/docker/minor-updates-887f07f54c
Bump golang from 1.24-alpine to 1.25-alpine in the minor-updates group
2025-08-27 11:02:16 -07:00
Owen Schwartz
100d8e6afe Merge pull request #114 from firecat53/1.4.2
Update version to 1.4.2
2025-08-27 11:01:18 -07:00
Scott Hansen
04f2048a0a Update flake.nix to 1.4.2 2025-08-27 10:58:00 -07:00
dependabot[bot]
04de5ef8ba Bump the prod-minor-updates group with 2 updates
Bumps the prod-minor-updates group with 2 updates: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/crypto` from 0.40.0 to 0.41.0
- [Commits](https://github.com/golang/crypto/compare/v0.40.0...v0.41.0)

Updates `golang.org/x/net` from 0.42.0 to 0.43.0
- [Commits](https://github.com/golang/net/compare/v0.42.0...v0.43.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 11:38:15 +00:00
dependabot[bot]
e77601cccc Bump golang from 1.24-alpine to 1.25-alpine in the minor-updates group
Bumps the minor-updates group with 1 update: golang.


Updates `golang` from 1.24-alpine to 1.25-alpine

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.25-alpine
  dependency-type: direct:production
  dependency-group: minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 09:47:03 +00:00
Owen
e9752f868e Merge branch 'main' into dev 2025-08-23 12:17:58 -07:00
Owen Schwartz
866afaf749 Merge pull request #108 from firecat53/main
Bugfix for #107. Only update main.go
2025-08-22 21:42:36 -07:00
Owen
a12ae17a66 Add note about config 2025-08-22 21:34:47 -07:00
Owen
e0cba2e5c6 Merge branch 'site-targets' into dev 2025-08-19 10:57:25 -07:00
Scott Hansen
79f3db6fb6 Bugfix for #107. Only update main.go 2025-08-16 15:25:23 -07:00
Owen Schwartz
009b4cf425 Merge pull request #107 from firecat53/main
Update version to 1.4.1 and update version_replaceme when using nix build
2025-08-15 09:40:40 -07:00
Scott Hansen
9c28d75155 Update version to 1.4.1 and update version_replaceme when using nix build 2025-08-14 11:47:40 -07:00
Owen
bad244d0ea Merge branch 'main' into dev 2025-08-13 14:56:02 -07:00
Owen
d013dc0543 Adjust logging 2025-08-13 14:18:47 -07:00
Owen
0047b54e94 Dont override ENV
Fixes #101
2025-08-12 20:44:34 -07:00
Owen
f0c8d2c7c7 Change permissions to 0600
Fixes #104
2025-08-11 08:15:36 -07:00
Owen
28b6865f73 Healthcheck working 2025-08-11 08:14:29 -07:00
Pallavi
d52f89f629 Split mTLS client and CA certificates 2025-08-05 01:08:29 +05:30
Owen
289cce3a22 Add health checks 2025-08-03 18:43:43 -07:00
Owen
e8612c7e6b Handle adding and removing healthchecks 2025-08-03 17:02:15 -07:00
Owen
6820f8d23e Add basic heathchecks 2025-08-03 16:12:00 -07:00
Owen
151d0e38e6 Stop sending requests when you get a terminate 2025-08-03 14:47:36 -07:00
Nemental
a9d8ec0b1e docs: update docker socket part 2025-07-30 15:28:55 +02:00
Nemental
e9dbfb239b fix: remove hardcoded protocol from socket path 2025-07-30 09:36:53 +02:00
Nemental
a79dccc0e4 feat: checksocket protocol support 2025-07-30 09:36:19 +02:00
Nemental
42dfb6b3d8 feat: add type and function for docker endpoint parsing 2025-07-30 09:31:41 +02:00
Owen Schwartz
3ccd755d55 Merge pull request #95 from fosrl/dependabot/go_modules/prod-patch-updates-e08645070f
Bump github.com/docker/docker from 28.3.2+incompatible to 28.3.3+incompatible in the prod-patch-updates group
2025-07-29 23:24:19 -07:00
Owen Schwartz
a0f0b674e8 Merge pull request #96 from firecat53/main
Update flake.nix to 1.4.0
2025-07-29 23:24:03 -07:00
Owen
9e675121d3 Dont reset dns 2025-07-29 22:42:54 -07:00
Owen
45d17da570 Fix the bind problem by just recreating the dev
TODO: WHY CANT WE REBIND TO A PORT - WE NEED TO FIX THIS BETTER
2025-07-29 20:58:48 -07:00
Owen
dfba35f8bb Use the tunnel ip 2025-07-29 16:31:42 -07:00
Scott Hansen
9e73aab21d Update flake.nix to 1.4.0 2025-07-29 14:14:42 -07:00
dependabot[bot]
e1ddad006a Bump github.com/docker/docker in the prod-patch-updates group
Bumps the prod-patch-updates group with 1 update: [github.com/docker/docker](https://github.com/docker/docker).


Updates `github.com/docker/docker` from 28.3.2+incompatible to 28.3.3+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.3.2...v28.3.3)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.3.3+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-29 16:39:02 +00:00
Owen
29567d6e0b Dont print private key 2025-07-28 20:07:13 -07:00
Owen
47321ea9ad Update readme: env 2025-07-28 12:34:38 -07:00
Owen
abfc9d8efc Update readme: cli 2025-07-28 12:12:40 -07:00
Owen
c6929621e7 Merge branch 'main' into dev 2025-07-28 12:02:22 -07:00
Owen
46993203a3 Update readme 2025-07-28 12:02:10 -07:00
Owen
8306084354 SSH not ready 2025-07-28 12:02:09 -07:00
Owen
02c1e2b7d0 Compute kind of works now!? 2025-07-28 12:02:09 -07:00
Owen
ae7e2a1055 Clean up operation 2025-07-28 12:02:09 -07:00
Owen Schwartz
88f1335cff Merge pull request #93 from Lokowitz/sync-go-version
Sync go version
2025-07-28 11:59:10 -07:00
Owen
8bf9c9795b Netstack working 2025-07-27 10:25:34 -07:00
Marvin
5d343cd420 modified: go.mod
modified:   go.sum
2025-07-26 13:25:52 +00:00
Marvin
d1473b7e22 go.mod aktualisieren 2025-07-26 10:32:20 +02:00
Marvin
2efbd7dd6a Dockerfile aktualisieren 2025-07-26 10:31:53 +02:00
Marvin
82a3a39a1f .go-version aktualisieren 2025-07-26 10:31:35 +02:00
Marvin
df09193834 cicd.yml aktualisieren 2025-07-26 10:31:20 +02:00
Marvin
b2fe4e3b03 test.yml aktualisieren 2025-07-26 10:31:05 +02:00
Owen
e14d53087f Starting to work on option 2025-07-25 16:16:33 -07:00
Owen
3583270f73 Adding option for netstack 2025-07-25 16:16:00 -07:00
Owen
f5be05c55a Add flag 2025-07-25 16:14:25 -07:00
Owen
d09e3fbd60 Proxies working 2025-07-25 16:10:53 -07:00
Owen
493831b5f0 Pm working 2025-07-25 13:09:11 -07:00
Owen
9fc692c090 Proxy working? 2025-07-25 12:00:09 -07:00
Owen
ccb7008579 Just hp like olm 2025-07-25 11:42:36 -07:00
Owen
f17dbe1fef Use normal udp 2025-07-25 11:05:24 -07:00
Owen
27561f52ca Dont restart netstack 2025-07-25 11:01:54 -07:00
Owen
499ebcd928 Maybe its working? 2025-07-25 10:59:34 -07:00
Owen
40dfab31a5 Maybe basic func 2025-07-25 10:50:02 -07:00
Owen
56377ec87e Exit well 2025-07-24 20:46:33 -07:00
Owen
008be54c55 Add get config 2025-07-24 12:40:14 -07:00
Owen
64c22a94a4 Log to file optionally and update config locations 2025-07-24 12:01:53 -07:00
Owen Schwartz
468c93c581 Merge pull request #91 from fosrl/dependabot/go_modules/prod-minor-updates-17f8beca3b
Bump software.sslmate.com/src/go-pkcs12 from 0.5.0 to 0.6.0 in the prod-minor-updates group
2025-07-23 11:26:32 -07:00
Owen Schwartz
c53b859cda Merge pull request #92 from nepthar/patch-1
Nit: Typo fix in help string
2025-07-23 11:26:15 -07:00
Jordan Parker
6cd824baf2 Nit: Typo fix in help string 2025-07-23 10:25:11 -04:00
dependabot[bot]
d8c5182acd Bump software.sslmate.com/src/go-pkcs12 in the prod-minor-updates group
Bumps the prod-minor-updates group with 1 update: software.sslmate.com/src/go-pkcs12.


Updates `software.sslmate.com/src/go-pkcs12` from 0.5.0 to 0.6.0

---
updated-dependencies:
- dependency-name: software.sslmate.com/src/go-pkcs12
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-23 10:01:13 +00:00
Owen
c8c4666d63 Change rm to keep 2025-07-22 11:36:31 -07:00
Owen
f1fcc13e66 Holepunch to the right endpoint 2025-07-21 17:04:05 -07:00
Owen Schwartz
52bbc2fe31 Merge pull request #90 from firecat53/main
Update flake.nix to 1.3.4
2025-07-21 14:54:22 -07:00
Scott Hansen
b5ee12f84a Update flake.nix for 1.3.4 2025-07-21 11:23:50 -07:00
Owen
510e78437c Add client type 2025-07-18 16:55:38 -07:00
Owen
e14cffce1c Merge branch 'main' into clients-fr 2025-07-18 16:53:27 -07:00
Owen
629a92ee81 Make client work for olm 2025-07-18 16:53:13 -07:00
Owen
56df75544d Adjust logging 2025-07-18 16:52:59 -07:00
Owen
5b2e743470 Remove defers causing bad file descriptor issues 2025-07-18 15:49:57 -07:00
Owen
b5025c142f Working on it 2025-07-18 15:25:39 -07:00
Owen
cd86e6b6de Dont ping if there is just 1 2025-07-17 15:01:02 -07:00
Owen
230c34e4e0 Make sure to only exclude if there is anouther 2025-07-16 21:15:03 -07:00
Owen
a038ce1458 Move docker messages to debug for #86 2025-07-14 10:16:06 -07:00
Owen Schwartz
cd83efd365 Merge pull request #85 from firecat53/main
Update flake to 1.3.2
2025-07-13 19:47:06 -07:00
Scott Hansen
702f39e870 Update flake to 1.3.2 2025-07-13 18:47:20 -07:00
Owen Schwartz
02b7ea51af Merge pull request #84 from fosrl/dev
Ping improvements, dependabot, hostname in docker
2025-07-13 16:26:11 -07:00
Owen
e8421364fc Merge branch 'woutervanelten-patch-2' into dev 2025-07-13 16:12:42 -07:00
Owen
7264bb7001 Merge branch 'patch-2' of github.com:woutervanelten/newt into woutervanelten-patch-2 2025-07-13 16:10:23 -07:00
Owen
86e262ac1e Merge branch 'main' into dev 2025-07-13 16:10:02 -07:00
Owen Schwartz
dcacc03e96 Merge pull request #81 from fosrl/dependabot/go_modules/prod-patch-updates-f7fa3bf88c
Bump github.com/vishvananda/netlink from 1.3.0 to 1.3.1 in the prod-patch-updates group
2025-07-13 16:09:48 -07:00
Owen
6f4469a5a4 Merge branch 'woutervanelten-patch-5' into dev 2025-07-13 16:09:33 -07:00
Owen
663e28329b Fix typo with _ 2025-07-13 16:08:32 -07:00
Owen
f513f97fc3 Working on better ping 2025-07-13 16:07:46 -07:00
dependabot[bot]
ce4f3e4cdf Bump github.com/vishvananda/netlink in the prod-patch-updates group
Bumps the prod-patch-updates group with 1 update: [github.com/vishvananda/netlink](https://github.com/vishvananda/netlink).


Updates `github.com/vishvananda/netlink` from 1.3.0 to 1.3.1
- [Release notes](https://github.com/vishvananda/netlink/releases)
- [Commits](https://github.com/vishvananda/netlink/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/vishvananda/netlink
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-13 23:07:14 +00:00
Owen
58a74fce6f Merge branch 'patch-5' of github.com:woutervanelten/newt into woutervanelten-patch-5 2025-07-13 16:06:54 -07:00
Owen Schwartz
fc965abbc4 Merge pull request #82 from fosrl/dependabot/go_modules/prod-minor-updates-d992c0ea53
Bump the prod-minor-updates group with 2 updates
2025-07-13 16:05:52 -07:00
Owen
b881808cae Loosen up the ping intervals 2025-07-11 16:35:39 -07:00
dependabot[bot]
6160e4c8a6 Bump the prod-minor-updates group with 2 updates
Bumps the prod-minor-updates group with 2 updates: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/crypto` from 0.39.0 to 0.40.0
- [Commits](https://github.com/golang/crypto/compare/v0.39.0...v0.40.0)

Updates `golang.org/x/net` from 0.41.0 to 0.42.0
- [Commits](https://github.com/golang/net/compare/v0.41.0...v0.42.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-11 09:32:02 +00:00
Wouter van Elten
4d343e3541 Update README.md for health check
Added explanation of health_file
2025-07-11 08:14:32 +02:00
Owen
71d1bbaaf2 Merge branch 'main' of github.com:fosrl/newt 2025-07-10 17:27:09 -07:00
Owen
9eb8e5122a Fix link again 2025-07-10 17:26:58 -07:00
Owen Schwartz
c593e2aa97 Merge pull request #72 from fosrl/dev
Dev
2025-07-10 17:25:54 -07:00
Owen
c3483ded8f Merge branch 'main' into dev 2025-07-10 17:25:25 -07:00
Owen
b1cfd3ba02 Update link 2025-07-10 17:23:26 -07:00
Owen
2d9b761de9 Get newt script 2025-07-10 17:17:12 -07:00
Owen
53397663ef Adjust logging 2025-07-08 17:18:43 -07:00
Owen
221d5862fb Fix disconnect errors about closed connection 2025-07-08 08:48:39 -07:00
Owen
e4bdbbec7c Adjust logging 2025-07-08 08:48:27 -07:00
Owen
07bd283604 Remove dup code 2025-07-05 18:14:33 -07:00
Owen
b7d4ea0c84 Fix nil issues 2025-07-03 20:17:21 -07:00
Owen
d10c5e0366 Merge branch 'main' into dev 2025-07-03 10:11:30 -07:00
Owen
61a9097baf Make linux clients build correctly 2025-07-03 10:09:55 -07:00
Owen
a88d25f369 Fix missing netstack pack 2025-07-03 09:06:26 -07:00
Owen
c2a326c70a Working packages? 2025-07-03 09:05:09 -07:00
Owen Schwartz
be56550da4 Merge pull request #75 from woutervanelten/patch-4
Update go.mod
2025-07-03 09:01:39 -07:00
Wouter van Elten
a896291831 Update go.mod 2025-07-01 17:38:54 +02:00
Wouter van Elten
9348842e2c added use of hostname if available 2025-07-01 17:26:49 +02:00
Owen
ec8fc20438 Comment out WIP clients flags 2025-06-30 09:49:27 -07:00
Owen
a39d056725 Remove binary mistake 2025-06-30 09:04:58 -07:00
Owen
450fc6c20f Merge branch 'dev' into clients-pops 2025-06-30 09:01:25 -07:00
Owen
837f749cdb Merge branch 'dev' into clients-pops 2025-06-30 09:00:26 -07:00
Owen
63aea704bd Change math random 2025-06-22 12:55:13 -04:00
Owen
c1a2a3208c Add log message 2025-06-22 12:54:13 -04:00
Owen
ac67df63fa Add way to stop initial ping 2025-06-22 12:50:55 -04:00
Owen
e642983b88 Move updates 2025-06-22 12:10:00 -04:00
Owen
e2dd965654 Merge branch 'dev' into clients-pops 2025-06-22 11:54:39 -04:00
Owen
227631665e Properly check the http status code now 2025-06-19 22:58:27 -04:00
Owen
ca3ffa00ed Failover is working? 2025-06-19 17:38:21 -04:00
Owen
6d3938e14e Send version 2025-06-19 16:39:33 -04:00
Owen
37191924ee Change terminate to reconnect 2025-06-19 16:32:07 -04:00
Owen
a14f70dbaa Faster detection on ws side 2025-06-19 16:30:31 -04:00
Owen
bb1318278a Reorg and add timeout 2025-06-19 15:59:21 -04:00
Owen
1c75eb3bee New tunnel reconnect works 2025-06-19 15:55:47 -04:00
Owen
4b64b04603 Update ping check 2025-06-18 22:54:13 -04:00
miloschwartz
b397016da8 give preference to previously connected node 2025-06-18 15:57:15 -04:00
Owen
850c230c4a Print ping latency better and add connect message 2025-06-17 15:22:35 -04:00
Owen
c82b84194f Add mtu to readme 2025-06-17 10:23:43 -04:00
Owen
50df49e556 Tweak to call out when docker deployment 2025-06-17 10:23:43 -04:00
Rob
5f9c041c6b Update README.md
added docker socket mount example
2025-06-17 10:23:43 -04:00
miloschwartz
95d4cb2758 add exit node name to logs 2025-06-16 22:00:39 -04:00
miloschwartz
bbea9a91da adjust weight calculation 2025-06-16 15:54:46 -04:00
Owen
8d4d8b91b9 Working on pining 2025-06-13 16:15:49 -04:00
Owen
d0e220511a First pass at pinging 2025-06-10 21:52:33 -04:00
Owen
22f44c860a Merge branch 'dev' into clients-pops 2025-06-10 09:38:53 -04:00
Owen
a5f4d5fdf6 Resolve merge issues 2025-06-09 22:26:31 -04:00
Owen
ce6d340a8d Update go 2025-06-09 22:24:49 -04:00
Owen
eaf812a2a7 Merge branch 'dev' into clients-pops 2025-06-09 18:38:48 -04:00
Owen
494e30704b Set to 127 2025-05-13 11:42:06 -04:00
Owen
175718a48e Handle order of opertions of hole punch better 2025-04-22 22:11:37 -04:00
Owen
6a146ed371 Add more sensible controls 2025-04-13 21:28:38 -04:00
Owen
027d9a059f Remove redundant log message 2025-04-12 17:51:03 -04:00
40 changed files with 5562 additions and 1020 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @oschwartz10612 @miloschwartz

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@ jobs:
skopeo --version
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
- name: Input check
run: |

View File

@@ -14,7 +14,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
days-before-stale: 14
days-before-close: 14

View File

@@ -31,7 +31,7 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: 1.25

View File

@@ -1,4 +1,5 @@
FROM golang:1.25-alpine AS builder
# FROM golang:1.25-alpine AS builder
FROM public.ecr.aws/docker/library/golang:1.25-alpine AS builder
# Install git and ca-certificates
RUN apk --no-cache add ca-certificates git tzdata
@@ -16,9 +17,10 @@ RUN go mod download
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /newt
ARG VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X main.newtVersion=${VERSION}" -o /newt
FROM alpine:3.23 AS runner
FROM public.ecr.aws/docker/library/alpine:3.23 AS runner
RUN apk --no-cache add ca-certificates tzdata iputils

View File

@@ -2,8 +2,11 @@
all: local
VERSION ?= dev
LDFLAGS = -X main.newtVersion=$(VERSION)
local:
CGO_ENABLED=0 go build -o ./bin/newt
CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o ./bin/newt
docker-build:
docker build -t fosrl/newt:latest .
@@ -40,31 +43,31 @@ go-build-release: \
go-build-release-freebsd-arm64
go-build-release-linux-arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/newt_linux_arm64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_arm64
go-build-release-linux-arm32-v7:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o bin/newt_linux_arm32
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_arm32
go-build-release-linux-arm32-v6:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o bin/newt_linux_arm32v6
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_arm32v6
go-build-release-linux-amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/newt_linux_amd64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_amd64
go-build-release-linux-riscv64:
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -o bin/newt_linux_riscv64
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_riscv64
go-build-release-darwin-arm64:
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/newt_darwin_arm64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/newt_darwin_arm64
go-build-release-darwin-amd64:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/newt_darwin_amd64
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_darwin_amd64
go-build-release-windows-amd64:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/newt_windows_amd64.exe
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_windows_amd64.exe
go-build-release-freebsd-amd64:
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/newt_freebsd_amd64
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_freebsd_amd64
go-build-release-freebsd-arm64:
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/newt_freebsd_arm64
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/newt_freebsd_arm64

View File

@@ -1,37 +0,0 @@
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@pangolin.net
whitelist-users:
- owen@pangolin.net
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

View File

@@ -2,6 +2,8 @@ package clients
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net"
@@ -34,14 +36,21 @@ type WgConfig struct {
IpAddress string `json:"ipAddress"`
Peers []Peer `json:"peers"`
Targets []Target `json:"targets"`
ChainId string `json:"chainId"`
}
type Target struct {
SourcePrefix string `json:"sourcePrefix"`
DestPrefix string `json:"destPrefix"`
RewriteTo string `json:"rewriteTo,omitempty"`
DisableIcmp bool `json:"disableIcmp,omitempty"`
PortRange []PortRange `json:"portRange,omitempty"`
SourcePrefix string `json:"sourcePrefix"`
SourcePrefixes []string `json:"sourcePrefixes"`
DestPrefix string `json:"destPrefix"`
RewriteTo string `json:"rewriteTo,omitempty"`
DisableIcmp bool `json:"disableIcmp,omitempty"`
PortRange []PortRange `json:"portRange,omitempty"`
ResourceId int `json:"resourceId,omitempty"`
Protocol string `json:"protocol,omitempty"` // for now practicably either http or https
HTTPTargets []netstack2.HTTPTarget `json:"httpTargets,omitempty"` // for http protocol, list of downstream services to load balance across
TLSCert string `json:"tlsCert,omitempty"` // PEM-encoded certificate for incoming HTTPS termination
TLSKey string `json:"tlsKey,omitempty"` // PEM-encoded private key for incoming HTTPS termination
}
type PortRange struct {
@@ -69,19 +78,20 @@ type PeerReading struct {
}
type WireGuardService struct {
interfaceName string
mtu int
client *websocket.Client
config WgConfig
key wgtypes.Key
newtId string
lastReadings map[string]PeerReading
mu sync.Mutex
Port uint16
host string
serverPubKey string
token string
stopGetConfig func()
interfaceName string
mtu int
client *websocket.Client
config WgConfig
key wgtypes.Key
newtId string
lastReadings map[string]PeerReading
mu sync.Mutex
Port uint16
host string
serverPubKey string
token string
stopGetConfig func()
pendingConfigChainId string
// Netstack fields
tun tun.Device
tnet *netstack2.Net
@@ -106,6 +116,13 @@ type WireGuardService struct {
wgTesterServer *wgtester.Server
}
// generateChainId generates a random chain ID for deduplicating round-trip messages.
func generateChainId() string {
b := make([]byte, 8)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
func NewWireGuardService(interfaceName string, port uint16, mtu int, host string, newtId string, wsClient *websocket.Client, dns string, useNativeInterface bool) (*WireGuardService, error) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
@@ -160,9 +177,8 @@ func NewWireGuardService(interfaceName string, port uint16, mtu int, host string
useNativeInterface: useNativeInterface,
}
// Create the holepunch manager with ResolveDomain function
// We'll need to pass a domain resolver function
service.holePunchManager = holepunch.NewManager(sharedBind, newtId, "newt", key.PublicKey().String())
// Create the holepunch manager
service.holePunchManager = holepunch.NewManager(sharedBind, newtId, "newt", key.PublicKey().String(), nil)
// Register websocket handlers
wsClient.RegisterHandler("newt/wg/receive-config", service.handleConfig)
@@ -172,6 +188,7 @@ func NewWireGuardService(interfaceName string, port uint16, mtu int, host string
wsClient.RegisterHandler("newt/wg/targets/add", service.handleAddTarget)
wsClient.RegisterHandler("newt/wg/targets/remove", service.handleRemoveTarget)
wsClient.RegisterHandler("newt/wg/targets/update", service.handleUpdateTarget)
wsClient.RegisterHandler("newt/wg/sync", service.handleSyncConfig)
return service, nil
}
@@ -194,6 +211,15 @@ func (s *WireGuardService) Close() {
s.stopGetConfig = nil
}
// Flush access logs before tearing down the tunnel
if s.tnet != nil {
if ph := s.tnet.GetProxyHandler(); ph != nil {
if al := ph.GetAccessLogger(); al != nil {
al.Close()
}
}
}
// Stop the direct UDP relay first
s.StopDirectUDPRelay()
@@ -277,7 +303,7 @@ func (s *WireGuardService) StartHolepunch(publicKey string, endpoint string, rel
}
if relayPort == 0 {
relayPort = 21820
relayPort = 21820
}
// Convert websocket.ExitNode to holepunch.ExitNode
@@ -440,9 +466,12 @@ func (s *WireGuardService) LoadRemoteConfig() error {
s.stopGetConfig()
s.stopGetConfig = nil
}
chainId := generateChainId()
s.pendingConfigChainId = chainId
s.stopGetConfig = s.client.SendMessageInterval("newt/wg/get-config", map[string]interface{}{
"publicKey": s.key.PublicKey().String(),
"port": s.Port,
"chainId": chainId,
}, 2*time.Second)
logger.Debug("Requesting WireGuard configuration from remote server")
@@ -467,6 +496,17 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) {
logger.Info("Error unmarshaling target data: %v", err)
return
}
// Deduplicate using chainId: discard responses that don't match the
// pending request, or that we have already processed.
if config.ChainId != "" {
if config.ChainId != s.pendingConfigChainId {
logger.Debug("Discarding duplicate/stale newt/wg/get-config response (chainId=%s, expected=%s)", config.ChainId, s.pendingConfigChainId)
return
}
s.pendingConfigChainId = "" // consume further duplicates are rejected
}
s.config = config
if s.stopGetConfig != nil {
@@ -492,6 +532,194 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) {
logger.Info("Client connectivity setup. Ready to accept connections from clients!")
}
// SyncConfig represents the configuration sent from server for syncing
type SyncConfig struct {
Targets []Target `json:"targets"`
Peers []Peer `json:"peers"`
}
func (s *WireGuardService) handleSyncConfig(msg websocket.WSMessage) {
var syncConfig SyncConfig
logger.Debug("Received sync message: %v", msg)
logger.Info("Received sync configuration from remote server")
jsonData, err := json.Marshal(msg.Data)
if err != nil {
logger.Error("Error marshaling sync data: %v", err)
return
}
if err := json.Unmarshal(jsonData, &syncConfig); err != nil {
logger.Error("Error unmarshaling sync data: %v", err)
return
}
// Sync peers
if err := s.syncPeers(syncConfig.Peers); err != nil {
logger.Error("Failed to sync peers: %v", err)
}
// Sync targets
if err := s.syncTargets(syncConfig.Targets); err != nil {
logger.Error("Failed to sync targets: %v", err)
}
}
// syncPeers synchronizes the current peers with the desired state
// It removes peers not in the desired list and adds missing ones
func (s *WireGuardService) syncPeers(desiredPeers []Peer) error {
if s.device == nil {
return fmt.Errorf("WireGuard device is not initialized")
}
// Get current peers from the device
currentConfig, err := s.device.IpcGet()
if err != nil {
return fmt.Errorf("failed to get current device config: %v", err)
}
// Parse current peer public keys
lines := strings.Split(currentConfig, "\n")
currentPeerKeys := make(map[string]bool)
for _, line := range lines {
if strings.HasPrefix(line, "public_key=") {
pubKey := strings.TrimPrefix(line, "public_key=")
currentPeerKeys[pubKey] = true
}
}
// Build a map of desired peers by their public key (normalized)
desiredPeerMap := make(map[string]Peer)
for _, peer := range desiredPeers {
// Normalize the public key for comparison
pubKey, err := wgtypes.ParseKey(peer.PublicKey)
if err != nil {
logger.Warn("Invalid public key in desired peers: %s", peer.PublicKey)
continue
}
normalizedKey := util.FixKey(pubKey.String())
desiredPeerMap[normalizedKey] = peer
}
// Remove peers that are not in the desired list
for currentKey := range currentPeerKeys {
if _, exists := desiredPeerMap[currentKey]; !exists {
// Parse the key back to get the original format for removal
removeConfig := fmt.Sprintf("public_key=%s\nremove=true", currentKey)
if err := s.device.IpcSet(removeConfig); err != nil {
logger.Warn("Failed to remove peer %s during sync: %v", currentKey, err)
} else {
logger.Info("Removed peer %s during sync", currentKey)
}
}
}
// Add peers that are missing
for normalizedKey, peer := range desiredPeerMap {
if _, exists := currentPeerKeys[normalizedKey]; !exists {
if err := s.addPeerToDevice(peer); err != nil {
logger.Warn("Failed to add peer %s during sync: %v", peer.PublicKey, err)
} else {
logger.Info("Added peer %s during sync", peer.PublicKey)
}
}
}
return nil
}
// syncTargets synchronizes the current targets with the desired state
// It removes targets not in the desired list and adds missing ones
func (s *WireGuardService) syncTargets(desiredTargets []Target) error {
if s.tnet == nil {
// Native interface mode - proxy features not available, skip silently
logger.Debug("Skipping target sync - using native interface (no proxy support)")
return nil
}
// Get current rules from the proxy handler
currentRules := s.tnet.GetProxySubnetRules()
// Build a map of current rules by source+dest prefix
type ruleKey struct {
sourcePrefix string
destPrefix string
}
currentRuleMap := make(map[ruleKey]bool)
for _, rule := range currentRules {
key := ruleKey{
sourcePrefix: rule.SourcePrefix.String(),
destPrefix: rule.DestPrefix.String(),
}
currentRuleMap[key] = true
}
// Build a map of desired targets
desiredTargetMap := make(map[ruleKey]Target)
for _, target := range desiredTargets {
key := ruleKey{
sourcePrefix: target.SourcePrefix,
destPrefix: target.DestPrefix,
}
desiredTargetMap[key] = target
}
// Remove targets that are not in the desired list
for _, rule := range currentRules {
key := ruleKey{
sourcePrefix: rule.SourcePrefix.String(),
destPrefix: rule.DestPrefix.String(),
}
if _, exists := desiredTargetMap[key]; !exists {
s.tnet.RemoveProxySubnetRule(rule.SourcePrefix, rule.DestPrefix)
logger.Info("Removed target %s -> %s during sync", rule.SourcePrefix.String(), rule.DestPrefix.String())
}
}
// Add targets that are missing
for key, target := range desiredTargetMap {
if _, exists := currentRuleMap[key]; !exists {
sourcePrefix, err := netip.ParsePrefix(target.SourcePrefix)
if err != nil {
logger.Warn("Invalid source prefix %s during sync: %v", target.SourcePrefix, err)
continue
}
destPrefix, err := netip.ParsePrefix(target.DestPrefix)
if err != nil {
logger.Warn("Invalid dest prefix %s during sync: %v", target.DestPrefix, err)
continue
}
var portRanges []netstack2.PortRange
for _, pr := range target.PortRange {
portRanges = append(portRanges, netstack2.PortRange{
Min: pr.Min,
Max: pr.Max,
Protocol: pr.Protocol,
})
}
s.tnet.AddProxySubnetRule(netstack2.SubnetRule{
SourcePrefix: sourcePrefix,
DestPrefix: destPrefix,
RewriteTo: target.RewriteTo,
PortRanges: portRanges,
DisableIcmp: target.DisableIcmp,
ResourceId: target.ResourceId,
Protocol: target.Protocol,
HTTPTargets: target.HTTPTargets,
TLSCert: target.TLSCert,
TLSKey: target.TLSKey,
})
logger.Info("Added target %s -> %s during sync", target.SourcePrefix, target.DestPrefix)
}
}
return nil
}
func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error {
s.mu.Lock()
@@ -615,6 +843,20 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error {
s.TunnelIP = tunnelIP.String()
// Configure the access log sender to ship compressed session logs via websocket
s.tnet.SetAccessLogSender(func(data string) error {
return s.client.SendMessageNoLog("newt/access-log", map[string]interface{}{
"compressed": data,
})
})
// Configure the HTTP request log sender to ship compressed request logs via websocket
s.tnet.SetHTTPRequestLogSender(func(data string) error {
return s.client.SendMessageNoLog("newt/request-log", map[string]interface{}{
"compressed": data,
})
})
// Create WireGuard device using the shared bind
s.device = device.NewDevice(s.tun, s.sharedBind, device.NewLogger(
device.LogLevelSilent, // Use silent logging by default - could be made configurable
@@ -695,6 +937,19 @@ func (s *WireGuardService) ensureWireguardPeers(peers []Peer) error {
return nil
}
// resolveSourcePrefixes returns the effective list of source prefixes for a target,
// supporting both the legacy single SourcePrefix field and the new SourcePrefixes array.
// If SourcePrefixes is non-empty it takes precedence; otherwise SourcePrefix is used.
func resolveSourcePrefixes(target Target) []string {
if len(target.SourcePrefixes) > 0 {
return target.SourcePrefixes
}
if target.SourcePrefix != "" {
return []string{target.SourcePrefix}
}
return nil
}
func (s *WireGuardService) ensureTargets(targets []Target) error {
if s.tnet == nil {
// Native interface mode - proxy features not available, skip silently
@@ -703,11 +958,6 @@ func (s *WireGuardService) ensureTargets(targets []Target) error {
}
for _, target := range targets {
sourcePrefix, err := netip.ParsePrefix(target.SourcePrefix)
if err != nil {
return fmt.Errorf("invalid CIDR %s: %v", target.SourcePrefix, err)
}
destPrefix, err := netip.ParsePrefix(target.DestPrefix)
if err != nil {
return fmt.Errorf("invalid CIDR %s: %v", target.DestPrefix, err)
@@ -722,9 +972,25 @@ func (s *WireGuardService) ensureTargets(targets []Target) error {
})
}
s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp)
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", target.SourcePrefix, target.DestPrefix, target.RewriteTo, target.PortRange)
for _, sp := range resolveSourcePrefixes(target) {
sourcePrefix, err := netip.ParsePrefix(sp)
if err != nil {
return fmt.Errorf("invalid CIDR %s: %v", sp, err)
}
s.tnet.AddProxySubnetRule(netstack2.SubnetRule{
SourcePrefix: sourcePrefix,
DestPrefix: destPrefix,
RewriteTo: target.RewriteTo,
PortRanges: portRanges,
DisableIcmp: target.DisableIcmp,
ResourceId: target.ResourceId,
Protocol: target.Protocol,
HTTPTargets: target.HTTPTargets,
TLSCert: target.TLSCert,
TLSKey: target.TLSKey,
})
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange)
}
}
return nil
@@ -1043,7 +1309,7 @@ func (s *WireGuardService) processPeerBandwidth(publicKey string, rxBytes, txByt
BytesOut: bytesOutMB,
}
}
return nil
}
}
@@ -1094,12 +1360,6 @@ func (s *WireGuardService) handleAddTarget(msg websocket.WSMessage) {
// Process all targets
for _, target := range targets {
sourcePrefix, err := netip.ParsePrefix(target.SourcePrefix)
if err != nil {
logger.Info("Invalid CIDR %s: %v", target.SourcePrefix, err)
continue
}
destPrefix, err := netip.ParsePrefix(target.DestPrefix)
if err != nil {
logger.Info("Invalid CIDR %s: %v", target.DestPrefix, err)
@@ -1109,15 +1369,32 @@ func (s *WireGuardService) handleAddTarget(msg websocket.WSMessage) {
var portRanges []netstack2.PortRange
for _, pr := range target.PortRange {
portRanges = append(portRanges, netstack2.PortRange{
Min: pr.Min,
Max: pr.Max,
Protocol: pr.Protocol,
Min: pr.Min,
Max: pr.Max,
Protocol: pr.Protocol,
})
}
s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp)
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", target.SourcePrefix, target.DestPrefix, target.RewriteTo, target.PortRange)
for _, sp := range resolveSourcePrefixes(target) {
sourcePrefix, err := netip.ParsePrefix(sp)
if err != nil {
logger.Info("Invalid CIDR %s: %v", sp, err)
continue
}
s.tnet.AddProxySubnetRule(netstack2.SubnetRule{
SourcePrefix: sourcePrefix,
DestPrefix: destPrefix,
RewriteTo: target.RewriteTo,
PortRanges: portRanges,
DisableIcmp: target.DisableIcmp,
ResourceId: target.ResourceId,
Protocol: target.Protocol,
HTTPTargets: target.HTTPTargets,
TLSCert: target.TLSCert,
TLSKey: target.TLSKey,
})
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange)
}
}
}
@@ -1146,21 +1423,21 @@ func (s *WireGuardService) handleRemoveTarget(msg websocket.WSMessage) {
// Process all targets
for _, target := range targets {
sourcePrefix, err := netip.ParsePrefix(target.SourcePrefix)
if err != nil {
logger.Info("Invalid CIDR %s: %v", target.SourcePrefix, err)
continue
}
destPrefix, err := netip.ParsePrefix(target.DestPrefix)
if err != nil {
logger.Info("Invalid CIDR %s: %v", target.DestPrefix, err)
continue
}
s.tnet.RemoveProxySubnetRule(sourcePrefix, destPrefix)
logger.Info("Removed target subnet %s with destination %s", target.SourcePrefix, target.DestPrefix)
for _, sp := range resolveSourcePrefixes(target) {
sourcePrefix, err := netip.ParsePrefix(sp)
if err != nil {
logger.Info("Invalid CIDR %s: %v", sp, err)
continue
}
s.tnet.RemoveProxySubnetRule(sourcePrefix, destPrefix)
logger.Info("Removed target subnet %s with destination %s", sp, target.DestPrefix)
}
}
}
@@ -1194,30 +1471,24 @@ func (s *WireGuardService) handleUpdateTarget(msg websocket.WSMessage) {
// Process all update requests
for _, target := range requests.OldTargets {
sourcePrefix, err := netip.ParsePrefix(target.SourcePrefix)
if err != nil {
logger.Info("Invalid CIDR %s: %v", target.SourcePrefix, err)
continue
}
destPrefix, err := netip.ParsePrefix(target.DestPrefix)
if err != nil {
logger.Info("Invalid CIDR %s: %v", target.DestPrefix, err)
continue
}
s.tnet.RemoveProxySubnetRule(sourcePrefix, destPrefix)
logger.Info("Removed target subnet %s with destination %s", target.SourcePrefix, target.DestPrefix)
for _, sp := range resolveSourcePrefixes(target) {
sourcePrefix, err := netip.ParsePrefix(sp)
if err != nil {
logger.Info("Invalid CIDR %s: %v", sp, err)
continue
}
s.tnet.RemoveProxySubnetRule(sourcePrefix, destPrefix)
logger.Info("Removed target subnet %s with destination %s", sp, target.DestPrefix)
}
}
for _, target := range requests.NewTargets {
// Now add the new target
sourcePrefix, err := netip.ParsePrefix(target.SourcePrefix)
if err != nil {
logger.Info("Invalid CIDR %s: %v", target.SourcePrefix, err)
continue
}
destPrefix, err := netip.ParsePrefix(target.DestPrefix)
if err != nil {
logger.Info("Invalid CIDR %s: %v", target.DestPrefix, err)
@@ -1227,14 +1498,32 @@ func (s *WireGuardService) handleUpdateTarget(msg websocket.WSMessage) {
var portRanges []netstack2.PortRange
for _, pr := range target.PortRange {
portRanges = append(portRanges, netstack2.PortRange{
Min: pr.Min,
Max: pr.Max,
Protocol: pr.Protocol,
Min: pr.Min,
Max: pr.Max,
Protocol: pr.Protocol,
})
}
s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp)
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", target.SourcePrefix, target.DestPrefix, target.RewriteTo, target.PortRange)
for _, sp := range resolveSourcePrefixes(target) {
sourcePrefix, err := netip.ParsePrefix(sp)
if err != nil {
logger.Info("Invalid CIDR %s: %v", sp, err)
continue
}
s.tnet.AddProxySubnetRule(netstack2.SubnetRule{
SourcePrefix: sourcePrefix,
DestPrefix: destPrefix,
RewriteTo: target.RewriteTo,
PortRanges: portRanges,
DisableIcmp: target.DisableIcmp,
ResourceId: target.ResourceId,
Protocol: target.Protocol,
HTTPTargets: target.HTTPTargets,
TLSCert: target.TLSCert,
TLSKey: target.TLSKey,
})
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange)
}
}
}

174
common.go
View File

@@ -5,8 +5,10 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"os/exec"
"regexp"
"strings"
"time"
@@ -206,6 +208,7 @@ func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) (stopC
logger.Warn(msgHealthFileWriteFailed, err)
}
}
return
}
case <-pingStopChan:
// Stop the goroutine when signaled
@@ -218,6 +221,25 @@ func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) (stopC
return stopChan, fmt.Errorf("initial ping attempts failed, continuing in background")
}
// shouldFireRecovery decides whether the data-plane recovery flow in
// startPingCheck should run on this tick. Recovery fires once when the
// consecutive-failure counter first crosses the threshold; the connectionLost
// flag prevents re-firing until a successful ping resets the state.
//
// This condition was previously inlined into startPingCheck and AND-ed with
// `currentInterval < maxInterval`, which silently broke recovery once
// pingInterval's default was bumped to 15s while maxInterval stayed at 6s
// (commit 8161fa6, March 2026): the gate became permanently false on default
// settings, so the recovery code never executed and ping failures climbed
// forever — the proximate cause of fosrl/newt#284, #310 and pangolin#1004.
//
// Recovery and backoff are independent concerns; the backoff ramp is now
// computed separately in the caller. Do not re-introduce currentInterval
// here.
func shouldFireRecovery(consecutiveFailures, failureThreshold int, connectionLost bool) bool {
return consecutiveFailures >= failureThreshold && !connectionLost
}
func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Client, tunnelID string) chan struct{} {
maxInterval := 6 * time.Second
currentInterval := pingInterval
@@ -277,35 +299,44 @@ func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Clien
// 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 after %d failures. Continuous reconnection attempts will be made.", consecutiveFailures)
if tunnelID != "" {
telemetry.IncReconnect(context.Background(), tunnelID, "client", telemetry.ReasonTimeout)
}
stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second)
// Send registration message to the server for backward compatibility
err := client.SendMessage("newt/wg/register", map[string]interface{}{
"publicKey": publicKey.String(),
"backwardsCompatible": true,
})
if shouldFireRecovery(consecutiveFailures, failureThreshold, connectionLost) {
connectionLost = true
logger.Warn("Connection to server lost after %d failures. Continuous reconnection attempts will be made.", consecutiveFailures)
if tunnelID != "" {
telemetry.IncReconnect(context.Background(), tunnelID, "client", telemetry.ReasonTimeout)
}
pingChainId := generateChainId()
pendingPingChainId = pingChainId
stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{
"chainId": pingChainId,
}, 3*time.Second)
// Send registration message to the server for backward compatibility
bcChainId := generateChainId()
pendingRegisterChainId = bcChainId
err := client.SendMessage("newt/wg/register", map[string]interface{}{
"publicKey": publicKey.String(),
"backwardsCompatible": true,
"chainId": bcChainId,
})
if err != nil {
logger.Error("Failed to send registration message: %v", err)
}
if healthFile != "" {
err = os.Remove(healthFile)
if err != nil {
logger.Error("Failed to send registration message: %v", err)
}
if healthFile != "" {
err = os.Remove(healthFile)
if err != nil {
logger.Error("Failed to remove health file: %v", err)
}
logger.Error("Failed to remove health file: %v", err)
}
}
currentInterval = time.Duration(float64(currentInterval) * 1.3) // Slower increase
}
// Backoff: ramp the periodic-ping interval up while we are
// past the failure threshold, capped at maxInterval. Kept
// independent of the recovery trigger above so the trigger
// fires on every outage regardless of pingInterval.
if consecutiveFailures >= failureThreshold && currentInterval < maxInterval {
currentInterval = time.Duration(float64(currentInterval) * 1.3)
if currentInterval > maxInterval {
currentInterval = maxInterval
}
ticker.Reset(currentInterval)
logger.Debug("Increased ping check interval to %v due to consecutive failures", currentInterval)
}
} else {
// Track recent latencies
@@ -363,27 +394,62 @@ func parseTargetData(data interface{}) (TargetData, error) {
return targetData, nil
}
// parseTargetString parses a target string in the format "listenPort:host:targetPort"
// It properly handles IPv6 addresses which must be in brackets: "listenPort:[ipv6]:targetPort"
// Examples:
// - IPv4: "3001:192.168.1.1:80"
// - IPv6: "3001:[::1]:8080" or "3001:[fd70:1452:b736:4dd5:caca:7db9:c588:f5b3]:80"
//
// Returns listenPort, targetAddress (in host:port format suitable for net.Dial), and error
func parseTargetString(target string) (int, string, error) {
// Find the first colon to extract the listen port
firstColon := strings.Index(target, ":")
if firstColon == -1 {
return 0, "", fmt.Errorf("invalid target format, no colon found: %s", target)
}
listenPortStr := target[:firstColon]
var listenPort int
_, err := fmt.Sscanf(listenPortStr, "%d", &listenPort)
if err != nil {
return 0, "", fmt.Errorf("invalid listen port: %s", listenPortStr)
}
if listenPort <= 0 || listenPort > 65535 {
return 0, "", fmt.Errorf("listen port out of range: %d", listenPort)
}
// The remainder is host:targetPort - use net.SplitHostPort which handles IPv6 brackets
remainder := target[firstColon+1:]
host, targetPort, err := net.SplitHostPort(remainder)
if err != nil {
return 0, "", fmt.Errorf("invalid host:port format '%s': %w", remainder, err)
}
// Reject empty host or target port
if host == "" {
return 0, "", fmt.Errorf("empty host in target: %s", target)
}
if targetPort == "" {
return 0, "", fmt.Errorf("empty target port in target: %s", target)
}
// Reconstruct the target address using JoinHostPort (handles IPv6 properly)
targetAddr := net.JoinHostPort(host, targetPort)
return listenPort, targetAddr, nil
}
func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto string, targetData TargetData) error {
for _, t := range targetData.Targets {
// Split the first number off of the target with : separator and use as the port
parts := strings.Split(t, ":")
if len(parts) != 3 {
logger.Info("Invalid target format: %s", t)
continue
}
// Get the port as an int
port := 0
_, err := fmt.Sscanf(parts[0], "%d", &port)
// Parse the target string, handling both IPv4 and IPv6 addresses
port, target, err := parseTargetString(t)
if err != nil {
logger.Info("Invalid port: %s", parts[0])
logger.Info("Invalid target format: %s (%v)", t, err)
continue
}
switch action {
case "add":
target := parts[1] + ":" + parts[2]
// Call updown script if provided
processedTarget := target
if updownScript != "" {
@@ -410,8 +476,6 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto
case "remove":
logger.Info("Removing target with port %d", port)
target := parts[1] + ":" + parts[2]
// Call updown script if provided
if updownScript != "" {
_, err := executeUpdownScript(action, proto, target)
@@ -420,7 +484,7 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto
}
}
err := pm.RemoveTarget(proto, tunnelIP, port)
err = pm.RemoveTarget(proto, tunnelIP, port)
if err != nil {
logger.Error("Failed to remove target: %v", err)
return err
@@ -475,15 +539,41 @@ func executeUpdownScript(action, proto, target string) (string, error) {
return target, nil
}
func sendBlueprint(client *websocket.Client) error {
if blueprintFile == "" {
// interpolateBlueprint finds all {{...}} tokens in the raw blueprint bytes and
// replaces recognised schemes with their resolved values. Currently supported:
//
// - env.<VAR> replaced with the value of the named environment variable
//
// Any token that does not match a supported scheme is left as-is so that
// future schemes (e.g. tag., api.) are preserved rather than silently dropped.
func interpolateBlueprint(data []byte) []byte {
re := regexp.MustCompile(`\{\{([^}]+)\}\}`)
return re.ReplaceAllFunc(data, func(match []byte) []byte {
// strip the surrounding {{ }}
inner := strings.TrimSpace(string(match[2 : len(match)-2]))
if strings.HasPrefix(inner, "env.") {
varName := strings.TrimPrefix(inner, "env.")
return []byte(os.Getenv(varName))
}
// unrecognised scheme leave the token untouched
return match
})
}
func sendBlueprint(client *websocket.Client, file string) error {
if file == "" {
return nil
}
// try to read the blueprint file
blueprintData, err := os.ReadFile(blueprintFile)
blueprintData, err := os.ReadFile(file)
if err != nil {
logger.Error("Failed to read blueprint file: %v", err)
} else {
// interpolate {{env.VAR}} (and any future schemes) before parsing
blueprintData = interpolateBlueprint(blueprintData)
// first we should convert the yaml to json and error if the yaml is bad
var yamlObj interface{}
var blueprintJsonData string

251
common_test.go Normal file
View File

@@ -0,0 +1,251 @@
package main
import (
"net"
"testing"
)
func TestParseTargetString(t *testing.T) {
tests := []struct {
name string
input string
wantListenPort int
wantTargetAddr string
wantErr bool
}{
// IPv4 test cases
{
name: "valid IPv4 basic",
input: "3001:192.168.1.1:80",
wantListenPort: 3001,
wantTargetAddr: "192.168.1.1:80",
wantErr: false,
},
{
name: "valid IPv4 localhost",
input: "8080:127.0.0.1:3000",
wantListenPort: 8080,
wantTargetAddr: "127.0.0.1:3000",
wantErr: false,
},
{
name: "valid IPv4 same ports",
input: "443:10.0.0.1:443",
wantListenPort: 443,
wantTargetAddr: "10.0.0.1:443",
wantErr: false,
},
// IPv6 test cases
{
name: "valid IPv6 loopback",
input: "3001:[::1]:8080",
wantListenPort: 3001,
wantTargetAddr: "[::1]:8080",
wantErr: false,
},
{
name: "valid IPv6 full address",
input: "80:[fd70:1452:b736:4dd5:caca:7db9:c588:f5b3]:8080",
wantListenPort: 80,
wantTargetAddr: "[fd70:1452:b736:4dd5:caca:7db9:c588:f5b3]:8080",
wantErr: false,
},
{
name: "valid IPv6 link-local",
input: "443:[fe80::1]:443",
wantListenPort: 443,
wantTargetAddr: "[fe80::1]:443",
wantErr: false,
},
{
name: "valid IPv6 all zeros compressed",
input: "8000:[::]:9000",
wantListenPort: 8000,
wantTargetAddr: "[::]:9000",
wantErr: false,
},
{
name: "valid IPv6 mixed notation",
input: "5000:[::ffff:192.168.1.1]:6000",
wantListenPort: 5000,
wantTargetAddr: "[::ffff:192.168.1.1]:6000",
wantErr: false,
},
// Hostname test cases
{
name: "valid hostname",
input: "8080:example.com:80",
wantListenPort: 8080,
wantTargetAddr: "example.com:80",
wantErr: false,
},
{
name: "valid hostname with subdomain",
input: "443:api.example.com:8443",
wantListenPort: 443,
wantTargetAddr: "api.example.com:8443",
wantErr: false,
},
{
name: "valid localhost hostname",
input: "3000:localhost:3000",
wantListenPort: 3000,
wantTargetAddr: "localhost:3000",
wantErr: false,
},
// Error cases
{
name: "invalid - no colons",
input: "invalid",
wantErr: true,
},
{
name: "invalid - empty string",
input: "",
wantErr: true,
},
{
name: "invalid - non-numeric listen port",
input: "abc:192.168.1.1:80",
wantErr: true,
},
{
name: "invalid - missing target port",
input: "3001:192.168.1.1",
wantErr: true,
},
{
name: "invalid - IPv6 without brackets",
input: "3001:fd70:1452:b736:4dd5:caca:7db9:c588:f5b3:80",
wantErr: true,
},
{
name: "invalid - only listen port",
input: "3001:",
wantErr: true,
},
{
name: "invalid - missing host",
input: "3001::80",
wantErr: true,
},
{
name: "invalid - IPv6 unclosed bracket",
input: "3001:[::1:80",
wantErr: true,
},
{
name: "invalid - listen port zero",
input: "0:192.168.1.1:80",
wantErr: true,
},
{
name: "invalid - listen port negative",
input: "-1:192.168.1.1:80",
wantErr: true,
},
{
name: "invalid - listen port out of range",
input: "70000:192.168.1.1:80",
wantErr: true,
},
{
name: "invalid - empty target port",
input: "3001:192.168.1.1:",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
listenPort, targetAddr, err := parseTargetString(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parseTargetString(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
return
}
if tt.wantErr {
return // Don't check other values if we expected an error
}
if listenPort != tt.wantListenPort {
t.Errorf("parseTargetString(%q) listenPort = %d, want %d", tt.input, listenPort, tt.wantListenPort)
}
if targetAddr != tt.wantTargetAddr {
t.Errorf("parseTargetString(%q) targetAddr = %q, want %q", tt.input, targetAddr, tt.wantTargetAddr)
}
})
}
}
// TestParseTargetStringNetDialCompatibility verifies that the output is compatible with net.Dial
func TestParseTargetStringNetDialCompatibility(t *testing.T) {
tests := []struct {
name string
input string
}{
{"IPv4", "8080:127.0.0.1:80"},
{"IPv6 loopback", "8080:[::1]:80"},
{"IPv6 full", "8080:[2001:db8::1]:80"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, targetAddr, err := parseTargetString(tt.input)
if err != nil {
t.Fatalf("parseTargetString(%q) unexpected error: %v", tt.input, err)
}
// Verify the format is valid for net.Dial by checking it can be split back
// This doesn't actually dial, just validates the format
_, _, err = net.SplitHostPort(targetAddr)
if err != nil {
t.Errorf("parseTargetString(%q) produced invalid net.Dial format %q: %v", tt.input, targetAddr, err)
}
})
}
}
// TestShouldFireRecovery is the regression guard for the broken trigger gate
// that prevented data-plane recovery from ever firing under default settings
// (fosrl/newt#284, #310, pangolin#1004). The pre-fix condition was
//
// consecutiveFailures >= failureThreshold && currentInterval < maxInterval
//
// which became permanently false once pingInterval's default was bumped from
// 3s to 15s in commit 8161fa6 — currentInterval starts at pingInterval=15s,
// maxInterval stayed at 6s, so 15<6 is false and the recovery branch never
// executed.
//
// The fix is to drop currentInterval from the trigger condition entirely;
// backoff is a separate concern computed in the caller. The cases below
// exercise the documented contract.
func TestShouldFireRecovery(t *testing.T) {
const threshold = 4
cases := []struct {
name string
failures int
connectionLost bool
want bool
}{
{"below threshold, fresh", 3, false, false},
{"below threshold, already lost", 3, true, false},
{"at threshold, fresh — recovery must fire", threshold, false, true},
{"at threshold, already lost — gate prevents re-fire", threshold, true, false},
{"far above threshold, fresh", 100, false, true},
{"far above threshold, already lost", 100, true, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := shouldFireRecovery(c.failures, threshold, c.connectionLost); got != c.want {
t.Errorf("shouldFireRecovery(failures=%d, threshold=%d, lost=%v) = %v, want %v",
c.failures, threshold, c.connectionLost, got, c.want)
}
})
}
}

View File

@@ -25,7 +25,7 @@
inherit (pkgs) lib;
# Update version when releasing
version = "1.8.0";
version = "1.12.5";
in
{
default = self.packages.${system}.pangolin-newt;
@@ -35,7 +35,7 @@
inherit version;
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
vendorHash = "sha256-Sib6AUCpMgxlMpTc2Esvs+UU0yduVOxWUgT44FHAI+k=";
vendorHash = "sha256-WfIK+Q8WQ372NzLw6DRapv1nYPduShi4KnVJBPk0Oz0=";
nativeInstallCheckInputs = [ pkgs.versionCheckHook ];

View File

@@ -1,7 +1,7 @@
#!/bin/bash
#!/bin/sh
# Get Newt - Cross-platform installation script
# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/newt/refs/heads/main/get-newt.sh | bash
# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/newt/refs/heads/main/get-newt.sh | sh
set -e
@@ -17,54 +17,51 @@ GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest"
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
printf '%b[INFO]%b %s\n' "${GREEN}" "${NC}" "$1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
printf '%b[WARN]%b %s\n' "${YELLOW}" "${NC}" "$1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
printf '%b[ERROR]%b %s\n' "${RED}" "${NC}" "$1"
}
# Function to get latest version from GitHub API
get_latest_version() {
local latest_info
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
print_error "Neither curl nor wget is available."
exit 1
fi
if [ -z "$latest_info" ]; then
print_error "Failed to fetch latest version information" >&2
print_error "Failed to fetch latest version info"
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/')
version=$(printf '%s' "$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
print_error "Could not parse version from GitHub API response"
exit 1
fi
# Remove 'v' prefix if present
version=$(echo "$version" | sed 's/^v//')
echo "$version"
version=$(printf '%s' "$version" | sed 's/^v//')
printf '%s' "$version"
}
# Detect OS and architecture
detect_platform() {
local os arch
# Detect OS
os=""
arch=""
case "$(uname -s)" in
Linux*) os="linux" ;;
Darwin*) os="darwin" ;;
@@ -75,12 +72,11 @@ detect_platform() {
exit 1
;;
esac
# Detect architecture
case "$(uname -m)" in
x86_64|amd64) arch="amd64" ;;
arm64|aarch64) arch="arm64" ;;
armv7l|armv6l)
armv7l|armv6l)
if [ "$os" = "linux" ]; then
if [ "$(uname -m)" = "armv6l" ]; then
arch="arm32v6"
@@ -88,10 +84,10 @@ detect_platform() {
arch="arm32"
fi
else
arch="arm64" # Default for non-Linux ARM
arch="arm64"
fi
;;
riscv64)
riscv64)
if [ "$os" = "linux" ]; then
arch="riscv64"
else
@@ -104,90 +100,166 @@ detect_platform() {
exit 1
;;
esac
echo "${os}_${arch}"
printf '%s_%s' "$os" "$arch"
}
# Get installation directory
# Determine installation directory (default fallback)
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"
case "$PLATFORM" in
*windows*)
echo "$HOME/bin"
;;
*)
echo "/usr/local/bin"
;;
esac
}
# Parse --path argument from args
# Returns the value after --path, or empty string if not provided
parse_path_arg() {
while [ $# -gt 0 ]; do
case "$1" in
--path)
if [ -n "$2" ]; then
printf '%s' "$2"
return
fi
;;
--path=*)
printf '%s' "${1#--path=}"
return
;;
esac
shift
done
}
# Detect an existing newt binary location.
# Tries unprivileged which first, then sudo which (for binaries only visible to root).
# Returns the full path of the binary, or empty string if not found.
detect_existing_binary() {
existing=""
# Try unprivileged which first
existing=$(command -v newt 2>/dev/null || true)
if [ -n "$existing" ]; then
printf '%s' "$existing"
return
fi
# Try sudo which — some installations land in paths only root can see in $PATH
if command -v sudo >/dev/null 2>&1; then
existing=$(sudo which newt 2>/dev/null || true)
if [ -n "$existing" ]; then
printf '%s' "$existing"
return
fi
fi
}
# Check if we need sudo for installation
needs_sudo() {
install_dir="$1"
if [ -w "$install_dir" ] 2>/dev/null; then
return 1 # No sudo needed
else
return 0 # Sudo needed
fi
}
# Get the appropriate command prefix (sudo or empty)
get_sudo_cmd() {
install_dir="$1"
if needs_sudo "$install_dir"; then
if command -v sudo >/dev/null 2>&1; then
echo "sudo"
else
print_error "Cannot write to ${install_dir} and sudo is not available."
print_error "Please run this script as root or install sudo."
exit 1
fi
else
echo ""
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"
platform="$1"
install_dir="$2"
sudo_cmd="$3"
custom_path="$4"
binary_name="newt_${platform}"
final_name="newt"
case "$platform" in
*windows*)
binary_name="${binary_name}.exe"
final_name="newt.exe"
;;
esac
download_url="${BASE_URL}/${binary_name}"
temp_file="/tmp/${final_name}"
# If a custom path is provided, use it directly; otherwise use install_dir/final_name
if [ -n "$custom_path" ]; then
final_path="$custom_path"
install_dir=$(dirname "$final_path")
else
final_path="${install_dir}/${final_name}"
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."
print_error "Neither curl nor wget is available."
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"
# Make executable before moving
chmod +x "$temp_file"
# Create install directory if it doesn't exist and move binary
if [ -n "$sudo_cmd" ]; then
$sudo_cmd mkdir -p "$install_dir"
print_status "Using sudo to install to ${install_dir}"
$sudo_cmd mv "$temp_file" "$final_path"
else
mkdir -p "$install_dir"
mv "$temp_file" "$final_path"
fi
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 "Add it with:"
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
install_dir="$1"
exe_suffix=""
case "$PLATFORM" in
*windows*) exe_suffix=".exe" ;;
esac
newt_path="${install_dir}/newt${exe_suffix}"
if [ -x "$newt_path" ]; then
print_status "Installation successful!"
print_status "newt version: $("$newt_path" --version 2>/dev/null || echo "unknown")"
print_status "newt version: $("$newt_path" --version 2>/dev/null || printf 'unknown')"
return 0
else
print_error "Installation failed. Binary not found or not executable."
@@ -197,35 +269,62 @@ verify_installation() {
# Main installation process
main() {
print_status "Installing latest version of newt..."
# Get latest version
print_status "Fetching latest version from GitHub..."
# --path explicitly overrides everything
CUSTOM_PATH=$(parse_path_arg "$@")
if [ -n "$CUSTOM_PATH" ]; then
print_status "Installing latest version of newt to ${CUSTOM_PATH} (--path override)..."
else
print_status "Installing latest version of newt..."
fi
print_status "Fetching latest version..."
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"
if [ -n "$CUSTOM_PATH" ]; then
# --path wins; derive INSTALL_DIR from it
INSTALL_DIR=$(dirname "$CUSTOM_PATH")
else
# Try to find an existing installation so we update the right place
EXISTING_BINARY=$(detect_existing_binary)
if [ -n "$EXISTING_BINARY" ]; then
print_status "Found existing newt binary at ${EXISTING_BINARY}"
CUSTOM_PATH="$EXISTING_BINARY"
INSTALL_DIR=$(dirname "$EXISTING_BINARY")
print_status "Will update existing installation at ${INSTALL_DIR}"
else
print_status "Run 'newt --help' to get started"
INSTALL_DIR=$(get_install_dir)
fi
fi
print_status "Install directory: ${INSTALL_DIR}"
# Check if we need sudo
SUDO_CMD=$(get_sudo_cmd "$INSTALL_DIR")
if [ -n "$SUDO_CMD" ]; then
print_status "Root privileges required for installation to ${INSTALL_DIR}"
fi
install_newt "$PLATFORM" "$INSTALL_DIR" "$SUDO_CMD" "$CUSTOM_PATH"
if [ -n "$CUSTOM_PATH" ]; then
if [ -x "$CUSTOM_PATH" ]; then
print_status "Installation successful!"
print_status "newt version: $("$CUSTOM_PATH" --version 2>/dev/null || printf 'unknown')"
print_status "newt is ready to use!"
else
print_error "Installation failed. Binary not found or not executable at ${CUSTOM_PATH}."
exit 1
fi
elif verify_installation "$INSTALL_DIR"; then
print_status "newt is ready to use!"
print_status "Run 'newt --help' to get started."
else
exit 1
fi

59
go.mod
View File

@@ -1,29 +1,30 @@
module github.com/fosrl/newt
go 1.25
go 1.25.0
require (
github.com/docker/docker v28.5.2+incompatible
github.com/gaissmai/bart v0.26.1
github.com/gorilla/websocket v1.5.3
github.com/prometheus/client_golang v1.23.2
github.com/vishvananda/netlink v1.3.1
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0
go.opentelemetry.io/otel/exporters/prometheus v0.61.0
go.opentelemetry.io/otel/metric v1.39.0
go.opentelemetry.io/otel/sdk v1.39.0
go.opentelemetry.io/otel/sdk/metric v1.39.0
golang.org/x/crypto v0.46.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
go.opentelemetry.io/otel/exporters/prometheus v0.65.0
go.opentelemetry.io/otel/metric v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/sdk/metric v1.43.0
golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
golang.org/x/net v0.48.0
golang.org/x/sys v0.39.0
golang.org/x/net v0.53.0
golang.org/x/sys v0.43.0
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
golang.zx2c4.com/wireguard/windows v0.5.3
google.golang.org/grpc v1.77.0
google.golang.org/grpc v1.81.0
gopkg.in/yaml.v3 v3.0.1
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c
software.sslmate.com/src/go-pkcs12 v0.7.0
@@ -44,7 +45,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
@@ -54,23 +55,23 @@ require (
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/protobuf v1.36.10 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

118
go.sum
View File

@@ -26,6 +26,8 @@ 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/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -41,8 +43,8 @@ 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.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -75,12 +77,12 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -93,56 +95,56 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0 h1:/+/+UjlXjFcdDlXxKL1PouzX8Z2Vl0OxolRKeBEgYDw=
go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0/go.mod h1:Ldm/PDuzY2DP7IypudopCR3OCOW42NJlN9+mNEroevo=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/exporters/prometheus v0.61.0 h1:cCyZS4dr67d30uDyh8etKM2QyDsQ4zC9ds3bdbrVoD0=
go.opentelemetry.io/otel/exporters/prometheus v0.61.0/go.mod h1:iivMuj3xpR2DkUrUya3TPS/Z9h3dz7h01GxU+fQBRNg=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0 h1:jhVIQEprwUTV+KfzzliLidclhoTOoHTgdz96kAyR8mU=
go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0/go.mod h1:4HsdbLUbernaTnA8CNaNE+1g026SciXb3juRYe3l8EY=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE=
go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
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-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
@@ -151,16 +153,16 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdI
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=

View File

@@ -5,7 +5,9 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
@@ -35,33 +37,38 @@ func (s Health) String() string {
// Config holds the health check configuration for a target
type Config struct {
ID int `json:"id"`
Enabled bool `json:"hcEnabled"`
Path string `json:"hcPath"`
Scheme string `json:"hcScheme"`
Mode string `json:"hcMode"`
Hostname string `json:"hcHostname"`
Port int `json:"hcPort"`
Interval int `json:"hcInterval"` // in seconds
UnhealthyInterval int `json:"hcUnhealthyInterval"` // in seconds
Timeout int `json:"hcTimeout"` // in seconds
Headers map[string]string `json:"hcHeaders"`
Method string `json:"hcMethod"`
Status int `json:"hcStatus"` // HTTP status code
TLSServerName string `json:"hcTlsServerName"`
ID int `json:"id"`
Enabled bool `json:"hcEnabled"`
Path string `json:"hcPath"`
Scheme string `json:"hcScheme"`
Mode string `json:"hcMode"`
Hostname string `json:"hcHostname"`
Port int `json:"hcPort"`
Interval int `json:"hcInterval"` // in seconds
UnhealthyInterval int `json:"hcUnhealthyInterval"` // in seconds
Timeout int `json:"hcTimeout"` // in seconds
FollowRedirects *bool `json:"hcFollowRedirects"`
Headers map[string]string `json:"hcHeaders"`
Method string `json:"hcMethod"`
Status int `json:"hcStatus"` // HTTP status code
TLSServerName string `json:"hcTlsServerName"`
HealthyThreshold int `json:"hcHealthyThreshold"` // consecutive successes required to become healthy
UnhealthyThreshold int `json:"hcUnhealthyThreshold"` // consecutive failures required to become unhealthy
}
// Target represents a health check target with its current status
type Target struct {
Config Config `json:"config"`
Status Health `json:"status"`
LastCheck time.Time `json:"lastCheck"`
LastError string `json:"lastError,omitempty"`
CheckCount int `json:"checkCount"`
timer *time.Timer
ctx context.Context
cancel context.CancelFunc
client *http.Client
Config Config `json:"config"`
Status Health `json:"status"`
LastCheck time.Time `json:"lastCheck"`
LastError string `json:"lastError,omitempty"`
CheckCount int `json:"checkCount"`
timer *time.Timer
ctx context.Context
cancel context.CancelFunc
client *http.Client
consecutiveSuccesses int
consecutiveFailures int
}
// StatusChangeCallback is called when any target's status changes
@@ -163,9 +170,16 @@ func (m *Monitor) addTargetUnsafe(config Config) error {
if config.Timeout == 0 {
config.Timeout = 5
}
if config.HealthyThreshold == 0 {
config.HealthyThreshold = 1
}
if config.UnhealthyThreshold == 0 {
config.UnhealthyThreshold = 1
}
logger.Debug("Target %d configuration: scheme=%s, method=%s, interval=%ds, timeout=%ds",
config.ID, config.Scheme, config.Method, config.Interval, config.Timeout)
logger.Debug("Target %d configuration: mode=%s, scheme=%s, method=%s, interval=%ds, timeout=%ds, healthyThreshold=%d, unhealthyThreshold=%d",
config.ID, config.Mode, config.Scheme, config.Method, config.Interval, config.Timeout,
config.HealthyThreshold, config.UnhealthyThreshold)
// Parse headers if provided as string
if len(config.Headers) == 0 && config.Path != "" {
@@ -187,6 +201,16 @@ func (m *Monitor) addTargetUnsafe(config Config) error {
ctx: ctx,
cancel: cancel,
client: &http.Client{
CheckRedirect: func() func(*http.Request, []*http.Request) error {
// Default to following redirects if not explicitly configured
followRedirects := config.FollowRedirects == nil || *config.FollowRedirects
if !followRedirects {
return func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}
return nil
}(),
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
// Configure TLS settings based on certificate enforcement
@@ -228,7 +252,7 @@ func (m *Monitor) RemoveTarget(id int) error {
// Notify callback of status change
if m.callback != nil {
go m.callback(m.GetTargets())
go m.callback(m.getAllTargetsUnsafe())
}
logger.Info("Successfully removed target %d", id)
@@ -261,7 +285,7 @@ func (m *Monitor) RemoveTargets(ids []int) error {
// Notify callback of status change if any targets were removed
if len(notFound) != len(ids) && m.callback != nil {
go m.callback(m.GetTargets())
go m.callback(m.getAllTargetsUnsafe())
}
if len(notFound) > 0 {
@@ -359,17 +383,75 @@ func (m *Monitor) monitorTarget(target *Target) {
}
}
// performHealthCheck performs a health check on a target
// performHealthCheck performs a health check on a target and applies threshold logic
func (m *Monitor) performHealthCheck(target *Target) {
target.CheckCount++
target.LastCheck = time.Now()
target.LastError = ""
// Build URL
url := fmt.Sprintf("%s://%s", target.Config.Scheme, target.Config.Hostname)
if target.Config.Port > 0 {
url = fmt.Sprintf("%s:%d", url, target.Config.Port)
var passed bool
var checkErr string
switch strings.ToLower(target.Config.Mode) {
case "tcp":
passed, checkErr = m.performTCPCheck(target)
default:
// "http", "https", or anything else falls through to HTTP
passed, checkErr = m.performHTTPCheck(target)
}
if passed {
target.consecutiveFailures = 0
target.consecutiveSuccesses++
logger.Debug("Target %d: check passed (consecutive successes: %d / threshold: %d)",
target.Config.ID, target.consecutiveSuccesses, target.Config.HealthyThreshold)
if target.consecutiveSuccesses >= target.Config.HealthyThreshold {
target.Status = StatusHealthy
target.LastError = ""
}
} else {
target.consecutiveSuccesses = 0
target.consecutiveFailures++
target.LastError = checkErr
logger.Debug("Target %d: check failed (consecutive failures: %d / threshold: %d): %s",
target.Config.ID, target.consecutiveFailures, target.Config.UnhealthyThreshold, checkErr)
if target.consecutiveFailures >= target.Config.UnhealthyThreshold {
target.Status = StatusUnhealthy
}
}
}
// performTCPCheck dials the target's host:port over TCP and returns whether it succeeded
func (m *Monitor) performTCPCheck(target *Target) (bool, string) {
address := net.JoinHostPort(target.Config.Hostname, strconv.Itoa(target.Config.Port))
timeout := time.Duration(target.Config.Timeout) * time.Second
logger.Debug("Target %d: performing TCP health check to %s (timeout: %v)",
target.Config.ID, address, timeout)
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
msg := fmt.Sprintf("TCP dial failed: %v", err)
logger.Warn("Target %d: %s", target.Config.ID, msg)
return false, msg
}
conn.Close()
logger.Debug("Target %d: TCP health check passed", target.Config.ID)
return true, ""
}
// performHTTPCheck performs an HTTP/HTTPS health check and returns whether it succeeded
func (m *Monitor) performHTTPCheck(target *Target) (bool, string) {
// Build URL (use net.JoinHostPort to properly handle IPv6 addresses with ports)
host := target.Config.Hostname
if target.Config.Port > 0 {
host = net.JoinHostPort(target.Config.Hostname, strconv.Itoa(target.Config.Port))
}
url := fmt.Sprintf("%s://%s", target.Config.Scheme, host)
if target.Config.Path != "" {
if !strings.HasPrefix(target.Config.Path, "/") {
url += "/"
@@ -377,7 +459,7 @@ func (m *Monitor) performHealthCheck(target *Target) {
url += target.Config.Path
}
logger.Debug("Target %d: performing health check %d to %s",
logger.Debug("Target %d: performing HTTP health check %d to %s",
target.Config.ID, target.CheckCount, url)
if target.Config.Scheme == "https" {
@@ -385,16 +467,15 @@ func (m *Monitor) performHealthCheck(target *Target) {
target.Config.ID, m.enforceCert)
}
// Create request
// Create request with timeout context
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(target.Config.Timeout)*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, target.Config.Method, url, nil)
if err != nil {
target.Status = StatusUnhealthy
target.LastError = fmt.Sprintf("failed to create request: %v", err)
logger.Warn("Target %d: failed to create request: %v", target.Config.ID, err)
return
msg := fmt.Sprintf("failed to create request: %v", err)
logger.Warn("Target %d: %s", target.Config.ID, msg)
return false, msg
}
// Add headers
@@ -410,43 +491,34 @@ func (m *Monitor) performHealthCheck(target *Target) {
// Perform request
resp, err := target.client.Do(req)
if err != nil {
target.Status = StatusUnhealthy
target.LastError = fmt.Sprintf("request failed: %v", err)
msg := fmt.Sprintf("request failed: %v", err)
logger.Warn("Target %d: health check failed: %v", target.Config.ID, err)
return
return false, msg
}
defer resp.Body.Close()
// Check response status
var expectedStatus int
if target.Config.Status > 0 {
expectedStatus = target.Config.Status
} else {
expectedStatus = 0 // Use range check for 200-299
// Check for specific status code
logger.Debug("Target %d: checking status against expected code %d", target.Config.ID, target.Config.Status)
if resp.StatusCode == target.Config.Status {
logger.Debug("Target %d: health check passed (status: %d)", target.Config.ID, resp.StatusCode)
return true, ""
}
msg := fmt.Sprintf("unexpected status code: %d (expected: %d)", resp.StatusCode, target.Config.Status)
logger.Warn("Target %d: %s", target.Config.ID, msg)
return false, msg
}
if expectedStatus > 0 {
logger.Debug("Target %d: checking health status against expected code %d", target.Config.ID, expectedStatus)
// Check for specific status code
if resp.StatusCode == expectedStatus {
target.Status = StatusHealthy
logger.Debug("Target %d: health check passed (status: %d, expected: %d)", target.Config.ID, resp.StatusCode, expectedStatus)
} else {
target.Status = StatusUnhealthy
target.LastError = fmt.Sprintf("unexpected status code: %d (expected: %d)", resp.StatusCode, expectedStatus)
logger.Warn("Target %d: health check failed with status code %d (expected: %d)", target.Config.ID, resp.StatusCode, expectedStatus)
}
} else {
// Check for 2xx range
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
target.Status = StatusHealthy
logger.Debug("Target %d: health check passed (status: %d)", target.Config.ID, resp.StatusCode)
} else {
target.Status = StatusUnhealthy
target.LastError = fmt.Sprintf("unhealthy status code: %d", resp.StatusCode)
logger.Warn("Target %d: health check failed with status code %d", target.Config.ID, resp.StatusCode)
}
// Default: check for 2xx range
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
logger.Debug("Target %d: health check passed (status: %d)", target.Config.ID, resp.StatusCode)
return true, ""
}
msg := fmt.Sprintf("unhealthy status code: %d", resp.StatusCode)
logger.Warn("Target %d: health check failed with status code %d", target.Config.ID, resp.StatusCode)
return false, msg
}
// Stop stops monitoring all targets
@@ -513,7 +585,7 @@ func (m *Monitor) DisableTarget(id int) error {
// Notify callback of status change
if m.callback != nil {
go m.callback(m.GetTargets())
go m.callback(m.getAllTargetsUnsafe())
}
} else {
logger.Debug("Target %d is already disabled", id)
@@ -521,3 +593,82 @@ func (m *Monitor) DisableTarget(id int) error {
return nil
}
// GetTargetIDs returns a slice of all current target IDs
func (m *Monitor) GetTargetIDs() []int {
m.mutex.RLock()
defer m.mutex.RUnlock()
ids := make([]int, 0, len(m.targets))
for id := range m.targets {
ids = append(ids, id)
}
return ids
}
// SyncTargets synchronizes the current targets to match the desired set.
// It removes targets not in the desired set and adds targets that are missing.
func (m *Monitor) SyncTargets(desiredConfigs []Config) error {
m.mutex.Lock()
defer m.mutex.Unlock()
logger.Info("Syncing health check targets: %d desired targets", len(desiredConfigs))
// Build a set of desired target IDs
desiredIDs := make(map[int]Config)
for _, config := range desiredConfigs {
desiredIDs[config.ID] = config
}
// Find targets to remove (exist but not in desired set)
var toRemove []int
for id := range m.targets {
if _, exists := desiredIDs[id]; !exists {
toRemove = append(toRemove, id)
}
}
// Remove targets that are not in the desired set
for _, id := range toRemove {
logger.Info("Sync: removing health check target %d", id)
if target, exists := m.targets[id]; exists {
target.cancel()
delete(m.targets, id)
}
}
// Add or update targets from the desired set
var addedCount, updatedCount int
for id, config := range desiredIDs {
if existing, exists := m.targets[id]; exists {
// Target exists - check if config changed and update if needed
// For now, we'll replace it to ensure config is up to date
logger.Debug("Sync: updating health check target %d", id)
existing.cancel()
delete(m.targets, id)
if err := m.addTargetUnsafe(config); err != nil {
logger.Error("Sync: failed to update target %d: %v", id, err)
return fmt.Errorf("failed to update target %d: %v", id, err)
}
updatedCount++
} else {
// Target doesn't exist - add it
logger.Debug("Sync: adding health check target %d", id)
if err := m.addTargetUnsafe(config); err != nil {
logger.Error("Sync: failed to add target %d: %v", id, err)
return fmt.Errorf("failed to add target %d: %v", id, err)
}
addedCount++
}
}
logger.Info("Sync complete: removed %d, added %d, updated %d targets",
len(toRemove), addedCount, updatedCount)
// Notify callback if any changes were made
if (len(toRemove) > 0 || addedCount > 0 || updatedCount > 0) && m.callback != nil {
go m.callback(m.getAllTargetsUnsafe())
}
return nil
}

View File

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

View File

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

346
main.go
View File

@@ -3,13 +3,16 @@ package main
import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"net"
"net/http"
"net/http/pprof"
"net/netip"
"os"
"os/signal"
@@ -45,6 +48,7 @@ type WgData struct {
TunnelIP string `json:"tunnelIP"`
Targets TargetsByType `json:"targets"`
HealthCheckTargets []healthcheck.Config `json:"healthCheckTargets"`
ChainId string `json:"chainId"`
}
type TargetsByType struct {
@@ -58,6 +62,7 @@ type TargetData struct {
type ExitNodeData struct {
ExitNodes []ExitNode `json:"exitNodes"`
ChainId string `json:"chainId"`
}
// ExitNode represents an exit node with an ID, endpoint, and weight.
@@ -124,9 +129,12 @@ var (
dockerEnforceNetworkValidationBool bool
pingInterval time.Duration
pingTimeout time.Duration
udpProxyIdleTimeout time.Duration
publicKey wgtypes.Key
pingStopChan chan struct{}
stopFunc func()
pendingRegisterChainId string
pendingPingChainId string
healthFile string
useNativeInterface bool
authorizedKeysFile string
@@ -147,8 +155,10 @@ var (
adminAddr string
region string
metricsAsyncBytes bool
blueprintFile string
noCloud bool
pprofEnabled bool
blueprintFile string
provisioningBlueprintFile string
noCloud bool
// New mTLS configuration variables
tlsClientCert string
@@ -157,8 +167,24 @@ var (
// Legacy PKCS12 support (deprecated)
tlsPrivateKey string
// Provisioning key exchanged once for a permanent newt ID + secret
provisioningKey string
// Optional name for the site created during provisioning
newtName string
// Path to config file (overrides CONFIG_FILE env var and default location)
configFile string
)
// generateChainId generates a random chain ID for deduplicating round-trip messages.
func generateChainId() string {
b := make([]byte, 8)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
func main() {
// Check for subcommands first (only principals exits early)
if len(os.Args) > 1 {
@@ -225,6 +251,7 @@ func runNewtMain(ctx context.Context) {
adminAddrEnv := os.Getenv("NEWT_ADMIN_ADDR")
regionEnv := os.Getenv("NEWT_REGION")
asyncBytesEnv := os.Getenv("NEWT_METRICS_ASYNC_BYTES")
pprofEnabledEnv := os.Getenv("NEWT_PPROF_ENABLED")
disableClientsEnv := os.Getenv("DISABLE_CLIENTS")
disableClients = disableClientsEnv == "true"
@@ -235,6 +262,7 @@ func runNewtMain(ctx context.Context) {
dockerSocket = os.Getenv("DOCKER_SOCKET")
pingIntervalStr := os.Getenv("PING_INTERVAL")
pingTimeoutStr := os.Getenv("PING_TIMEOUT")
udpProxyIdleTimeoutStr := os.Getenv("NEWT_UDP_PROXY_IDLE_TIMEOUT")
dockerEnforceNetworkValidation = os.Getenv("DOCKER_ENFORCE_NETWORK_VALIDATION")
healthFile = os.Getenv("HEALTH_FILE")
// authorizedKeysFile = os.Getenv("AUTHORIZED_KEYS_FILE")
@@ -259,8 +287,12 @@ func runNewtMain(ctx context.Context) {
tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT")
}
blueprintFile = os.Getenv("BLUEPRINT_FILE")
provisioningBlueprintFile = os.Getenv("PROVISIONING_BLUEPRINT_FILE")
noCloudEnv := os.Getenv("NO_CLOUD")
noCloud = noCloudEnv == "true"
provisioningKey = os.Getenv("NEWT_PROVISIONING_KEY")
newtName = os.Getenv("NEWT_NAME")
configFile = os.Getenv("CONFIG_FILE")
if endpoint == "" {
flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server")
@@ -302,13 +334,25 @@ func runNewtMain(ctx context.Context) {
flag.StringVar(&dockerSocket, "docker-socket", "", "Path or address to Docker socket (typically unix:///var/run/docker.sock)")
}
if pingIntervalStr == "" {
flag.StringVar(&pingIntervalStr, "ping-interval", "3s", "Interval for pinging the server (default 3s)")
flag.StringVar(&pingIntervalStr, "ping-interval", "15s", "Interval for pinging the server (default 15s)")
}
if pingTimeoutStr == "" {
flag.StringVar(&pingTimeoutStr, "ping-timeout", "5s", " Timeout for each ping (default 5s)")
flag.StringVar(&pingTimeoutStr, "ping-timeout", "7s", " Timeout for each ping (default 7s)")
}
if udpProxyIdleTimeoutStr == "" {
flag.StringVar(&udpProxyIdleTimeoutStr, "udp-proxy-idle-timeout", "90s", "Idle timeout for UDP proxied client flows before cleanup")
}
// load the prefer endpoint just as a flag
flag.StringVar(&preferEndpoint, "prefer-endpoint", "", "Prefer this endpoint for the connection (if set, will override the endpoint from the server)")
if provisioningKey == "" {
flag.StringVar(&provisioningKey, "provisioning-key", "", "One-time provisioning key used to obtain a newt ID and secret from the server")
}
if newtName == "" {
flag.StringVar(&newtName, "name", "", "Name for the site created during provisioning (supports {{env.VAR}} interpolation)")
}
if configFile == "" {
flag.StringVar(&configFile, "config-file", "", "Path to config file (overrides CONFIG_FILE env var and default location)")
}
// Add new mTLS flags
if tlsClientCert == "" {
@@ -330,30 +374,31 @@ func runNewtMain(ctx context.Context) {
if pingIntervalStr != "" {
pingInterval, err = time.ParseDuration(pingIntervalStr)
if err != nil {
fmt.Printf("Invalid PING_INTERVAL value: %s, using default 3 seconds\n", pingIntervalStr)
pingInterval = 3 * time.Second
fmt.Printf("Invalid PING_INTERVAL value: %s, using default 15 seconds\n", pingIntervalStr)
pingInterval = 15 * time.Second
}
} else {
pingInterval = 3 * time.Second
pingInterval = 15 * time.Second
}
if pingTimeoutStr != "" {
pingTimeout, err = time.ParseDuration(pingTimeoutStr)
if err != nil {
fmt.Printf("Invalid PING_TIMEOUT value: %s, using default 5 seconds\n", pingTimeoutStr)
pingTimeout = 5 * time.Second
fmt.Printf("Invalid PING_TIMEOUT value: %s, using default 7 seconds\n", pingTimeoutStr)
pingTimeout = 7 * time.Second
}
} else {
pingTimeout = 5 * time.Second
pingTimeout = 7 * time.Second
}
if portStr != "" {
portInt, err := strconv.Atoi(portStr)
if err != nil {
logger.Warn("Failed to parse PORT, choosing a random port")
} else {
port = uint16(portInt)
if udpProxyIdleTimeoutStr != "" {
udpProxyIdleTimeout, err = time.ParseDuration(udpProxyIdleTimeoutStr)
if err != nil || udpProxyIdleTimeout <= 0 {
fmt.Printf("Invalid NEWT_UDP_PROXY_IDLE_TIMEOUT/--udp-proxy-idle-timeout value: %s, using default 90 seconds\n", udpProxyIdleTimeoutStr)
udpProxyIdleTimeout = 90 * time.Second
}
} else {
udpProxyIdleTimeout = 90 * time.Second
}
if dockerEnforceNetworkValidation == "" {
@@ -365,6 +410,9 @@ func runNewtMain(ctx context.Context) {
if blueprintFile == "" {
flag.StringVar(&blueprintFile, "blueprint-file", "", "Path to blueprint file (if unset, no blueprint will be applied)")
}
if provisioningBlueprintFile == "" {
flag.StringVar(&provisioningBlueprintFile, "provisioning-blueprint-file", "", "Path to blueprint file applied once after a provisioning credential exchange (if unset, no provisioning blueprint will be applied)")
}
if noCloudEnv == "" {
flag.BoolVar(&noCloud, "no-cloud", false, "Disable cloud failover")
}
@@ -399,6 +447,14 @@ func runNewtMain(ctx context.Context) {
metricsAsyncBytes = v
}
}
// pprof debug endpoint toggle
if pprofEnabledEnv == "" {
flag.BoolVar(&pprofEnabled, "pprof", false, "Enable pprof debug endpoints on admin server")
} else {
if v, err := strconv.ParseBool(pprofEnabledEnv); err == nil {
pprofEnabled = v
}
}
// Optional region flag (resource attribute)
if regionEnv == "" {
flag.StringVar(&region, "region", "", "Optional region resource attribute (also NEWT_REGION)")
@@ -441,6 +497,15 @@ func runNewtMain(ctx context.Context) {
tlsClientCAs = append(tlsClientCAs, tlsClientCAsFlag...)
}
if portStr != "" {
portInt, err := strconv.Atoi(portStr)
if err != nil {
logger.Warn("Failed to parse PORT, choosing a random port")
} else {
port = uint16(portInt)
}
}
if *version {
fmt.Println("Newt version " + newtVersion)
os.Exit(0)
@@ -477,7 +542,7 @@ func runNewtMain(ctx context.Context) {
if telErr != nil {
logger.Warn("Telemetry init failed: %v", telErr)
}
if tel != nil {
if tel != nil && (metricsEnabled || pprofEnabled) {
// Admin HTTP server (exposes /metrics when Prometheus exporter is enabled)
logger.Debug("Starting metrics server on %s", tcfg.AdminAddr)
mux := http.NewServeMux()
@@ -485,6 +550,14 @@ func runNewtMain(ctx context.Context) {
if tel.PrometheusHandler != nil {
mux.Handle("/metrics", tel.PrometheusHandler)
}
if pprofEnabled {
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
logger.Info("pprof debugging enabled on %s/debug/pprof/", tcfg.AdminAddr)
}
admin := &http.Server{
Addr: tcfg.AdminAddr,
Handler: otelhttp.NewHandler(mux, "newt-admin"),
@@ -565,13 +638,22 @@ func runNewtMain(ctx context.Context) {
id, // CLI arg takes precedence
secret, // CLI arg takes precedence
endpoint,
pingInterval,
pingTimeout,
30*time.Second,
opt,
websocket.WithConfigFile(configFile),
)
if err != nil {
logger.Fatal("Failed to create client: %v", err)
}
// If a provisioning key was supplied via CLI / env and the config file did
// not already carry one, inject it now so provisionIfNeeded() can use it.
if provisioningKey != "" && client.GetConfig().ProvisioningKey == "" {
client.GetConfig().ProvisioningKey = provisioningKey
}
if newtName != "" && client.GetConfig().Name == "" {
client.GetConfig().Name = newtName
}
endpoint = client.GetConfig().Endpoint // Update endpoint from config
id = client.GetConfig().ID // Update ID from config
// Update site labels for metrics with the resolved ID
@@ -688,6 +770,24 @@ func runNewtMain(ctx context.Context) {
defer func() {
telemetry.IncSiteRegistration(ctx, regResult)
}()
// Deduplicate using chainId: if the server echoes back a chainId we have
// already consumed (or one that doesn't match our current pending request),
// throw the message away to avoid setting up the tunnel twice.
var chainData struct {
ChainId string `json:"chainId"`
}
if jsonBytes, err := json.Marshal(msg.Data); err == nil {
_ = json.Unmarshal(jsonBytes, &chainData)
}
if chainData.ChainId != "" {
if chainData.ChainId != pendingRegisterChainId {
logger.Debug("Discarding duplicate/stale newt/wg/connect (chainId=%s, expected=%s)", chainData.ChainId, pendingRegisterChainId)
return
}
pendingRegisterChainId = "" // consume further duplicates with this id are rejected
}
if stopFunc != nil {
stopFunc() // stop the ws from sending more requests
stopFunc = nil // reset stopFunc to nil to avoid double stopping
@@ -811,6 +911,7 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
// Create proxy manager
pm = proxy.NewProxyManager(tnet)
pm.SetAsyncBytes(metricsAsyncBytes)
pm.SetUDPIdleTimeout(udpProxyIdleTimeout)
// Set tunnel_id for metrics (WireGuard peer public key)
pm.SetTunnelID(wgData.PublicKey)
@@ -872,8 +973,11 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
}
// Request exit nodes from the server
pingChainId := generateChainId()
pendingPingChainId = pingChainId
stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{
"noCloud": noCloud,
"chainId": pingChainId,
}, 3*time.Second)
logger.Info("Tunnel destroyed, ready for reconnection")
@@ -902,6 +1006,7 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
client.RegisterHandler("newt/ping/exitNodes", func(msg websocket.WSMessage) {
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
@@ -921,6 +1026,14 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
}
exitNodes := exitNodeData.ExitNodes
if exitNodeData.ChainId != "" {
if exitNodeData.ChainId != pendingPingChainId {
logger.Debug("Discarding duplicate/stale newt/ping/exitNodes (chainId=%s, expected=%s)", exitNodeData.ChainId, pendingPingChainId)
return
}
pendingPingChainId = "" // consume further duplicates with this id are rejected
}
if len(exitNodes) == 0 {
logger.Info("No exit nodes provided")
return
@@ -953,11 +1066,14 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
},
}
chainId := generateChainId()
pendingRegisterChainId = chainId
stopFunc = client.SendMessageInterval(topicWGRegister, map[string]interface{}{
"publicKey": publicKey.String(),
"pingResults": pingResults,
"newtVersion": newtVersion,
}, 1*time.Second)
"chainId": chainId,
}, 2*time.Second)
return
}
@@ -1056,11 +1172,14 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
}
// Send the ping results to the cloud for selection
chainId := generateChainId()
pendingRegisterChainId = chainId
stopFunc = client.SendMessageInterval(topicWGRegister, map[string]interface{}{
"publicKey": publicKey.String(),
"pingResults": pingResults,
"newtVersion": newtVersion,
}, 1*time.Second)
"chainId": chainId,
}, 2*time.Second)
logger.Debug("Sent exit node ping results to cloud for selection: pingResults=%+v", pingResults)
})
@@ -1165,6 +1284,153 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
}
})
// Register handler for syncing targets (TCP, UDP, and health checks)
client.RegisterHandler("newt/sync", func(msg websocket.WSMessage) {
logger.Info("Received sync message")
// if there is no wgData or pm, we can't sync targets
if wgData.TunnelIP == "" || pm == nil {
logger.Info(msgNoTunnelOrProxy)
return
}
// Define the sync data structure
type SyncData struct {
Targets TargetsByType `json:"targets"`
HealthCheckTargets []healthcheck.Config `json:"healthCheckTargets"`
}
var syncData SyncData
jsonData, err := json.Marshal(msg.Data)
if err != nil {
logger.Error("Error marshaling sync data: %v", err)
return
}
if err := json.Unmarshal(jsonData, &syncData); err != nil {
logger.Error("Error unmarshaling sync data: %v", err)
return
}
logger.Debug("Sync data received: TCP targets=%d, UDP targets=%d, health check targets=%d",
len(syncData.Targets.TCP), len(syncData.Targets.UDP), len(syncData.HealthCheckTargets))
//TODO: TEST AND IMPLEMENT THIS
// // Build sets of desired targets (port -> target string)
// desiredTCP := make(map[int]string)
// for _, t := range syncData.Targets.TCP {
// parts := strings.Split(t, ":")
// if len(parts) != 3 {
// logger.Warn("Invalid TCP target format: %s", t)
// continue
// }
// port := 0
// if _, err := fmt.Sscanf(parts[0], "%d", &port); err != nil {
// logger.Warn("Invalid port in TCP target: %s", parts[0])
// continue
// }
// desiredTCP[port] = parts[1] + ":" + parts[2]
// }
// desiredUDP := make(map[int]string)
// for _, t := range syncData.Targets.UDP {
// parts := strings.Split(t, ":")
// if len(parts) != 3 {
// logger.Warn("Invalid UDP target format: %s", t)
// continue
// }
// port := 0
// if _, err := fmt.Sscanf(parts[0], "%d", &port); err != nil {
// logger.Warn("Invalid port in UDP target: %s", parts[0])
// continue
// }
// desiredUDP[port] = parts[1] + ":" + parts[2]
// }
// // Get current targets from proxy manager
// currentTCP, currentUDP := pm.GetTargets()
// // Sync TCP targets
// // Remove TCP targets not in desired set
// if tcpForIP, ok := currentTCP[wgData.TunnelIP]; ok {
// for port := range tcpForIP {
// if _, exists := desiredTCP[port]; !exists {
// logger.Info("Sync: removing TCP target on port %d", port)
// targetStr := fmt.Sprintf("%d:%s", port, tcpForIP[port])
// updateTargets(pm, "remove", wgData.TunnelIP, "tcp", TargetData{Targets: []string{targetStr}})
// }
// }
// }
// // Add TCP targets that are missing
// for port, target := range desiredTCP {
// needsAdd := true
// if tcpForIP, ok := currentTCP[wgData.TunnelIP]; ok {
// if currentTarget, exists := tcpForIP[port]; exists {
// // Check if target address changed
// if currentTarget == target {
// needsAdd = false
// } else {
// // Target changed, remove old one first
// logger.Info("Sync: updating TCP target on port %d", port)
// targetStr := fmt.Sprintf("%d:%s", port, currentTarget)
// updateTargets(pm, "remove", wgData.TunnelIP, "tcp", TargetData{Targets: []string{targetStr}})
// }
// }
// }
// if needsAdd {
// logger.Info("Sync: adding TCP target on port %d -> %s", port, target)
// targetStr := fmt.Sprintf("%d:%s", port, target)
// updateTargets(pm, "add", wgData.TunnelIP, "tcp", TargetData{Targets: []string{targetStr}})
// }
// }
// // Sync UDP targets
// // Remove UDP targets not in desired set
// if udpForIP, ok := currentUDP[wgData.TunnelIP]; ok {
// for port := range udpForIP {
// if _, exists := desiredUDP[port]; !exists {
// logger.Info("Sync: removing UDP target on port %d", port)
// targetStr := fmt.Sprintf("%d:%s", port, udpForIP[port])
// updateTargets(pm, "remove", wgData.TunnelIP, "udp", TargetData{Targets: []string{targetStr}})
// }
// }
// }
// // Add UDP targets that are missing
// for port, target := range desiredUDP {
// needsAdd := true
// if udpForIP, ok := currentUDP[wgData.TunnelIP]; ok {
// if currentTarget, exists := udpForIP[port]; exists {
// // Check if target address changed
// if currentTarget == target {
// needsAdd = false
// } else {
// // Target changed, remove old one first
// logger.Info("Sync: updating UDP target on port %d", port)
// targetStr := fmt.Sprintf("%d:%s", port, currentTarget)
// updateTargets(pm, "remove", wgData.TunnelIP, "udp", TargetData{Targets: []string{targetStr}})
// }
// }
// }
// if needsAdd {
// logger.Info("Sync: adding UDP target on port %d -> %s", port, target)
// targetStr := fmt.Sprintf("%d:%s", port, target)
// updateTargets(pm, "add", wgData.TunnelIP, "udp", TargetData{Targets: []string{targetStr}})
// }
// }
// // Sync health check targets
// if err := healthMonitor.SyncTargets(syncData.HealthCheckTargets); err != nil {
// logger.Error("Failed to sync health check targets: %v", err)
// } else {
// logger.Info("Successfully synced health check targets")
// }
logger.Info("Sync complete")
})
// Register handler for Docker socket check
client.RegisterHandler("newt/socket/check", func(msg websocket.WSMessage) {
logger.Debug("Received Docker socket check request")
@@ -1562,8 +1828,11 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
stopFunc()
}
// request from the server the list of nodes to ping
pingChainId := generateChainId()
pendingPingChainId = pingChainId
stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{
"noCloud": noCloud,
"chainId": pingChainId,
}, 3*time.Second)
logger.Debug("Requesting exit nodes from server")
@@ -1572,17 +1841,46 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
} else {
logger.Warn("CLIENTS WILL NOT WORK ON THIS VERSION OF NEWT WITH THIS VERSION OF PANGOLIN, PLEASE UPDATE THE SERVER TO 1.13 OR HIGHER OR DOWNGRADE NEWT")
}
sendBlueprint(client, blueprintFile)
if client.WasJustProvisioned() {
logger.Info("Provisioning detected sending provisioning blueprint")
sendBlueprint(client, provisioningBlueprintFile)
}
} else {
// Resend current health check status for all targets in case the server
// missed updates while newt was disconnected.
targets := healthMonitor.GetTargets()
if len(targets) > 0 {
healthStatuses := make(map[int]interface{})
for id, target := range targets {
healthStatuses[id] = map[string]interface{}{
"status": target.Status.String(),
"lastCheck": target.LastCheck.Format(time.RFC3339),
"checkCount": target.CheckCount,
"lastError": target.LastError,
"config": target.Config,
}
}
logger.Debug("Reconnected: resending health check status for %d targets", len(healthStatuses))
if err := client.SendMessage("newt/healthcheck/status", map[string]interface{}{
"targets": healthStatuses,
}); err != nil {
logger.Error("Failed to resend health check status on reconnect: %v", err)
}
}
}
// Send registration message to the server for backward compatibility
bcChainId := generateChainId()
pendingRegisterChainId = bcChainId
err := client.SendMessage(topicWGRegister, map[string]interface{}{
"publicKey": publicKey.String(),
"newtVersion": newtVersion,
"backwardsCompatible": true,
"chainId": bcChainId,
})
sendBlueprint(client)
if err != nil {
logger.Error("Failed to send registration message: %v", err)
return err
@@ -1647,6 +1945,8 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
pm.Stop()
}
client.SendMessage("newt/disconnecting", map[string]any{})
if client != nil {
client.Close()
}

514
netstack2/access_log.go Normal file
View File

@@ -0,0 +1,514 @@
package netstack2
import (
"bytes"
"compress/zlib"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"net"
"sort"
"sync"
"time"
"github.com/fosrl/newt/logger"
)
const (
// flushInterval is how often the access logger flushes completed sessions to the server
flushInterval = 60 * time.Second
// maxBufferedSessions is the max number of completed sessions to buffer before forcing a flush
maxBufferedSessions = 100
// sessionGapThreshold is the maximum gap between the end of one connection
// and the start of the next for them to be considered part of the same session.
// If the gap exceeds this, a new consolidated session is created.
sessionGapThreshold = 5 * time.Second
// minConnectionsToConsolidate is the minimum number of connections in a group
// before we bother consolidating. Groups smaller than this are sent as-is.
minConnectionsToConsolidate = 2
)
// SendFunc is a callback that sends compressed access log data to the server.
// The data is a base64-encoded zlib-compressed JSON array of AccessSession objects.
type SendFunc func(data string) error
// AccessSession represents a tracked access session through the proxy
type AccessSession struct {
SessionID string `json:"sessionId"`
ResourceID int `json:"resourceId"`
SourceAddr string `json:"sourceAddr"`
DestAddr string `json:"destAddr"`
Protocol string `json:"protocol"`
StartedAt time.Time `json:"startedAt"`
EndedAt time.Time `json:"endedAt,omitempty"`
BytesTx int64 `json:"bytesTx"`
BytesRx int64 `json:"bytesRx"`
ConnectionCount int `json:"connectionCount,omitempty"` // number of raw connections merged into this session (0 or 1 = single)
}
// udpSessionKey identifies a unique UDP "session" by src -> dst
type udpSessionKey struct {
srcAddr string
dstAddr string
protocol string
}
// consolidationKey groups connections that may be part of the same logical session.
// Source port is intentionally excluded so that many ephemeral-port connections
// from the same source IP to the same destination are grouped together.
type consolidationKey struct {
sourceIP string // IP only, no port
destAddr string // full host:port of the destination
protocol string
resourceID int
}
// AccessLogger tracks access sessions for resources and periodically
// flushes completed sessions to the server via a configurable SendFunc.
type AccessLogger struct {
mu sync.Mutex
sessions map[string]*AccessSession // active sessions: sessionID -> session
udpSessions map[udpSessionKey]*AccessSession // active UDP sessions for dedup
completedSessions []*AccessSession // completed sessions waiting to be flushed
udpTimeout time.Duration
sendFn SendFunc
stopCh chan struct{}
flushDone chan struct{} // closed after the flush goroutine exits
}
// NewAccessLogger creates a new access logger.
// udpTimeout controls how long a UDP session is kept alive without traffic before being ended.
func NewAccessLogger(udpTimeout time.Duration) *AccessLogger {
al := &AccessLogger{
sessions: make(map[string]*AccessSession),
udpSessions: make(map[udpSessionKey]*AccessSession),
completedSessions: make([]*AccessSession, 0),
udpTimeout: udpTimeout,
stopCh: make(chan struct{}),
flushDone: make(chan struct{}),
}
go al.backgroundLoop()
return al
}
// SetSendFunc sets the callback used to send compressed access log batches
// to the server. This can be called after construction once the websocket
// client is available.
func (al *AccessLogger) SetSendFunc(fn SendFunc) {
al.mu.Lock()
defer al.mu.Unlock()
al.sendFn = fn
}
// generateSessionID creates a random session identifier
func generateSessionID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}
// StartTCPSession logs the start of a TCP session and returns a session ID.
func (al *AccessLogger) StartTCPSession(resourceID int, srcAddr, dstAddr string) string {
sessionID := generateSessionID()
now := time.Now()
session := &AccessSession{
SessionID: sessionID,
ResourceID: resourceID,
SourceAddr: srcAddr,
DestAddr: dstAddr,
Protocol: "tcp",
StartedAt: now,
}
al.mu.Lock()
al.sessions[sessionID] = session
al.mu.Unlock()
logger.Info("ACCESS START session=%s resource=%d proto=tcp src=%s dst=%s time=%s",
sessionID, resourceID, srcAddr, dstAddr, now.Format(time.RFC3339))
return sessionID
}
// EndTCPSession logs the end of a TCP session and queues it for sending.
func (al *AccessLogger) EndTCPSession(sessionID string) {
now := time.Now()
al.mu.Lock()
session, ok := al.sessions[sessionID]
if ok {
session.EndedAt = now
delete(al.sessions, sessionID)
al.completedSessions = append(al.completedSessions, session)
}
shouldFlush := len(al.completedSessions) >= maxBufferedSessions
al.mu.Unlock()
if ok {
duration := now.Sub(session.StartedAt)
logger.Info("ACCESS END session=%s resource=%d proto=tcp src=%s dst=%s started=%s ended=%s duration=%s",
sessionID, session.ResourceID, session.SourceAddr, session.DestAddr,
session.StartedAt.Format(time.RFC3339), now.Format(time.RFC3339), duration)
}
if shouldFlush {
al.flush()
}
}
// TrackUDPSession starts or returns an existing UDP session. Returns the session ID.
func (al *AccessLogger) TrackUDPSession(resourceID int, srcAddr, dstAddr string) string {
key := udpSessionKey{
srcAddr: srcAddr,
dstAddr: dstAddr,
protocol: "udp",
}
al.mu.Lock()
defer al.mu.Unlock()
if existing, ok := al.udpSessions[key]; ok {
return existing.SessionID
}
sessionID := generateSessionID()
now := time.Now()
session := &AccessSession{
SessionID: sessionID,
ResourceID: resourceID,
SourceAddr: srcAddr,
DestAddr: dstAddr,
Protocol: "udp",
StartedAt: now,
}
al.sessions[sessionID] = session
al.udpSessions[key] = session
logger.Info("ACCESS START session=%s resource=%d proto=udp src=%s dst=%s time=%s",
sessionID, resourceID, srcAddr, dstAddr, now.Format(time.RFC3339))
return sessionID
}
// EndUDPSession ends a UDP session and queues it for sending.
func (al *AccessLogger) EndUDPSession(sessionID string) {
now := time.Now()
al.mu.Lock()
session, ok := al.sessions[sessionID]
if ok {
session.EndedAt = now
delete(al.sessions, sessionID)
key := udpSessionKey{
srcAddr: session.SourceAddr,
dstAddr: session.DestAddr,
protocol: "udp",
}
delete(al.udpSessions, key)
al.completedSessions = append(al.completedSessions, session)
}
shouldFlush := len(al.completedSessions) >= maxBufferedSessions
al.mu.Unlock()
if ok {
duration := now.Sub(session.StartedAt)
logger.Info("ACCESS END session=%s resource=%d proto=udp src=%s dst=%s started=%s ended=%s duration=%s",
sessionID, session.ResourceID, session.SourceAddr, session.DestAddr,
session.StartedAt.Format(time.RFC3339), now.Format(time.RFC3339), duration)
}
if shouldFlush {
al.flush()
}
}
// backgroundLoop handles periodic flushing and stale session reaping.
func (al *AccessLogger) backgroundLoop() {
defer close(al.flushDone)
flushTicker := time.NewTicker(flushInterval)
defer flushTicker.Stop()
reapTicker := time.NewTicker(30 * time.Second)
defer reapTicker.Stop()
for {
select {
case <-al.stopCh:
return
case <-flushTicker.C:
al.flush()
case <-reapTicker.C:
al.reapStaleSessions()
}
}
}
// reapStaleSessions cleans up UDP sessions that were not properly ended.
func (al *AccessLogger) reapStaleSessions() {
al.mu.Lock()
defer al.mu.Unlock()
staleThreshold := time.Now().Add(-5 * time.Minute)
for key, session := range al.udpSessions {
if session.StartedAt.Before(staleThreshold) && session.EndedAt.IsZero() {
now := time.Now()
session.EndedAt = now
duration := now.Sub(session.StartedAt)
logger.Info("ACCESS END (reaped) session=%s resource=%d proto=udp src=%s dst=%s started=%s ended=%s duration=%s",
session.SessionID, session.ResourceID, session.SourceAddr, session.DestAddr,
session.StartedAt.Format(time.RFC3339), now.Format(time.RFC3339), duration)
al.completedSessions = append(al.completedSessions, session)
delete(al.sessions, session.SessionID)
delete(al.udpSessions, key)
}
}
}
// extractIP strips the port from an address string and returns just the IP.
// If the address has no port component it is returned as-is.
func extractIP(addr string) string {
host, _, err := net.SplitHostPort(addr)
if err != nil {
// Might already be a bare IP
return addr
}
return host
}
// consolidateSessions takes a slice of completed sessions and merges bursts of
// short-lived connections from the same source IP to the same destination into
// single higher-level session entries.
//
// The algorithm:
// 1. Group sessions by (sourceIP, destAddr, protocol, resourceID).
// 2. Within each group, sort by StartedAt.
// 3. Walk through the sorted list and merge consecutive sessions whose gap
// (previous EndedAt → next StartedAt) is ≤ sessionGapThreshold.
// 4. For merged sessions the earliest StartedAt and latest EndedAt are kept,
// bytes are summed, and ConnectionCount records how many raw connections
// were folded in. If the merged connections used more than one source port,
// SourceAddr is set to just the IP (port omitted).
// 5. Groups with fewer than minConnectionsToConsolidate members are passed
// through unmodified.
func consolidateSessions(sessions []*AccessSession) []*AccessSession {
if len(sessions) <= 1 {
return sessions
}
// Group sessions by consolidation key
groups := make(map[consolidationKey][]*AccessSession)
for _, s := range sessions {
key := consolidationKey{
sourceIP: extractIP(s.SourceAddr),
destAddr: s.DestAddr,
protocol: s.Protocol,
resourceID: s.ResourceID,
}
groups[key] = append(groups[key], s)
}
result := make([]*AccessSession, 0, len(sessions))
for key, group := range groups {
// Small groups don't need consolidation
if len(group) < minConnectionsToConsolidate {
result = append(result, group...)
continue
}
// Sort the group by start time so we can detect gaps
sort.Slice(group, func(i, j int) bool {
return group[i].StartedAt.Before(group[j].StartedAt)
})
// Walk through and merge runs that are within the gap threshold
var merged []*AccessSession
cur := cloneSession(group[0])
cur.ConnectionCount = 1
sourcePorts := make(map[string]struct{})
sourcePorts[cur.SourceAddr] = struct{}{}
for i := 1; i < len(group); i++ {
s := group[i]
// Determine the gap: from the latest end time we've seen so far to the
// start of the next connection.
gapRef := cur.EndedAt
if gapRef.IsZero() {
gapRef = cur.StartedAt
}
gap := s.StartedAt.Sub(gapRef)
if gap <= sessionGapThreshold {
// Merge into the current consolidated session
cur.ConnectionCount++
cur.BytesTx += s.BytesTx
cur.BytesRx += s.BytesRx
sourcePorts[s.SourceAddr] = struct{}{}
// Extend EndedAt to the latest time
endTime := s.EndedAt
if endTime.IsZero() {
endTime = s.StartedAt
}
if endTime.After(cur.EndedAt) {
cur.EndedAt = endTime
}
} else {
// Gap exceeded — finalize the current session and start a new one
finalizeMergedSourceAddr(cur, key.sourceIP, sourcePorts)
merged = append(merged, cur)
cur = cloneSession(s)
cur.ConnectionCount = 1
sourcePorts = make(map[string]struct{})
sourcePorts[s.SourceAddr] = struct{}{}
}
}
// Finalize the last accumulated session
finalizeMergedSourceAddr(cur, key.sourceIP, sourcePorts)
merged = append(merged, cur)
result = append(result, merged...)
}
return result
}
// cloneSession creates a shallow copy of an AccessSession.
func cloneSession(s *AccessSession) *AccessSession {
cp := *s
return &cp
}
// finalizeMergedSourceAddr sets the SourceAddr on a consolidated session.
// If multiple distinct source addresses (ports) were seen, the port is
// stripped and only the IP is kept so the log isn't misleading.
func finalizeMergedSourceAddr(s *AccessSession, sourceIP string, ports map[string]struct{}) {
if len(ports) > 1 {
// Multiple source ports — just report the IP
s.SourceAddr = sourceIP
}
// Otherwise keep the original SourceAddr which already has ip:port
}
// flush drains the completed sessions buffer, consolidates bursts of
// short-lived connections, compresses with zlib, and sends via the SendFunc.
func (al *AccessLogger) flush() {
al.mu.Lock()
if len(al.completedSessions) == 0 {
al.mu.Unlock()
return
}
batch := al.completedSessions
al.completedSessions = make([]*AccessSession, 0)
sendFn := al.sendFn
al.mu.Unlock()
if sendFn == nil {
logger.Debug("Access logger: no send function configured, discarding %d sessions", len(batch))
return
}
// Consolidate bursts of short-lived connections into higher-level sessions
originalCount := len(batch)
batch = consolidateSessions(batch)
if len(batch) != originalCount {
logger.Info("Access logger: consolidated %d raw connections into %d sessions", originalCount, len(batch))
}
compressed, err := compressSessions(batch)
if err != nil {
logger.Error("Access logger: failed to compress %d sessions: %v", len(batch), err)
return
}
if err := sendFn(compressed); err != nil {
logger.Error("Access logger: failed to send %d sessions: %v", len(batch), err)
// Re-queue the batch so we don't lose data
al.mu.Lock()
al.completedSessions = append(batch, al.completedSessions...)
// Cap re-queued data to prevent unbounded growth if server is unreachable
if len(al.completedSessions) > maxBufferedSessions*5 {
dropped := len(al.completedSessions) - maxBufferedSessions*5
al.completedSessions = al.completedSessions[:maxBufferedSessions*5]
logger.Warn("Access logger: buffer overflow, dropped %d oldest sessions", dropped)
}
al.mu.Unlock()
return
}
logger.Info("Access logger: sent %d sessions to server", len(batch))
}
// compressSessions JSON-encodes the sessions, compresses with zlib, and returns
// a base64-encoded string suitable for embedding in a JSON message.
func compressSessions(sessions []*AccessSession) (string, error) {
jsonData, err := json.Marshal(sessions)
if err != nil {
return "", err
}
var buf bytes.Buffer
w, err := zlib.NewWriterLevel(&buf, zlib.BestCompression)
if err != nil {
return "", err
}
if _, err := w.Write(jsonData); err != nil {
w.Close()
return "", err
}
if err := w.Close(); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
}
// Close shuts down the background loop, ends all active sessions,
// and performs one final flush to send everything to the server.
func (al *AccessLogger) Close() {
// Signal the background loop to stop
select {
case <-al.stopCh:
// Already closed
return
default:
close(al.stopCh)
}
// Wait for the background loop to exit so we don't race on flush
<-al.flushDone
al.mu.Lock()
now := time.Now()
// End all active sessions and move them to the completed buffer
for _, session := range al.sessions {
if session.EndedAt.IsZero() {
session.EndedAt = now
duration := now.Sub(session.StartedAt)
logger.Info("ACCESS END (shutdown) session=%s resource=%d proto=%s src=%s dst=%s started=%s ended=%s duration=%s",
session.SessionID, session.ResourceID, session.Protocol, session.SourceAddr, session.DestAddr,
session.StartedAt.Format(time.RFC3339), now.Format(time.RFC3339), duration)
al.completedSessions = append(al.completedSessions, session)
}
}
al.sessions = make(map[string]*AccessSession)
al.udpSessions = make(map[udpSessionKey]*AccessSession)
al.mu.Unlock()
// Final flush to send all remaining sessions to the server
al.flush()
}

View File

@@ -0,0 +1,811 @@
package netstack2
import (
"testing"
"time"
)
func TestExtractIP(t *testing.T) {
tests := []struct {
name string
addr string
expected string
}{
{"ipv4 with port", "192.168.1.1:12345", "192.168.1.1"},
{"ipv4 without port", "192.168.1.1", "192.168.1.1"},
{"ipv6 with port", "[::1]:12345", "::1"},
{"ipv6 without port", "::1", "::1"},
{"empty string", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractIP(tt.addr)
if result != tt.expected {
t.Errorf("extractIP(%q) = %q, want %q", tt.addr, result, tt.expected)
}
})
}
}
func TestConsolidateSessions_Empty(t *testing.T) {
result := consolidateSessions(nil)
if result != nil {
t.Errorf("expected nil, got %v", result)
}
result = consolidateSessions([]*AccessSession{})
if len(result) != 0 {
t.Errorf("expected empty slice, got %d items", len(result))
}
}
func TestConsolidateSessions_SingleSession(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
{
SessionID: "abc123",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(1 * time.Second),
},
}
result := consolidateSessions(sessions)
if len(result) != 1 {
t.Fatalf("expected 1 session, got %d", len(result))
}
if result[0].SourceAddr != "10.0.0.1:5000" {
t.Errorf("expected source addr preserved, got %q", result[0].SourceAddr)
}
}
func TestConsolidateSessions_MergesBurstFromSameSourceIP(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
BytesTx: 100,
BytesRx: 200,
},
{
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(300 * time.Millisecond),
BytesTx: 150,
BytesRx: 250,
},
{
SessionID: "s3",
ResourceID: 1,
SourceAddr: "10.0.0.1:5002",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(400 * time.Millisecond),
EndedAt: now.Add(500 * time.Millisecond),
BytesTx: 50,
BytesRx: 75,
},
}
result := consolidateSessions(sessions)
if len(result) != 1 {
t.Fatalf("expected 1 consolidated session, got %d", len(result))
}
s := result[0]
if s.ConnectionCount != 3 {
t.Errorf("expected ConnectionCount=3, got %d", s.ConnectionCount)
}
if s.SourceAddr != "10.0.0.1" {
t.Errorf("expected source addr to be IP only (multiple ports), got %q", s.SourceAddr)
}
if s.DestAddr != "192.168.1.100:443" {
t.Errorf("expected dest addr preserved, got %q", s.DestAddr)
}
if s.StartedAt != now {
t.Errorf("expected StartedAt to be earliest time")
}
if s.EndedAt != now.Add(500*time.Millisecond) {
t.Errorf("expected EndedAt to be latest time")
}
expectedTx := int64(300)
expectedRx := int64(525)
if s.BytesTx != expectedTx {
t.Errorf("expected BytesTx=%d, got %d", expectedTx, s.BytesTx)
}
if s.BytesRx != expectedRx {
t.Errorf("expected BytesRx=%d, got %d", expectedRx, s.BytesRx)
}
}
func TestConsolidateSessions_SameSourcePortPreserved(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
},
{
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(300 * time.Millisecond),
},
}
result := consolidateSessions(sessions)
if len(result) != 1 {
t.Fatalf("expected 1 session, got %d", len(result))
}
if result[0].SourceAddr != "10.0.0.1:5000" {
t.Errorf("expected source addr with port preserved when all ports are the same, got %q", result[0].SourceAddr)
}
if result[0].ConnectionCount != 2 {
t.Errorf("expected ConnectionCount=2, got %d", result[0].ConnectionCount)
}
}
func TestConsolidateSessions_GapSplitsSessions(t *testing.T) {
now := time.Now()
// First burst
sessions := []*AccessSession{
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
},
{
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(300 * time.Millisecond),
},
// Big gap here (10 seconds)
{
SessionID: "s3",
ResourceID: 1,
SourceAddr: "10.0.0.1:5002",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(10 * time.Second),
EndedAt: now.Add(10*time.Second + 100*time.Millisecond),
},
{
SessionID: "s4",
ResourceID: 1,
SourceAddr: "10.0.0.1:5003",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(10*time.Second + 200*time.Millisecond),
EndedAt: now.Add(10*time.Second + 300*time.Millisecond),
},
}
result := consolidateSessions(sessions)
if len(result) != 2 {
t.Fatalf("expected 2 consolidated sessions (gap split), got %d", len(result))
}
// Find the sessions by their start time
var first, second *AccessSession
for _, s := range result {
if s.StartedAt.Equal(now) {
first = s
} else {
second = s
}
}
if first == nil || second == nil {
t.Fatal("could not find both consolidated sessions")
}
if first.ConnectionCount != 2 {
t.Errorf("first burst: expected ConnectionCount=2, got %d", first.ConnectionCount)
}
if second.ConnectionCount != 2 {
t.Errorf("second burst: expected ConnectionCount=2, got %d", second.ConnectionCount)
}
}
func TestConsolidateSessions_DifferentDestinationsNotMerged(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
},
{
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:8080",
Protocol: "tcp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(300 * time.Millisecond),
},
}
result := consolidateSessions(sessions)
// Each goes to a different dest port so they should not be merged
if len(result) != 2 {
t.Fatalf("expected 2 sessions (different destinations), got %d", len(result))
}
}
func TestConsolidateSessions_DifferentProtocolsNotMerged(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
},
{
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:443",
Protocol: "udp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(300 * time.Millisecond),
},
}
result := consolidateSessions(sessions)
if len(result) != 2 {
t.Fatalf("expected 2 sessions (different protocols), got %d", len(result))
}
}
func TestConsolidateSessions_DifferentResourceIDsNotMerged(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
},
{
SessionID: "s2",
ResourceID: 2,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(300 * time.Millisecond),
},
}
result := consolidateSessions(sessions)
if len(result) != 2 {
t.Fatalf("expected 2 sessions (different resource IDs), got %d", len(result))
}
}
func TestConsolidateSessions_DifferentSourceIPsNotMerged(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
},
{
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.2:5001",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(300 * time.Millisecond),
},
}
result := consolidateSessions(sessions)
if len(result) != 2 {
t.Fatalf("expected 2 sessions (different source IPs), got %d", len(result))
}
}
func TestConsolidateSessions_OutOfOrderInput(t *testing.T) {
now := time.Now()
// Provide sessions out of chronological order to verify sorting
sessions := []*AccessSession{
{
SessionID: "s3",
ResourceID: 1,
SourceAddr: "10.0.0.1:5002",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(400 * time.Millisecond),
EndedAt: now.Add(500 * time.Millisecond),
BytesTx: 30,
},
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
BytesTx: 10,
},
{
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(300 * time.Millisecond),
BytesTx: 20,
},
}
result := consolidateSessions(sessions)
if len(result) != 1 {
t.Fatalf("expected 1 consolidated session, got %d", len(result))
}
s := result[0]
if s.ConnectionCount != 3 {
t.Errorf("expected ConnectionCount=3, got %d", s.ConnectionCount)
}
if s.StartedAt != now {
t.Errorf("expected StartedAt to be earliest time")
}
if s.EndedAt != now.Add(500*time.Millisecond) {
t.Errorf("expected EndedAt to be latest time")
}
if s.BytesTx != 60 {
t.Errorf("expected BytesTx=60, got %d", s.BytesTx)
}
}
func TestConsolidateSessions_ExactlyAtGapThreshold(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
},
{
// Starts exactly sessionGapThreshold after s1 ends — should still merge
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(100*time.Millisecond + sessionGapThreshold),
EndedAt: now.Add(100*time.Millisecond + sessionGapThreshold + 50*time.Millisecond),
},
}
result := consolidateSessions(sessions)
if len(result) != 1 {
t.Fatalf("expected 1 session (gap exactly at threshold merges), got %d", len(result))
}
if result[0].ConnectionCount != 2 {
t.Errorf("expected ConnectionCount=2, got %d", result[0].ConnectionCount)
}
}
func TestConsolidateSessions_JustOverGapThreshold(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
},
{
// Starts 1ms over the gap threshold after s1 ends — should split
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(100*time.Millisecond + sessionGapThreshold + 1*time.Millisecond),
EndedAt: now.Add(100*time.Millisecond + sessionGapThreshold + 50*time.Millisecond),
},
}
result := consolidateSessions(sessions)
if len(result) != 2 {
t.Fatalf("expected 2 sessions (gap just over threshold splits), got %d", len(result))
}
}
func TestConsolidateSessions_UDPSessions(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
{
SessionID: "u1",
ResourceID: 5,
SourceAddr: "10.0.0.1:6000",
DestAddr: "192.168.1.100:53",
Protocol: "udp",
StartedAt: now,
EndedAt: now.Add(50 * time.Millisecond),
BytesTx: 64,
BytesRx: 512,
},
{
SessionID: "u2",
ResourceID: 5,
SourceAddr: "10.0.0.1:6001",
DestAddr: "192.168.1.100:53",
Protocol: "udp",
StartedAt: now.Add(100 * time.Millisecond),
EndedAt: now.Add(150 * time.Millisecond),
BytesTx: 64,
BytesRx: 256,
},
{
SessionID: "u3",
ResourceID: 5,
SourceAddr: "10.0.0.1:6002",
DestAddr: "192.168.1.100:53",
Protocol: "udp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(250 * time.Millisecond),
BytesTx: 64,
BytesRx: 128,
},
}
result := consolidateSessions(sessions)
if len(result) != 1 {
t.Fatalf("expected 1 consolidated UDP session, got %d", len(result))
}
s := result[0]
if s.Protocol != "udp" {
t.Errorf("expected protocol=udp, got %q", s.Protocol)
}
if s.ConnectionCount != 3 {
t.Errorf("expected ConnectionCount=3, got %d", s.ConnectionCount)
}
if s.SourceAddr != "10.0.0.1" {
t.Errorf("expected source addr to be IP only, got %q", s.SourceAddr)
}
if s.BytesTx != 192 {
t.Errorf("expected BytesTx=192, got %d", s.BytesTx)
}
if s.BytesRx != 896 {
t.Errorf("expected BytesRx=896, got %d", s.BytesRx)
}
}
func TestConsolidateSessions_MixedGroupsSomeConsolidatedSomeNot(t *testing.T) {
now := time.Now()
sessions := []*AccessSession{
// Group 1: 3 connections to :443 from same IP — should consolidate
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
},
{
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(300 * time.Millisecond),
},
{
SessionID: "s3",
ResourceID: 1,
SourceAddr: "10.0.0.1:5002",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(400 * time.Millisecond),
EndedAt: now.Add(500 * time.Millisecond),
},
// Group 2: 1 connection to :8080 from different IP — should pass through
{
SessionID: "s4",
ResourceID: 2,
SourceAddr: "10.0.0.2:6000",
DestAddr: "192.168.1.200:8080",
Protocol: "tcp",
StartedAt: now.Add(1 * time.Second),
EndedAt: now.Add(2 * time.Second),
},
}
result := consolidateSessions(sessions)
if len(result) != 2 {
t.Fatalf("expected 2 sessions total, got %d", len(result))
}
var consolidated, passthrough *AccessSession
for _, s := range result {
if s.ConnectionCount > 1 {
consolidated = s
} else {
passthrough = s
}
}
if consolidated == nil {
t.Fatal("expected a consolidated session")
}
if consolidated.ConnectionCount != 3 {
t.Errorf("consolidated: expected ConnectionCount=3, got %d", consolidated.ConnectionCount)
}
if passthrough == nil {
t.Fatal("expected a passthrough session")
}
if passthrough.SessionID != "s4" {
t.Errorf("passthrough: expected session s4, got %s", passthrough.SessionID)
}
}
func TestConsolidateSessions_OverlappingConnections(t *testing.T) {
now := time.Now()
// Connections that overlap in time (not sequential)
sessions := []*AccessSession{
{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(5 * time.Second),
BytesTx: 100,
},
{
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(1 * time.Second),
EndedAt: now.Add(3 * time.Second),
BytesTx: 200,
},
{
SessionID: "s3",
ResourceID: 1,
SourceAddr: "10.0.0.1:5002",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(2 * time.Second),
EndedAt: now.Add(6 * time.Second),
BytesTx: 300,
},
}
result := consolidateSessions(sessions)
if len(result) != 1 {
t.Fatalf("expected 1 consolidated session, got %d", len(result))
}
s := result[0]
if s.ConnectionCount != 3 {
t.Errorf("expected ConnectionCount=3, got %d", s.ConnectionCount)
}
if s.StartedAt != now {
t.Error("expected StartedAt to be earliest")
}
if s.EndedAt != now.Add(6*time.Second) {
t.Error("expected EndedAt to be the latest end time")
}
if s.BytesTx != 600 {
t.Errorf("expected BytesTx=600, got %d", s.BytesTx)
}
}
func TestConsolidateSessions_DoesNotMutateOriginals(t *testing.T) {
now := time.Now()
s1 := &AccessSession{
SessionID: "s1",
ResourceID: 1,
SourceAddr: "10.0.0.1:5000",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now,
EndedAt: now.Add(100 * time.Millisecond),
BytesTx: 100,
}
s2 := &AccessSession{
SessionID: "s2",
ResourceID: 1,
SourceAddr: "10.0.0.1:5001",
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(200 * time.Millisecond),
EndedAt: now.Add(300 * time.Millisecond),
BytesTx: 200,
}
// Save original values
origS1Addr := s1.SourceAddr
origS1Bytes := s1.BytesTx
origS2Addr := s2.SourceAddr
origS2Bytes := s2.BytesTx
_ = consolidateSessions([]*AccessSession{s1, s2})
if s1.SourceAddr != origS1Addr {
t.Errorf("s1.SourceAddr was mutated: %q -> %q", origS1Addr, s1.SourceAddr)
}
if s1.BytesTx != origS1Bytes {
t.Errorf("s1.BytesTx was mutated: %d -> %d", origS1Bytes, s1.BytesTx)
}
if s2.SourceAddr != origS2Addr {
t.Errorf("s2.SourceAddr was mutated: %q -> %q", origS2Addr, s2.SourceAddr)
}
if s2.BytesTx != origS2Bytes {
t.Errorf("s2.BytesTx was mutated: %d -> %d", origS2Bytes, s2.BytesTx)
}
}
func TestConsolidateSessions_ThreeBurstsWithGaps(t *testing.T) {
now := time.Now()
sessions := make([]*AccessSession, 0, 9)
// Burst 1: 3 connections at t=0
for i := 0; i < 3; i++ {
sessions = append(sessions, &AccessSession{
SessionID: generateSessionID(),
ResourceID: 1,
SourceAddr: "10.0.0.1:" + string(rune('A'+i)),
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(time.Duration(i*100) * time.Millisecond),
EndedAt: now.Add(time.Duration(i*100+50) * time.Millisecond),
})
}
// Burst 2: 3 connections at t=20s (well past the 5s gap)
for i := 0; i < 3; i++ {
sessions = append(sessions, &AccessSession{
SessionID: generateSessionID(),
ResourceID: 1,
SourceAddr: "10.0.0.1:" + string(rune('D'+i)),
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(20*time.Second + time.Duration(i*100)*time.Millisecond),
EndedAt: now.Add(20*time.Second + time.Duration(i*100+50)*time.Millisecond),
})
}
// Burst 3: 3 connections at t=40s
for i := 0; i < 3; i++ {
sessions = append(sessions, &AccessSession{
SessionID: generateSessionID(),
ResourceID: 1,
SourceAddr: "10.0.0.1:" + string(rune('G'+i)),
DestAddr: "192.168.1.100:443",
Protocol: "tcp",
StartedAt: now.Add(40*time.Second + time.Duration(i*100)*time.Millisecond),
EndedAt: now.Add(40*time.Second + time.Duration(i*100+50)*time.Millisecond),
})
}
result := consolidateSessions(sessions)
if len(result) != 3 {
t.Fatalf("expected 3 consolidated sessions (3 bursts), got %d", len(result))
}
for _, s := range result {
if s.ConnectionCount != 3 {
t.Errorf("expected each burst to have ConnectionCount=3, got %d (started=%v)", s.ConnectionCount, s.StartedAt)
}
}
}
func TestFinalizeMergedSourceAddr(t *testing.T) {
s := &AccessSession{SourceAddr: "10.0.0.1:5000"}
ports := map[string]struct{}{"10.0.0.1:5000": {}}
finalizeMergedSourceAddr(s, "10.0.0.1", ports)
if s.SourceAddr != "10.0.0.1:5000" {
t.Errorf("single port: expected addr preserved, got %q", s.SourceAddr)
}
s2 := &AccessSession{SourceAddr: "10.0.0.1:5000"}
ports2 := map[string]struct{}{"10.0.0.1:5000": {}, "10.0.0.1:5001": {}}
finalizeMergedSourceAddr(s2, "10.0.0.1", ports2)
if s2.SourceAddr != "10.0.0.1" {
t.Errorf("multiple ports: expected IP only, got %q", s2.SourceAddr)
}
}
func TestCloneSession(t *testing.T) {
original := &AccessSession{
SessionID: "test",
ResourceID: 42,
SourceAddr: "1.2.3.4:100",
DestAddr: "5.6.7.8:443",
Protocol: "tcp",
BytesTx: 999,
}
clone := cloneSession(original)
if clone == original {
t.Error("clone should be a different pointer")
}
if clone.SessionID != original.SessionID {
t.Error("clone should have same SessionID")
}
// Mutating clone should not affect original
clone.BytesTx = 0
clone.SourceAddr = "changed"
if original.BytesTx != 999 {
t.Error("mutating clone affected original BytesTx")
}
if original.SourceAddr != "1.2.3.4:100" {
t.Error("mutating clone affected original SourceAddr")
}
}

View File

@@ -137,14 +137,33 @@ func (h *TCPHandler) InstallTCPHandler() error {
// handleTCPConn handles a TCP connection by proxying it to the actual target
func (h *TCPHandler) handleTCPConn(netstackConn *gonet.TCPConn, id stack.TransportEndpointID) {
defer netstackConn.Close()
// Extract source and target address from the connection ID
// Extract source and target address from the connection ID first so they
// are available for HTTP routing before any defer is set up.
srcIP := id.RemoteAddress.String()
srcPort := id.RemotePort
dstIP := id.LocalAddress.String()
dstPort := id.LocalPort
// For HTTP/HTTPS ports, look up the matching subnet rule. If the rule has
// Protocol configured, hand the connection off to the HTTP handler which
// takes full ownership of the lifecycle (the defer close must not be
// installed before this point).
if (dstPort == 80 || dstPort == 443) && h.proxyHandler != nil && h.proxyHandler.httpHandler != nil {
srcAddr, _ := netip.ParseAddr(srcIP)
dstAddr, _ := netip.ParseAddr(dstIP)
rule := h.proxyHandler.subnetLookup.Match(srcAddr, dstAddr, dstPort, tcp.ProtocolNumber)
if rule != nil && rule.Protocol != "" && len(rule.HTTPTargets) > 0 {
logger.Info("TCP Forwarder: Routing %s:%d -> %s:%d to HTTP handler (%s)",
srcIP, srcPort, dstIP, dstPort, rule.Protocol)
h.proxyHandler.httpHandler.HandleConn(netstackConn, rule)
return
}
// Otherwise fall through to raw TCP forwarding (e.g. CIDR resources
// that happen to use port 80/443 without HTTP configuration).
}
defer netstackConn.Close()
logger.Info("TCP Forwarder: Handling connection %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort)
// Check if there's a destination rewrite for this connection (e.g., localhost targets)
@@ -158,6 +177,18 @@ func (h *TCPHandler) handleTCPConn(netstackConn *gonet.TCPConn, id stack.Transpo
targetAddr := fmt.Sprintf("%s:%d", actualDstIP, dstPort)
// Look up resource ID and start access session if applicable
var accessSessionID string
if h.proxyHandler != nil {
resourceId := h.proxyHandler.LookupResourceId(srcIP, dstIP, dstPort, uint8(tcp.ProtocolNumber))
if resourceId != 0 {
if al := h.proxyHandler.GetAccessLogger(); al != nil {
srcAddr := fmt.Sprintf("%s:%d", srcIP, srcPort)
accessSessionID = al.StartTCPSession(resourceId, srcAddr, targetAddr)
}
}
}
// Create context with timeout for connection establishment
ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout)
defer cancel()
@@ -167,11 +198,26 @@ func (h *TCPHandler) handleTCPConn(netstackConn *gonet.TCPConn, id stack.Transpo
targetConn, err := d.DialContext(ctx, "tcp", targetAddr)
if err != nil {
logger.Info("TCP Forwarder: Failed to connect to %s: %v", targetAddr, err)
// End access session on connection failure
if accessSessionID != "" {
if al := h.proxyHandler.GetAccessLogger(); al != nil {
al.EndTCPSession(accessSessionID)
}
}
// Connection failed, netstack will handle RST
return
}
defer targetConn.Close()
// End access session when connection closes
if accessSessionID != "" {
defer func() {
if al := h.proxyHandler.GetAccessLogger(); al != nil {
al.EndTCPSession(accessSessionID)
}
}()
}
logger.Info("TCP Forwarder: Successfully connected to %s, starting bidirectional copy", targetAddr)
// Bidirectional copy between netstack and target
@@ -280,6 +326,27 @@ func (h *UDPHandler) handleUDPConn(netstackConn *gonet.UDPConn, id stack.Transpo
targetAddr := fmt.Sprintf("%s:%d", actualDstIP, dstPort)
// Look up resource ID and start access session if applicable
var accessSessionID string
if h.proxyHandler != nil {
resourceId := h.proxyHandler.LookupResourceId(srcIP, dstIP, dstPort, uint8(udp.ProtocolNumber))
if resourceId != 0 {
if al := h.proxyHandler.GetAccessLogger(); al != nil {
srcAddr := fmt.Sprintf("%s:%d", srcIP, srcPort)
accessSessionID = al.TrackUDPSession(resourceId, srcAddr, targetAddr)
}
}
}
// End access session when UDP handler returns (timeout or error)
if accessSessionID != "" {
defer func() {
if al := h.proxyHandler.GetAccessLogger(); al != nil {
al.EndUDPSession(accessSessionID)
}
}()
}
// Resolve target address
remoteUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr)
if err != nil {

418
netstack2/http_handler.go Normal file
View File

@@ -0,0 +1,418 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/
package netstack2
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"time"
"github.com/fosrl/newt/logger"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
// ---------------------------------------------------------------------------
// HTTPTarget
// ---------------------------------------------------------------------------
// HTTPTarget describes a single downstream HTTP or HTTPS service that the
// proxy should forward requests to.
type HTTPTarget struct {
DestAddr string `json:"destAddr"` // IP address or hostname of the downstream service
DestPort uint16 `json:"destPort"` // TCP port of the downstream service
Scheme string `json:"scheme"` // When true the outbound leg uses HTTPS
}
// ---------------------------------------------------------------------------
// HTTPHandler
// ---------------------------------------------------------------------------
// HTTPHandler intercepts TCP connections from the netstack forwarder on ports
// 80 and 443 and services them as HTTP or HTTPS, reverse-proxying each request
// to downstream targets specified by the matching SubnetRule.
//
// HTTP and raw TCP are fully separate: a connection is only routed here when
// its SubnetRule has Protocol set ("http" or "https"). All other connections
// on those ports fall through to the normal raw-TCP path.
//
// Incoming TLS termination (Protocol == "https") is performed per-connection
// using the certificate and key stored in the rule, so different subnet rules
// can present different certificates without sharing any state.
//
// Outbound connections to downstream targets honour HTTPTarget.UseHTTPS
// independently of the incoming protocol.
type HTTPHandler struct {
stack *stack.Stack
proxyHandler *ProxyHandler
requestLogger *HTTPRequestLogger
listener *chanListener
server *http.Server
// proxyCache holds pre-built *httputil.ReverseProxy values keyed by the
// canonical target URL string ("scheme://host:port"). Building a proxy is
// cheap, but reusing one preserves the underlying http.Transport connection
// pool, which matters for throughput.
proxyCache sync.Map // map[string]*httputil.ReverseProxy
// tlsCache holds pre-parsed *tls.Config values keyed by the concatenation
// of the PEM certificate and key. Parsing a keypair is relatively expensive
// and the same cert is likely reused across many connections.
tlsCache sync.Map // map[string]*tls.Config
}
// ---------------------------------------------------------------------------
// chanListener net.Listener backed by a channel
// ---------------------------------------------------------------------------
// chanListener implements net.Listener by receiving net.Conn values over a
// buffered channel. This lets the netstack TCP forwarder hand off connections
// directly to a running http.Server without any real OS socket.
type chanListener struct {
connCh chan net.Conn
closed chan struct{}
once sync.Once
}
func newChanListener() *chanListener {
return &chanListener{
connCh: make(chan net.Conn, 128),
closed: make(chan struct{}),
}
}
// Accept blocks until a connection is available or the listener is closed.
func (l *chanListener) Accept() (net.Conn, error) {
select {
case conn, ok := <-l.connCh:
if !ok {
return nil, net.ErrClosed
}
return conn, nil
case <-l.closed:
return nil, net.ErrClosed
}
}
// Close shuts down the listener; subsequent Accept calls return net.ErrClosed.
func (l *chanListener) Close() error {
l.once.Do(func() { close(l.closed) })
return nil
}
// Addr returns a placeholder address (the listener has no real OS socket).
func (l *chanListener) Addr() net.Addr {
return &net.TCPAddr{}
}
// send delivers conn to the listener. Returns false if the listener is already
// closed, in which case the caller is responsible for closing conn.
func (l *chanListener) send(conn net.Conn) bool {
select {
case l.connCh <- conn:
return true
case <-l.closed:
return false
}
}
// ---------------------------------------------------------------------------
// httpConnCtx conn wrapper that carries a SubnetRule through the listener
// ---------------------------------------------------------------------------
// httpConnCtx wraps a net.Conn so the matching SubnetRule and TLS state can
// be passed through the chanListener into the http.Server's ConnContext
// callback, making them available to request handlers via the request context.
type httpConnCtx struct {
net.Conn
rule *SubnetRule
isTLS bool // true when the conn was wrapped with tls.Server
}
// connCtxKey is the unexported context key used to store a *SubnetRule on the
// per-connection context created by http.Server.ConnContext.
type connCtxKey struct{}
// connTLSKey is the unexported context key used to store the isTLS flag on
// the per-connection context created by http.Server.ConnContext.
type connTLSKey struct{}
// ---------------------------------------------------------------------------
// Constructor and lifecycle
// ---------------------------------------------------------------------------
// NewHTTPHandler creates an HTTPHandler attached to the given stack and
// ProxyHandler. Call Start to begin serving connections.
func NewHTTPHandler(s *stack.Stack, ph *ProxyHandler) *HTTPHandler {
return &HTTPHandler{
stack: s,
proxyHandler: ph,
}
}
// SetRequestLogger attaches an HTTPRequestLogger so that every proxied request
// is recorded and periodically shipped to the server.
func (h *HTTPHandler) SetRequestLogger(rl *HTTPRequestLogger) {
h.requestLogger = rl
}
// Start launches the internal http.Server that services connections delivered
// via HandleConn. The server runs for the lifetime of the HTTPHandler; call
// Close to stop it.
func (h *HTTPHandler) Start() error {
h.listener = newChanListener()
h.server = &http.Server{
Handler: http.HandlerFunc(h.handleRequest),
// ConnContext runs once per accepted connection and attaches the
// SubnetRule carried by httpConnCtx to the connection's context so
// that handleRequest can retrieve it without any global state.
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
if cc, ok := c.(*httpConnCtx); ok {
ctx = context.WithValue(ctx, connCtxKey{}, cc.rule)
ctx = context.WithValue(ctx, connTLSKey{}, cc.isTLS)
}
return ctx
},
}
go func() {
if err := h.server.Serve(h.listener); err != nil && err != http.ErrServerClosed {
logger.Error("HTTP handler: server exited unexpectedly: %v", err)
}
}()
logger.Debug("HTTP handler: ready — routing determined per SubnetRule on ports 80/443")
return nil
}
// HandleConn accepts a TCP connection from the netstack forwarder together
// with the SubnetRule that matched it. The HTTP handler takes full ownership
// of the connection's lifecycle; the caller must NOT close conn after this call.
//
// When rule.Protocol is "https", TLS termination is performed on conn using
// the certificate and key stored in rule.TLSCert and rule.TLSKey before the
// connection is passed to the HTTP server. The HTTP server itself is always
// plain-HTTP; TLS is fully unwrapped at this layer.
func (h *HTTPHandler) HandleConn(conn net.Conn, rule *SubnetRule) {
var effectiveConn net.Conn = conn
if rule.Protocol == "https" {
// Only perform TLS termination for connections arriving on port 443.
// Connections on port 80 are passed through as plain HTTP so that
// handleRequest can issue the HTTP→HTTPS redirect.
doTLS := false
if tcpAddr, ok := conn.LocalAddr().(*net.TCPAddr); ok {
doTLS = tcpAddr.Port == 443
}
if doTLS {
tlsCfg, err := h.getTLSConfig(rule)
if err != nil {
logger.Error("HTTP handler: cannot build TLS config for connection from %s: %v",
conn.RemoteAddr(), err)
conn.Close()
return
}
// tls.Server wraps the raw conn; the TLS handshake is deferred until
// the first Read, which the http.Server will trigger naturally.
effectiveConn = tls.Server(conn, tlsCfg)
}
}
wrapped := &httpConnCtx{Conn: effectiveConn, rule: rule, isTLS: effectiveConn != conn}
if !h.listener.send(wrapped) {
// Listener is already closed — clean up the orphaned connection.
effectiveConn.Close()
}
}
// Close gracefully shuts down the HTTP server and the underlying channel
// listener, causing the goroutine started in Start to exit.
func (h *HTTPHandler) Close() error {
if h.server != nil {
if err := h.server.Close(); err != nil {
return err
}
}
if h.listener != nil {
h.listener.Close()
}
return nil
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
// getTLSConfig returns a *tls.Config for the cert/key pair in rule, using a
// cache to avoid re-parsing the same keypair on every connection.
// The cache key is the concatenation of the PEM cert and key strings, so
// different rules that happen to share the same material hit the same entry.
func (h *HTTPHandler) getTLSConfig(rule *SubnetRule) (*tls.Config, error) {
cacheKey := rule.TLSCert + "|" + rule.TLSKey
if v, ok := h.tlsCache.Load(cacheKey); ok {
return v.(*tls.Config), nil
}
cert, err := tls.X509KeyPair([]byte(rule.TLSCert), []byte(rule.TLSKey))
if err != nil {
return nil, fmt.Errorf("failed to parse TLS keypair: %w", err)
}
cfg := &tls.Config{
Certificates: []tls.Certificate{cert},
}
// LoadOrStore is safe under concurrent calls: if two goroutines race here
// both will produce a valid config; the loser's work is discarded.
actual, _ := h.tlsCache.LoadOrStore(cacheKey, cfg)
return actual.(*tls.Config), nil
}
// getProxy returns a cached *httputil.ReverseProxy for the given target,
// creating one on first use. Reusing the proxy preserves its http.Transport
// connection pool, avoiding repeated TCP/TLS handshakes to the downstream.
func (h *HTTPHandler) getProxy(target HTTPTarget) *httputil.ReverseProxy {
scheme := target.Scheme
cacheKey := fmt.Sprintf("%s://%s:%d", scheme, target.DestAddr, target.DestPort)
if v, ok := h.proxyCache.Load(cacheKey); ok {
return v.(*httputil.ReverseProxy)
}
targetURL := &url.URL{
Scheme: scheme,
Host: fmt.Sprintf("%s:%d", target.DestAddr, target.DestPort),
}
var transport http.RoundTripper = http.DefaultTransport
if target.Scheme == "https" {
// Allow self-signed certificates on downstream HTTPS targets.
transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec // downstream self-signed certs are a supported configuration
},
}
}
proxy := &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) {
pr.SetURL(targetURL)
if host := pr.In.Host; host != "" {
pr.Out.Host = host
}
// SetXForwarded sets X-Forwarded-For from the inbound request's
// RemoteAddr (the WireGuard/netstack client address), along with
// X-Forwarded-Host and X-Forwarded-Proto. Using Rewrite instead of
// Director means the proxy does not append its own automatic
// X-Forwarded-For entry, so the header is set exactly once.
pr.SetXForwarded()
},
Transport: transport,
}
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
logger.Error("HTTP handler: upstream error (%s %s -> %s): %v",
r.Method, r.URL.RequestURI(), cacheKey, err)
http.Error(w, "Bad Gateway", http.StatusBadGateway)
}
actual, _ := h.proxyCache.LoadOrStore(cacheKey, proxy)
return actual.(*httputil.ReverseProxy)
}
// statusCapture wraps an http.ResponseWriter and records the HTTP status code
// written by the upstream handler. If WriteHeader is never called the status
// defaults to 200 (http.StatusOK), matching net/http semantics.
type statusCapture struct {
http.ResponseWriter
status int
}
func (sc *statusCapture) WriteHeader(code int) {
sc.status = code
sc.ResponseWriter.WriteHeader(code)
}
func (sc *statusCapture) Unwrap() http.ResponseWriter {
return sc.ResponseWriter
}
func (sc *statusCapture) Flush() {
if flusher, ok := sc.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
func (sc *statusCapture) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := sc.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("underlying response writer does not support hijacking")
}
return hijacker.Hijack()
}
// handleRequest is the http.Handler entry point. It retrieves the SubnetRule
// attached to the connection by ConnContext, selects the first configured
// downstream target, and forwards the request via the cached ReverseProxy.
//
// TODO: add host/path-based routing across multiple HTTPTargets once the
// configuration model evolves beyond a single target per rule.
func (h *HTTPHandler) handleRequest(w http.ResponseWriter, r *http.Request) {
rule, _ := r.Context().Value(connCtxKey{}).(*SubnetRule)
if rule == nil || len(rule.HTTPTargets) == 0 {
logger.Error("HTTP handler: no downstream targets for request %s %s", r.Method, r.URL.RequestURI())
http.Error(w, "no targets configured", http.StatusBadGateway)
return
}
// If the rule is HTTPS but the incoming request arrived over plain HTTP
// (port 80), redirect to HTTPS. We use the isTLS flag stored on the
// connection context rather than r.TLS, because Go's http.Server calls
// ConnectionState() before the TLS handshake completes, so r.TLS.Version
// is 0 even for genuine TLS connections at that point.
isTLS, _ := r.Context().Value(connTLSKey{}).(bool)
if rule.Protocol == "https" && !isTLS {
host := r.Host
if host == "" {
host = r.URL.Host
}
httpsURL := "https://" + host + r.RequestURI
logger.Info("HTTP handler: redirecting %s %s -> %s (TLS cert present)", r.Method, r.URL.RequestURI(), httpsURL)
http.Redirect(w, r, httpsURL, http.StatusPermanentRedirect)
return
}
target := rule.HTTPTargets[0]
scheme := target.Scheme
logger.Info("HTTP handler: %s %s -> %s://%s:%d",
r.Method, r.URL.RequestURI(), scheme, target.DestAddr, target.DestPort)
timestamp := time.Now()
sc := &statusCapture{ResponseWriter: w, status: http.StatusOK}
h.getProxy(target).ServeHTTP(sc, r)
if h.requestLogger != nil && rule.ResourceId != 0 {
h.requestLogger.LogRequest(HTTPRequestLog{
ResourceID: rule.ResourceId,
Timestamp: timestamp,
Method: r.Method,
Scheme: rule.Protocol,
Host: r.Host,
Path: r.URL.Path,
RawQuery: r.URL.RawQuery,
UserAgent: r.UserAgent(),
SourceAddr: r.RemoteAddr,
TLS: rule.Protocol == "https",
})
}
}

View File

@@ -0,0 +1,97 @@
package netstack2
import (
"context"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/gorilla/websocket"
)
func TestHTTPHandlerProxiesWebSocketUpgrade(t *testing.T) {
upgrader := websocket.Upgrader{}
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.Errorf("upgrade failed: %v", err)
return
}
defer conn.Close()
messageType, payload, err := conn.ReadMessage()
if err != nil {
t.Errorf("read failed: %v", err)
return
}
if err := conn.WriteMessage(messageType, append([]byte("echo:"), payload...)); err != nil {
t.Errorf("write failed: %v", err)
}
}))
defer backend.Close()
backendURL, err := url.Parse(backend.URL)
if err != nil {
t.Fatalf("parse backend URL: %v", err)
}
backendHost, backendPort, err := net.SplitHostPort(backendURL.Host)
if err != nil {
t.Fatalf("split backend host: %v", err)
}
port, err := net.LookupPort("tcp", backendPort)
if err != nil {
t.Fatalf("parse backend port: %v", err)
}
handler := NewHTTPHandler(nil, nil)
rule := &SubnetRule{
Protocol: "http",
HTTPTargets: []HTTPTarget{
{
DestAddr: backendHost,
DestPort: uint16(port),
Scheme: backendURL.Scheme,
},
},
}
frontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), connCtxKey{}, rule)
handler.handleRequest(w, r.WithContext(ctx))
}))
defer frontend.Close()
frontendURL, err := url.Parse(frontend.URL)
if err != nil {
t.Fatalf("parse frontend URL: %v", err)
}
wsURL := url.URL{
Scheme: "ws",
Host: frontendURL.Host,
Path: "/socket",
RawQuery: "token=test",
}
conn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
if err != nil {
t.Fatalf("dial websocket through proxy: %v", err)
}
defer conn.Close()
if err := conn.WriteMessage(websocket.TextMessage, []byte("hello")); err != nil {
t.Fatalf("write websocket message: %v", err)
}
messageType, payload, err := conn.ReadMessage()
if err != nil {
t.Fatalf("read websocket message: %v", err)
}
if messageType != websocket.TextMessage {
t.Fatalf("message type = %d, want %d", messageType, websocket.TextMessage)
}
if got, want := string(payload), "echo:hello"; got != want {
t.Fatalf("payload = %q, want %q", got, want)
}
}

View File

@@ -0,0 +1,48 @@
package netstack2
import (
"crypto/tls"
"net"
"testing"
)
// tlsConnStub is a minimal net.Conn that also exposes TLS state, matching
// *tls.Conn's ConnectionState used by net/http.Server.
type tlsConnStub struct {
net.Conn
state tls.ConnectionState
}
func (t *tlsConnStub) ConnectionState() tls.ConnectionState {
return t.state
}
func TestHTTPConnCtxForwardsConnectionState(t *testing.T) {
c1, c2 := net.Pipe()
defer c1.Close()
defer c2.Close()
inner := &tlsConnStub{
Conn: c1,
state: tls.ConnectionState{Version: tls.VersionTLS12, HandshakeComplete: true},
}
wrapped := &httpConnCtx{Conn: inner, rule: nil}
got := wrapped.ConnectionState()
if got.Version != tls.VersionTLS12 || !got.HandshakeComplete {
t.Fatalf("ConnectionState = %+v, want TLS 1.2 and HandshakeComplete", got)
}
}
func TestHTTPConnCtxConnectionStatePlainTCP(t *testing.T) {
c1, c2 := net.Pipe()
defer c1.Close()
defer c2.Close()
wrapped := &httpConnCtx{Conn: c1, rule: nil}
got := wrapped.ConnectionState()
if got.Version != 0 {
t.Fatalf("expected zero ConnectionState for plain conn, got %+v", got)
}
_ = c2
}

View File

@@ -0,0 +1,175 @@
package netstack2
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/json"
"sync"
"time"
"github.com/fosrl/newt/logger"
)
// HTTPRequestLog represents a single HTTP/HTTPS request proxied through the handler.
type HTTPRequestLog struct {
RequestID string `json:"requestId"`
ResourceID int `json:"resourceId"`
Timestamp time.Time `json:"timestamp"`
Method string `json:"method"`
Scheme string `json:"scheme"`
Host string `json:"host"`
Path string `json:"path"`
RawQuery string `json:"rawQuery,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
SourceAddr string `json:"sourceAddr"`
TLS bool `json:"tls"`
}
// HTTPRequestLogger buffers HTTP request logs and periodically flushes them
// to the server via a configurable SendFunc.
type HTTPRequestLogger struct {
mu sync.Mutex
pending []HTTPRequestLog
sendFn SendFunc
stopCh chan struct{}
flushDone chan struct{}
}
// NewHTTPRequestLogger creates a new HTTPRequestLogger and starts its background flush loop.
func NewHTTPRequestLogger() *HTTPRequestLogger {
rl := &HTTPRequestLogger{
pending: make([]HTTPRequestLog, 0),
stopCh: make(chan struct{}),
flushDone: make(chan struct{}),
}
go rl.backgroundLoop()
return rl
}
// SetSendFunc sets the callback used to send compressed HTTP request log batches
// to the server. This can be called after construction once the websocket
// client is available.
func (rl *HTTPRequestLogger) SetSendFunc(fn SendFunc) {
rl.mu.Lock()
defer rl.mu.Unlock()
rl.sendFn = fn
}
// LogRequest adds an HTTP request log entry to the buffer. If the buffer
// reaches maxBufferedSessions entries a flush is triggered immediately.
func (rl *HTTPRequestLogger) LogRequest(log HTTPRequestLog) {
if log.RequestID == "" {
log.RequestID = generateSessionID()
}
rl.mu.Lock()
rl.pending = append(rl.pending, log)
shouldFlush := len(rl.pending) >= maxBufferedSessions
rl.mu.Unlock()
if shouldFlush {
rl.flush()
}
}
// backgroundLoop handles periodic flushing of buffered request logs.
func (rl *HTTPRequestLogger) backgroundLoop() {
defer close(rl.flushDone)
ticker := time.NewTicker(flushInterval)
defer ticker.Stop()
for {
select {
case <-rl.stopCh:
return
case <-ticker.C:
rl.flush()
}
}
}
// flush drains the pending buffer, compresses with zlib, and sends via the SendFunc.
// On send failure the batch is re-queued, capped at maxBufferedSessions*5 entries
// to prevent unbounded memory growth when the server is unreachable.
func (rl *HTTPRequestLogger) flush() {
rl.mu.Lock()
if len(rl.pending) == 0 {
rl.mu.Unlock()
return
}
batch := rl.pending
rl.pending = make([]HTTPRequestLog, 0)
sendFn := rl.sendFn
rl.mu.Unlock()
if sendFn == nil {
logger.Debug("HTTP request logger: no send function configured, discarding %d requests", len(batch))
return
}
compressed, err := compressRequestLogs(batch)
if err != nil {
logger.Error("HTTP request logger: failed to compress %d requests: %v", len(batch), err)
return
}
if err := sendFn(compressed); err != nil {
logger.Error("HTTP request logger: failed to send %d requests: %v", len(batch), err)
// Re-queue the batch so we don't lose data
rl.mu.Lock()
rl.pending = append(batch, rl.pending...)
// Cap re-queued data to prevent unbounded growth if server is unreachable
if len(rl.pending) > maxBufferedSessions*5 {
dropped := len(rl.pending) - maxBufferedSessions*5
rl.pending = rl.pending[:maxBufferedSessions*5]
logger.Warn("HTTP request logger: buffer overflow, dropped %d oldest requests", dropped)
}
rl.mu.Unlock()
return
}
logger.Info("HTTP request logger: sent %d requests to server", len(batch))
}
// compressRequestLogs JSON-encodes the request logs, compresses with zlib, and
// returns a base64-encoded string suitable for embedding in a JSON message.
func compressRequestLogs(logs []HTTPRequestLog) (string, error) {
jsonData, err := json.Marshal(logs)
if err != nil {
return "", err
}
var buf bytes.Buffer
w, err := zlib.NewWriterLevel(&buf, zlib.BestCompression)
if err != nil {
return "", err
}
if _, err := w.Write(jsonData); err != nil {
w.Close()
return "", err
}
if err := w.Close(); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
}
// Close shuts down the background loop and performs one final flush to send
// any remaining buffered requests to the server.
func (rl *HTTPRequestLogger) Close() {
select {
case <-rl.stopCh:
// Already closed
return
default:
close(rl.stopCh)
}
// Wait for the background loop to exit so we don't race on flush
<-rl.flushDone
rl.flush()
}

View File

@@ -22,6 +22,12 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)
const (
// udpAccessSessionTimeout is how long a UDP access session stays alive without traffic
// before being considered ended by the access logger
udpAccessSessionTimeout = 120 * time.Second
)
// PortRange represents an allowed range of ports (inclusive) with optional protocol filtering
// Protocol can be "tcp", "udp", or "" (empty string means both protocols)
type PortRange struct {
@@ -46,115 +52,32 @@ type SubnetRule struct {
DisableIcmp bool // If true, ICMP traffic is blocked for this subnet
RewriteTo string // Optional rewrite address for DNAT - can be IP/CIDR or domain name
PortRanges []PortRange // empty slice means all ports allowed
ResourceId int // Optional resource ID from the server for access logging
// HTTP proxy configuration (optional).
// When Protocol is non-empty the TCP connection is handled by HTTPHandler
// instead of the raw TCP forwarder.
Protocol string // "", "http", or "https" — controls the incoming (client-facing) protocol
HTTPTargets []HTTPTarget // downstream services to proxy requests to
TLSCert string // PEM-encoded certificate for incoming HTTPS termination
TLSKey string // PEM-encoded private key for incoming HTTPS termination
}
// ruleKey is used as a map key for fast O(1) lookups
type ruleKey struct {
sourcePrefix string
destPrefix string
}
// SubnetLookup provides fast IP subnet and port matching with O(1) lookup performance
type SubnetLookup struct {
mu sync.RWMutex
rules map[ruleKey]*SubnetRule // Map for O(1) lookups by prefix combination
}
// NewSubnetLookup creates a new subnet lookup table
func NewSubnetLookup() *SubnetLookup {
return &SubnetLookup{
rules: make(map[ruleKey]*SubnetRule),
}
}
// AddSubnet adds a subnet rule with source and destination prefixes and optional port restrictions
// If portRanges is nil or empty, all ports are allowed for this subnet
// rewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com")
func (sl *SubnetLookup) AddSubnet(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool) {
sl.mu.Lock()
defer sl.mu.Unlock()
key := ruleKey{
sourcePrefix: sourcePrefix.String(),
destPrefix: destPrefix.String(),
}
sl.rules[key] = &SubnetRule{
SourcePrefix: sourcePrefix,
DestPrefix: destPrefix,
DisableIcmp: disableIcmp,
RewriteTo: rewriteTo,
PortRanges: portRanges,
}
}
// RemoveSubnet removes a subnet rule from the lookup table
func (sl *SubnetLookup) RemoveSubnet(sourcePrefix, destPrefix netip.Prefix) {
sl.mu.Lock()
defer sl.mu.Unlock()
key := ruleKey{
sourcePrefix: sourcePrefix.String(),
destPrefix: destPrefix.String(),
}
delete(sl.rules, key)
}
// Match checks if a source IP, destination IP, port, and protocol match any subnet rule
// Returns the matched rule if ALL of these conditions are met:
// - The source IP is in the rule's source prefix
// - The destination IP is in the rule's destination prefix
// - The port is in an allowed range (or no port restrictions exist)
// - The protocol matches (or the port range allows both protocols)
//
// proto should be header.TCPProtocolNumber or header.UDPProtocolNumber
// Returns nil if no rule matches
func (sl *SubnetLookup) Match(srcIP, dstIP netip.Addr, port uint16, proto tcpip.TransportProtocolNumber) *SubnetRule {
// GetAllRules returns a copy of all subnet rules
func (sl *SubnetLookup) GetAllRules() []SubnetRule {
sl.mu.RLock()
defer sl.mu.RUnlock()
// Iterate through all rules to find matching source and destination prefixes
// This is O(n) but necessary since we need to check prefix containment, not exact match
for _, rule := range sl.rules {
// Check if source and destination IPs match their respective prefixes
if !rule.SourcePrefix.Contains(srcIP) {
var rules []SubnetRule
for _, destTriePtr := range sl.sourceTrie.All() {
if destTriePtr == nil {
continue
}
if !rule.DestPrefix.Contains(dstIP) {
continue
}
if rule.DisableIcmp && (proto == header.ICMPv4ProtocolNumber || proto == header.ICMPv6ProtocolNumber) {
// ICMP is disabled for this subnet
return nil
}
// Both IPs match - now check port restrictions
// If no port ranges specified, all ports are allowed
if len(rule.PortRanges) == 0 {
return rule
}
// Check if port and protocol are in any of the allowed ranges
for _, pr := range rule.PortRanges {
if port >= pr.Min && port <= pr.Max {
// Check protocol compatibility
if pr.Protocol == "" {
// Empty protocol means allow both TCP and UDP
return rule
}
// Check if the packet protocol matches the port range protocol
if (pr.Protocol == "tcp" && proto == header.TCPProtocolNumber) ||
(pr.Protocol == "udp" && proto == header.UDPProtocolNumber) {
return rule
}
// Port matches but protocol doesn't - continue checking other ranges
}
for _, rule := range destTriePtr.rules {
rules = append(rules, *rule)
}
}
return nil
return rules
}
// connKey uniquely identifies a connection for NAT tracking
@@ -166,6 +89,17 @@ type connKey struct {
proto uint8
}
// reverseConnKey uniquely identifies a connection for reverse NAT lookup (reply direction)
// Key structure: (rewrittenTo, originalSrcIP, originalSrcPort, originalDstPort, proto)
// This allows O(1) lookup of NAT entries for reply packets
type reverseConnKey struct {
rewrittenTo string // The address we rewrote to (becomes src in replies)
originalSrcIP string // Original source IP (becomes dst in replies)
originalSrcPort uint16 // Original source port (becomes dst port in replies)
originalDstPort uint16 // Original destination port (becomes src port in replies)
proto uint8
}
// destKey identifies a destination for handler lookups (without source port since it may change)
type destKey struct {
srcIP string
@@ -188,13 +122,18 @@ type ProxyHandler struct {
tcpHandler *TCPHandler
udpHandler *UDPHandler
icmpHandler *ICMPHandler
httpHandler *HTTPHandler
subnetLookup *SubnetLookup
natTable map[connKey]*natState
destRewriteTable map[destKey]netip.Addr // Maps original dest to rewritten dest for handler lookups
reverseNatTable map[reverseConnKey]*natState // Reverse lookup map for O(1) reply packet NAT
destRewriteTable map[destKey]netip.Addr // Maps original dest to rewritten dest for handler lookups
resourceTable map[destKey]int // Maps connection key to resource ID for access logging
natMu sync.RWMutex
enabled bool
icmpReplies chan []byte // Channel for ICMP reply packets to be sent back through the tunnel
notifiable channel.Notification // Notification handler for triggering reads
accessLogger *AccessLogger // Access logger for tracking sessions
httpRequestLogger *HTTPRequestLogger // HTTP request logger for proxied HTTP/HTTPS requests
}
// ProxyHandlerOptions configures the proxy handler
@@ -215,8 +154,11 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) {
enabled: true,
subnetLookup: NewSubnetLookup(),
natTable: make(map[connKey]*natState),
reverseNatTable: make(map[reverseConnKey]*natState),
destRewriteTable: make(map[destKey]netip.Addr),
resourceTable: make(map[destKey]int),
icmpReplies: make(chan []byte, 256), // Buffer for ICMP reply packets
accessLogger: NewAccessLogger(udpAccessSessionTimeout),
proxyEp: channel.New(1024, uint32(options.MTU), ""),
proxyStack: stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{
@@ -232,12 +174,24 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) {
}),
}
// Initialize TCP handler if enabled
// Initialize TCP handler if enabled. The HTTP handler piggybacks on the
// TCP forwarder — TCPHandler.handleTCPConn checks the subnet rule for
// ports 80/443 and routes matching connections to the HTTP handler, so
// the HTTP handler is always initialised alongside TCP.
if options.EnableTCP {
handler.tcpHandler = NewTCPHandler(handler.proxyStack, handler)
if err := handler.tcpHandler.InstallTCPHandler(); err != nil {
return nil, fmt.Errorf("failed to install TCP handler: %v", err)
}
handler.httpHandler = NewHTTPHandler(handler.proxyStack, handler)
if err := handler.httpHandler.Start(); err != nil {
return nil, fmt.Errorf("failed to start HTTP handler: %v", err)
}
handler.httpRequestLogger = NewHTTPRequestLogger()
handler.httpHandler.SetRequestLogger(handler.httpRequestLogger)
logger.Debug("ProxyHandler: HTTP handler enabled")
}
// Initialize UDP handler if enabled
@@ -276,16 +230,14 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) {
return handler, nil
}
// AddSubnetRule adds a subnet with optional port restrictions to the proxy handler
// sourcePrefix: The IP prefix of the peer sending the data
// destPrefix: The IP prefix of the destination
// rewriteTo: Optional address to rewrite destination to - can be IP/CIDR or domain name
// If portRanges is nil or empty, all ports are allowed for this subnet
func (p *ProxyHandler) AddSubnetRule(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool) {
// AddSubnetRule adds a subnet rule to the proxy handler.
// HTTP proxy behaviour is configured via rule.Protocol, rule.HTTPTargets,
// rule.TLSCert, and rule.TLSKey; leave Protocol empty for raw TCP/UDP.
func (p *ProxyHandler) AddSubnetRule(rule SubnetRule) {
if p == nil || !p.enabled {
return
}
p.subnetLookup.AddSubnet(sourcePrefix, destPrefix, rewriteTo, portRanges, disableIcmp)
p.subnetLookup.AddSubnet(rule)
}
// RemoveSubnetRule removes a subnet from the proxy handler
@@ -296,6 +248,69 @@ func (p *ProxyHandler) RemoveSubnetRule(sourcePrefix, destPrefix netip.Prefix) {
p.subnetLookup.RemoveSubnet(sourcePrefix, destPrefix)
}
// GetAllRules returns all subnet rules from the proxy handler
func (p *ProxyHandler) GetAllRules() []SubnetRule {
if p == nil || !p.enabled {
return nil
}
return p.subnetLookup.GetAllRules()
}
// LookupResourceId looks up the resource ID for a connection
// Returns 0 if no resource ID is associated with this connection
func (p *ProxyHandler) LookupResourceId(srcIP, dstIP string, dstPort uint16, proto uint8) int {
if p == nil || !p.enabled {
return 0
}
key := destKey{
srcIP: srcIP,
dstIP: dstIP,
dstPort: dstPort,
proto: proto,
}
p.natMu.RLock()
defer p.natMu.RUnlock()
return p.resourceTable[key]
}
// GetAccessLogger returns the access logger for session tracking
func (p *ProxyHandler) GetAccessLogger() *AccessLogger {
if p == nil {
return nil
}
return p.accessLogger
}
// SetAccessLogSender configures the function used to send compressed access log
// batches to the server. This should be called once the websocket client is available.
func (p *ProxyHandler) SetAccessLogSender(fn SendFunc) {
if p == nil || !p.enabled || p.accessLogger == nil {
return
}
p.accessLogger.SetSendFunc(fn)
}
// GetHTTPRequestLogger returns the HTTP request logger.
func (p *ProxyHandler) GetHTTPRequestLogger() *HTTPRequestLogger {
if p == nil {
return nil
}
return p.httpRequestLogger
}
// SetHTTPRequestLogSender configures the function used to send compressed HTTP
// request log batches to the server. This should be called once the websocket
// client is available.
func (p *ProxyHandler) SetHTTPRequestLogSender(fn SendFunc) {
if p == nil || !p.enabled || p.httpRequestLogger == nil {
return
}
p.httpRequestLogger.SetSendFunc(fn)
}
// LookupDestinationRewrite looks up the rewritten destination for a connection
// This is used by TCP/UDP handlers to find the actual target address
func (p *ProxyHandler) LookupDestinationRewrite(srcIP, dstIP string, dstPort uint16, proto uint8) (netip.Addr, bool) {
@@ -458,8 +473,22 @@ func (p *ProxyHandler) HandleIncomingPacket(packet []byte) bool {
// Check if the source IP, destination IP, port, and protocol match any subnet rule
matchedRule := p.subnetLookup.Match(srcAddr, dstAddr, dstPort, protocol)
if matchedRule != nil {
logger.Debug("HandleIncomingPacket: Matched rule for %s -> %s (proto=%d, port=%d)",
srcAddr, dstAddr, protocol, dstPort)
logger.Debug("HandleIncomingPacket: Matched rule for %s -> %s (proto=%d, port=%d, resourceId=%d)",
srcAddr, dstAddr, protocol, dstPort, matchedRule.ResourceId)
// Store resource ID for connections without DNAT as well
if matchedRule.ResourceId != 0 && matchedRule.RewriteTo == "" {
dKey := destKey{
srcIP: srcAddr.String(),
dstIP: dstAddr.String(),
dstPort: dstPort,
proto: uint8(protocol),
}
p.natMu.Lock()
p.resourceTable[dKey] = matchedRule.ResourceId
p.natMu.Unlock()
}
// Check if we need to perform DNAT
if matchedRule.RewriteTo != "" {
// Create connection tracking key using original destination
@@ -491,6 +520,13 @@ func (p *ProxyHandler) HandleIncomingPacket(packet []byte) bool {
proto: uint8(protocol),
}
// Store resource ID for access logging if present
if matchedRule.ResourceId != 0 {
p.natMu.Lock()
p.resourceTable[dKey] = matchedRule.ResourceId
p.natMu.Unlock()
}
// Check if we already have a NAT entry for this connection
p.natMu.RLock()
existingEntry, exists := p.natTable[key]
@@ -517,12 +553,37 @@ func (p *ProxyHandler) HandleIncomingPacket(packet []byte) bool {
// Store NAT state for this connection
p.natMu.Lock()
p.natTable[key] = &natState{
natEntry := &natState{
originalDst: dstAddr,
rewrittenTo: newDst,
}
p.natTable[key] = natEntry
// Create reverse lookup key for O(1) reply packet lookups
// Key: (rewrittenTo, originalSrcIP, originalSrcPort, originalDstPort, proto)
reverseKey := reverseConnKey{
rewrittenTo: newDst.String(),
originalSrcIP: srcAddr.String(),
originalSrcPort: srcPort,
originalDstPort: dstPort,
proto: uint8(protocol),
}
p.reverseNatTable[reverseKey] = natEntry
// Store destination rewrite for handler lookups
p.destRewriteTable[dKey] = newDst
// Also store the resource ID under the rewritten destination key so that
// TCP/UDP handlers can find it after DNAT (they see the post-NAT dst IP).
if matchedRule.ResourceId != 0 {
rewrittenKey := destKey{
srcIP: srcAddr.String(),
dstIP: newDst.String(),
dstPort: dstPort,
proto: uint8(protocol),
}
p.resourceTable[rewrittenKey] = matchedRule.ResourceId
}
p.natMu.Unlock()
logger.Debug("New NAT entry for connection: %s -> %s", dstAddr, newDst)
}
@@ -719,20 +780,22 @@ func (p *ProxyHandler) ReadOutgoingPacket() *buffer.View {
return view
}
// Look up NAT state for reverse translation
// The key uses the original dst (before rewrite), so for replies we need to
// find the entry where the rewritten address matches the current source
// Look up NAT state for reverse translation using O(1) reverse lookup map
// Key: (rewrittenTo, originalSrcIP, originalSrcPort, originalDstPort, proto)
// For reply packets:
// - reply's srcIP = rewrittenTo (the address we rewrote to)
// - reply's dstIP = originalSrcIP (original source IP)
// - reply's srcPort = originalDstPort (original destination port)
// - reply's dstPort = originalSrcPort (original source port)
p.natMu.RLock()
var natEntry *natState
for k, entry := range p.natTable {
// Match: reply's dst should be original src, reply's src should be rewritten dst
if k.srcIP == dstIP.String() && k.srcPort == dstPort &&
entry.rewrittenTo.String() == srcIP.String() && k.dstPort == srcPort &&
k.proto == uint8(protocol) {
natEntry = entry
break
}
reverseKey := reverseConnKey{
rewrittenTo: srcIP.String(), // Reply's source is the rewritten address
originalSrcIP: dstIP.String(), // Reply's destination is the original source
originalSrcPort: dstPort, // Reply's destination port is the original source port
originalDstPort: srcPort, // Reply's source port is the original destination port
proto: uint8(protocol),
}
natEntry := p.reverseNatTable[reverseKey]
p.natMu.RUnlock()
if natEntry != nil {
@@ -776,6 +839,21 @@ func (p *ProxyHandler) Close() error {
return nil
}
// Shut down access logger
if p.accessLogger != nil {
p.accessLogger.Close()
}
// Shut down HTTP request logger
if p.httpRequestLogger != nil {
p.httpRequestLogger.Close()
}
// Shut down HTTP handler
if p.httpHandler != nil {
p.httpHandler.Close()
}
// Close ICMP replies channel
if p.icmpReplies != nil {
close(p.icmpReplies)

201
netstack2/subnet_lookup.go Normal file
View File

@@ -0,0 +1,201 @@
package netstack2
import (
"net/netip"
"sync"
"github.com/gaissmai/bart"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
// SubnetLookup provides fast IP subnet and port matching using BART (Binary Aggregated Range Tree)
// This uses BART Table for O(log n) prefix matching with Supernets() for efficient lookups
//
// Architecture:
// - Two-level BART structure for matching both source AND destination prefixes
// - Level 1: Source prefix -> Level 2 (destination prefix -> rules)
// - This reduces search space: only check destination prefixes for matching source prefixes
type SubnetLookup struct {
mu sync.RWMutex
// Two-level BART structure:
// Level 1: Source prefix -> Level 2 (destination prefix -> rules)
// This allows us to first match source prefix, then only check destination prefixes
// for matching source prefixes, reducing the search space significantly
sourceTrie *bart.Table[*destTrie]
}
// destTrie is a BART for destination prefixes, containing the actual rules
type destTrie struct {
trie *bart.Table[[]*SubnetRule]
rules []*SubnetRule // All rules for this source prefix (for iteration if needed)
}
// NewSubnetLookup creates a new subnet lookup table using BART
func NewSubnetLookup() *SubnetLookup {
return &SubnetLookup{
sourceTrie: &bart.Table[*destTrie]{},
}
}
// prefixEqual compares two prefixes after masking to handle host bits correctly.
// For example, 10.0.0.5/24 and 10.0.0.0/24 are treated as equal.
func prefixEqual(a, b netip.Prefix) bool {
return a.Masked() == b.Masked()
}
// AddSubnet adds a subnet rule to the lookup table.
// If rule.PortRanges is nil or empty, all ports are allowed.
// rule.RewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com").
// HTTP proxy behaviour is driven by rule.Protocol, rule.HTTPTargets, rule.TLSCert, and rule.TLSKey.
func (sl *SubnetLookup) AddSubnet(rule SubnetRule) {
sl.mu.Lock()
defer sl.mu.Unlock()
rulePtr := &rule
// Canonicalize source prefix to handle host bits correctly
canonicalSourcePrefix := rule.SourcePrefix.Masked()
// Get or create destination trie for this source prefix
destTriePtr, exists := sl.sourceTrie.Get(canonicalSourcePrefix)
if !exists {
// Create new destination trie for this source prefix
destTriePtr = &destTrie{
trie: &bart.Table[[]*SubnetRule]{},
rules: make([]*SubnetRule, 0),
}
sl.sourceTrie.Insert(canonicalSourcePrefix, destTriePtr)
}
// Canonicalize destination prefix to handle host bits correctly
// BART masks prefixes internally, so we need to match that behavior in our bookkeeping
canonicalDestPrefix := rule.DestPrefix.Masked()
// Add rule to destination trie
// Original behavior: overwrite if same (sourcePrefix, destPrefix) exists
// Store as single-element slice to match original overwrite behavior
destTriePtr.trie.Insert(canonicalDestPrefix, []*SubnetRule{rulePtr})
// Update destTriePtr.rules - remove old rule with same canonical prefix if exists, then add new one
// Use canonical comparison to handle cases like 10.0.0.5/24 vs 10.0.0.0/24
newRules := make([]*SubnetRule, 0, len(destTriePtr.rules)+1)
for _, r := range destTriePtr.rules {
if !prefixEqual(r.DestPrefix, canonicalDestPrefix) || !prefixEqual(r.SourcePrefix, canonicalSourcePrefix) {
newRules = append(newRules, r)
}
}
newRules = append(newRules, rulePtr)
destTriePtr.rules = newRules
}
// RemoveSubnet removes a subnet rule from the lookup table
func (sl *SubnetLookup) RemoveSubnet(sourcePrefix, destPrefix netip.Prefix) {
sl.mu.Lock()
defer sl.mu.Unlock()
// Canonicalize prefixes to handle host bits correctly
canonicalSourcePrefix := sourcePrefix.Masked()
canonicalDestPrefix := destPrefix.Masked()
destTriePtr, exists := sl.sourceTrie.Get(canonicalSourcePrefix)
if !exists {
return
}
// Remove the rule - original behavior: delete exact (sourcePrefix, destPrefix) combination
// BART masks prefixes internally, so Delete works with canonical form
destTriePtr.trie.Delete(canonicalDestPrefix)
// Also remove from destTriePtr.rules using canonical comparison
// This ensures we remove rules even if they were added with host bits set
newDestRules := make([]*SubnetRule, 0, len(destTriePtr.rules))
for _, r := range destTriePtr.rules {
if !prefixEqual(r.DestPrefix, canonicalDestPrefix) || !prefixEqual(r.SourcePrefix, canonicalSourcePrefix) {
newDestRules = append(newDestRules, r)
}
}
destTriePtr.rules = newDestRules
// Check if the trie is actually empty using BART's Size() method
// This is more efficient than iterating and ensures we clean up empty tries
// even if there were stale entries in the rules slice (which shouldn't happen
// with proper canonicalization, but this provides a definitive check)
if destTriePtr.trie.Size() == 0 {
sl.sourceTrie.Delete(canonicalSourcePrefix)
}
}
// Match checks if a source IP, destination IP, port, and protocol match any subnet rule
// Returns the matched rule if ALL of these conditions are met:
// - The source IP is in the rule's source prefix
// - The destination IP is in the rule's destination prefix
// - The port is in an allowed range (or no port restrictions exist)
// - The protocol matches (or the port range allows both protocols)
//
// proto should be header.TCPProtocolNumber, header.UDPProtocolNumber, or header.ICMPv4ProtocolNumber
// Returns nil if no rule matches
// This uses BART's Supernets() for O(log n) prefix matching instead of O(n) iteration
func (sl *SubnetLookup) Match(srcIP, dstIP netip.Addr, port uint16, proto tcpip.TransportProtocolNumber) *SubnetRule {
sl.mu.RLock()
defer sl.mu.RUnlock()
// Convert IP addresses to /32 (IPv4) or /128 (IPv6) prefixes
// Supernets() finds all prefixes that contain this IP (i.e., are supernets of /32 or /128)
srcPrefix := netip.PrefixFrom(srcIP, srcIP.BitLen())
dstPrefix := netip.PrefixFrom(dstIP, dstIP.BitLen())
// Step 1: Find all source prefixes that contain srcIP using BART's Supernets
// This is O(log n) instead of O(n) iteration
// Supernets returns all prefixes that are supernets (contain) the given prefix
for _, destTriePtr := range sl.sourceTrie.Supernets(srcPrefix) {
if destTriePtr == nil {
continue
}
// Step 2: Find all destination prefixes that contain dstIP
// This is also O(log n) for each matching source prefix
for _, rules := range destTriePtr.trie.Supernets(dstPrefix) {
if rules == nil {
continue
}
// Step 3: Check each rule for ICMP and port restrictions
for _, rule := range rules {
// Handle ICMP before port range check — ICMP has no ports
if proto == header.ICMPv4ProtocolNumber || proto == header.ICMPv6ProtocolNumber {
if rule.DisableIcmp {
return nil
}
// ICMP is allowed; port ranges don't apply to ICMP
return rule
}
// Check port restrictions
if len(rule.PortRanges) == 0 {
// No port restrictions, match!
return rule
}
// Check if port and protocol are in any of the allowed ranges
for _, pr := range rule.PortRanges {
if port >= pr.Min && port <= pr.Max {
// Check protocol compatibility
if pr.Protocol == "" {
// Empty protocol means allow both TCP and UDP
return rule
}
// Check if the packet protocol matches the port range protocol
if (pr.Protocol == "tcp" && proto == header.TCPProtocolNumber) ||
(pr.Protocol == "udp" && proto == header.UDPProtocolNumber) {
return rule
}
// Port matches but protocol doesn't - continue checking other ranges
}
}
}
}
}
return nil
}

View File

@@ -351,13 +351,13 @@ func (net *Net) ListenUDP(laddr *net.UDPAddr) (*gonet.UDPConn, error) {
return net.DialUDP(laddr, nil)
}
// AddProxySubnetRule adds a subnet rule to the proxy handler
// If portRanges is nil or empty, all ports are allowed for this subnet
// rewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com")
func (net *Net) AddProxySubnetRule(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool) {
// AddProxySubnetRule adds a subnet rule to the proxy handler.
// HTTP proxy behaviour is configured via rule.Protocol, rule.HTTPTargets,
// rule.TLSCert, and rule.TLSKey; leave Protocol empty for raw TCP/UDP.
func (net *Net) AddProxySubnetRule(rule SubnetRule) {
tun := (*netTun)(net)
if tun.proxyHandler != nil {
tun.proxyHandler.AddSubnetRule(sourcePrefix, destPrefix, rewriteTo, portRanges, disableIcmp)
tun.proxyHandler.AddSubnetRule(rule)
}
}
@@ -369,6 +369,15 @@ func (net *Net) RemoveProxySubnetRule(sourcePrefix, destPrefix netip.Prefix) {
}
}
// GetProxySubnetRules returns all subnet rules from the proxy handler
func (net *Net) GetProxySubnetRules() []SubnetRule {
tun := (*netTun)(net)
if tun.proxyHandler != nil {
return tun.proxyHandler.GetAllRules()
}
return nil
}
// GetProxyHandler returns the proxy handler (for advanced use cases)
// Returns nil if proxy is not enabled
func (net *Net) GetProxyHandler() *ProxyHandler {
@@ -376,6 +385,25 @@ func (net *Net) GetProxyHandler() *ProxyHandler {
return tun.proxyHandler
}
// SetAccessLogSender configures the function used to send compressed access log
// batches to the server. This should be called once the websocket client is available.
func (net *Net) SetAccessLogSender(fn SendFunc) {
tun := (*netTun)(net)
if tun.proxyHandler != nil {
tun.proxyHandler.SetAccessLogSender(fn)
}
}
// SetHTTPRequestLogSender configures the function used to send compressed HTTP
// request log batches to the server. This should be called once the websocket
// client is available.
func (net *Net) SetHTTPRequestLogSender(fn SendFunc) {
tun := (*netTun)(net)
if tun.proxyHandler != nil {
tun.proxyHandler.SetHTTPRequestLogSender(fn)
}
}
type PingConn struct {
laddr PingAddr
raddr PingAddr

View File

@@ -120,7 +120,7 @@ func configureDarwin(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
prefix, _ := ipNet.Mask.Size()
ipStr := fmt.Sprintf("%s/%d", ip.String(), prefix)
cmd := exec.Command("ifconfig", interfaceName, "inet", ipStr, ip.String(), "alias")
cmd := exec.Command("/sbin/ifconfig", interfaceName, "inet", ipStr, ip.String(), "alias")
logger.Info("Running command: %v", cmd)
out, err := cmd.CombinedOutput()
@@ -129,7 +129,7 @@ func configureDarwin(interfaceName string, ip net.IP, ipNet *net.IPNet) error {
}
// Bring up the interface
cmd = exec.Command("ifconfig", interfaceName, "up")
cmd = exec.Command("/sbin/ifconfig", interfaceName, "up")
logger.Info("Running command: %v", cmd)
out, err = cmd.CombinedOutput()

View File

@@ -21,7 +21,32 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
)
const errUnsupportedProtoFmt = "unsupported protocol: %s"
const (
errUnsupportedProtoFmt = "unsupported protocol: %s"
maxUDPPacketSize = 65507 // Maximum UDP packet size
defaultUDPIdleTimeout = 90 * time.Second
)
// udpBufferPool provides reusable buffers for UDP packet handling.
// This reduces GC pressure from frequent large allocations.
var udpBufferPool = sync.Pool{
New: func() any {
buf := make([]byte, maxUDPPacketSize)
return &buf
},
}
// getUDPBuffer retrieves a buffer from the pool.
func getUDPBuffer() *[]byte {
return udpBufferPool.Get().(*[]byte)
}
// putUDPBuffer clears and returns a buffer to the pool.
func putUDPBuffer(buf *[]byte) {
// Clear the buffer to prevent data leakage
clear(*buf)
udpBufferPool.Put(buf)
}
// Target represents a proxy target with its address and port
type Target struct {
@@ -44,6 +69,7 @@ type ProxyManager struct {
tunnels map[string]*tunnelEntry
asyncBytes bool
flushStop chan struct{}
udpIdleTimeout time.Duration
}
// tunnelEntry holds per-tunnel attributes and (optional) async counters.
@@ -105,13 +131,9 @@ func classifyProxyError(err error) string {
if errors.Is(err, net.ErrClosed) {
return "closed"
}
if ne, ok := err.(net.Error); ok {
if ne.Timeout() {
return "timeout"
}
if ne.Temporary() {
return "temporary"
}
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
return "timeout"
}
msg := strings.ToLower(err.Error())
switch {
@@ -133,6 +155,7 @@ func NewProxyManager(tnet *netstack.Net) *ProxyManager {
listeners: make([]*gonet.TCPListener, 0),
udpConns: make([]*gonet.UDPConn, 0),
tunnels: make(map[string]*tunnelEntry),
udpIdleTimeout: defaultUDPIdleTimeout,
}
}
@@ -210,6 +233,7 @@ func NewProxyManagerWithoutTNet() *ProxyManager {
udpTargets: make(map[string]map[int]string),
listeners: make([]*gonet.TCPListener, 0),
udpConns: make([]*gonet.UDPConn, 0),
udpIdleTimeout: defaultUDPIdleTimeout,
}
}
@@ -346,6 +370,17 @@ func (pm *ProxyManager) SetAsyncBytes(b bool) {
go pm.flushLoop()
}
}
// SetUDPIdleTimeout configures when idle UDP client flows are reclaimed.
func (pm *ProxyManager) SetUDPIdleTimeout(d time.Duration) {
pm.mutex.Lock()
defer pm.mutex.Unlock()
if d <= 0 {
pm.udpIdleTimeout = defaultUDPIdleTimeout
return
}
pm.udpIdleTimeout = d
}
func (pm *ProxyManager) flushLoop() {
flushInterval := 2 * time.Second
if v := os.Getenv("OTEL_METRIC_EXPORT_INTERVAL"); v != "" {
@@ -437,14 +472,6 @@ func (pm *ProxyManager) Stop() error {
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...)
}
// // Clear the target maps
// for k := range pm.tcpTargets {
// delete(pm.tcpTargets, k)
// }
// for k := range pm.udpTargets {
// delete(pm.udpTargets, k)
// }
// Give active connections a chance to close gracefully
time.Sleep(100 * time.Millisecond)
@@ -498,7 +525,7 @@ func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string)
if !pm.running {
return
}
if ne, ok := err.(net.Error); ok && !ne.Temporary() {
if errors.Is(err, net.ErrClosed) {
logger.Info("TCP listener closed, stopping proxy handler for %v", listener.Addr())
return
}
@@ -564,7 +591,9 @@ func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string)
}
func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
buffer := make([]byte, 65507) // Max UDP packet size
bufPtr := getUDPBuffer()
defer putUDPBuffer(bufPtr)
buffer := *bufPtr
clientConns := make(map[string]*net.UDPConn)
var clientsMutex sync.RWMutex
@@ -583,7 +612,7 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
}
// Check for connection closed conditions
if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") {
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
logger.Info("UDP connection closed, stopping proxy handler")
// Clean up existing client connections
@@ -632,6 +661,9 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
telemetry.IncProxyAccept(context.Background(), pm.currentTunnelID, "udp", "failure", classifyProxyError(err))
continue
}
// Prevent idle UDP client goroutines from living forever and
// retaining large per-connection buffers.
_ = targetConn.SetReadDeadline(time.Now().Add(pm.udpIdleTimeout))
tunnelID := pm.currentTunnelID
telemetry.IncProxyAccept(context.Background(), tunnelID, "udp", "success", "")
telemetry.IncProxyConnectionEvent(context.Background(), tunnelID, "udp", telemetry.ProxyConnectionOpened)
@@ -647,7 +679,10 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
go func(clientKey string, targetConn *net.UDPConn, remoteAddr net.Addr, tunnelID string) {
start := time.Now()
result := "success"
bufPtr := getUDPBuffer()
defer func() {
// Return buffer to pool first
putUDPBuffer(bufPtr)
// Always clean up when this goroutine exits
clientsMutex.Lock()
if storedConn, exists := clientConns[clientKey]; exists && storedConn == targetConn {
@@ -662,10 +697,18 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
telemetry.IncProxyConnectionEvent(context.Background(), tunnelID, "udp", telemetry.ProxyConnectionClosed)
}()
buffer := make([]byte, 65507)
buffer := *bufPtr
for {
n, _, err := targetConn.ReadFromUDP(buffer)
if err != nil {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return
}
// Connection closed is normal during cleanup
if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) {
return // defer will handle cleanup, result stays "success"
}
logger.Error("Error reading from target: %v", err)
result = "failure"
return // defer will handle cleanup
@@ -704,6 +747,8 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
delete(clientConns, clientKey)
clientsMutex.Unlock()
} else if pm.currentTunnelID != "" && written > 0 {
// Extend idle timeout whenever client traffic is observed.
_ = targetConn.SetReadDeadline(time.Now().Add(pm.udpIdleTimeout))
if pm.asyncBytes {
if e := pm.getEntry(pm.currentTunnelID); e != nil {
e.bytesInUDP.Add(uint64(written))
@@ -736,3 +781,28 @@ func (pm *ProxyManager) PrintTargets() {
}
}
}
// GetTargets returns a copy of the current TCP and UDP targets
// Returns map[listenIP]map[port]targetAddress for both TCP and UDP
func (pm *ProxyManager) GetTargets() (tcpTargets map[string]map[int]string, udpTargets map[string]map[int]string) {
pm.mutex.RLock()
defer pm.mutex.RUnlock()
tcpTargets = make(map[string]map[int]string)
for listenIP, targets := range pm.tcpTargets {
tcpTargets[listenIP] = make(map[int]string)
for port, targetAddr := range targets {
tcpTargets[listenIP][port] = targetAddr
}
}
udpTargets = make(map[string]map[int]string)
for listenIP, targets := range pm.udpTargets {
udpTargets[listenIP] = make(map[int]string)
for port, targetAddr := range targets {
udpTargets[listenIP][port] = targetAddr
}
}
return tcpTargets, udpTargets
}

60
testing/ws_client.py Normal file
View File

@@ -0,0 +1,60 @@
import asyncio
import sys
import websockets
# Argument parsing: Check if HOST and PORT are provided
if len(sys.argv) < 3 or len(sys.argv) > 4:
print("Usage: python ws_client.py <HOST_IP> <HOST_PORT> [ws|wss]")
# Example: python ws_client.py 127.0.0.1 8765
# Example: python ws_client.py 127.0.0.1 8765 wss
sys.exit(1)
HOST = sys.argv[1]
try:
PORT = int(sys.argv[2])
except ValueError:
print("Error: HOST_PORT must be an integer.")
sys.exit(1)
if len(sys.argv) == 4:
SCHEME = sys.argv[3].lower()
if SCHEME not in ("ws", "wss"):
print("Error: scheme must be 'ws' or 'wss'.")
sys.exit(1)
else:
SCHEME = "ws"
URI = f"{SCHEME}://{HOST}:{PORT}"
# The message to send to the server
MESSAGE = "Hello WebSocket Server! How are you?"
async def main():
print(f"Connecting to {URI}...")
try:
async with websockets.connect(URI) as websocket:
print(f"Connected to server.")
print(f"Sending message: '{MESSAGE}'")
await websocket.send(MESSAGE)
response = await websocket.recv()
print("-" * 30)
print(f"Received response from server:")
print(f"-> Data: '{response}'")
except ConnectionRefusedError:
print(f"Error: Connection to {URI} was refused. Is the server running?")
except websockets.exceptions.InvalidMessage as e:
print(f"Error: Server did not respond with a valid WebSocket handshake: {e}")
except Exception as e:
print(f"Error during communication: {e}")
print("-" * 30)
print("Client finished.")
asyncio.run(main())

49
testing/ws_server.py Normal file
View File

@@ -0,0 +1,49 @@
import asyncio
import sys
import websockets
# Optionally take in a positional arg for the port
if len(sys.argv) > 1:
try:
PORT = int(sys.argv[1])
except ValueError:
print("Invalid port number. Using default port 8765.")
PORT = 8765
else:
PORT = 8765
# Define the server host
HOST = "0.0.0.0"
async def handle_client(websocket):
client_address = websocket.remote_address
print(f"Client connected: {client_address[0]}:{client_address[1]}")
try:
async for message in websocket:
print("-" * 30)
print(f"Received message from {client_address[0]}:{client_address[1]}:")
print(f"-> Data: '{message}'")
response = f"Hello client! Server received: '{message.upper()}'"
await websocket.send(response)
print(f"Sent response back to client.")
except websockets.exceptions.ConnectionClosedOK:
print(f"Client {client_address[0]}:{client_address[1]} disconnected cleanly.")
except websockets.exceptions.ConnectionClosedError as e:
print(f"Client {client_address[0]}:{client_address[1]} disconnected with error: {e}")
async def main():
print(f"WebSocket Server listening on {HOST}:{PORT}")
async with websockets.serve(handle_client, HOST, PORT):
await asyncio.Future() # Run forever
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nServer stopped.")

View File

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

View File

@@ -2,6 +2,7 @@ package websocket
import (
"bytes"
"compress/gzip"
"crypto/tls"
"crypto/x509"
"encoding/json"
@@ -37,16 +38,22 @@ type Client struct {
isConnected bool
reconnectMux sync.RWMutex
pingInterval time.Duration
pingTimeout time.Duration
onConnect func() error
onTokenUpdate func(token string)
writeMux sync.Mutex
clientType string // Type of client (e.g., "newt", "olm")
configFilePath string // Optional override for the config file path
tlsConfig TLSConfig
metricsCtxMu sync.RWMutex
metricsCtx context.Context
configNeedsSave bool // Flag to track if config needs to be saved
serverVersion string
configVersion int64 // Latest config version received from server
configVersionMux sync.RWMutex
processingMessage bool // Flag to track if a message is currently being processed
processingMux sync.RWMutex // Protects processingMessage
processingWg sync.WaitGroup // WaitGroup to wait for message processing to complete
justProvisioned bool // Set to true when provisionIfNeeded exchanges a key for permanent credentials
}
type ClientOption func(*Client)
@@ -72,6 +79,12 @@ func WithBaseURL(url string) ClientOption {
}
// WithTLSConfig sets the TLS configuration for the client
func WithConfigFile(path string) ClientOption {
return func(c *Client) {
c.configFilePath = path
}
}
func WithTLSConfig(config TLSConfig) ClientOption {
return func(c *Client) {
c.tlsConfig = config
@@ -90,6 +103,16 @@ func (c *Client) OnTokenUpdate(callback func(token string)) {
c.onTokenUpdate = callback
}
// WasJustProvisioned reports whether the client exchanged a provisioning key
// for permanent credentials during the most recent connection attempt. It
// consumes the flag subsequent calls return false until provisioning occurs
// again (which, in practice, never happens once credentials are persisted).
func (c *Client) WasJustProvisioned() bool {
v := c.justProvisioned
c.justProvisioned = false
return v
}
func (c *Client) metricsContext() context.Context {
c.metricsCtxMu.RLock()
defer c.metricsCtxMu.RUnlock()
@@ -111,7 +134,7 @@ func (c *Client) MetricsContext() context.Context {
}
// NewClient creates a new websocket client
func NewClient(clientType string, ID, secret string, endpoint string, pingInterval time.Duration, pingTimeout time.Duration, opts ...ClientOption) (*Client, error) {
func NewClient(clientType string, ID, secret string, endpoint string, pingInterval time.Duration, opts ...ClientOption) (*Client, error) {
config := &Config{
ID: ID,
Secret: secret,
@@ -126,7 +149,6 @@ func NewClient(clientType string, ID, secret string, endpoint string, pingInterv
reconnectInterval: 3 * time.Second,
isConnected: false,
pingInterval: pingInterval,
pingTimeout: pingTimeout,
clientType: clientType,
}
@@ -154,6 +176,20 @@ func (c *Client) GetServerVersion() string {
return c.serverVersion
}
// GetConfigVersion returns the latest config version received from server
func (c *Client) GetConfigVersion() int64 {
c.configVersionMux.RLock()
defer c.configVersionMux.RUnlock()
return c.configVersion
}
// setConfigVersion updates the config version
func (c *Client) setConfigVersion(version int64) {
c.configVersionMux.Lock()
defer c.configVersionMux.Unlock()
c.configVersion = version
}
// Connect establishes the WebSocket connection
func (c *Client) Connect() error {
go c.connectWithRetry()
@@ -235,13 +271,17 @@ func (c *Client) SendMessageInterval(messageType string, data interface{}, inter
stopChan := make(chan struct{})
go func() {
count := 0
maxAttempts := 10
maxAttempts := 16
c.reconnectMux.RLock()
connected := c.isConnected
c.reconnectMux.RUnlock()
err := c.SendMessage(messageType, data) // Send immediately
if err != nil {
logger.Error("Failed to send initial message: %v", err)
} else if connected {
count++
}
count++
ticker := time.NewTicker(interval)
defer ticker.Stop()
@@ -252,11 +292,15 @@ func (c *Client) SendMessageInterval(messageType string, data interface{}, inter
logger.Info("SendMessageInterval timed out after %d attempts for message type: %s", maxAttempts, messageType)
return
}
c.reconnectMux.RLock()
connected = c.isConnected
c.reconnectMux.RUnlock()
err = c.SendMessage(messageType, data)
if err != nil {
logger.Error("Failed to send message: %v", err)
} else if connected {
count++
}
count++
case <-stopChan:
return
}
@@ -463,6 +507,11 @@ func (c *Client) connectWithRetry() {
func (c *Client) establishConnection() error {
ctx := context.Background()
// Exchange provisioning key for permanent credentials if needed.
if err := c.provisionIfNeeded(); err != nil {
return fmt.Errorf("failed to provision newt credentials: %w", err)
}
// Get token for authentication
token, err := c.getToken()
if err != nil {
@@ -641,7 +690,61 @@ func (c *Client) setupPKCS12TLS() (*tls.Config, error) {
}
// pingMonitor sends pings at a short interval and triggers reconnect on failure
func (c *Client) sendPing() {
if c.conn == nil {
return
}
// Skip ping if a message is currently being processed
c.processingMux.RLock()
isProcessing := c.processingMessage
c.processingMux.RUnlock()
if isProcessing {
logger.Debug("Skipping ping, message is being processed")
return
}
c.configVersionMux.RLock()
configVersion := c.configVersion
c.configVersionMux.RUnlock()
pingMsg := WSMessage{
Type: "newt/ping",
Data: map[string]interface{}{},
ConfigVersion: configVersion,
}
c.writeMux.Lock()
if c.conn == nil {
c.writeMux.Unlock()
return
}
err := c.conn.WriteJSON(pingMsg)
if err == nil {
telemetry.IncWSMessage(c.metricsContext(), "out", "ping")
}
c.writeMux.Unlock()
if err != nil {
// 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)
telemetry.IncWSKeepaliveFailure(c.metricsContext(), "ping_write")
telemetry.IncWSReconnect(c.metricsContext(), "ping_write")
c.reconnect()
return
}
}
}
func (c *Client) pingMonitor() {
// Send an immediate ping as soon as we connect
c.sendPing()
ticker := time.NewTicker(c.pingInterval)
defer ticker.Stop()
@@ -650,29 +753,7 @@ func (c *Client) pingMonitor() {
case <-c.done:
return
case <-ticker.C:
if c.conn == nil {
return
}
c.writeMux.Lock()
err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(c.pingTimeout))
if err == nil {
telemetry.IncWSMessage(c.metricsContext(), "out", "ping")
}
c.writeMux.Unlock()
if err != nil {
// 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)
telemetry.IncWSKeepaliveFailure(c.metricsContext(), "ping_write")
telemetry.IncWSReconnect(c.metricsContext(), "ping_write")
c.reconnect()
return
}
}
c.sendPing()
}
}
}
@@ -709,10 +790,13 @@ func (c *Client) readPumpWithDisconnectDetection(started time.Time) {
disconnectResult = "success"
return
default:
var msg WSMessage
err := c.conn.ReadJSON(&msg)
msgType, p, err := c.conn.ReadMessage()
if err == nil {
telemetry.IncWSMessage(c.metricsContext(), "in", "text")
if msgType == websocket.BinaryMessage {
telemetry.IncWSMessage(c.metricsContext(), "in", "binary")
} else {
telemetry.IncWSMessage(c.metricsContext(), "in", "text")
}
}
if err != nil {
// Check if we're shutting down before logging error
@@ -737,9 +821,47 @@ func (c *Client) readPumpWithDisconnectDetection(started time.Time) {
}
}
// Update config version from incoming message
var data []byte
if msgType == websocket.BinaryMessage {
gr, err := gzip.NewReader(bytes.NewReader(p))
if err != nil {
logger.Error("WebSocket failed to create gzip reader: %v", err)
continue
}
data, err = io.ReadAll(gr)
gr.Close()
if err != nil {
logger.Error("WebSocket failed to decompress message: %v", err)
continue
}
} else {
data = p
}
var msg WSMessage
if err = json.Unmarshal(data, &msg); err != nil {
logger.Error("WebSocket failed to parse message: %v", err)
continue
}
c.setConfigVersion(msg.ConfigVersion)
c.handlersMux.RLock()
if handler, ok := c.handlers[msg.Type]; ok {
// Mark that we're processing a message
c.processingMux.Lock()
c.processingMessage = true
c.processingMux.Unlock()
c.processingWg.Add(1)
handler(msg)
// Mark that we're done processing
c.processingWg.Done()
c.processingMux.Lock()
c.processingMessage = false
c.processingMux.Unlock()
}
c.handlersMux.RUnlock()
}
@@ -749,10 +871,12 @@ func (c *Client) readPumpWithDisconnectDetection(started time.Time) {
func (c *Client) reconnect() {
c.setConnected(false)
telemetry.SetWSConnectionState(false)
c.writeMux.Lock()
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
c.writeMux.Unlock()
// Only reconnect if we're not shutting down
select {

View File

@@ -1,16 +1,29 @@
package websocket
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
"github.com/fosrl/newt/logger"
)
func getConfigPath(clientType string) string {
func getConfigPath(clientType string, overridePath string) string {
if overridePath != "" {
return overridePath
}
configFile := os.Getenv("CONFIG_FILE")
if configFile == "" {
var configDir string
@@ -36,7 +49,7 @@ func getConfigPath(clientType string) string {
func (c *Client) loadConfig() error {
originalConfig := *c.config // Store original config to detect changes
configPath := getConfigPath(c.clientType)
configPath := getConfigPath(c.clientType, c.configFilePath)
if c.config.ID != "" && c.config.Secret != "" && c.config.Endpoint != "" {
logger.Debug("Config already provided, skipping loading from file")
@@ -58,6 +71,11 @@ func (c *Client) loadConfig() error {
}
return err
}
if len(bytes.TrimSpace(data)) == 0 {
logger.Info("Config file at %s is empty, will initialize it with provided values", configPath)
c.configNeedsSave = true
return nil
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
@@ -83,6 +101,14 @@ func (c *Client) loadConfig() error {
c.config.Endpoint = config.Endpoint
c.baseURL = config.Endpoint
}
// Always load the provisioning key from the file if not already set
if c.config.ProvisioningKey == "" {
c.config.ProvisioningKey = config.ProvisioningKey
}
// Always load the name from the file if not already set
if c.config.Name == "" {
c.config.Name = config.Name
}
// Check if CLI args provided values that override file values
if (!fileHadID && originalConfig.ID != "") ||
@@ -105,7 +131,7 @@ func (c *Client) saveConfig() error {
return nil
}
configPath := getConfigPath(c.clientType)
configPath := getConfigPath(c.clientType, c.configFilePath)
data, err := json.MarshalIndent(c.config, "", " ")
if err != nil {
return err
@@ -118,3 +144,139 @@ func (c *Client) saveConfig() error {
}
return err
}
// interpolateString replaces {{env.VAR}} tokens in s with the corresponding
// environment variable values. Tokens that do not match a supported scheme are
// left unchanged, mirroring the blueprint interpolation logic.
func interpolateString(s string) string {
re := regexp.MustCompile(`\{\{([^}]+)\}\}`)
return re.ReplaceAllStringFunc(s, func(match string) string {
inner := strings.TrimSpace(match[2 : len(match)-2])
if strings.HasPrefix(inner, "env.") {
varName := strings.TrimPrefix(inner, "env.")
return os.Getenv(varName)
}
return match
})
}
// provisionIfNeeded checks whether a provisioning key is present and, if so,
// exchanges it for a newt ID and secret by calling the registration endpoint.
// On success the config is updated in-place and flagged for saving so that
// subsequent runs use the permanent credentials directly.
func (c *Client) provisionIfNeeded() error {
if c.config.ProvisioningKey == "" {
return nil
}
// If we already have both credentials there is nothing to provision.
if c.config.ID != "" && c.config.Secret != "" {
logger.Debug("Credentials already present, skipping provisioning")
return nil
}
logger.Info("Provisioning key found exchanging for newt credentials...")
baseURL, err := url.Parse(c.baseURL)
if err != nil {
return fmt.Errorf("failed to parse base URL for provisioning: %w", err)
}
baseEndpoint := strings.TrimRight(baseURL.String(), "/")
// Interpolate any {{env.VAR}} tokens in the name before sending.
name := interpolateString(c.config.Name)
reqBody := map[string]interface{}{
"provisioningKey": c.config.ProvisioningKey,
}
if name != "" {
reqBody["name"] = name
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal provisioning request: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(
ctx,
"POST",
baseEndpoint+"/api/v1/auth/newt/register",
bytes.NewBuffer(jsonData),
)
if err != nil {
return fmt.Errorf("failed to create provisioning request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
// Mirror the TLS setup used by getToken so mTLS / self-signed CAs work.
var tlsCfg *tls.Config
if c.tlsConfig.ClientCertFile != "" || c.tlsConfig.ClientKeyFile != "" ||
len(c.tlsConfig.CAFiles) > 0 || c.tlsConfig.PKCS12File != "" {
tlsCfg, err = c.setupTLS()
if err != nil {
return fmt.Errorf("failed to setup TLS for provisioning: %w", err)
}
}
if os.Getenv("SKIP_TLS_VERIFY") == "true" {
if tlsCfg == nil {
tlsCfg = &tls.Config{}
}
tlsCfg.InsecureSkipVerify = true
logger.Debug("TLS certificate verification disabled for provisioning via SKIP_TLS_VERIFY")
}
httpClient := &http.Client{}
if tlsCfg != nil {
httpClient.Transport = &http.Transport{TLSClientConfig: tlsCfg}
}
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("provisioning request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
logger.Debug("Provisioning response body: %s", string(body))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("provisioning endpoint returned status %d: %s", resp.StatusCode, string(body))
}
var provResp ProvisioningResponse
if err := json.Unmarshal(body, &provResp); err != nil {
return fmt.Errorf("failed to decode provisioning response: %w", err)
}
if !provResp.Success {
return fmt.Errorf("provisioning failed: %s", provResp.Message)
}
if provResp.Data.NewtID == "" || provResp.Data.Secret == "" {
return fmt.Errorf("provisioning response is missing newt ID or secret")
}
logger.Info("Successfully provisioned newt ID: %s", provResp.Data.NewtID)
// Persist the returned credentials and clear the one-time provisioning key
// so subsequent runs authenticate normally.
c.config.ID = provResp.Data.NewtID
c.config.Secret = provResp.Data.Secret
c.config.ProvisioningKey = ""
c.config.Name = ""
c.configNeedsSave = true
c.justProvisioned = true
// Save immediately so that if the subsequent connection attempt fails the
// provisioning key is already gone from disk and the next retry uses the
// permanent credentials instead of trying to provision again.
if err := c.saveConfig(); err != nil {
logger.Error("Failed to save config after provisioning: %v", err)
}
return nil
}

35
websocket/config_test.go Normal file
View File

@@ -0,0 +1,35 @@
package websocket
import (
"os"
"path/filepath"
"testing"
)
func TestLoadConfig_EmptyFileMarksConfigForSave(t *testing.T) {
t.Setenv("CONFIG_FILE", "")
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config.json")
if err := os.WriteFile(configPath, []byte(""), 0o644); err != nil {
t.Fatalf("failed to create empty config file: %v", err)
}
client := &Client{
config: &Config{
Endpoint: "https://example.com",
ProvisioningKey: "spk-test",
},
clientType: "newt",
configFilePath: configPath,
}
if err := client.loadConfig(); err != nil {
t.Fatalf("loadConfig returned error for empty file: %v", err)
}
if !client.configNeedsSave {
t.Fatal("expected empty config file to mark configNeedsSave")
}
}

View File

@@ -1,10 +1,12 @@
package websocket
type Config struct {
ID string `json:"id"`
Secret string `json:"secret"`
Endpoint string `json:"endpoint"`
TlsClientCert string `json:"tlsClientCert"`
ID string `json:"id"`
Secret string `json:"secret"`
Endpoint string `json:"endpoint"`
TlsClientCert string `json:"tlsClientCert"`
ProvisioningKey string `json:"provisioningKey,omitempty"`
Name string `json:"name,omitempty"`
}
type TokenResponse struct {
@@ -16,7 +18,17 @@ type TokenResponse struct {
Message string `json:"message"`
}
type WSMessage struct {
Type string `json:"type"`
Data interface{} `json:"data"`
type ProvisioningResponse struct {
Data struct {
NewtID string `json:"newtId"`
Secret string `json:"secret"`
} `json:"data"`
Success bool `json:"success"`
Message string `json:"message"`
}
type WSMessage struct {
Type string `json:"type"`
Data interface{} `json:"data"`
ConfigVersion int64 `json:"configVersion,omitempty"`
}