Compare commits

..

752 Commits

Author SHA1 Message Date
dependabot[bot]
d082283af9 chore(deps): bump docker/login-action
Bumps the github-actions-dependencies group with 1 update: [docker/login-action](https://github.com/docker/login-action).


Updates `docker/login-action` from 4.3.0 to 4.4.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](c99871dec2...af1e73f918)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 09:44:34 +00:00
Owen Schwartz
527edc8d80 Merge pull request #400 from fosrl/dev
Properly ignore bin
2026-07-02 17:20:19 -04:00
Owen
584ea9f4a8 Properly ignore bin 2026-07-02 17:19:59 -04:00
Owen Schwartz
3dfd1f459d Merge pull request #396 from fosrl/dependabot/github_actions/github-actions-dependencies-429e143d22
chore(deps): bump the github-actions-dependencies group across 1 directory with 9 updates
2026-07-02 17:15:57 -04:00
dependabot[bot]
f4f2bcadb8 chore(deps): bump the github-actions-dependencies group across 1 directory with 9 updates
Bumps the github-actions-dependencies group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) | `6.1.1` | `6.2.1` |
| [actions/checkout](https://github.com/actions/checkout) | `6` | `7` |
| [docker/login-action](https://github.com/docker/login-action) | `4.1.0` | `4.3.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.1.0` | `4.2.0` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `7.2.0` | `7.3.0` |
| [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `4.0.0` | `4.2.0` |
| [actions/setup-go](https://github.com/actions/setup-go) | `6.4.0` | `6.5.0` |
| [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `4.1.0` | `4.1.1` |
| [softprops/action-gh-release](https://github.com/softprops/action-gh-release) | `3.0.0` | `3.0.1` |



Updates `aws-actions/configure-aws-credentials` from 6.1.1 to 6.2.1
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](d979d5b3a7...254c19bd24)

Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

Updates `docker/login-action` from 4.1.0 to 4.3.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](4907a6ddec...c99871dec2)

Updates `docker/setup-buildx-action` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](d7f5e7f509...bb05f3f551)

Updates `docker/build-push-action` from 7.2.0 to 7.3.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](f9f3042f7e...53b7df96c9)

Updates `docker/setup-qemu-action` from 4.0.0 to 4.2.0
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](ce360397dd...96fe6ef7f3)

Updates `actions/setup-go` from 6.4.0 to 6.5.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4a3601121d...924ae3a1cd)

Updates `actions/attest-build-provenance` from 4.1.0 to 4.1.1
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](a2bbfa2537...0f67c3f485)

Updates `softprops/action-gh-release` from 3.0.0 to 3.0.1
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](b430933298...718ea10b13)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-dependencies
- dependency-name: actions/setup-go
  dependency-version: 6.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: 6.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
- dependency-name: docker/build-push-action
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
- dependency-name: docker/login-action
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 21:13:13 +00:00
Owen Schwartz
3fd6bafb69 Merge pull request #397 from fosrl/dependabot/go_modules/go-dependencies-8b6ef06786
chore(deps): bump the go-dependencies group with 18 updates
2026-07-02 17:10:48 -04:00
Owen Schwartz
758992e84c Merge pull request #399 from fosrl/dev
1.14.0
2026-07-02 17:08:26 -04:00
Owen
0034053aee Comment the handlers back in 2026-07-02 16:29:57 -04:00
Owen
e151160c33 remove 2026-07-02 16:24:23 -04:00
Owen
6b66548d8f Fix clients targets sync and remove nat entries 2026-07-02 16:17:52 -04:00
dependabot[bot]
0731ac5f2a chore(nix): fix hash for updated go dependencies 2026-07-02 15:56:05 +00:00
dependabot[bot]
a9a93c78c1 chore(deps): bump the go-dependencies group with 18 updates
Bumps the go-dependencies group with 18 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/coder/websocket](https://github.com/coder/websocket) | `1.8.14` | `1.8.15` |
| [github.com/gaissmai/bart](https://github.com/gaissmai/bart) | `0.26.1` | `0.28.0` |
| [github.com/moby/moby/api](https://github.com/moby/moby) | `1.54.2` | `1.55.0` |
| [github.com/moby/moby/client](https://github.com/moby/moby) | `0.4.1` | `0.5.0` |
| [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.68.0` | `0.69.0` |
| [go.opentelemetry.io/contrib/instrumentation/runtime](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.68.0` | `0.69.0` |
| [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) | `1.43.0` | `1.44.0` |
| [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc](https://github.com/open-telemetry/opentelemetry-go) | `1.43.0` | `1.44.0` |
| [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://github.com/open-telemetry/opentelemetry-go) | `1.43.0` | `1.44.0` |
| [go.opentelemetry.io/otel/exporters/prometheus](https://github.com/open-telemetry/opentelemetry-go) | `0.65.0` | `0.66.0` |
| [go.opentelemetry.io/otel/metric](https://github.com/open-telemetry/opentelemetry-go) | `1.43.0` | `1.44.0` |
| [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) | `1.43.0` | `1.44.0` |
| [go.opentelemetry.io/otel/sdk/metric](https://github.com/open-telemetry/opentelemetry-go) | `1.43.0` | `1.44.0` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.52.0` | `0.53.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.55.0` | `0.56.0` |
| [golang.org/x/sys](https://github.com/golang/sys) | `0.45.0` | `0.46.0` |
| [google.golang.org/grpc](https://github.com/grpc/grpc-go) | `1.81.1` | `1.82.0` |
| software.sslmate.com/src/go-pkcs12 | `0.7.1` | `0.7.3` |


Updates `github.com/coder/websocket` from 1.8.14 to 1.8.15
- [Release notes](https://github.com/coder/websocket/releases)
- [Commits](https://github.com/coder/websocket/compare/v1.8.14...v1.8.15)

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

Updates `github.com/moby/moby/api` from 1.54.2 to 1.55.0
- [Release notes](https://github.com/moby/moby/releases)
- [Commits](https://github.com/moby/moby/compare/api/v1.54.2...api/v1.55.0)

Updates `github.com/moby/moby/client` from 0.4.1 to 0.5.0
- [Release notes](https://github.com/moby/moby/releases)
- [Changelog](https://github.com/moby/moby/blob/v0.5.0/CHANGELOG.md)
- [Commits](https://github.com/moby/moby/compare/v0.4.1...v0.5.0)

Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.68.0 to 0.69.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.68.0...zpages/v0.69.0)

Updates `go.opentelemetry.io/contrib/instrumentation/runtime` from 0.68.0 to 0.69.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.68.0...zpages/v0.69.0)

Updates `go.opentelemetry.io/otel` from 1.43.0 to 1.44.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.43.0...v1.44.0)

Updates `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc` from 1.43.0 to 1.44.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.43.0...v1.44.0)

Updates `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc` from 1.43.0 to 1.44.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.43.0...v1.44.0)

Updates `go.opentelemetry.io/otel/exporters/prometheus` from 0.65.0 to 0.66.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.65.0...metric/x/v0.66.0)

Updates `go.opentelemetry.io/otel/metric` from 1.43.0 to 1.44.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.43.0...v1.44.0)

Updates `go.opentelemetry.io/otel/sdk` from 1.43.0 to 1.44.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.43.0...v1.44.0)

Updates `go.opentelemetry.io/otel/sdk/metric` from 1.43.0 to 1.44.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.43.0...v1.44.0)

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

Updates `golang.org/x/net` from 0.55.0 to 0.56.0
- [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0)

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

Updates `google.golang.org/grpc` from 1.81.1 to 1.82.0
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.81.1...v1.82.0)

Updates `software.sslmate.com/src/go-pkcs12` from 0.7.1 to 0.7.3

---
updated-dependencies:
- dependency-name: github.com/coder/websocket
  dependency-version: 1.8.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-dependencies
- dependency-name: github.com/gaissmai/bart
  dependency-version: 0.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: github.com/moby/moby/api
  dependency-version: 1.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: github.com/moby/moby/client
  dependency-version: 0.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
  dependency-version: 0.69.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: go.opentelemetry.io/contrib/instrumentation/runtime
  dependency-version: 0.69.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: go.opentelemetry.io/otel/exporters/prometheus
  dependency-version: 0.66.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: go.opentelemetry.io/otel/metric
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: go.opentelemetry.io/otel/sdk/metric
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: golang.org/x/crypto
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: golang.org/x/net
  dependency-version: 0.56.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: golang.org/x/sys
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-dependencies
- dependency-name: software.sslmate.com/src/go-pkcs12
  dependency-version: 0.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 15:54:58 +00:00
Owen Schwartz
b84436af7a Merge pull request #374 from fosrl/chore/dependabot-single-pr-groups
chore(dependabot): group dependency updates into single PRs per ecosystem
2026-07-02 11:53:12 -04:00
Owen
8bf9a2bc0c Fix the sync causing proxy to freak out 2026-07-02 11:51:29 -04:00
Owen
e158c90e34 Add the remote subnets to the sync 2026-07-01 15:34:12 -04:00
Owen
3996333ded Split out connect function 2026-07-01 15:23:48 -04:00
Owen
df23a9f0fe Windows restart and service restart 2026-07-01 15:18:29 -04:00
Owen
19942675f7 Support all config in file 2026-07-01 14:41:12 -04:00
Owen
880ea06dca Change interface and add log message 2026-06-30 18:16:09 -04:00
Owen
8236cb420f Allow some flapping before log 2026-06-29 20:46:06 -04:00
Owen
dabeb63fc2 Add sync 2026-06-29 18:34:57 -04:00
Owen
e5db8dedcc Add sync 2026-06-29 12:00:41 -04:00
Owen
72eb9515b4 Refactor 2026-06-29 11:55:27 -04:00
Owen
fcbdd0b650 Groundwork for monitoring and updating the dns 2026-06-29 11:55:27 -04:00
Owen
0c196b9d5d Add the update endpoints 2026-06-26 23:24:40 -04:00
Owen
3a412fded2 Basic native root mode works on linux 2026-06-26 18:00:12 -04:00
Owen
237587149b Implement first draft on native on main tunnel 2026-06-26 16:41:10 -04:00
Owen
689acff6f8 Pull the version from the new api 2026-06-25 15:37:51 -04:00
Owen Schwartz
5a5897ee6f Merge pull request #387 from DanivosYoun/fix/ws-half-open-detection
Detect dead/half-open control websocket (read deadline + protocol ping)
2026-06-23 11:47:47 -07:00
DanivosYoun
855b2b272f fix(websocket): detect dead/half-open control connection (read deadline + protocol ping)
The control websocket had no read deadline and only sent application-level
"newt/ping" JSON (never protocol PING frames), so a half-open connection — for
example a cloud load balancer keeping the socket open after the backend API
server restarted — was never detected. ReadMessage blocked forever while the
buffered ping writes kept succeeding, so newt stayed "connected" to a dead peer
and only recovered after a manual restart.

- Arm a read deadline (pongWait = 2x pingInterval, min 20s) and refresh it on
  every pong and every inbound message.
- Send a protocol-level PING alongside the existing app ping, so a compliant
  server replies with a PONG that refreshes the read deadline.
- On ping-write failure, close the connection and let the read pump perform a
  single reconnect (previously sendPing and the read-pump defer could both
  trigger a reconnect, racing two connections).
- Give each connection a lifecycle channel so its ping monitor stops when the
  connection ends (previously one ping-monitor goroutine leaked per reconnect).
2026-06-22 11:51:03 +09:00
Owen
0019d0dc51 Update log message to not print 2026-06-12 15:44:43 -07:00
Owen Schwartz
80e5f664b7 Merge pull request #381 from fosrl/dev
Remove binary

Former-commit-id: 247691609f
2026-06-10 11:13:40 -07:00
Owen
df93f01e74 Remove binary
Former-commit-id: baf38fac2f
2026-06-10 11:13:03 -07:00
Owen Schwartz
5e0eef3b27 Merge pull request #380 from fosrl/dev
1.13.0

Former-commit-id: 04abc80e6c
2026-06-10 11:12:40 -07:00
Owen
ea3d9869c5 Add more context to error message
Former-commit-id: 8540cf1c34
2026-06-09 20:50:02 -07:00
Owen
9f895ac7ec Support backward compatability with external
Former-commit-id: c2f2f68858
2026-06-09 20:43:43 -07:00
Owen
364073d28c Add some preflight check for vnc reachability
Former-commit-id: 6d3312fe89
2026-06-09 16:01:45 -07:00
Owen
8eb9661827 Dont time out the health checks for http
Former-commit-id: 740c9a8c22
2026-06-08 15:27:08 -07:00
Owen
25960e4354 Support using the cert for openssh connections
Former-commit-id: bdb14e8c0f
2026-06-08 15:00:12 -07:00
Marc Schäfer
753a2176e9 chore(dependabot): group dependency updates into single PRs per ecosystem
Former-commit-id: 5f0ed2afe6
2026-06-07 11:06:21 +02:00
Owen
060fcb18d5 Merge branch 'main' into dev
Former-commit-id: a5fd9110ca
2026-06-06 16:09:45 -07:00
Owen Schwartz
12af1f77fd Merge pull request #272 from eleboucher/watch-blueprint
feat: watch blueprint.yaml for update and auto resend
Former-commit-id: 9ef04039b1
2026-06-06 15:34:53 -07:00
Owen
8c698ffaee Add file to function call
Former-commit-id: 090ad07991
2026-06-06 15:34:22 -07:00
Marc Schäfer
d360392214 Merge branch 'main' into watch-blueprint
Former-commit-id: 2ad89915c4
2026-06-06 23:23:22 +02:00
Owen
1dbd1936ba Quiet some logs
Former-commit-id: 1546e8a89a
2026-06-05 17:09:58 -07:00
Owen
40223eac90 Support commands over the ssh tunnel
Former-commit-id: b87b9e10dc
2026-06-05 13:55:43 -07:00
Owen
17c4fe9b2b Use logger not printf
Former-commit-id: 3ef2550687
2026-06-04 16:57:51 -07:00
Owen Schwartz
c515c4cd5f Merge pull request #372 from fosrl/dev
1.13.0

Former-commit-id: 0beee679ca
2026-06-04 11:57:28 -07:00
Owen
17a5689cf2 Update Flake
Former-commit-id: 49d6cf595d
2026-06-04 11:51:40 -07:00
Owen
3d8a269819 Fix windows compatibility issues
Former-commit-id: 0e690a6a84
2026-06-04 11:47:49 -07:00
Owen
86180c0ee8 Merge branch 'main' into dev
Former-commit-id: c22d603fa1
2026-06-04 11:29:09 -07:00
Owen
8eddb908e0 Handle browser gateway push pam auth
Former-commit-id: 1ba9a419cb
2026-06-04 11:28:20 -07:00
Owen Schwartz
74ce376c28 Merge pull request #367 from fosrl/dependabot/github_actions/docker/setup-buildx-action-4.1.0
chore(deps): bump docker/setup-buildx-action from 4.0.0 to 4.1.0

Former-commit-id: 21f465444a
2026-06-04 10:58:30 -07:00
Owen Schwartz
2d369176a1 Merge pull request #366 from fosrl/dependabot/github_actions/docker/build-push-action-7.2.0
chore(deps): bump docker/build-push-action from 7.1.0 to 7.2.0

Former-commit-id: 272e6cf402
2026-06-04 10:58:13 -07:00
Owen Schwartz
a5828af214 Merge pull request #340 from fosrl/dependabot/go_modules/golang.zx2c4.com/wireguard/windows-1.0.1
chore(deps): bump golang.zx2c4.com/wireguard/windows from 0.5.3 to 1.0.1

Former-commit-id: 0737d6c5cc
2026-06-04 10:58:05 -07:00
Owen Schwartz
62dc2d3e96 Merge pull request #365 from fosrl/dependabot/github_actions/actions/stale-10.3.0
chore(deps): bump actions/stale from 10.2.0 to 10.3.0

Former-commit-id: 7127740219
2026-06-04 10:58:00 -07:00
Owen Schwartz
7d84393dd0 Merge pull request #370 from immanuwell/fix/docker-fetch-duplicate-log-and-reliable-ping-dead-code
fix: remove duplicate wrong error log in socket/fetch and dead code in reliablePing
Former-commit-id: 4c56a2d02c
2026-06-04 10:57:04 -07:00
dependabot[bot]
ac0079b7e2 chore(nix): fix hash for updated go dependencies
Former-commit-id: b6f00a9500
2026-06-04 17:54:30 +00:00
Owen Schwartz
02fc8b0684 Merge pull request #351 from fosrl/dependabot/github_actions/aws-actions/configure-aws-credentials-6.1.1
chore(deps): bump aws-actions/configure-aws-credentials from 6.1.0 to 6.1.1

Former-commit-id: 4b81b34eae
2026-06-04 10:53:51 -07:00
dependabot[bot]
5fb1ba5b35 chore(deps): bump docker/build-push-action from 7.1.0 to 7.2.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.1.0 to 7.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](bcafcacb16...f9f3042f7e)

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

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: efc17644d1
2026-06-04 17:53:40 +00:00
dependabot[bot]
9f014b19f8 chore(deps): bump actions/stale from 10.2.0 to 10.3.0
Bumps [actions/stale](https://github.com/actions/stale) from 10.2.0 to 10.3.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](b5d41d4e1d...eb5cf3af3a)

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

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: eca54190b0
2026-06-04 17:53:38 +00:00
dependabot[bot]
da794460e5 chore(deps): bump docker/setup-buildx-action from 4.0.0 to 4.1.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](4d04d5d948...d7f5e7f509)

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

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: bb094ce3fc
2026-06-04 17:53:31 +00:00
dependabot[bot]
acb21b1203 chore(deps): bump golang.zx2c4.com/wireguard/windows from 0.5.3 to 1.0.1
Bumps golang.zx2c4.com/wireguard/windows from 0.5.3 to 1.0.1.

---
updated-dependencies:
- dependency-name: golang.zx2c4.com/wireguard/windows
  dependency-version: 1.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: d1f4b5bc80
2026-06-04 17:53:25 +00:00
Owen Schwartz
1f80280384 Merge pull request #350 from fosrl/dependabot/github_actions/sigstore/cosign-installer-4.1.2
chore(deps): bump sigstore/cosign-installer from 4.1.1 to 4.1.2

Former-commit-id: f922155843
2026-06-04 10:51:36 -07:00
Owen Schwartz
66714adb0a Merge pull request #356 from fosrl/dependabot/go_modules/prod-patch-updates-d9ad24fc58
chore(deps): bump the prod-patch-updates group across 1 directory with 2 updates

Former-commit-id: 1f16708a05
2026-06-04 10:51:28 -07:00
dependabot[bot]
a868719459 chore(nix): fix hash for updated go dependencies
Former-commit-id: 88c1c2e0b5
2026-06-04 17:22:48 +00:00
dependabot[bot]
b2c73a40df chore(deps): bump the prod-patch-updates group across 1 directory with 2 updates
Bumps the prod-patch-updates group with 2 updates in the / directory: [google.golang.org/grpc](https://github.com/grpc/grpc-go) and software.sslmate.com/src/go-pkcs12.


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

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

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.81.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: software.sslmate.com/src/go-pkcs12
  dependency-version: 0.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: 69ed08222f
2026-06-04 17:21:44 +00:00
Owen Schwartz
a82942381b Merge pull request #371 from fosrl/fix/go-deps-security-updates
Update golang.org/x modules for security fixes

Former-commit-id: 0169123d55
2026-06-04 10:19:31 -07:00
Marc Schäfer
fde46cb587 fix(nix): update Go module vendor hash
Former-commit-id: ac9852a3cb
2026-06-04 14:28:28 +02:00
Marc Schäfer
a8d91756ca fix(deps): update golang.org/x modules for security fixes
Update golang.org/x/crypto, golang.org/x/net and golang.org/x/sys
to versions containing security fixes reported by Docker Scout.


Former-commit-id: f91deeede7
2026-06-04 12:53:53 +02:00
Owen
e5bf31d118 Cert auth working with mode push
Former-commit-id: b0d894f05a
2026-05-31 20:59:50 -07:00
Owen
e77b1d9363 Fall back gracefully when home dir does not exist
Former-commit-id: 07bf861179
2026-05-31 16:24:57 -07:00
Owen
8191f30e77 Make usernames work with native auth
Former-commit-id: 55eabb16cb
2026-05-31 16:07:33 -07:00
Owen
b35dd025bd Get native auth working
Former-commit-id: e5345f6717
2026-05-30 11:53:31 -07:00
immanuwell
763ef3726e fix: remove duplicate wrong error log in socket/fetch handler and dead code in reliablePing
Former-commit-id: d57b7ccb53
2026-05-30 21:55:46 +04:00
Owen
e3fe89b431 Remove github.com/msteinert/pam/v2
Former-commit-id: b33a12a528
2026-05-29 15:57:39 -07:00
Owen
3c47124894 Merge branch 'rdp-ssh' into dev
Former-commit-id: 57575490e1
2026-05-28 15:40:22 -07:00
Owen
1c591d8f1b Merge branch 'main' into dev
Former-commit-id: e5e72ba615
2026-05-28 15:38:46 -07:00
Owen
7ff97eda51 Add restart endpoint
Former-commit-id: 734542c3ed
2026-05-28 15:38:09 -07:00
Owen
e223a35a62 Reexec if the config changes
Former-commit-id: a7b028b154
2026-05-28 15:38:09 -07:00
Owen
71eb811829 Add sighup and block flag to block connections
Former-commit-id: 694a7986f5
2026-05-28 15:38:09 -07:00
Owen
34d2cad512 Add to cicd
Former-commit-id: 3d57d37c9e
2026-05-28 15:38:09 -07:00
Owen
7c22fe274a Add advantech package
Former-commit-id: 8955213aef
2026-05-28 15:38:09 -07:00
Owen
d2bd8cb97d Add advantech router app updates
Former-commit-id: ec89eb7d0f
2026-05-28 15:38:09 -07:00
Owen
24c4fbc0b0 Keep checking if we need to update
Former-commit-id: 535fd717be
2026-05-28 15:38:09 -07:00
Owen
e1b6fa9008 Add permissions check, shasum check, & build info
Former-commit-id: 357f718e00
2026-05-28 15:38:09 -07:00
Owen
c058500932 Auto update newt
Former-commit-id: 42cb8e7908
2026-05-28 15:38:08 -07:00
miloschwartz
3264aa0975 update issue template
Former-commit-id: 6ccfe96e69
2026-05-25 21:40:43 -07:00
Owen
71ab4da7f5 Handle the different modes
Former-commit-id: 5d84fa9141
2026-05-22 13:49:54 -07:00
Owen
2a3ec1953f Add --disable-ssh flag to replace --auth-daemon
Former-commit-id: 60d67ee9a1
2026-05-22 13:39:14 -07:00
Owen
865697204b Bring the naitve ssh pam to the browser gateway
Former-commit-id: 1ff26b7acd
2026-05-22 11:30:21 -07:00
Owen
6bf6b47d18 Add pam to native ssh server
Former-commit-id: e6267cc1fc
2026-05-22 11:20:21 -07:00
Owen
b9536597d2 Remove nativeSsh config with shell
Former-commit-id: e36ae4b746
2026-05-22 11:19:57 -07:00
Owen
647a6d2635 Add credentials store and use the auth daemon info
Former-commit-id: 631eab53d1
2026-05-21 21:09:51 -07:00
Owen
677146e20e Fix exit not closing session
Former-commit-id: f217ddf6a7
2026-05-21 20:38:38 -07:00
Owen
097450ae15 Bind the ssh server to the newt ip
Former-commit-id: 9640ada8b7
2026-05-21 20:30:17 -07:00
Owen
cc7b2a5ade Keep host key in memory
Former-commit-id: 388795ecf4
2026-05-21 20:16:46 -07:00
Owen
7636549e9a Pty to find its own shell
Former-commit-id: 133311f1c4
2026-05-21 18:23:11 -07:00
Owen
cb5d87587c Basic ssh server for private resources created
Former-commit-id: e0d65d8125
2026-05-21 18:19:39 -07:00
Owen Schwartz
2cd9ffc582 Merge pull request #362 from fosrl/ci/use-go-version-file
Use Go version specified in go.mod for CI tests

Former-commit-id: c168ddac90
2026-05-20 20:07:16 -07:00
Marc Schäfer
8d30a584b5 ci(test): use Go version from go.mod
Former-commit-id: 88d946c9b4
2026-05-21 02:42:59 +02:00
Owen Schwartz
936642cef9 Merge pull request #358 from cwiggs/feat/ci-go-test
feat(ci): run a go test before we build

Former-commit-id: c9be684b91
2026-05-20 14:51:17 -07:00
Owen Schwartz
9d060c2ac5 Merge pull request #361 from fosrl/dev
Housekeeping

Former-commit-id: 8f321365d5
2026-05-20 14:48:53 -07:00
Owen Schwartz
fba7943cd3 Merge pull request #349 from fosrl/issue-348
Refactor Docker client imports and container handling logic

Former-commit-id: 21e2a7b91f
2026-05-16 21:11:12 -07:00
dependabot[bot]
f41e15fcb3 chore(deps): bump sigstore/cosign-installer from 4.1.1 to 4.1.2
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v4.1.1...6f9f17788090df1f26f669e9d70d6ae9567deba6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: 24f5d55cce
2026-05-16 21:57:19 +00:00
Owen Schwartz
0d528abab8 Merge pull request #360 from fosrl/github-action-cosign
Upgrade cosign installer to v4.1.2 and pin cosign version

Former-commit-id: 2562bda187
2026-05-16 14:55:22 -07:00
Marc Schäfer
16cb83fd28 fix(security): update cosign to v3.0.6 and installer to 4.1.2
Former-commit-id: 8736f89291
2026-05-16 16:31:27 +02:00
Owen
e438a249dd Support ssh private key
Former-commit-id: 1f8be1d826
2026-05-15 16:07:05 -07:00
Owen
40710e5197 Support add and remove for gateway
Former-commit-id: 28cddf7066
2026-05-15 14:59:37 -07:00
Owen
385e9c0a83 Support per target auth token
Former-commit-id: 710408ac67
2026-05-15 13:46:39 -07:00
Chris Wiggins
d5c3dedca6 feat(ci): run a go test before we build
Former-commit-id: 54a13b268e
2026-05-15 11:48:50 -06:00
Owen Schwartz
1de3076808 Merge pull request #353 from rinseaid/fix/x-forwarded-proto-tls
Fix X-Forwarded-Proto always set to "http" for TLS connections

Former-commit-id: 37aff0daa5
2026-05-13 20:54:06 -07:00
Owen Schwartz
b0f8589443 Delete .github/workflows/build-patched.yml
Former-commit-id: 55ca18a1db
2026-05-13 20:53:27 -07:00
rinseaid
a855047139 Add workflow to build patched image to ghcr.io/rinseaid/newt:patched
Former-commit-id: b6e2d61a18
2026-05-13 22:40:22 -04:00
rinseaid
79e21b7917 Fix X-Forwarded-Proto always set to "http" for TLS connections
httpConnCtx wraps *tls.Conn behind net.Conn, so Go's http.Server
cannot detect TLS via type assertion and r.TLS is always nil.
SetXForwarded() then always writes X-Forwarded-Proto: http.

Override using the isTLS context flag already set by ConnContext.


Former-commit-id: 817824bd6f
2026-05-13 22:38:35 -04:00
Owen
c55071c60a Basic browser gateway target support
Former-commit-id: 559b7021fe
2026-05-13 16:24:26 -07:00
Owen
b72c8e643d Split out rdp
Former-commit-id: bd53edf8e4
2026-05-11 21:49:55 -07:00
Owen
7ace4978a4 Add vnc
Former-commit-id: 825a7f460f
2026-05-11 21:13:55 -07:00
Owen
832b06d8d5 Add browser gateway
Former-commit-id: 67b81c1f40
2026-05-11 18:08:47 -07:00
Owen
c9ac8fc28f Remove test
Former-commit-id: efa1ecf61e
2026-05-11 18:04:51 -07:00
Owen
74acbad133 Update log message
Former-commit-id: d28c2aaa87
2026-05-11 10:34:30 -07:00
dependabot[bot]
7ff795b6bb chore(deps): bump aws-actions/configure-aws-credentials
Bumps [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) from 6.1.0 to 6.1.1.
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](ec61189d14...d979d5b3a7)

---
updated-dependencies:
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: 6.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: 820978ae6f
2026-05-11 14:36:15 +00:00
Marc Schäfer
8246eb1e36 refactor: update Docker client imports and adjust container handling logic
Signed-off-by: Marc Schäfer <git@marcschaeferger.de>

Former-commit-id: 0e2d7057b6
2026-05-10 10:50:38 +02:00
Marc Schäfer
9342546075 Merge pull request #312 from fosrl/dependabot/github_actions/softprops/action-gh-release-3.0.0
chore(deps): bump softprops/action-gh-release from 2.6.1 to 3.0.0

Former-commit-id: ae48df97c8
2026-05-09 15:57:52 +02:00
Marc Schäfer
0d26fb6de4 Merge pull request #313 from fosrl/dependabot/github_actions/aws-actions/configure-aws-credentials-6.1.0
chore(deps): bump aws-actions/configure-aws-credentials from 6.0.0 to 6.1.0

Former-commit-id: df125affe4
2026-05-09 15:57:40 +02:00
Marc Schäfer
9de90429d3 Merge pull request #314 from fosrl/dependabot/github_actions/docker/build-push-action-7.1.0
chore(deps): bump docker/build-push-action from 7.0.0 to 7.1.0

Former-commit-id: 848a23a535
2026-05-09 15:57:29 +02:00
Marc Schäfer
e167c35293 Merge pull request #315 from fosrl/dependabot/github_actions/actions/setup-go-6.4.0
chore(deps): bump actions/setup-go from 6.2.0 to 6.4.0

Former-commit-id: 1a1d58c4dc
2026-05-09 15:57:15 +02:00
Owen Schwartz
81a4f3fd27 Merge pull request #346 from fosrl/dev
Fix redirect

Former-commit-id: 0da8a29a61
2026-05-08 11:06:31 -07:00
Owen
5541f92d07 Bump version
Former-commit-id: a1218ab67a
2026-05-08 11:05:24 -07:00
Owen
74b4b7d76d Merge branch 'main' into dev
Former-commit-id: bb84762b16
2026-05-08 11:05:07 -07:00
Owen
328b1aced3 Fix the redirect
Former-commit-id: 86155072de
2026-05-08 11:03:00 -07:00
Owen Schwartz
655cfa45fb Merge pull request #345 from LaurenceJJones/investigate/https-permanent-redirect-loop
fix(http): populate Request.TLS for private HTTPS via httpConnCtx

Former-commit-id: 4a9a4c4eec
2026-05-08 09:48:22 -07:00
Owen Schwartz
b3e4964e52 Merge pull request #344 from LaurenceJJones/investigate/private-http-redirect-500
fix(http): Set host header based on in

Former-commit-id: 21c744fe84
2026-05-08 09:47:09 -07:00
Laurence
f871b5e324 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.


Former-commit-id: 146e7835eb
2026-05-08 15:17:31 +01:00
Laurence
85d51a300e 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


Former-commit-id: 6aa94c0c2a
2026-05-08 13:45:50 +01:00
Owen Schwartz
cd9fb3d013 Merge pull request #342 from fosrl/dev
1.12.4

Former-commit-id: 542c70b326
2026-05-07 17:41:03 -07:00
Owen
6b42cac02d Retry interval while we are disconnected
Former-commit-id: 663e98af60
2026-05-07 17:27:01 -07:00
Owen
d5f48f782f Increase max attempts
Former-commit-id: 901ec71baf
2026-05-07 17:25:13 -07:00
Owen
cd5f5c079c Merge branch 'main' into dev
Former-commit-id: 9bc0204f57
2026-05-07 17:24:34 -07:00
Daniel Snider
1f2d40c793 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.


Former-commit-id: 1e77b09e3b
2026-05-07 16:57:31 -07:00
Owen
5e7cd9c0b9 Bump version
Former-commit-id: 74fd3f3aa3
2026-05-07 16:24:30 -07:00
Owen
a84bae3bc1 Attempt to fix nix issue
Former-commit-id: e8dc19a62b
2026-05-07 16:23:59 -07:00
Owen
34c5260e49 Fix not logging when rewriting nat
Former-commit-id: 9ff32b8a8b
2026-05-07 16:16:47 -07:00
dependabot[bot]
d16b856719 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>
Former-commit-id: 9edaac9c11
2026-05-05 18:11:26 -07:00
dependabot[bot]
f1bb6c663d chore(nix): fix hash for updated go dependencies
Former-commit-id: ced87b1d5e
2026-05-05 18:11:21 -07:00
dependabot[bot]
b09f67c377 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>
Former-commit-id: 3aaebe64fb
2026-05-05 18:11:21 -07:00
Owen
46d4d6539a Try to fix failover not working
Former-commit-id: 27f7ca6bb9
2026-05-05 11:40:39 -07:00
Owen
367e4b03fe Update status code
Former-commit-id: 5090907307
2026-04-30 15:55:52 -07:00
Owen
07613ebe05 Fix incorrect redirect logic
Former-commit-id: a6533b3fa0
2026-04-29 21:11:07 -07:00
Owen Schwartz
81b6daceba Merge pull request #336 from fosrl/dev
1.12.3

Former-commit-id: 57aa2e2e2c
2026-04-29 16:02:49 -07:00
Owen Schwartz
72a209c7ae Merge pull request #334 from LaurenceJJones/private-http-websocket
enhance(http): Support websocket upgrades

Former-commit-id: 5724c516dc
2026-04-29 15:58:30 -07:00
Owen
72c58c721b Add some test scripts for ws and move to testing/
Former-commit-id: b33c3b8849
2026-04-29 15:57:31 -07:00
Laurence
dd735555c4 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.


Former-commit-id: 8e19e475bf
2026-04-29 07:12:35 +01:00
Owen Schwartz
51d7558ec5 Merge pull request #333 from fosrl/dev
Dont block tcp for http unless there are targets

Former-commit-id: 9e92c42876
2026-04-28 14:51:01 -07:00
Owen
ad8013ff77 Dont block tcp for http unless there are targets
Former-commit-id: 66c72bbe2e
2026-04-28 14:29:55 -07:00
Owen Schwartz
7315f381d7 Merge pull request #331 from fosrl/dev
Follow redirects by default for backward compat

Former-commit-id: ffd26f9a6d
2026-04-28 10:13:49 -07:00
Owen
2038383393 Follow redirects by default for backward compat
Fixes #330


Former-commit-id: 7610aa40bf
2026-04-28 10:10:28 -07:00
Owen Schwartz
1ecf6efdf5 Merge pull request #328 from fosrl/dev
Quiet message

Former-commit-id: bf33a66043
2026-04-27 20:11:01 -07:00
Owen
3a5ce705b8 Quiet message
Former-commit-id: 23caf57bf4
2026-04-27 20:10:35 -07:00
Owen Schwartz
809e6cff30 Merge pull request #327 from fosrl/dev
1.12.0

Former-commit-id: df3aa60cf5
2026-04-27 20:08:45 -07:00
Owen
96327034af Fix crashing when removing hc
Former-commit-id: 5c43db466a
2026-04-27 15:03:36 -07:00
Owen Schwartz
5564eaa02f Merge pull request #323 from fosrl/dev
1.12.0-rc.1

Former-commit-id: cc663f1636
2026-04-24 13:42:38 -07:00
Owen
9545854cb5 Hard code the ifconfig path
Former-commit-id: 1a67ff30c2
2026-04-24 10:39:44 -07:00
Owen
d28b9bbca3 Fix transport issue
Former-commit-id: bfd61ca511
2026-04-22 21:36:16 -07:00
Owen
232b3936de Try to add redirect
Former-commit-id: 294f99e024
2026-04-22 20:12:51 -07:00
Owen Schwartz
5914b3107a Merge pull request #322 from fosrl/dev
Revert nix in cicd

Former-commit-id: af2ecf486a
2026-04-22 11:40:45 -07:00
Owen
015fe4b8db Revert nix version in cicd
Former-commit-id: efd6743ce4
2026-04-22 11:40:12 -07:00
Owen Schwartz
25eb79823f Merge pull request #321 from fosrl/dev
1.12.0-rc.0

Former-commit-id: a0d2bb999a
2026-04-22 11:35:31 -07:00
Owen
6b669db101 Merge branch 'main' into dev
Former-commit-id: 5d889fbc09
2026-04-22 11:34:40 -07:00
Owen
c0b00f7faa Merge branch 'fix-nix' into dev
Former-commit-id: 1a7cf06ff8
2026-04-22 11:31:58 -07:00
Owen
a223f4e6aa Merge branch 'http-ha' into dev
Former-commit-id: 35a334c842
2026-04-21 15:07:05 -07:00
Owen
64b0bd2c70 Merge branch 'alerting-rules' into dev
Former-commit-id: c8e5112a2a
2026-04-21 15:06:50 -07:00
Owen
7c3e0de5c9 Remove hc id
Former-commit-id: 8bfb4659c0
2026-04-20 21:52:21 -07:00
Owen
b1db040bb9 Fix nil pointer
Former-commit-id: 309f9caad2
2026-04-20 15:05:07 -07:00
Owen
3aae8264a8 Add x-forwarded-for
Former-commit-id: 26de268466
2026-04-20 15:04:59 -07:00
Owen
8a63f5cd12 Find old bins and support freebsd
Former-commit-id: 0f927a37ab
2026-04-16 21:47:48 -07:00
Owen
9725c266be Use follow redirects bool
Former-commit-id: e8961c5de5
2026-04-15 21:36:40 -07:00
Owen
b81c252586 Updating with new methods
Former-commit-id: 9bb8eaeadb
2026-04-15 21:01:04 -07:00
Owen Schwartz
b22f6f19bc Merge pull request #317 from fosrl/fix-nix
fix nix

Former-commit-id: d3d10d02e8
2026-04-14 14:24:26 -07:00
Owen
6ef16b8c3a Update nix version in cicd
Former-commit-id: d133d69cb9
2026-04-14 14:22:52 -07:00
Owen
e9d2aed39a Update version
Former-commit-id: 50be4f617e
2026-04-14 14:22:48 -07:00
Owen
1043ee4758 Merge branch 'main' into dev
Former-commit-id: be1cd190e7
2026-04-14 14:17:42 -07:00
Owen
be17fc554f Add ldflags version to local
Former-commit-id: 5c9d13bcca
2026-04-13 17:00:06 -07:00
Owen Schwartz
53588baa61 Merge pull request #306 from LaurenceJJones/investigate/heap-leak-udp-proxy
fix(proxy): reclaim idle UDP flows and make timeout configurable

Former-commit-id: dc2e23380a
2026-04-13 10:27:37 -07:00
dependabot[bot]
0a043f2205 chore(deps): bump actions/setup-go from 6.2.0 to 6.4.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.2.0 to 6.4.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](7a3fe6cf4c...4a3601121d)

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

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: 89b024b010
2026-04-13 10:36:49 +00:00
dependabot[bot]
e9ac43d12b chore(deps): bump docker/build-push-action from 7.0.0 to 7.1.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](d08e5c354a...bcafcacb16)

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

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: 5969119382
2026-04-13 10:36:44 +00:00
dependabot[bot]
54a8390007 chore(deps): bump aws-actions/configure-aws-credentials
Bumps [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](8df5847569...ec61189d14)

---
updated-dependencies:
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: 4205049c81
2026-04-13 10:36:39 +00:00
dependabot[bot]
4e12ef169d chore(deps): bump softprops/action-gh-release from 2.6.1 to 3.0.0
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.6.1 to 3.0.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](153bb8e044...b430933298)

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

Signed-off-by: dependabot[bot] <support@github.com>
Former-commit-id: 276dad8990
2026-04-13 10:36:35 +00:00
Marc Schäfer
a7b512833b 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

Former-commit-id: 3d2b73d417
2026-04-12 22:49:53 +02:00
Owen
b3a23a719f Add logging
Former-commit-id: 12776d65c1
2026-04-11 21:56:28 -07:00
Laurence
139de4a029 Merge remote-tracking branch 'upstream/dev' into investigate/heap-leak-udp-proxy
Made-with: Cursor

# Conflicts:
#	proxy/manager.go


Former-commit-id: 0569525743
2026-04-10 13:36:13 +01:00
Owen
f8d8eaab2c Switch to scheme
Former-commit-id: 342af9e42d
2026-04-09 17:21:36 -04:00
Owen
90d486ca7f Pass the new data down from the websocket
Former-commit-id: 092535441e
2026-04-09 16:13:19 -04:00
Owen
aa50a339d2 Adjust to use data saved inside of the subnet rule
Former-commit-id: 5848c8d4b4
2026-04-09 16:04:11 -04:00
Owen Schwartz
236e213b7b Merge pull request #277 from LaurenceJJones/refactor/proxy-udp-buffer-pool
perf(proxy): add sync.Pool for UDP buffers

Former-commit-id: 6becf0f719
2026-04-09 13:09:06 -04:00
Owen
51c3f720fa Basic http is working
Former-commit-id: 47c646bc33
2026-04-09 11:43:26 -04:00
Laurence
93c7cafc33 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

Former-commit-id: 4d8d00241d
2026-04-09 15:59:03 +01:00
Laurence
57831f9473 fix(proxy): reclaim idle UDP flows and make timeout configurable
Former-commit-id: 31f899588f
2026-04-09 15:45:55 +01:00
dependabot[bot]
9442142c32 chore(nix): fix hash for updated go dependencies
Former-commit-id: 0104fb9b2d
2026-04-09 02:01:28 +00:00
dependabot[bot]
5fb2fecc87 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>
Former-commit-id: 6dd9c4b0d1
2026-04-09 02:00:10 +00:00
Owen Schwartz
fdb85e7c25 Merge pull request #299 from fosrl/dependabot/go_modules/prod-minor-updates-7fd0df0afe
chore(deps): bump the prod-minor-updates group with 2 updates

Former-commit-id: e4daa620c7
2026-04-08 21:58:53 -04:00
Owen Schwartz
004c599c19 Merge pull request #302 from LaurenceJJones/fix/config-file-provision-save
fix: allow empty config file bootstrap before provisioning
Former-commit-id: 7e1e3408d5
2026-04-08 21:58:07 -04:00
Laurence
2ab9fb901a 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


Former-commit-id: d7c3c38d24
2026-04-08 14:13:13 +01:00
Owen
3a0dd20bd3 Add CODEOWNERS
Former-commit-id: 27e471942e
2026-04-07 11:34:18 -04:00
dependabot[bot]
0dc50321c6 chore(nix): fix hash for updated go dependencies
Former-commit-id: f5f2ba38d7
2026-04-07 09:47:01 +00:00
dependabot[bot]
753be0f74b 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>
Former-commit-id: 8cf3942366
2026-04-07 09:45:12 +00:00
Marc Schäfer
363b37e241 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

Former-commit-id: f7bb240c74
2026-04-06 17:28:26 +02:00
Marc Schäfer
a33508429c 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

Former-commit-id: cbd17ff249
2026-04-06 17:28:05 +02:00
Marc Schäfer
d748f64f79 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

Former-commit-id: b7f2445cfd
2026-04-06 17:27:44 +02:00
Marc Schäfer
5f8a984733 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

Former-commit-id: 88d954fc64
2026-04-06 17:27:25 +02:00
Marc Schäfer
42fb9a675c 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

Former-commit-id: 9b2d1f2a10
2026-04-06 17:27:07 +02:00
Marc Schäfer
df79474869 Merge pull request #298 from fosrl/dependabot/go_modules/prod-minor-updates-497a73c3c2
chore(deps): bump the prod-minor-updates group with 13 updates

Former-commit-id: caa5a6a476
2026-04-06 17:23:16 +02:00
dependabot[bot]
ec96bdbf1d chore(nix): fix hash for updated go dependencies
Former-commit-id: 74183952fb
2026-04-06 10:12:08 +00:00
dependabot[bot]
cc4f34b48e 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>
Former-commit-id: 05fc12f66e
2026-04-06 10:10:38 +00:00
dependabot[bot]
9f68e9e2cf 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>
Former-commit-id: 56cc225bd3
2026-04-06 10:10:31 +00:00
dependabot[bot]
e62d5c463f 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>
Former-commit-id: fee7fbe20a
2026-04-06 10:10:27 +00:00
dependabot[bot]
deb2328f85 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>
Former-commit-id: bc6661faa5
2026-04-06 10:10:21 +00:00
dependabot[bot]
5f0fec22ad 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>
Former-commit-id: db6cabc6d7
2026-04-06 10:10:17 +00:00
dependabot[bot]
e0177f7a4d 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>
Former-commit-id: 3f32c9e8ef
2026-04-06 10:10:11 +00:00
Owen Schwartz
5e18816dcc Merge pull request #290 from fosrl/dev
1.11.0

Former-commit-id: cd4782265a
2026-04-03 17:37:05 -04:00
Owen
1749d7c044 Delete bad bp
Former-commit-id: 184bfb12d6
2026-04-03 17:36:48 -04:00
Owen
c4711203f4 Remove files
Former-commit-id: 2e02c9b7a9
2026-04-03 16:49:09 -04:00
Owen Schwartz
31c0a4fee1 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

Former-commit-id: 5c329be1f3
2026-04-03 16:47:50 -04:00
Marc Schäfer
8d1a1f8d4a 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

Former-commit-id: 732e788c66
2026-04-03 14:36:14 +02:00
Marc Schäfer
6f027542a2 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

Former-commit-id: aa42b3623d
2026-04-03 14:35:45 +02:00
Marc Schäfer
f5905f1402 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

Former-commit-id: 4f42560e26
2026-04-03 14:33:52 +02:00
dependabot[bot]
e89e5cd2d5 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>
Former-commit-id: c2187de482
2026-04-03 12:33:48 +00:00
dependabot[bot]
a8f7551750 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>
Former-commit-id: 5ced7d6909
2026-04-03 12:33:07 +00:00
Marc Schäfer
a0867d5537 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

Former-commit-id: 4e6e79ad21
2026-04-03 14:32:59 +02:00
Marc Schäfer
ea66070ad2 Merge branch 'main' into dependabot/github_actions/aquasecurity/trivy-action-0.35.0
Former-commit-id: abe6e2e400
2026-04-03 14:32:11 +02:00
Marc Schäfer
89fa24e7a5 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

Former-commit-id: f432a17c16
2026-04-03 14:31:45 +02:00
Marc Schäfer
3479cd0647 Merge branch 'main' into dependabot/github_actions/docker/login-action-4.0.0
Former-commit-id: 6f96169ff1
2026-04-03 14:31:00 +02:00
Marc Schäfer
92c73cf019 Merge branch 'main' into dependabot/github_actions/docker/build-push-action-7.0.0
Former-commit-id: 575942c4be
2026-04-03 14:29:10 +02:00
dependabot[bot]
86d2e51386 chore(nix): fix hash for updated go dependencies
Former-commit-id: 16864fc1d7
2026-04-03 12:28:34 +00:00
dependabot[bot]
736fb19d3b 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>
Former-commit-id: f925c681d2
2026-04-03 12:27:19 +00:00
Marc Schäfer
d6ac75fd50 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

Former-commit-id: e01b0ae9c7
2026-04-03 14:26:08 +02:00
Owen
e3105ad4ce Add provisioning blueprint file
Former-commit-id: f4d071fe27
2026-04-02 21:39:59 -04:00
Owen
05be670f1c Send health checks to the server on reconnect
Former-commit-id: 8d82460a76
2026-03-31 17:06:07 -07:00
Owen
4ac3dea71b Add name to provisioning
Former-commit-id: 5208117c56
2026-03-30 17:18:22 -07:00
Owen
c969f44071 Merge branch 'main' into logging-provision
Former-commit-id: 381f5a619c
2026-03-29 21:19:53 -07:00
Owen Schwartz
9293c71fb5 Merge pull request #285 from fosrl/dev
1.10.4

Former-commit-id: b6f13a1b55
2026-03-29 12:25:10 -07:00
Owen
c191ff6cad Add chain id to ping
Former-commit-id: cdaf4f7898
2026-03-29 12:00:17 -07:00
Owen
74b7caf7ec Merge branch 'main' into dev
Former-commit-id: d4a5ac8682
2026-03-29 11:40:34 -07:00
Owen
ee1911709f Add chainId based dedup
Former-commit-id: 1057013b50
2026-03-27 11:55:34 -07:00
Owen
2cf60d0fdb Allow blueprint interpolation for env vars
Former-commit-id: fc4b375bf1
2026-03-26 20:05:04 -07:00
Owen
7f67714707 Add --config-file
Former-commit-id: baca04ee58
2026-03-26 17:31:04 -07:00
Owen
1a4cdf7fc3 Provisioning key working
Former-commit-id: b43572dd8d
2026-03-26 17:23:19 -07:00
Owen
195fb57f03 Process log to form sessions
Former-commit-id: 69019d5655
2026-03-24 17:26:44 -07:00
Owen
d3fef27949 Saving and sending access logs pass 1
Former-commit-id: 0f57985b6f
2026-03-23 16:39:01 -07:00
dependabot[bot]
32a689151e chore(nix): fix hash for updated go dependencies
Former-commit-id: 212bdf765a
2026-03-19 02:16:03 +00:00
dependabot[bot]
3cdad1f371 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>
Former-commit-id: b045a0f5d4
2026-03-19 02:14:44 +00:00
Owen Schwartz
c5db50c948 Merge pull request #274 from LaurenceJJones/refactor/proxy-cleanup-basics
refactor(proxy): cleanup basics - constants, remove dead code, fix de…

Former-commit-id: a2683eb385
2026-03-18 15:39:43 -07:00
Owen Schwartz
0e690089f5 Merge pull request #280 from LaurenceJJones/fix/healthcheck-ipv6
fix(healthcheck): Support ipv6 healthchecks

Former-commit-id: d3722c2519
2026-03-18 15:38:15 -07:00
Laurence
129ca0ad87 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


Former-commit-id: 8fda35db4f
2026-03-18 13:37:31 +00:00
Owen Schwartz
439685ec2a Merge pull request #269 from LaurenceJJones/feature/pprof-endpoint
feat(admin): Add pprof endpoints

Former-commit-id: de4353f2e6
2026-03-17 11:42:08 -07:00
Owen Schwartz
acdb460eba Merge pull request #279 from fosrl/dev
1.10.3

Former-commit-id: b398f531f0
2026-03-16 16:47:39 -07:00
Owen
824606e8fb Bump ping interval up
Former-commit-id: 8161fa6626
2026-03-16 14:33:40 -07:00
Owen
87d03bd589 Remove redundant info
Former-commit-id: 24dfb3a8a2
2026-03-16 13:50:45 -07:00
Laurence
3ccf13354c 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


Former-commit-id: 13448f76aa
2026-03-16 14:11:14 +00:00
Owen
acbbce787c Send disconnecting message
Former-commit-id: d4ebb3e2af
2026-03-15 17:42:03 -07:00
Erwan Leboucher
17a4db6810 feat: watch blueprint.yaml for update and auto resend
Add a file watcher that re-pushes the blueprint to the server whenever
the blueprint file is modified while Newt is running, matching the
reactive behavior of the Docker event monitor.


Former-commit-id: 025497138d
2026-03-15 16:14:00 +01:00
Owen
7ddcfe968d Clean up to match olm
Former-commit-id: bf029b7bb2
2026-03-14 11:57:37 -07:00
Owen
19f1f38dcf Merge branch 'dev' into msg-opt
Former-commit-id: 745d2dbc7e
2026-03-13 17:10:49 -07:00
Owen
ca22eb9a96 Clean up previous logging
Former-commit-id: c7b01288e0
2026-03-13 11:45:36 -07:00
Owen
b6ed0c7b57 Add optional compression
Former-commit-id: 539e595c48
2026-03-12 17:49:05 -07:00
Owen
72ea572acb Allow passing public dns into resolve
Former-commit-id: ef03b4566d
2026-03-12 16:41:41 -07:00
Laurence
82074919d1 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


Former-commit-id: 836144aebf
2026-03-12 09:22:50 +00:00
Owen
8bc4e414e7 Merge branch 'dev' of github.com:fosrl/newt into dev
Former-commit-id: a1df3d7ff0
2026-03-11 17:28:16 -07:00
Laurence
1ba0d23a16 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


Former-commit-id: d68a13ea1f
2026-03-10 10:01:28 -07:00
dependabot[bot]
a5a0f42890 chore(nix): fix hash for updated go dependencies
Former-commit-id: d7741df514
2026-03-09 10:29:50 +00:00
dependabot[bot]
cc7a19bace chore(nix): fix hash for updated go dependencies
Former-commit-id: 8e188933a2
2026-03-09 10:29:45 +00:00
dependabot[bot]
59afb7da69 chore(nix): fix hash for updated go dependencies
Former-commit-id: a13c7c6e65
2026-03-09 10:29:43 +00:00
dependabot[bot]
12a3e987b2 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>
Former-commit-id: bc44ca1aba
2026-03-09 10:28:02 +00:00
dependabot[bot]
30ee82af2a 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>
Former-commit-id: a76089db98
2026-03-09 10:27:59 +00:00
dependabot[bot]
4774ad9054 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>
Former-commit-id: 627ec2fdbc
2026-03-09 10:27:55 +00:00
Owen
9377df6526 Set newt version in dockerfile
Former-commit-id: 44ca592a5c
2026-03-08 11:28:56 -07:00
Owen
199f936046 Set newt version in dockerfile
Former-commit-id: accac75a53
2026-03-08 11:26:35 -07:00
Laurence
4ce7b433ff 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.


Former-commit-id: 768415f90b
2026-03-07 21:32:36 -08:00
Owen
bc5eaa9a35 Make sure to set version and fix prepare issue
Former-commit-id: e1edbcea07
2026-03-07 12:34:55 -08:00
Owen
a99c57b5e1 Merge branch 'main' into dev
Former-commit-id: da9825d030
2026-03-07 12:34:45 -08:00
Owen
e03963003d Make sure to set version and fix prepare issue
Former-commit-id: afdb1fc977
2026-03-07 12:32:49 -08:00
Owen
5b44966809 Make sure to skip prepare
Former-commit-id: 392e4c83bf
2026-03-07 10:37:44 -08:00
Owen
a8063cdffe Make sure to skip prepare
Former-commit-id: 1bd1133ac2
2026-03-07 10:36:18 -08:00
Owen
6d4f51078d Build full arn
Former-commit-id: a85454e770
2026-03-07 10:20:18 -08:00
Owen
838f99686b Build full arn
Former-commit-id: fac0f5b197
2026-03-07 10:17:14 -08:00
Marc Schäfer
1d57344168 fix(ci): Refactor CI/CD workflow for AWS and image management
Updated CI/CD workflow to improve AWS role handling and image tagging.

Former-commit-id: 068145c539
2026-03-07 10:07:55 -08:00
Marc Schäfer
ddc98070bb fix(ci): Use AWS SelfHosted runner to fix pull and install request limit
Former-commit-id: 91a035f4ab
2026-03-07 10:07:55 -08:00
Owen
72e2ebc0b2 Temp lets ignore the sync messages
Former-commit-id: e68b65683f
2026-03-06 15:14:48 -08:00
Owen
362659ef28 Merge branch 'dev' into msg-opt
Former-commit-id: 7d6825132b
2026-03-03 16:56:41 -08:00
Owen Schwartz
cc7e6a8b25 Merge pull request #250 from fosrl/dev
1.10.2

Former-commit-id: beaf386615
2026-03-03 16:49:18 -08:00
Owen
d4edcf39f1 Fix icmp when ports disabled
Fixes #247


Former-commit-id: e474866f84
2026-03-03 16:38:11 -08:00
Owen
372aa726c1 Fix --port
Former-commit-id: 7920295b8c
2026-03-03 16:27:57 -08:00
Owen
d4a515cd76 Update the get all rules
Former-commit-id: 6371e980d2
2026-03-03 16:11:32 -08:00
Owen Schwartz
0040d66fd9 Merge pull request #249 from fosrl/dependabot/go_modules/prod-minor-updates-26e8ddd78e
chore(deps): bump the prod-minor-updates group with 3 updates

Former-commit-id: bf7882eacc
2026-03-03 16:10:38 -08:00
dependabot[bot]
e6b0cd3807 chore(nix): fix hash for updated go dependencies
Former-commit-id: 86a7d79092
2026-03-03 09:46:50 +00:00
dependabot[bot]
4fbeca9ab1 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>
Former-commit-id: 1c22555bd4
2026-03-03 09:45:25 +00:00
Owen Schwartz
74cf94639e 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

Former-commit-id: 30ade8102d
2026-03-02 21:02:09 -08:00
Owen
7815fe4074 Mutex on handlers, slight change to ping message and handler
Former-commit-id: 15ea631b96
2026-03-02 20:56:36 -08:00
Owen
301bba3b08 Working on message versioning
Former-commit-id: 4e854b5f96
2026-03-02 20:56:18 -08:00
Owen
4daf103b5b Add version and send it down
Former-commit-id: 287eef0f44
2026-03-02 18:27:26 -08:00
Owen
99a429dc1b Merge branch 'dev' into msg-opt
Former-commit-id: f982e6b629
2026-03-02 18:13:55 -08:00
Owen
9a5dfd1bdb Merge branch 'LaurenceJJones-optimize-reverse-nat-lookup' into dev
Former-commit-id: 6c6ba45024
2026-03-02 18:13:12 -08:00
Owen
9f39f77554 Merge branch 'optimize-reverse-nat-lookup' of github.com:LaurenceJJones/newt into LaurenceJJones-optimize-reverse-nat-lookup
Former-commit-id: 3f84354e7f
2026-03-02 18:13:05 -08:00
Owen
6d6717952c Support prefixes sent from server
Former-commit-id: 039ae07b7b
2026-03-02 18:11:20 -08:00
Owen Schwartz
3120d10f7e Merge pull request #204 from LaurenceJJones/optimize-subnet-lookup-bart
perf(netstack2): optimize subnet rule matching with BART

Former-commit-id: ec399e9d4d
2026-03-02 18:10:01 -08:00
Marc Schäfer
157d48bb13 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

Former-commit-id: 19f143fc6a
2026-03-03 00:51:39 +01:00
Marc Schäfer
6776e4c706 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

Former-commit-id: 6d5e099681
2026-03-03 00:51:13 +01:00
Marc Schäfer
4ac0add080 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

Former-commit-id: d6ea37f45c
2026-03-03 00:50:44 +01:00
Marc Schäfer
d00eab0651 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

Former-commit-id: de3d358fe9
2026-03-03 00:50:15 +01:00
Marc Schäfer
add13d7379 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

Former-commit-id: 4bcd2ffc59
2026-03-03 00:49:45 +01:00
dependabot[bot]
a2f3341e81 chore(nix): fix hash for updated go dependencies
Former-commit-id: 06e175ac9a
2026-03-02 19:55:17 +00:00
dependabot[bot]
ac8791dab1 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>
Former-commit-id: 6e79eef224
2026-03-02 19:53:59 +00:00
Marc Schäfer
3abe585227 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

Former-commit-id: 23dc230654
2026-03-02 20:50:35 +01:00
dependabot[bot]
71749a4ade chore(nix): fix hash for updated go dependencies
Former-commit-id: 9b8bc7b66e
2026-03-01 21:07:12 +00:00
dependabot[bot]
3fcf1f7427 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>
Former-commit-id: c5ae4a8f11
2026-03-01 21:05:54 +00:00
Owen
9353186b60 Pull from ecr
Former-commit-id: b2600b0dab
2026-02-26 21:48:11 -08:00
Owen Schwartz
41b90d8f30 Merge pull request #245 from fosrl/dev
generate random user password

Former-commit-id: 1656141599
2026-02-26 20:55:37 -08:00
miloschwartz
5ba9e6702c generate random user password
Former-commit-id: 1bf89a2cc9
2026-02-26 10:30:24 -08:00
Owen
d1df2c2830 Update iss
Former-commit-id: 555e1ddc7c
2026-02-25 14:57:32 -08:00
Owen
e4abce97c1 Fix port parsing
Fixes #243


Former-commit-id: 514c94519e
2026-02-25 11:52:15 -08:00
Owen Schwartz
0a6fd9b7c8 Merge pull request #242 from fosrl/dev
1.10.0

Former-commit-id: 1a3eaedfa5
2026-02-22 16:36:25 -08:00
Owen
b2c102cdec Merge branch 'main' into dev
Former-commit-id: 01e2ba31b7
2026-02-22 16:36:15 -08:00
Laurence
4d478ba276 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.


Former-commit-id: 9738565a3a
2026-02-22 14:22:23 +00:00
Laurence
8f22937c1b 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

Former-commit-id: c42a606bbd
2026-02-22 14:22:20 +00:00
Laurence Jones
ce290a1ba2 Merge branch 'fosrl:main' into optimize-reverse-nat-lookup
Former-commit-id: 5977667291
2026-02-22 12:09:01 +00:00
miloschwartz
cadd64091a support sudo configuration and daemon mode
Former-commit-id: 556be90b7e
2026-02-20 20:42:42 -08:00
Owen
f0618cf3ab Allow sudo passwordless
Former-commit-id: 5d04be92f7
2026-02-17 22:36:28 -08:00
miloschwartz
d6a29673ab fix flag
Former-commit-id: b7af49d759
2026-02-17 22:10:01 -08:00
Owen
78219f1302 Add daemon into newt
Former-commit-id: 00a5fa1f37
2026-02-17 22:10:01 -08:00
miloschwartz
889e0439a8 remove defaults
Former-commit-id: d256d6c746
2026-02-17 22:10:01 -08:00
miloschwartz
2f7843859b add auth daemon
Former-commit-id: 2cc957d55f
2026-02-17 22:10:01 -08:00
Owen
01c3c6de7e Add round trip tracking for any message
Former-commit-id: d98eaa88b3
2026-02-17 22:10:01 -08:00
Owen
2649a125cd Add basic newt command relay to auth daemon
Former-commit-id: 5b884042cd
2026-02-17 22:10:01 -08:00
Owen
95542c7cb7 Remove legacy ssh
Former-commit-id: 2265b61381
2026-02-17 22:10:01 -08:00
miloschwartz
a1b1e495d2 fix flag
Former-commit-id: 60dac98514
2026-02-17 21:01:10 -08:00
Owen
dcf953b495 Add daemon into newt
Former-commit-id: 759e4c5bac
2026-02-17 14:44:28 -08:00
miloschwartz
b705d52660 remove defaults
Former-commit-id: 8609be130e
2026-02-16 20:50:13 -08:00
miloschwartz
51faf1637e add auth daemon
Former-commit-id: e06b8de0a7
2026-02-16 20:36:13 -08:00
Owen
1246725e02 Add round trip tracking for any message
Former-commit-id: 0af6fb8fef
2026-02-16 20:29:19 -08:00
Owen
7cd12be043 Add basic newt command relay to auth daemon
Former-commit-id: 9526768dfe
2026-02-16 20:04:33 -08:00
Owen
8ad2deea92 Remove legacy ssh
Former-commit-id: 051ab6ca9d
2026-02-16 17:55:17 -08:00
Owen
95bb165e10 Update example domain
Former-commit-id: 50fbfdc262
2026-02-16 17:54:19 -08:00
dependabot[bot]
4dcafc9abd 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>
Former-commit-id: 5fb60baa14
2026-02-16 10:42:19 +00:00
dependabot[bot]
262f939f78 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>
Former-commit-id: ea2e166973
2026-02-02 10:54:56 +00:00
dependabot[bot]
1a6a9699e1 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>
Former-commit-id: a8549f32e9
2026-02-02 10:54:51 +00:00
dependabot[bot]
6e0c1dc383 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>
Former-commit-id: ad6bbd47ad
2026-02-02 10:54:45 +00:00
dependabot[bot]
e0bb4808b9 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>
Former-commit-id: f6b7aaedfd
2026-01-26 10:50:02 +00:00
Owen
31c36dffb9 Merge branch 'main' of github.com:fosrl/newt into dev
Former-commit-id: 2055b773fd
2026-01-21 15:59:03 -08:00
Owen
a28e28cc5d Show download script to update
Former-commit-id: 1c9c98e2f6
2026-01-19 21:25:28 -08:00
dependabot[bot]
a217da8c13 chore(nix): fix hash for updated go dependencies
Former-commit-id: 9c57677493
2026-01-19 17:33:19 -08:00
dependabot[bot]
641d5a74ac 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>
Former-commit-id: ff825a51dd
2026-01-19 17:33:19 -08:00
dependabot[bot]
6bb6c52e26 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>
Former-commit-id: cdfc5733f0
2026-01-19 17:33:02 -08:00
dependabot[bot]
b8a6e492f9 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>
Former-commit-id: cadbb50bdf
2026-01-19 17:32:56 -08:00
dependabot[bot]
634a62693a 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>
Former-commit-id: 4ac33c824b
2026-01-19 17:32:46 -08:00
dependabot[bot]
acc8f7a409 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>
Former-commit-id: d91228f636
2026-01-19 17:32:25 -08:00
dependabot[bot]
432f2ff7b2 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>
Former-commit-id: 6c3b85bb9a
2026-01-19 17:32:18 -08:00
Owen
fa4217a31d Add stale bot
Former-commit-id: 77d99f1722
2026-01-19 17:11:48 -08:00
Owen
dcab3a46d0 Disable metrics by default
Former-commit-id: 43e1341352
2026-01-18 15:20:13 -08:00
Owen
2712bcda8c Dont block waiting for a rebind signal
Former-commit-id: daa1a90e05
2026-01-18 11:36:42 -08:00
Owen
7609599129 Handle rebind in the polling function
Former-commit-id: 3739c237c7
2026-01-18 11:36:30 -08:00
Owen
ddccf9b200 Try to close the socket first
Former-commit-id: ddde1758e5
2026-01-17 17:35:10 -08:00
Owen
8ca03eadcd Rebind in shared bind
Former-commit-id: dca29781f3
2026-01-17 17:06:01 -08:00
Owen
1616fd9d68 Filter out no bandwidth peers
Former-commit-id: 91bfd69179
2026-01-16 17:54:05 -08:00
Owen
0954f213dc Allow updating the intervals
Former-commit-id: 060d876429
2026-01-14 17:09:27 -08:00
Owen
2a2bb19ea3 Fix bug where not all routes are added
Former-commit-id: 69952efe89
2026-01-12 16:01:15 -08:00
Owen
95453cec15 Merge branch 'mobile' of github.com:fosrl/newt into mobile
Former-commit-id: 66949ca047
2026-01-12 14:22:01 -08:00
Owen
7ed9e258ba Try to improve cpu usage
Former-commit-id: 8c12db6dff
2026-01-12 14:21:05 -08:00
Owen
1f9e84d4aa Add noop for android ios
Former-commit-id: b84d465763
2026-01-12 12:31:38 -08:00
miloschwartz
586ccae211 quiet and logs and fix ios errors
Former-commit-id: a62567997d
2026-01-01 17:29:02 -05:00
Owen
1e243a0cdc Fix incrementor not updating; restrict routes to darwin
Former-commit-id: 9bb4bbccb8
2025-12-31 15:58:04 -05:00
Owen
00538f0c7c Handle android and ios in routes
Former-commit-id: c3fad797e5
2025-12-31 15:43:16 -05:00
Owen
dfb71f20ff Add mobile subs for permission
Former-commit-id: 0168b4796e
2025-12-30 10:31:35 -05:00
Owen
2c6c1dc1fe Merge branch 'main' into dev
Former-commit-id: 6c05d76c88
2025-12-24 15:18:11 -05:00
Owen
fd623ca47d Reuse http client for each target
Fixes #220


Former-commit-id: a701add824
2025-12-24 10:58:46 -05:00
Owen
8deb6b226a Dont run on v tags
Former-commit-id: d754cea397
2025-12-23 17:54:31 -05:00
Owen
45153dd7b8 Quiet up HandleIncomingPacket
Former-commit-id: 31d52ad3ff
2025-12-23 10:29:15 -05:00
Owen
b87b8c3168 Fix latest tag
Former-commit-id: e1ee4dc8f2
2025-12-22 21:32:47 -05:00
Varun Narravula
dfad7d852a ci: update nix go vendor hash if needed for dependabot PRs
Former-commit-id: f9b6f36b4f
2025-12-22 19:43:48 -05:00
Varun Narravula
d0b1876ac4 chore: add direnv and nix result dirs to gitignore
Former-commit-id: 0e961761b8
2025-12-22 19:43:48 -05:00
Varun Narravula
51dc2ca875 ci: build nix package when go.mod is changed
Former-commit-id: baf1b9b972
2025-12-22 19:43:48 -05:00
Varun Narravula
c50a2ea93b fix(nix): disable tests, set meta.mainProgram for package
Former-commit-id: f078136b5a
2025-12-22 19:43:48 -05:00
Varun Narravula
2fbed8b879 chore(nix): sync version number with latest version
Former-commit-id: ca341a8bb0
2025-12-22 19:43:48 -05:00
Owen
14757e575b Merge branch 'dev'
Former-commit-id: 80ae03997a
2025-12-22 16:15:41 -05:00
Owen
03c9e957a4 Quiet up logs
Former-commit-id: 5c94789d9a
2025-12-22 14:31:44 -05:00
Owen
0f8ac38b66 Fix makefile cicd binaries
Former-commit-id: 6c65cc8e5e
2025-12-21 21:34:56 -05:00
Owen
5d4ec7a52c Add back release and binaries
Former-commit-id: a21a8e90fa
2025-12-21 21:01:04 -05:00
Owen
7d331c5c61 Add back release and binaries
Former-commit-id: 3d5335f2cb
2025-12-21 21:00:45 -05:00
Owen Schwartz
902063d207 Merge pull request #214 from fosrl/dev
1.8.0-rc.0

Former-commit-id: 94788edce3
2025-12-21 20:59:32 -05:00
Owen
f15f86364d Merge branch 'main' into dev
Former-commit-id: 2bbe037544
2025-12-21 20:57:45 -05:00
Owen
9fc6cccb30 Tie siteIds to exit node
Former-commit-id: 9b015e9f7c
2025-12-19 10:54:21 -05:00
Owen
8a2df2cc3c Prevent sigsegv with bad address
Fixes #210
Fixes #201


Former-commit-id: 3305f711b9
2025-12-18 10:29:37 -05:00
Owen
78d4337748 Take 21820 from config
Former-commit-id: ff7fe1275b
2025-12-16 18:35:25 -05:00
Owen
aff9c1ee49 Take 21820 from config
Former-commit-id: 1cbf41e094
2025-12-16 18:33:05 -05:00
Owen Schwartz
7c112def58 Merge pull request #208 from fosrl/icmp2
Support ICMP test requests for clients

Former-commit-id: 9bc35433ef
2025-12-16 17:19:22 -05:00
Owen
94c5e72b8b Install iputils not ping
Former-commit-id: b8349aab4e
2025-12-16 17:16:58 -05:00
Owen
fbdfd16633 Merge branch 'dev' into icmp2
Former-commit-id: 3f29a553ae
2025-12-16 17:15:15 -05:00
Owen
2de9b9e3e0 Merge branch 'main' into dev
Former-commit-id: 745045f619
2025-12-16 17:15:06 -05:00
Owen
3c7d491133 Add fallback to non privileged ping
Former-commit-id: 3783a12055
2025-12-16 17:05:36 -05:00
Owen
fd1f2fac40 Disabling icmp ping
Former-commit-id: a9b84c8c09
2025-12-16 16:30:14 -05:00
Owen
dd04fdd1df Merge branch 'dev' into icmp2
Former-commit-id: 5c5ef4c7e6
2025-12-16 13:48:00 -05:00
Owen
7e00e1ad26 Add disable icmp
Former-commit-id: 6e9249e664
2025-12-16 13:47:45 -05:00
Owen
0c482bf0ce Handle reply correctly
Former-commit-id: 55be2a52a5
2025-12-16 12:23:12 -05:00
Owen
5f2d9115c9 Icmp2
Former-commit-id: 058330d41b
2025-12-16 12:05:59 -05:00
Laurence
9762db4853 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.


Former-commit-id: 1cf75b00ff
2025-12-16 08:16:37 +00:00
Owen Schwartz
e9a526814e Merge pull request #203 from fosrl/port-firewall
Port firewalling for Private Resources

Former-commit-id: 5e7b970115
2025-12-15 22:16:02 -05:00
Owen
e9e9f8fc20 Add test udp server and client
Former-commit-id: dc180abba9
2025-12-15 22:11:57 -05:00
Owen
a832405e5f Allow proto restriction
Former-commit-id: 004bb9b12d
2025-12-15 18:37:34 -05:00
Owen
fc493abbbb Fix healthcheck interval not resetting
Ref PAN-158


Former-commit-id: 0637360b31
2025-12-15 12:10:47 -05:00
Owen Schwartz
59cae0d9b0 Merge pull request #200 from water-sucks/parallelize-makefile
feat(build): parallelize go-build-release and github actions with matrix

Former-commit-id: d5e0771094
2025-12-13 09:51:59 -05:00
Varun Narravula
7ecd70aa1c feat(ci): use matrix for building all makefile targets in parallel
Former-commit-id: 1dcb68d694
2025-12-12 15:51:53 -08:00
Varun Narravula
f3e59a822c feat(build): double-tag docker release builds with "latest" and tag name
Former-commit-id: 865ac4b682
2025-12-12 15:51:20 -08:00
Varun Narravula
9b8aacd1a8 feat(build): parallelize go-build-release using separate arch rules
Former-commit-id: de5627b0b7
2025-12-12 15:51:15 -08:00
Owen
bb93fee614 Print version before otel
Former-commit-id: 44470abd54
2025-12-12 14:32:12 -05:00
Owen
ec4619583f Remove accidental file
Former-commit-id: 4bb0537c39
2025-12-11 23:27:13 -05:00
Owen
f78c900d54 Fix test
Former-commit-id: 92fb96f9bd
2025-12-11 23:24:14 -05:00
Owen Schwartz
9c5aab638b Merge pull request #199 from water-sucks/update-nix-hash
fix(nix): use correct hash for vendored deps

Former-commit-id: b68b7fe49d
2025-12-11 23:21:57 -05:00
Varun Narravula
e0fdb1bf33 feat(nix): sync version number
Former-commit-id: 1da424bb20
2025-12-11 17:53:07 -08:00
Varun Narravula
da9ad06a44 fix(nix): use correct hash for vendored deps
Former-commit-id: 22e5104a41
2025-12-11 17:52:52 -08:00
Owen
ba51b4b5ea Make sure to process version first
Former-commit-id: b96adeaa5b
2025-12-11 19:35:08 -05:00
Owen
a001b372c5 Update cicd
Former-commit-id: 533e0b9ca7
2025-12-11 16:40:50 -05:00
Owen
9fcfc6b236 Make cicd create draft
Former-commit-id: bd86abe8d5
2025-12-11 16:09:24 -05:00
Owen
7610ee6550 Merge branch 'dev'
Former-commit-id: d978b27ebc
2025-12-11 16:01:59 -05:00
Owen
21f34d4acc Fix host header not working in health checks
Former-commit-id: cdfcf49d89
2025-12-11 14:20:52 -05:00
Owen
073fa30d66 Update iss
Former-commit-id: 2fb4bf09ea
2025-12-11 12:29:49 -05:00
Owen
61e0e6e5e4 Merge branch 'windows' into dev
Former-commit-id: dddae547f5
2025-12-11 12:22:03 -05:00
Owen
0b904d0ba8 Adjust debug function
Former-commit-id: 73a14f5fa1
2025-12-11 12:21:54 -05:00
Owen
2b66535db2 Add iss file
Former-commit-id: 67d5217379
2025-12-11 11:57:06 -05:00
Owen
cb745081aa Update readme
Former-commit-id: 9f1f1328f6
2025-12-10 16:24:22 -05:00
Owen
5070264599 Kind of working
Former-commit-id: 30da7eaa8b
2025-12-10 15:32:49 -05:00
Owen
7689682a5a Rename logs, optional port
Former-commit-id: 0fca3457c3
2025-12-10 14:07:53 -05:00
Owen Schwartz
c5b357c0f6 Merge pull request #197 from water-sucks/fix-nix-flake
fix(nix): use new version number, update deps hash

Former-commit-id: 1271e8235e
2025-12-09 18:22:57 -05:00
Varun Narravula
8b0831823e fix(nix): sync release version
Former-commit-id: 24c6edf3e0
2025-12-09 15:16:46 -08:00
Varun Narravula
7f89eba525 fix(nix): update go deps hash
Former-commit-id: 1875c987fe
2025-12-09 15:16:37 -08:00
Owen
400318bbf1 Working on ipv6 stuff
Former-commit-id: 7cb1f7e2c2
2025-12-09 17:10:38 -05:00
Owen Schwartz
3476e8ebe1 Merge pull request #196 from marcschaeferger/release/1.6.1
Fix Isssue 194 failed go test before 1.7.0-rc1

Former-commit-id: 3f4f4fa15c
2025-12-09 17:09:47 -05:00
Marc Schäfer
30ee22b774 testdata: add expected telemetry metrics for connection attempts and events
Former-commit-id: bf33a3d81f
2025-12-09 22:51:48 +01:00
Owen
545a98928f Fix formatting?
Former-commit-id: 21ffc0ff4b
2025-12-08 15:45:29 -05:00
Owen
911d370173 Revert some to secret
Former-commit-id: 13de05eec6
2025-12-08 15:40:52 -05:00
Owen
5275cca272 Set username explicitly
Former-commit-id: 0e76b77adc
2025-12-08 15:38:56 -05:00
Owen
f64f009469 Remove bad test
Former-commit-id: c604f46065
2025-12-08 14:18:40 -05:00
Owen
1145bb7611 Update to secrets
Former-commit-id: f02e29f4dd
2025-12-08 14:12:03 -05:00
Owen
2afb855369 Merge branch 'dev'
Former-commit-id: 6d79856895
2025-12-08 12:17:29 -05:00
Owen
39bc78c728 Make ipc cross platform
Former-commit-id: bbece243dd
2025-12-08 12:17:11 -05:00
Owen Schwartz
824390a664 Merge pull request #192 from fosrl/dev
Add robust client connectivity support

Former-commit-id: 6948066ae4
2025-12-08 12:05:03 -05:00
Owen
097ac5068b Handle server version and prevent backward issues with clients
Former-commit-id: 3bcafbf07a
2025-12-08 11:48:14 -05:00
Owen
28cea95eb0 Update readme
Former-commit-id: 87e2eb33db
2025-12-07 21:31:28 -05:00
Owen
e366c7d83f Fix adding new exit nodes to hp not sending interval
Former-commit-id: 5ce3f4502d
2025-12-07 12:05:39 -05:00
Owen
e1e812b419 Merge branch 'main' into dev
Former-commit-id: e5e733123b
2025-12-06 21:09:48 -05:00
Owen Schwartz
11eb0aebc0 Merge pull request #186 from fosrl/dependabot/docker/minor-updates-60be0b6e22
Bump alpine from 3.22 to 3.23 in the minor-updates group

Former-commit-id: f417ee32fb
2025-12-06 12:00:18 -05:00
Owen Schwartz
01e8b29d48 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

Former-commit-id: 37c96d0b3e
2025-12-06 12:00:11 -05:00
Owen Schwartz
7dadba1a09 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

Former-commit-id: 78dc39e153
2025-12-06 12:00:05 -05:00
Owen Schwartz
c71dfd848f 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

Former-commit-id: 71485743ad
2025-12-06 11:59:57 -05:00
Owen Schwartz
653a2eaec1 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

Former-commit-id: 458912e5be
2025-12-06 11:59:51 -05:00
Owen Schwartz
ef5f9929b8 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

Former-commit-id: 2bc91d6c68
2025-12-06 11:59:46 -05:00
Owen Schwartz
6ac88ee946 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

Former-commit-id: 95c3efc365
2025-12-06 11:59:40 -05:00
Owen
3f072b3c8f Localhost working - is this the best way to do it?
Former-commit-id: 72a9e111dc
2025-12-05 16:33:43 -05:00
dependabot[bot]
862fb3f7a0 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>
Former-commit-id: 3c86edf0d5
2025-12-05 09:16:49 +00:00
dependabot[bot]
ccba829a3e 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>
Former-commit-id: 32b1b817ac
2025-12-05 09:16:47 +00:00
Owen
93e924eae1 Support connection testing in native
Former-commit-id: 02949be245
2025-12-04 21:48:32 -05:00
Owen
28ab22797a Check permissions
Former-commit-id: 6d51cbf0c0
2025-12-04 21:39:32 -05:00
Owen
560fa38cf8 Change DNS lookup to conntrack
Former-commit-id: 4dbf200cca
2025-12-04 20:13:48 -05:00
Owen
9e5a8b1cef Change to disable clients
Former-commit-id: d8b4fb4acb
2025-12-04 20:13:35 -05:00
Owen Schwartz
69b7ce51b4 Merge pull request #185 from water-sucks/add-tls-server-name-to-healthchecks
feat(healthcheck): add TLS SNI header to request when needed

Former-commit-id: ac691517ae
2025-12-04 14:41:16 -05:00
Varun Narravula
f46445f57e 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.


Former-commit-id: 8a45f6fd63
2025-12-04 14:27:04 -05:00
Owen Schwartz
d1f557b6fe Merge pull request #184 from water-sucks/nix-fixes-this
fix(nix): resolve issues and revamp the flake

Former-commit-id: 7f650bbfdf
2025-12-04 10:23:20 -05:00
Varun Narravula
1101bb140d chore(nix): sync to latest version number for newt package
Former-commit-id: 15b40b0f24
2025-12-03 23:36:33 -05:00
Varun Narravula
6cedf1dd67 feat(nix): disable cgo for the newt package
Former-commit-id: e27e6fbce8
2025-12-03 23:36:33 -05:00
Varun Narravula
f09e5122a0 chore(nix): add water-sucks to maintainers list
Former-commit-id: f9fb13a0d7
2025-12-03 23:36:33 -05:00
Varun Narravula
7c931dd7af refactor(nix): remove with keyword antipattern
Former-commit-id: 8db50d94c0
2025-12-03 23:36:33 -05:00
Varun Narravula
1d63906358 fix(nix): use correct hash for vendored deps
Former-commit-id: 09568c1aaf
2025-12-03 23:36:26 -05:00
Varun Narravula
d56b67c295 fix(nix): replace version string sub with ldflags, use gitignore on src
Former-commit-id: c7d656214f
2025-12-03 23:33:47 -05:00
Varun Narravula
95558348b3 chore(nix): use nixpkgs-unstable branch and update flake inputs
Former-commit-id: d981a82b1c
2025-12-03 23:33:47 -05:00
Owen
74ed865ec9 Add caching to the dns requests - is this good enough?
Former-commit-id: 5dd5a56379
2025-12-03 22:00:23 -05:00
Owen
9d7801d952 Working on more hp
Former-commit-id: 8c4d6e2e0a
2025-12-03 20:49:46 -05:00
Owen
e156a16a0f Also close the clients
Former-commit-id: 284f1ce627
2025-12-02 11:17:34 -05:00
Owen
bc533acd4a Fix some ipv4 in v6 issues
Former-commit-id: cd466ac43f
2025-12-01 17:54:38 -05:00
Owen
87da12580f Holepunch tester working?
Former-commit-id: 2256d1f041
2025-12-01 17:44:33 -05:00
Owen
0e3c612b2a Handle hp and other stuff
Former-commit-id: 40ca839771
2025-12-01 16:20:30 -05:00
Owen
f1855e5987 Handle holepunches better
Former-commit-id: 01ec6a0ce0
2025-12-01 13:54:14 -05:00
Owen
12cbb78a68 Dont throw errors on cleanup
Former-commit-id: d04f6cf702
2025-11-30 19:45:25 -05:00
Owen
a97a8c4b0e Speed much better!
Former-commit-id: cdaff27964
2025-11-30 11:24:50 -05:00
Owen
c0486068e5 Working but no wgtester? - revert if bad
Former-commit-id: de96be810b
2025-11-29 17:38:34 -05:00
Owen Schwartz
d9d31e02ed 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

Former-commit-id: ba43083f04
2025-11-29 13:03:33 -05:00
Owen
7726a80798 Kind of working - revert if not
Former-commit-id: 5196effdb8
2025-11-26 17:57:27 -05:00
Owen
5c95e57a3d Make hp regular
Former-commit-id: d6edd6ca01
2025-11-26 17:39:10 -05:00
Owen
b265e33f6f Move network to newt - handle --native mode
Former-commit-id: 1b1323b553
2025-11-26 15:06:16 -05:00
Owen
bd61bc64df Rewriting desitnation works
Former-commit-id: bb95d10e86
2025-11-26 14:28:51 -05:00
Owen
5b6d8ab347 Add rewriteTo
Former-commit-id: da04746781
2025-11-25 11:29:41 -05:00
dependabot[bot]
dd88dffc4e 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>
Former-commit-id: a38e0b3e98
2025-11-24 10:00:04 +00:00
dependabot[bot]
10b7b156d4 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>
Former-commit-id: 6ced7b5af0
2025-11-24 09:59:58 +00:00
Owen
c1223fa764 Add utility functions
Former-commit-id: 61b9615aea
2025-11-23 17:07:40 -05:00
dependabot[bot]
773a4dcc88 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>
Former-commit-id: 39f5782583
2025-11-20 02:47:14 +00:00
Owen
c0c07f6d5a Export wireguard logger
Former-commit-id: 025c94e586
2025-11-18 14:53:12 -05:00
Owen
3a76ad55d4 Update logger to take in when initing
Former-commit-id: 75e666c396
2025-11-17 21:49:07 -05:00
Owen
d5c92c9191 Fix resolve
Former-commit-id: 82a999eb87
2025-11-17 18:07:36 -05:00
Owen
b73ec3b82a Update clients
Former-commit-id: 921e72f628
2025-11-17 15:55:24 -05:00
Owen
9ff802c82b Remove native and add util
Former-commit-id: 46b33fdca6
2025-11-17 15:32:22 -05:00
Owen
c2181ad4d2 Make logger extensible
Former-commit-id: 9caa9fa31e
2025-11-17 13:49:43 -05:00
Owen
d34583808f Shift things around - remove native
Former-commit-id: dbbea6b34c
2025-11-17 13:39:32 -05:00
Owen
2d47810883 Remove proxy manager and break out subnet proxy
Former-commit-id: 491180c6a1
2025-11-15 21:46:32 -05:00
Owen
2a3a75f722 Centralize some functions
Former-commit-id: f49a276259
2025-11-15 16:32:02 -05:00
Owen
fd5ec1d671 Update to use new packages
Former-commit-id: c71c6e0b1a
2025-11-15 16:14:40 -05:00
Owen
bfd4298949 UDP WORKING!
Former-commit-id: 972c9a9760
2025-11-14 15:30:26 -05:00
Owen
02d762fc26 TCP WORKING!
Former-commit-id: 8f7ee2a8dc
2025-11-14 15:23:20 -05:00
Owen
3a4cde2264 REmove readme
Former-commit-id: a737c3e8de
2025-11-10 21:37:03 -05:00
Owen
f7913aa173 Experiment
Former-commit-id: 1ba10c1b68
2025-11-10 21:33:31 -05:00
dependabot[bot]
8ead2a5182 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>
Former-commit-id: b1f2fe8283
2025-11-10 11:52:43 +00:00
dependabot[bot]
2290644709 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>
Former-commit-id: a1fdb06add
2025-11-10 11:52:40 +00:00
dependabot[bot]
0f95ef108e 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>
Former-commit-id: 25d5fab02b
2025-11-10 11:52:31 +00:00
Owen
c252e4133b Using 2 nics not working
Former-commit-id: 2c8755f346
2025-11-05 21:46:29 -08:00
Owen
a5bdfb4707 Bring in netstack locally
Former-commit-id: 348cac66c8
2025-11-05 13:39:54 -08:00
Owen
466ec019d7 Merge branch 'main' into dev
Former-commit-id: 6226a262d6
2025-11-04 16:50:11 -08:00
Owen
d96e47b1b0 Merge branch 'main' into dev
Former-commit-id: 5b70feb6a5
2025-11-04 16:50:01 -08:00
Owen Schwartz
409f1c3d37 Merge pull request #169 from marcschaeferger/gh-action
Fix Github CICD Action and add Improvements

Former-commit-id: 0ec18d6655
2025-10-28 21:11:56 -07:00
Marc Schäfer
2736c95bd8 testdata: add expected telemetry metrics for connection attempts and events
Former-commit-id: 7d60240572
2025-10-28 23:17:05 +01:00
Marc Schäfer
e46c79cb97 Added Improvements for CICD Action
Former-commit-id: ee3e7d1442
2025-10-28 23:14:40 +01:00
Owen
2a89273d75 Update cicd
Former-commit-id: 527321a415
2025-10-27 21:26:14 -07:00
Owen
30c6ca08d6 Merge branch 'main' into dev
Former-commit-id: ff07692248
2025-10-27 21:25:20 -07:00
Owen
cfa5178087 Add doc for SKIP_TLS_VERIFY
Former-commit-id: 8d3ae5afd7
2025-10-27 21:25:12 -07:00
Owen
82507f0fb0 Add doc for SKIP_TLS_VERIFY
Former-commit-id: ed99dce7e0
2025-10-27 21:24:22 -07:00
Owen Schwartz
c15f9005c4 Merge pull request #166 from marcschaeferger/gh-action
Adding GHCR to CI/CD Release Workflow & further improvements

Former-commit-id: f1e07272bd
2025-10-20 17:21:05 -07:00
Marc Schäfer
fa1e866dee ci(actions): change runner from ubuntu-latest to amd64-runner for CI/CD workflows
Former-commit-id: a1a3d63fcf
2025-10-21 02:17:49 +02:00
Marc Schäfer
9b8fbe36e7 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


Former-commit-id: 2a273dc435
2025-10-21 00:22:32 +02:00
Marc Schäfer
de445fbd74 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


Former-commit-id: ec05686523
2025-10-21 00:21:28 +02:00
Owen Schwartz
1ac5a93961 Merge pull request #165 from marcschaeferger/ghcr
feat(actions): Sync Images from Docker to GHCR

Former-commit-id: 915e7e44d1
2025-10-20 12:32:41 -07:00
Marc Schäfer
f3fae9d415 feat(actions): Sync Images from Docker to GHCR
Former-commit-id: a729b91ac3
2025-10-20 21:30:31 +02:00
Owen
4a1a4d4313 Update domain
Former-commit-id: ddc37658df
2025-10-19 15:12:15 -07:00
Owen
80c6d0ba18 Merge branch 'dev' of github.com:fosrl/newt into dev
Former-commit-id: 7c780f7a4f
2025-10-16 21:09:41 -07:00
Owen
95b88df5b9 Merge branch 'main' of github.com:fosrl/newt
Former-commit-id: 6b1c1ed077
2025-10-16 21:06:33 -07:00
Owen Schwartz
2844052c8f Merge pull request #162 from marcschaeferger/otel
Adding OpenTelemetry Metrics and Tracing

Former-commit-id: 7a07437b22
2025-10-16 20:48:37 -07:00
Owen
75aa397c5c Add log message that the server is on
Former-commit-id: d63d8d6f5e
2025-10-16 20:42:02 -07:00
Owen
318ca88a91 Add documentation for cli and reporg
Former-commit-id: bda1d04f67
2025-10-16 20:39:41 -07:00
Owen
a44e86e1a7 Update runner
Former-commit-id: 7f8ee37c7f
2025-10-16 17:51:25 -07:00
Marc Schäfer
884b3c9a3c Remove Coolify Code
Former-commit-id: 6d2073a478
2025-10-11 18:46:02 +02:00
Owen Schwartz
9b542e4ea5 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

Former-commit-id: 6048f244f1
2025-10-11 09:41:30 -07:00
Owen Schwartz
41da9c4376 Merge pull request #159 from fosrl/dependabot/go_modules/prod-minor-updates-a55d2abe4a
Bump the prod-minor-updates group with 2 updates

Former-commit-id: 9fec22a53b
2025-10-11 09:41:20 -07:00
Marc Schäfer
2f75e8ea75 Adding OpenTelemetry Metrics and Tracing
Former-commit-id: c086e69dd0
2025-10-11 18:19:51 +02:00
dependabot[bot]
9233fcf919 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>
Former-commit-id: c729ab5fc6
2025-10-09 09:34:56 +00:00
dependabot[bot]
cfcd6f55e5 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>
Former-commit-id: 552617cbb5
2025-10-09 09:34:49 +00:00
Owen Schwartz
4337301c75 Merge pull request #157 from fosrl/dev
No cloud, config file overwriting, hp

Former-commit-id: b383cec0b0
2025-10-08 17:42:45 -07:00
Owen Schwartz
bd6d0c54c9 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

Former-commit-id: fb110ba2a1
2025-10-08 17:40:23 -07:00
dependabot[bot]
41e0d4fd97 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>
Former-commit-id: f287888480
2025-10-08 09:23:09 +00:00
Owen
f39ea4d93a Try to fix overwriting config file
Former-commit-id: 348b8f6b94
2025-10-01 10:31:14 -07:00
miloschwartz
e10af9a0f9 update template
Former-commit-id: 71c5bf7e65
2025-09-29 16:38:49 -07:00
Owen
34a2ab7fc3 Add timeouts to hp
Former-commit-id: dda0b414cc
2025-09-29 14:55:26 -07:00
Owen
b4b59fab7c Add no cloud option
Former-commit-id: 8f224e2a45
2025-09-29 12:25:07 -07:00
Owen Schwartz
70904cc5b7 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

Former-commit-id: 90243cd6c6
2025-09-28 17:58:58 -07:00
Owen Schwartz
7b7270affa Merge pull request #153 from fosrl/dev
Dev

Former-commit-id: 9b79af10ed
2025-09-28 17:58:38 -07:00
Owen
606a549b87 Merge branch 'dev' into docker-events
Former-commit-id: 31b1ffcbe9
2025-09-28 17:44:09 -07:00
dependabot[bot]
8f3b5d3a5d 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>
Former-commit-id: f1c4e1db71
2025-09-28 23:31:05 +00:00
Owen
71756a236d Merge branch 'main' into dev
Former-commit-id: 72a61d0933
2025-09-28 16:27:05 -07:00
Owen
b3105fa077 Merge branch 'main' of github.com:fosrl/newt
Former-commit-id: e489a2cc66
2025-09-28 16:26:58 -07:00
Owen
15ba03b1c5 Pick up the existing interface private key
Former-commit-id: 4e648af8e9
2025-09-28 16:26:36 -07:00
Owen
6af90c18a8 Fix generateAndSaveKeyTo
Former-commit-id: 5d891225de
2025-09-28 11:28:31 -07:00
Owen Schwartz
0f1a06f32d Merge pull request #152 from didotb/didotb-docs-blueprint-file
docs: Add blueprint-file as a new cli arg and env var
Former-commit-id: 9864965381
2025-09-25 18:08:50 -07:00
Owen
0609f57cb0 Add logging to config
Former-commit-id: 75f6362a90
2025-09-25 17:18:28 -07:00
Andrew Barrientos
7df4624a29 docs: Add new cli arg and env var
Include blueprint-file as an option in the cli arguments and environment variable

Former-commit-id: 30907188fb
2025-09-26 06:46:32 +08:00
Owen Schwartz
26ebd03310 Merge pull request #147 from marcschaeferger/Dependency-Update-09-25
Golang Dependency Update 09-2025

Former-commit-id: 5f11df8df2
2025-09-21 20:10:13 -04:00
Owen Schwartz
d0f612a4a5 Merge pull request #146 from marcschaeferger/github-actions
fix(gh-actions): Workflow does not contain permissions

Former-commit-id: 7eea6dd335
2025-09-21 20:09:35 -04:00
Marc Schäfer
1b2ec74395 fix(deps): add missing gopkg.in/yaml.v3 v3.0.1 back
Former-commit-id: 9dc5a3d91c
2025-09-22 00:40:18 +02:00
Marc Schäfer
991cc775d0 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
Former-commit-id: 1881309148
2025-09-22 00:30:33 +02:00
Marc Schäfer
2cbc080242 fix(gh-actions): Workflow does not contain permissions
Former-commit-id: aff928e60f
2025-09-22 00:22:42 +02:00
Owen
b170ebc2c1 Watching socket and quiteting some events
Former-commit-id: f6e7bfe8ea
2025-09-21 11:32:47 -04:00
Owen
3088e92c2c React to docker events
Former-commit-id: 60873f0a4f
2025-09-21 11:19:52 -04:00
Owen Schwartz
6bfc15f552 Merge pull request #132 from fosrl/dependabot/github_actions/actions/setup-go-6
Bump actions/setup-go from 5 to 6

Former-commit-id: 50bb81981b
2025-09-20 11:43:42 -04:00
Owen Schwartz
1b760cb92f Merge pull request #143 from rgutmen/mlts-pkcs12-compatibility
Mlts pkcs12 compatibility

Former-commit-id: 4ced99fa3f
2025-09-20 11:43:24 -04:00
rgutmen
1fd73523cd Support TLS_CLIENT_CERT, TLS_CLIENT_KEY and TLS_CA_CERT in Docker Compose
Former-commit-id: 9bd96ac540
2025-09-20 09:15:58 +01:00
Owen Schwartz
f13bde32d5 Merge pull request #142 from marcschaeferger/main
Add Badges to README.md

Former-commit-id: c673743692
2025-09-19 11:55:03 -04:00
Marc Schäfer
9a03dbbe12 feat(Docs): Add License Badge and PkgGo Badge
Former-commit-id: a08a3b9665
2025-09-19 16:34:44 +02:00
Marc Schäfer
3208e42c69 feat(Docs): Addding GoReport Badge
Former-commit-id: 0fc13be413
2025-09-19 16:25:04 +02:00
Owen
fc5c9390aa Quiet up the logs
Former-commit-id: 92cedd00b3
2025-09-15 10:58:40 -07:00
Owen
7728988194 Add blueprint yaml sending
Former-commit-id: 8b0cc36554
2025-09-08 15:25:05 -07:00
dependabot[bot]
a912040f78 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>
Former-commit-id: ba9ca9f097
2025-09-08 10:14:37 +00:00
Owen
789d5f851c Merge branch 'main' into dev
Former-commit-id: 8b4a88937c
2025-09-06 17:38:46 -07:00
Owen Schwartz
594748cb74 Merge pull request #129 from l3pr-org/main
Implement more privacy-respecting DNS service

Former-commit-id: 58412a7a61
2025-09-04 10:39:33 -07:00
Stanley Wisnioski
6f71f0345b Update README.md
Updated README.md to reflect change of default DNS server from Google to Quad9.

Former-commit-id: 2675b812aa
2025-09-04 10:03:58 -04:00
Stanley Wisnioski
d97cf01ad4 Change DNS Server in clients.go
Changed DNS server from Google (8.8.8.8) to Quad9 (9.9.9.9)

Former-commit-id: 217a9346c6
2025-09-04 10:00:48 -04:00
Stanley Wisnioski
39ee3d05aa Change DNS Server
Changed DNS server from Google (8.8.8.8) to Quad9 (9.9.9.9)

Former-commit-id: eda8073bce
2025-09-04 09:58:43 -04:00
Owen
568b699c32 Ensure backward compatability with --docker-socket
Former-commit-id: 2969f9d2d6
2025-09-02 14:08:24 -07:00
Owen
31057e4947 Ensure backward compatability with --docker-socket
Former-commit-id: 07b7025a24
2025-09-02 13:56:18 -07:00
Owen
c6f94945b8 Make sure to call stop function inside of clients
Former-commit-id: 502ebfc362
2025-09-01 15:45:23 -07:00
Owen
63e924a46b Limit the amount of times the send message sends
Fixes #115


Former-commit-id: 288413fd15
2025-09-01 11:53:46 -07:00
Owen
a6d8e71b7a Print the body for debug
Former-commit-id: 0ba44206b1
2025-09-01 11:51:23 -07:00
Owen
2c7841f285 Update docs with enforce-hc-cert
Former-commit-id: 3f8dcd8f22
2025-09-01 10:59:54 -07:00
Owen
1927e8b54a Allow health check to http self signed by default
Fixes #122


Former-commit-id: c5c0143013
2025-09-01 10:56:08 -07:00
Owen
5196b9113e Merge branch 'main' of github.com:fosrl/newt
Former-commit-id: 87ac5c97e3
2025-08-30 18:07:22 -07:00
Owen
c2d6a97c7b Merge branch 'Pallavikumarimdb-feat/Split-mTLS-client-and-CA-certificates'
Former-commit-id: e2238c3cc8
2025-08-30 18:07:07 -07:00
Owen
97c4d3cb55 Merge branch 'feat/Split-mTLS-client-and-CA-certificates' of github.com:Pallavikumarimdb/newt into Pallavikumarimdb-feat/Split-mTLS-client-and-CA-certificates
Former-commit-id: 58a67328d3
2025-08-30 18:06:18 -07:00
Owen Schwartz
2b1073581e Merge pull request #97 from Nemental/feat/docker-socket-protocol
feat: docker socket protocol
Former-commit-id: 002fdc4d3f
2025-08-30 16:53:21 -07:00
Owen Schwartz
d1812a4060 Merge pull request #117 from fosrl/dependabot/github_actions/docker/setup-buildx-action-3
Bump docker/setup-buildx-action from 2 to 3

Former-commit-id: 9a1fa2c19f
2025-08-30 16:52:06 -07:00
Owen Schwartz
919c2a7616 Merge pull request #118 from fosrl/dependabot/github_actions/actions/setup-go-5
Bump actions/setup-go from 4 to 5

Former-commit-id: a6797172ef
2025-08-30 16:51:59 -07:00
Owen Schwartz
158a34b61a Merge pull request #119 from fosrl/dependabot/github_actions/docker/login-action-3
Bump docker/login-action from 2 to 3

Former-commit-id: d373de7fa1
2025-08-30 16:51:52 -07:00
Owen Schwartz
8f8e8b4ab5 Merge pull request #120 from fosrl/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 3 to 5

Former-commit-id: f876bad632
2025-08-30 16:51:45 -07:00
dependabot[bot]
1afdc3de88 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>
Former-commit-id: 54b096e6a7
2025-08-30 22:26:53 +00:00
dependabot[bot]
a304cbe616 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>
Former-commit-id: 10720afd31
2025-08-30 22:26:50 +00:00
dependabot[bot]
2cb438bb6c 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>
Former-commit-id: 0b37f20d5d
2025-08-30 22:26:47 +00:00
dependabot[bot]
6a4c77b359 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>
Former-commit-id: aa6e54f383
2025-08-30 22:26:43 +00:00
Owen Schwartz
638f25901e Merge pull request #116 from Lokowitz/update-version
Update version

Former-commit-id: 30f8eb9785
2025-08-30 15:26:09 -07:00
Marvin
cb64df557f Update go.mod
Former-commit-id: e765d9c774
2025-08-28 17:34:34 +02:00
Marvin
1c3fb339d8 Update test.yml
Former-commit-id: 3ae4ac23ef
2025-08-28 17:33:59 +02:00
Marvin
532786b567 Update cicd.yml
Former-commit-id: 6a98b90b01
2025-08-28 17:33:39 +02:00
Marvin
96a740e2c2 Update dependabot.yml
Former-commit-id: e0ce9d4e48
2025-08-28 17:33:04 +02:00
Marvin
6e097ab22b Update .go-version
Former-commit-id: 5914c9ed33
2025-08-28 17:32:27 +02:00
Owen Schwartz
33f930b8bf Merge pull request #103 from fosrl/dependabot/go_modules/prod-minor-updates-50897cc7ef
Bump the prod-minor-updates group with 2 updates

Former-commit-id: 109bda961f
2025-08-27 11:02:27 -07:00
Owen Schwartz
ad7778eed9 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

Former-commit-id: c2a93134b1
2025-08-27 11:02:16 -07:00
Owen Schwartz
fd4c406168 Merge pull request #114 from firecat53/1.4.2
Update version to 1.4.2

Former-commit-id: 100d8e6afe
2025-08-27 11:01:18 -07:00
Scott Hansen
3b7f26a2bf Update flake.nix to 1.4.2
Former-commit-id: 04f2048a0a
2025-08-27 10:58:00 -07:00
dependabot[bot]
7fbe36a856 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>
Former-commit-id: 04de5ef8ba
2025-08-25 11:38:15 +00:00
dependabot[bot]
42224b3746 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>
Former-commit-id: e77601cccc
2025-08-25 09:47:03 +00:00
Owen
734cc3fee8 Merge branch 'main' into dev
Former-commit-id: e9752f868e
2025-08-23 12:17:58 -07:00
Owen Schwartz
ece2707214 Merge pull request #108 from firecat53/main
Bugfix for #107. Only update main.go

Former-commit-id: 866afaf749
2025-08-22 21:42:36 -07:00
Owen
3068b92cd8 Add note about config
Former-commit-id: a12ae17a66
2025-08-22 21:34:47 -07:00
Owen
f84cbadce3 Merge branch 'site-targets' into dev
Former-commit-id: e0cba2e5c6
2025-08-19 10:57:25 -07:00
Scott Hansen
8eb81980e8 Bugfix for #107. Only update main.go
Former-commit-id: 79f3db6fb6
2025-08-16 15:25:23 -07:00
Owen Schwartz
a82734dec5 Merge pull request #107 from firecat53/main
Update version to 1.4.1 and update version_replaceme when using nix build

Former-commit-id: 009b4cf425
2025-08-15 09:40:40 -07:00
Scott Hansen
a13d8d9465 Update version to 1.4.1 and update version_replaceme when using nix build
Former-commit-id: 9c28d75155
2025-08-14 11:47:40 -07:00
Owen
c1d8f0a4f7 Merge branch 'main' into dev
Former-commit-id: bad244d0ea
2025-08-13 14:56:02 -07:00
Owen
8993452bcc Adjust logging
Former-commit-id: d013dc0543
2025-08-13 14:18:47 -07:00
Owen
0e7d22e54f Dont override ENV
Fixes #101


Former-commit-id: 0047b54e94
2025-08-12 20:44:34 -07:00
Owen
e4440415f5 Change permissions to 0600
Fixes #104


Former-commit-id: f0c8d2c7c7
2025-08-11 08:15:36 -07:00
Owen
fbba37ec0a Healthcheck working
Former-commit-id: 28b6865f73
2025-08-11 08:14:29 -07:00
Pallavi
074ca33459 Split mTLS client and CA certificates
Former-commit-id: d52f89f629
2025-08-05 01:08:29 +05:30
Owen
2f71585252 Add health checks
Former-commit-id: 289cce3a22
2025-08-03 18:43:43 -07:00
Owen
53988a5f2c Handle adding and removing healthchecks
Former-commit-id: e8612c7e6b
2025-08-03 17:02:15 -07:00
Owen
f6c5b84e8c Add basic heathchecks
Former-commit-id: 6820f8d23e
2025-08-03 16:12:00 -07:00
Owen
e9cab2705c Stop sending requests when you get a terminate
Former-commit-id: 151d0e38e6
2025-08-03 14:47:36 -07:00
Nemental
8fa6e8438c docs: update docker socket part
Former-commit-id: a9d8ec0b1e
2025-07-30 15:28:55 +02:00
Nemental
e86543c8a2 fix: remove hardcoded protocol from socket path
Former-commit-id: e9dbfb239b
2025-07-30 09:36:53 +02:00
Nemental
1ffe6b6b67 feat: checksocket protocol support
Former-commit-id: a79dccc0e4
2025-07-30 09:36:19 +02:00
Nemental
0874bc4786 feat: add type and function for docker endpoint parsing
Former-commit-id: 42dfb6b3d8
2025-07-30 09:31:41 +02:00
Owen Schwartz
b5dacf8d43 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

Former-commit-id: 3ccd755d55
2025-07-29 23:24:19 -07:00
Owen Schwartz
ae6719fcc7 Merge pull request #96 from firecat53/main
Update flake.nix to 1.4.0

Former-commit-id: a0f0b674e8
2025-07-29 23:24:03 -07:00
Owen
3b820ba2df Dont reset dns
Former-commit-id: 9e675121d3
2025-07-29 22:42:54 -07:00
Owen
6ec569aa35 Fix the bind problem by just recreating the dev
TODO: WHY CANT WE REBIND TO A PORT - WE NEED TO FIX THIS BETTER

Former-commit-id: 45d17da570
2025-07-29 20:58:48 -07:00
Owen
45330be3c2 Use the tunnel ip
Former-commit-id: dfba35f8bb
2025-07-29 16:31:42 -07:00
Scott Hansen
70a85233d9 Update flake.nix to 1.4.0
Former-commit-id: 9e73aab21d
2025-07-29 14:14:42 -07:00
dependabot[bot]
65e0c8b0e9 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>
Former-commit-id: e1ddad006a
2025-07-29 16:39:02 +00:00
Owen
15be45f3de Dont print private key
Former-commit-id: 29567d6e0b
2025-07-28 20:07:13 -07:00
Owen
76beba8076 Update readme: env
Former-commit-id: 47321ea9ad
2025-07-28 12:34:38 -07:00
Owen
768215bdfd Update readme: cli
Former-commit-id: abfc9d8efc
2025-07-28 12:12:40 -07:00
Owen
ccb192892b Merge branch 'main' into dev
Former-commit-id: c6929621e7
2025-07-28 12:02:22 -07:00
Owen
5808477e45 Update readme
Former-commit-id: 46993203a3
2025-07-28 12:02:10 -07:00
Owen
bfad013035 SSH not ready
Former-commit-id: 8306084354
2025-07-28 12:02:09 -07:00
Owen
b2037ef61e Compute kind of works now!?
Former-commit-id: 02c1e2b7d0
2025-07-28 12:02:09 -07:00
Owen
cec5188faa Clean up operation
Former-commit-id: ae7e2a1055
2025-07-28 12:02:09 -07:00
Owen Schwartz
d4874de40d Merge pull request #93 from Lokowitz/sync-go-version
Sync go version

Former-commit-id: 88f1335cff
2025-07-28 11:59:10 -07:00
Owen
07c0664691 Netstack working
Former-commit-id: 8bf9c9795b
2025-07-27 10:25:34 -07:00
Marvin
2aea0bd859 modified: go.mod
modified:   go.sum


Former-commit-id: 5d343cd420
2025-07-26 13:25:52 +00:00
Marvin
3ee17f7cd0 go.mod aktualisieren
Former-commit-id: d1473b7e22
2025-07-26 10:32:20 +02:00
Marvin
87011018ff Dockerfile aktualisieren
Former-commit-id: 2efbd7dd6a
2025-07-26 10:31:53 +02:00
Marvin
d46436ccf6 .go-version aktualisieren
Former-commit-id: 82a3a39a1f
2025-07-26 10:31:35 +02:00
Marvin
4c6dc32bca cicd.yml aktualisieren
Former-commit-id: df09193834
2025-07-26 10:31:20 +02:00
Marvin
e1529af62a test.yml aktualisieren
Former-commit-id: b2fe4e3b03
2025-07-26 10:31:05 +02:00
Owen
56520b599e Starting to work on option
Former-commit-id: e14d53087f
2025-07-25 16:16:33 -07:00
Owen
6e2e8d90d8 Adding option for netstack
Former-commit-id: 3583270f73
2025-07-25 16:16:00 -07:00
Owen
fa358d48af Add flag
Former-commit-id: f5be05c55a
2025-07-25 16:14:25 -07:00
Owen
a628f9da49 Proxies working
Former-commit-id: d09e3fbd60
2025-07-25 16:10:53 -07:00
Owen
8dce0d17c0 Pm working
Former-commit-id: 493831b5f0
2025-07-25 13:09:11 -07:00
Owen
9a0b01e33c Proxy working?
Former-commit-id: 9fc692c090
2025-07-25 12:00:09 -07:00
Owen
1f523fb23b Just hp like olm
Former-commit-id: ccb7008579
2025-07-25 11:42:36 -07:00
Owen
e593d22d3c Use normal udp
Former-commit-id: f17dbe1fef
2025-07-25 11:05:24 -07:00
Owen
b4197029dd Dont restart netstack
Former-commit-id: 27561f52ca
2025-07-25 11:01:54 -07:00
Owen
749c80a49b Maybe its working?
Former-commit-id: 499ebcd928
2025-07-25 10:59:34 -07:00
Owen
1153da39c3 Maybe basic func
Former-commit-id: 40dfab31a5
2025-07-25 10:50:02 -07:00
Owen
ee595c712f Exit well
Former-commit-id: 56377ec87e
2025-07-24 20:46:33 -07:00
Owen
9e595765f0 Add get config
Former-commit-id: 008be54c55
2025-07-24 12:40:14 -07:00
Owen
b2724a704c Log to file optionally and update config locations
Former-commit-id: 64c22a94a4
2025-07-24 12:01:53 -07:00
Owen Schwartz
c1c7b16a28 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

Former-commit-id: 468c93c581
2025-07-23 11:26:32 -07:00
Owen Schwartz
bff566ce03 Merge pull request #92 from nepthar/patch-1
Nit: Typo fix in help string
Former-commit-id: c53b859cda
2025-07-23 11:26:15 -07:00
Jordan Parker
fae3f8432b Nit: Typo fix in help string
Former-commit-id: 6cd824baf2
2025-07-23 10:25:11 -04:00
dependabot[bot]
24ff68ae2a 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>
Former-commit-id: d8c5182acd
2025-07-23 10:01:13 +00:00
Owen
89413788e9 Change rm to keep
Former-commit-id: c8c4666d63
2025-07-22 11:36:31 -07:00
Owen
e72fbb5ffe Holepunch to the right endpoint
Former-commit-id: f1fcc13e66
2025-07-21 17:04:05 -07:00
Owen Schwartz
97e28d85cc Merge pull request #90 from firecat53/main
Update flake.nix to 1.3.4

Former-commit-id: 52bbc2fe31
2025-07-21 14:54:22 -07:00
Scott Hansen
ea85c76a51 Update flake.nix for 1.3.4
Former-commit-id: b5ee12f84a
2025-07-21 11:23:50 -07:00
Owen
363a9ddf42 Add client type
Former-commit-id: 510e78437c
2025-07-18 16:55:38 -07:00
Owen
19ec2d52df Merge branch 'main' into clients-fr
Former-commit-id: e14cffce1c
2025-07-18 16:53:27 -07:00
Owen
2ad4ef3b56 Make client work for olm
Former-commit-id: 629a92ee81
2025-07-18 16:53:13 -07:00
Owen
1a52d8a2b4 Adjust logging
Former-commit-id: 56df75544d
2025-07-18 16:52:59 -07:00
Owen
13cbc4d649 Remove defers causing bad file descriptor issues
Former-commit-id: 5b2e743470
2025-07-18 15:49:57 -07:00
Owen
afb0329ff8 Working on it
Former-commit-id: b5025c142f
2025-07-18 15:25:39 -07:00
Owen
42c91e45f2 Dont ping if there is just 1
Former-commit-id: cd86e6b6de
2025-07-17 15:01:02 -07:00
Owen
89fc09158a Make sure to only exclude if there is anouther
Former-commit-id: 230c34e4e0
2025-07-16 21:15:03 -07:00
Owen
4f73487783 Move docker messages to debug for #86
Former-commit-id: a038ce1458
2025-07-14 10:16:06 -07:00
Owen Schwartz
d8aa736ac5 Merge pull request #85 from firecat53/main
Update flake to 1.3.2

Former-commit-id: cd83efd365
2025-07-13 19:47:06 -07:00
Scott Hansen
b7f0bf87ba Update flake to 1.3.2
Former-commit-id: 702f39e870
2025-07-13 18:47:20 -07:00
Owen Schwartz
7baabf989e Merge pull request #84 from fosrl/dev
Ping improvements, dependabot, hostname in docker

Former-commit-id: 02b7ea51af
2025-07-13 16:26:11 -07:00
Owen
a85b2bba2f Merge branch 'woutervanelten-patch-2' into dev
Former-commit-id: e8421364fc
2025-07-13 16:12:42 -07:00
Owen
dee9696daf Merge branch 'patch-2' of github.com:woutervanelten/newt into woutervanelten-patch-2
Former-commit-id: 7264bb7001
2025-07-13 16:10:23 -07:00
Owen
c38b00c5fb Merge branch 'main' into dev
Former-commit-id: 86e262ac1e
2025-07-13 16:10:02 -07:00
Owen Schwartz
910f900fd5 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

Former-commit-id: dcacc03e96
2025-07-13 16:09:48 -07:00
Owen
3c1869cb6e Merge branch 'woutervanelten-patch-5' into dev
Former-commit-id: 6f4469a5a4
2025-07-13 16:09:33 -07:00
Owen
a8ca248e26 Fix typo with _
Former-commit-id: 663e28329b
2025-07-13 16:08:32 -07:00
Owen
6a4b5826ff Working on better ping
Former-commit-id: f513f97fc3
2025-07-13 16:07:46 -07:00
dependabot[bot]
218d2d250b 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>
Former-commit-id: ce4f3e4cdf
2025-07-13 23:07:14 +00:00
Owen
3dafc8b07e Merge branch 'patch-5' of github.com:woutervanelten/newt into woutervanelten-patch-5
Former-commit-id: 58a74fce6f
2025-07-13 16:06:54 -07:00
Owen Schwartz
850d985c4e Merge pull request #82 from fosrl/dependabot/go_modules/prod-minor-updates-d992c0ea53
Bump the prod-minor-updates group with 2 updates

Former-commit-id: fc965abbc4
2025-07-13 16:05:52 -07:00
Owen
3ba2156b52 Loosen up the ping intervals
Former-commit-id: b881808cae
2025-07-11 16:35:39 -07:00
dependabot[bot]
9735c85354 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>
Former-commit-id: 6160e4c8a6
2025-07-11 09:32:02 +00:00
Wouter van Elten
cf87d101f6 Update README.md for health check
Added explanation of health_file

Former-commit-id: 4d343e3541
2025-07-11 08:14:32 +02:00
Owen
0d5aa53eef Merge branch 'main' of github.com:fosrl/newt
Former-commit-id: 71d1bbaaf2
2025-07-10 17:27:09 -07:00
Owen
0c4d66962e Fix link again
Former-commit-id: 9eb8e5122a
2025-07-10 17:26:58 -07:00
Owen Schwartz
256d3e6ede Merge pull request #72 from fosrl/dev
Dev

Former-commit-id: c593e2aa97
2025-07-10 17:25:54 -07:00
Owen
8a3d72f6d2 Merge branch 'main' into dev
Former-commit-id: c3483ded8f
2025-07-10 17:25:25 -07:00
Owen
51a03a49f9 Update link
Former-commit-id: b1cfd3ba02
2025-07-10 17:23:26 -07:00
Owen
44d6b6060c Get newt script
Former-commit-id: 2d9b761de9
2025-07-10 17:17:12 -07:00
Owen
8d61b9d628 Adjust logging
Former-commit-id: 53397663ef
2025-07-08 17:18:43 -07:00
Owen
c21e29ffd1 Fix disconnect errors about closed connection
Former-commit-id: 221d5862fb
2025-07-08 08:48:39 -07:00
Owen
0d060f4529 Adjust logging
Former-commit-id: e4bdbbec7c
2025-07-08 08:48:27 -07:00
Owen
0d4a5cfc17 Remove dup code
Former-commit-id: 07bd283604
2025-07-05 18:14:33 -07:00
Owen
5ce4455b99 Fix nil issues
Former-commit-id: b7d4ea0c84
2025-07-03 20:17:21 -07:00
Owen
e45fb33840 Merge branch 'main' into dev
Former-commit-id: d10c5e0366
2025-07-03 10:11:30 -07:00
Owen
465bf1bd37 Make linux clients build correctly
Former-commit-id: 61a9097baf
2025-07-03 10:09:55 -07:00
Owen
d1397c30fe Fix missing netstack pack
Former-commit-id: a88d25f369
2025-07-03 09:06:26 -07:00
Owen
2b01c03a80 Working packages?
Former-commit-id: c2a326c70a
2025-07-03 09:05:09 -07:00
Owen Schwartz
a9dc4cf4f4 Merge pull request #75 from woutervanelten/patch-4
Update go.mod

Former-commit-id: be56550da4
2025-07-03 09:01:39 -07:00
Wouter van Elten
38ea265bb7 Update go.mod
Former-commit-id: a896291831
2025-07-01 17:38:54 +02:00
Wouter van Elten
883937f9ac added use of hostname if available
Former-commit-id: 9348842e2c
2025-07-01 17:26:49 +02:00
Owen
3d7e0c3abe Comment out WIP clients flags
Former-commit-id: ec8fc20438
2025-06-30 09:49:27 -07:00
Owen
f4ed9b4d49 Remove binary mistake
Former-commit-id: a39d056725
2025-06-30 09:04:58 -07:00
Owen
43a07a62db Merge branch 'dev' into clients-pops
Former-commit-id: 450fc6c20f
2025-06-30 09:01:25 -07:00
Owen
12b92cb45f Merge branch 'dev' into clients-pops
Former-commit-id: 837f749cdb
2025-06-30 09:00:26 -07:00
Owen
beeb75655b Change math random
Former-commit-id: 63aea704bd
2025-06-22 12:55:13 -04:00
Owen
5cf684c81a Add log message
Former-commit-id: c1a2a3208c
2025-06-22 12:54:13 -04:00
Owen
3fab3e3e20 Add way to stop initial ping
Former-commit-id: ac67df63fa
2025-06-22 12:50:55 -04:00
Owen
0090f78f15 Move updates
Former-commit-id: e642983b88
2025-06-22 12:10:00 -04:00
Owen
c12d73f975 Merge branch 'dev' into clients-pops
Former-commit-id: e2dd965654
2025-06-22 11:54:39 -04:00
Owen
da4e572939 Properly check the http status code now
Former-commit-id: 227631665e
2025-06-19 22:58:27 -04:00
Owen
761e46d1b1 Failover is working?
Former-commit-id: ca3ffa00ed
2025-06-19 17:38:21 -04:00
Owen
bc4f453bae Send version
Former-commit-id: 6d3938e14e
2025-06-19 16:39:33 -04:00
Owen
4a97c35b81 Change terminate to reconnect
Former-commit-id: 37191924ee
2025-06-19 16:32:07 -04:00
Owen
5abf4611e5 Faster detection on ws side
Former-commit-id: a14f70dbaa
2025-06-19 16:30:31 -04:00
Owen
5e7906a354 Reorg and add timeout
Former-commit-id: bb1318278a
2025-06-19 15:59:21 -04:00
Owen
4338045b78 New tunnel reconnect works
Former-commit-id: 1c75eb3bee
2025-06-19 15:55:47 -04:00
Owen
dfc5f2b253 Update ping check
Former-commit-id: 4b64b04603
2025-06-18 22:54:13 -04:00
miloschwartz
098f48d544 give preference to previously connected node
Former-commit-id: b397016da8
2025-06-18 15:57:15 -04:00
Owen
d0ef069e9f Print ping latency better and add connect message
Former-commit-id: 850c230c4a
2025-06-17 15:22:35 -04:00
Owen
64b28627fa Add mtu to readme
Former-commit-id: c82b84194f
2025-06-17 10:23:43 -04:00
Owen
6e1740ff33 Tweak to call out when docker deployment
Former-commit-id: 50df49e556
2025-06-17 10:23:43 -04:00
Rob
f269ba1060 Update README.md
added docker socket mount example

Former-commit-id: 5f9c041c6b
2025-06-17 10:23:43 -04:00
miloschwartz
eada420dd0 add exit node name to logs
Former-commit-id: 95d4cb2758
2025-06-16 22:00:39 -04:00
miloschwartz
2eef0e3f33 adjust weight calculation
Former-commit-id: bbea9a91da
2025-06-16 15:54:46 -04:00
Owen
22b2dabdb6 Working on pining
Former-commit-id: 8d4d8b91b9
2025-06-13 16:15:49 -04:00
Owen
b3c0134462 First pass at pinging
Former-commit-id: d0e220511a
2025-06-10 21:52:33 -04:00
Owen
c28956c4a6 Merge branch 'dev' into clients-pops
Former-commit-id: 22f44c860a
2025-06-10 09:38:53 -04:00
Owen
d0f997a5d8 Resolve merge issues
Former-commit-id: a5f4d5fdf6
2025-06-09 22:26:31 -04:00
Owen
4f84f747ee Update go
Former-commit-id: ce6d340a8d
2025-06-09 22:24:49 -04:00
Owen
028144bb1a Merge branch 'dev' into clients-pops
Former-commit-id: eaf812a2a7
2025-06-09 18:38:48 -04:00
Owen
311061a7e4 Set to 127
Former-commit-id: 494e30704b
2025-05-13 11:42:06 -04:00
Owen
e0a5ea5a4e Handle order of opertions of hole punch better
Former-commit-id: 175718a48e
2025-04-22 22:11:37 -04:00
Owen
0e4b638fb7 Add more sensible controls
Former-commit-id: 6a146ed371
2025-04-13 21:28:38 -04:00
Owen
b1edef2407 Remove redundant log message
Former-commit-id: 027d9a059f
2025-04-12 17:51:03 -04:00
90 changed files with 8224 additions and 3248 deletions

View File

@@ -1,6 +1,6 @@
.gitignore
.dockerignore
newt
bin/
*.json
README.md
Makefile

View File

@@ -14,12 +14,13 @@ body:
label: Environment
description: Please fill out the relevant details below for your environment.
value: |
- OS Type & Version: (e.g., Ubuntu 22.04)
- OS Type & Version:
- Pangolin Version:
- Edition (Community or Enterprise):
- Gerbil Version:
- Traefik Version:
- Newt Version:
- Olm Version: (if applicable)
- Client Version:
validations:
required: true

View File

@@ -1,40 +1,32 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 1
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
go-dependencies:
patterns:
- "*"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 1
groups:
patch-updates:
update-types:
- "patch"
minor-updates:
update-types:
- "minor"
docker-dependencies:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 1
groups:
github-actions-dependencies:
patterns:
- "*"

View File

@@ -68,7 +68,7 @@ jobs:
echo "image_created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
@@ -95,7 +95,7 @@ jobs:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
@@ -158,7 +158,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
@@ -237,20 +237,20 @@ jobs:
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
#- name: Set up QEMU
# uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
# uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
#- name: Set up Docker Buildx
# uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
# uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log in to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -264,12 +264,12 @@ jobs:
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
# Build ONLY amd64 and push arch-specific tag suffixes used later for manifest creation.
- name: Build and push (amd64 -> *:amd64-TAG)
id: build_amd
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: true
@@ -309,7 +309,7 @@ jobs:
IMAGE_CREATED: ${{ needs.pre-run.outputs.image_created }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
@@ -368,14 +368,14 @@ jobs:
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
- name: Log in to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -389,12 +389,12 @@ jobs:
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
# Build ONLY arm64 and push arch-specific tag suffixes used later for manifest creation.
- name: Build and push (arm64 -> *:arm64-TAG)
id: build_arm
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: true
@@ -434,7 +434,7 @@ jobs:
IMAGE_CREATED: ${{ needs.pre-run.outputs.image_created }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
@@ -483,14 +483,14 @@ jobs:
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
- name: Log in to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -504,14 +504,14 @@ jobs:
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Build and push (arm/v7 -> *:armv7-TAG)
id: build_armv7
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: true
@@ -556,14 +556,14 @@ jobs:
#PUBLISH_MINOR: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_minor || vars.PUBLISH_MINOR }}
steps:
- name: Log in to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -577,7 +577,7 @@ jobs:
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
- name: Set up Docker Buildx (needed for imagetools)
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Create & push multi-arch index (GHCR :TAG) via imagetools
shell: bash
@@ -642,7 +642,7 @@ jobs:
IMAGE_CREATED: ${{ needs.pre-run.outputs.image_created }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
@@ -656,19 +656,19 @@ jobs:
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
- name: Install Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
- name: Log in to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -692,7 +692,7 @@ jobs:
sudo apt-get install -y jq
- name: Set up Docker Buildx (needed for imagetools)
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Resolve multi-arch digest refs (by TAG)
shell: bash
@@ -732,7 +732,7 @@ jobs:
fi
- name: Attest build provenance (GHCR) (digest)
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: ${{ env.GHCR_IMAGE }}
subject-digest: ${{ env.GHCR_DIGEST }}
@@ -742,7 +742,7 @@ jobs:
- name: Attest build provenance (Docker Hub)
continue-on-error: true
if: ${{ env.DH_DIGEST != '' }}
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: index.docker.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
subject-digest: ${{ env.DH_DIGEST }}
@@ -750,9 +750,9 @@ jobs:
show-summary: true
- name: Install cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: "v3.0.2"
cosign-release: v3.0.6
- name: Sanity check cosign private key
env:
@@ -897,14 +897,29 @@ jobs:
set -euo pipefail
make -j 10 go-build-release VERSION="${TAG}"
- name: Build Advantech packages
shell: bash
run: |
set -euo pipefail
ADVANTECH_DIR="packages/advantech"
mkdir -p "${ADVANTECH_DIR}/bin"
install -m 0755 "bin/newt_linux_arm64" "${ADVANTECH_DIR}/bin/newt_linux_arm64"
install -m 0755 "bin/newt_linux_arm32" "${ADVANTECH_DIR}/bin/newt_linux_arm32"
for platform in v2 v2i v3 v4 v4i; do
make -C "${ADVANTECH_DIR}" PLATFORM="${platform}"
done
- name: Create GitHub Release (draft)
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
tag_name: ${{ env.TAG }}
generate_release_notes: true
prerelease: ${{ env.IS_RC == 'true' }}
files: |
bin/*
packages/advantech/*.tgz
fail_on_unmatched_files: true
draft: true
body: |
@@ -925,7 +940,7 @@ jobs:
permissions: write-all
steps:
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600

View File

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

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

@@ -10,6 +10,20 @@ on:
- dev
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
- name: Run Go tests
run: make test
build:
runs-on: ubuntu-latest
strategy:
@@ -28,12 +42,12 @@ jobs:
- go-build-release-windows-amd64
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: 1.25
go-version-file: go.mod
- name: Build targets via `make`
run: make ${{ matrix.target }}

View File

@@ -27,6 +27,10 @@ RUN apk --no-cache add ca-certificates tzdata iputils
COPY --from=builder /newt /usr/local/bin/
COPY entrypoint.sh /
# Marks this as an official Fossorial container image.
# Auto-update is disabled in official images — update by pulling a new image tag.
ENV NEWT_SYSTEM_SUBSTRATE="CONTAINER"
# Admin/metrics endpoint (Prometheus scrape)
EXPOSE 2112

View File

@@ -1,4 +1,4 @@
.PHONY: all local docker-build docker-build-release
.PHONY: all local test docker-build docker-build-release
all: local
@@ -6,7 +6,10 @@ VERSION ?= dev
LDFLAGS = -X main.newtVersion=$(VERSION)
local:
CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o ./bin/newt
CGO_ENABLED=0 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=$(shell go env GOOS)_$(shell go env GOARCH)" -o ./bin/newt
test:
go test ./...
docker-build:
docker build -t fosrl/newt:latest .
@@ -43,31 +46,31 @@ go-build-release: \
go-build-release-freebsd-arm64
go-build-release-linux-arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_arm64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=linux_arm64" -o bin/newt_linux_arm64
go-build-release-linux-arm32-v7:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_arm32
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=linux_arm32" -o bin/newt_linux_arm32
go-build-release-linux-arm32-v6:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_arm32v6
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=linux_arm32v6" -o bin/newt_linux_arm32v6
go-build-release-linux-amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_amd64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=linux_amd64" -o bin/newt_linux_amd64
go-build-release-linux-riscv64:
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -ldflags "$(LDFLAGS)" -o bin/newt_linux_riscv64
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=linux_riscv64" -o bin/newt_linux_riscv64
go-build-release-darwin-arm64:
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/newt_darwin_arm64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=darwin_arm64" -o bin/newt_darwin_arm64
go-build-release-darwin-amd64:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_darwin_amd64
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=darwin_amd64" -o bin/newt_darwin_amd64
go-build-release-windows-amd64:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_windows_amd64.exe
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=windows_amd64" -o bin/newt_windows_amd64.exe
go-build-release-freebsd-amd64:
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/newt_freebsd_amd64
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=freebsd_amd64" -o bin/newt_freebsd_amd64
go-build-release-freebsd-arm64:
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/newt_freebsd_arm64
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags "$(LDFLAGS) -X main.newtPlatform=freebsd_arm64" -o bin/newt_freebsd_arm64

View File

@@ -1,14 +1,10 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"runtime"
"github.com/fosrl/newt/authdaemon"
"github.com/fosrl/newt/logger"
)
const (
@@ -16,64 +12,6 @@ const (
defaultCACertPath = "/etc/ssh/ca.pem"
)
var (
errPresharedKeyRequired = errors.New("auth-daemon-key is required when --auth-daemon is enabled")
errRootRequired = errors.New("auth-daemon must be run as root (use sudo)")
authDaemonServer *authdaemon.Server // Global auth daemon server instance
)
// startAuthDaemon initializes and starts the auth daemon in the background.
// It validates requirements (Linux, root, preshared key) and starts the server
// in a goroutine so it runs alongside normal newt operation.
func startAuthDaemon(ctx context.Context) error {
// Validation
if runtime.GOOS != "linux" {
return fmt.Errorf("auth-daemon is only supported on Linux, not %s", runtime.GOOS)
}
if os.Geteuid() != 0 {
return errRootRequired
}
// Use defaults if not set
principalsFile := authDaemonPrincipalsFile
if principalsFile == "" {
principalsFile = defaultPrincipalsPath
}
caCertPath := authDaemonCACertPath
if caCertPath == "" {
caCertPath = defaultCACertPath
}
// Create auth daemon server
cfg := authdaemon.Config{
DisableHTTPS: true, // We run without HTTP server in newt
PresharedKey: "this-key-is-not-used", // Not used in embedded mode, but set to non-empty to satisfy validation
PrincipalsFilePath: principalsFile,
CACertPath: caCertPath,
Force: true,
GenerateRandomPassword: authDaemonGenerateRandomPassword,
}
srv, err := authdaemon.NewServer(cfg)
if err != nil {
return fmt.Errorf("create auth daemon server: %w", err)
}
authDaemonServer = srv
// Start the auth daemon in a goroutine so it runs alongside newt
go func() {
logger.Info("Auth daemon starting (native mode, no HTTP server)")
if err := srv.Run(ctx); err != nil {
logger.Error("Auth daemon error: %v", err)
}
logger.Info("Auth daemon stopped")
}()
return nil
}
// runPrincipalsCmd executes the principals subcommand logic
func runPrincipalsCmd(args []string) {
opts := struct {
PrincipalsFile string
@@ -82,7 +20,6 @@ func runPrincipalsCmd(args []string) {
PrincipalsFile: defaultPrincipalsPath,
}
// Parse flags manually
for i := 0; i < len(args); i++ {
switch args[i] {
case "--principals-file":
@@ -109,14 +46,12 @@ func runPrincipalsCmd(args []string) {
}
}
// Validation
if opts.Username == "" {
fmt.Fprintf(os.Stderr, "Error: username is required\n")
printPrincipalsHelp()
os.Exit(1)
}
// Get principals
list, err := authdaemon.GetPrincipals(opts.PrincipalsFile, opts.Username)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)

View File

@@ -19,7 +19,7 @@ func (s *Server) ProcessConnection(req ConnectionRequest) {
if err := ensureUser(req.Username, req.Metadata, s.cfg.GenerateRandomPassword); err != nil {
logger.Warn("auth-daemon: ensure user: %v", err)
}
if cfg.PrincipalsFilePath != "" {
if cfg.PrincipalsFilePath != "" && req.NiceId != "" {
if err := writePrincipals(cfg.PrincipalsFilePath, req.Username, req.NiceId); err != nil {
logger.Warn("auth-daemon: write principals: %v", err)
}

View File

@@ -14,9 +14,9 @@ func (s *Server) registerRoutes() {
// ConnectionMetadata is the metadata object in POST /connection.
type ConnectionMetadata struct {
SudoMode string `json:"sudoMode"` // "none" | "full" | "commands"
SudoCommands []string `json:"sudoCommands"` // used when sudoMode is "commands"
SudoCommands []string `json:"sudoCommands"` // used when sudoMode is "commands"
Homedir bool `json:"homedir"`
Groups []string `json:"groups"` // system groups to add the user to
Groups []string `json:"groups"` // system groups to add the user to
}
// ConnectionRequest is the JSON body for POST /connection.

View File

@@ -23,13 +23,13 @@ import (
type Config struct {
// DisableHTTPS: when true, Run() does not start the HTTPS server (for embedded use inside Newt). Call ProcessConnection directly for connection events.
DisableHTTPS bool
Port int // Required when DisableHTTPS is false. Listen port for the HTTPS server. No default.
PresharedKey string // Required when DisableHTTPS is false. HTTP auth (Authorization: Bearer <key> or X-Preshared-Key: <key>). No default.
CACertPath string // Required. Where to write the CA cert (e.g. /etc/ssh/ca.pem). No default.
Force bool // If true, overwrite existing CA cert (and other items) when content differs. Default false.
PrincipalsFilePath string // Required. Path to the principals data file (JSON: username -> array of principals). No default.
GenerateRandomPassword bool // If true, set a random password on users when they are provisioned (for SSH PermitEmptyPasswords no).
DisableHTTPS bool
Port int // Required when DisableHTTPS is false. Listen port for the HTTPS server. No default.
PresharedKey string // Required when DisableHTTPS is false. HTTP auth (Authorization: Bearer <key> or X-Preshared-Key: <key>). No default.
CACertPath string // Required. Where to write the CA cert (e.g. /etc/ssh/ca.pem). No default.
Force bool // If true, overwrite existing CA cert (and other items) when content differs. Default false.
PrincipalsFilePath string // Required. Path to the principals data file (JSON: username -> array of principals). No default.
GenerateRandomPassword bool // If true, set a random password on users when they are provisioned (for SSH PermitEmptyPasswords no).
}
type Server struct {
@@ -136,7 +136,7 @@ func NewServer(cfg Config) (*Server, error) {
// When DisableHTTPS is true, Run() blocks on ctx only and does not listen; use ProcessConnection for connection events.
func (s *Server) Run(ctx context.Context) error {
if s.cfg.DisableHTTPS {
logger.Info("auth-daemon running (HTTPS disabled)")
logger.Debug("auth-daemon running (HTTPS disabled)")
<-ctx.Done()
s.cleanupPrincipalsFile()
return nil

View File

@@ -0,0 +1,144 @@
package browsergateway
import (
"crypto/subtle"
"errors"
"net"
"net/http"
"strings"
"sync"
"github.com/fosrl/newt/nativessh"
)
// Forwarding buffer size. RDP graphics traffic is bursty and TLS records cap
// at ~16 KiB, so 64 KiB lets a couple of records pile up per syscall/frame
// without wasting memory per session.
const forwardBufSize = 64 * 1024
// ListenPort is the port the browser gateway HTTP server listens on inside the
// WireGuard netstack. This is a fixed value shared between newt and pangolin.
// Targets do not overlap with this port because they start at 40000.
const ListenPort = 39999
// Target represents an allowed proxy destination for the browser gateway.
// Only connections whose (Type, Destination, DestinationPort, AuthToken) match a
// registered Target will be forwarded; all others are rejected.
type Target struct {
ID int
Type string // "rdp" | "ssh" | "vnc"
Destination string
DestinationPort int
AuthToken string // per-target secret; must match the token supplied by the client
}
// Config holds the configuration for a Gateway.
type Config struct {
// AuthToken is used only for NativeSSH mode (which has no external target
// to match against). For all proxy targets (RDP/SSH/VNC), auth tokens are
// stored per-Target and validated by isAllowed.
AuthToken string
// SSHCredentials are used by native SSH browser sessions for certificate
// validation against the in-memory CA/principal store.
SSHCredentials *nativessh.CredentialStore
}
// Gateway is a browser-based RDP/SSH/VNC WebSocket proxy.
// Create one with New and mount it via RegisterHandlers or the individual
// HandleRDP / HandleSSH / HandleVNC http.HandlerFunc methods.
type Gateway struct {
authToken string
sshCreds *nativessh.CredentialStore
mu sync.RWMutex
targets map[int]Target // keyed by Target.ID
server *http.Server
}
// New creates a new Gateway from the provided Config.
func New(cfg Config) *Gateway {
return &Gateway{
authToken: cfg.AuthToken,
sshCreds: cfg.SSHCredentials,
targets: make(map[int]Target),
}
}
// SetTargets replaces the entire allowed-destination list atomically.
func (g *Gateway) SetTargets(targets []Target) {
g.mu.Lock()
defer g.mu.Unlock()
g.targets = make(map[int]Target, len(targets))
for _, t := range targets {
g.targets[t.ID] = t
}
}
// AddTarget adds or updates a single allowed destination.
func (g *Gateway) AddTarget(t Target) {
g.mu.Lock()
defer g.mu.Unlock()
g.targets[t.ID] = t
}
// RemoveTarget removes an allowed destination by its ID.
func (g *Gateway) RemoveTarget(id int) {
g.mu.Lock()
defer g.mu.Unlock()
delete(g.targets, id)
}
// isAllowed reports whether a connection to (targetType, host, port) with the
// given authToken is permitted. The token is compared against the per-target
// AuthToken using constant-time comparison to prevent timing attacks.
func (g *Gateway) isAllowed(targetType, host string, port int, authToken string) bool {
g.mu.RLock()
defer g.mu.RUnlock()
for _, t := range g.targets {
if t.Type == targetType && t.Destination == host && t.DestinationPort == port {
return subtle.ConstantTimeCompare([]byte(authToken), []byte(t.AuthToken)) == 1
}
}
return false
}
// isTokenValid reports whether the given authToken matches any registered
// target of the specified type. Used for native SSH mode where there is no
// external destination to match against.
func (g *Gateway) isTokenValid(targetType, authToken string) bool {
g.mu.RLock()
defer g.mu.RUnlock()
for _, t := range g.targets {
if t.Type == targetType {
if subtle.ConstantTimeCompare([]byte(authToken), []byte(t.AuthToken)) == 1 {
return true
}
}
}
return false
}
// Start serves the browser gateway HTTP server on the provided listener.
// It returns nil when the listener is closed (normal shutdown).
func (g *Gateway) Start(ln net.Listener) error {
mux := http.NewServeMux()
g.RegisterHandlers(mux)
g.server = &http.Server{Handler: mux}
err := g.server.Serve(ln)
if err == nil ||
errors.Is(err, net.ErrClosed) ||
errors.Is(err, http.ErrServerClosed) ||
strings.Contains(err.Error(), "use of closed") ||
strings.Contains(err.Error(), "invalid state") {
return nil
}
return err
}
// RegisterHandlers registers the /rdp, /ssh, and /vnc routes on mux.
func (g *Gateway) RegisterHandlers(mux *http.ServeMux) {
mux.HandleFunc("/gateway/rdp", g.HandleRDP)
mux.HandleFunc("/gateway/ssh", g.HandleSSH)
mux.HandleFunc("/gateway/vnc", g.handleVNC)
}

View File

@@ -0,0 +1,88 @@
package browsergateway
import (
"encoding/asn1"
"fmt"
)
// RDCleanPath PDU version (BASE_VERSION + 1 = 3389 + 1).
const rdCleanPathVersion = int64(3390)
// rdCleanPathPdu is a Go translation of the ASN.1 SEQUENCE defined in the
// ironrdp-rdcleanpath crate. All optional fields use EXPLICIT context-specific
// tagging, matching the Rust `der::Sequence` derivation with
// `tag_mode = "EXPLICIT"`.
//
// We only need a subset of fields for the basic proxy flow, but the struct
// declares every tag we may encounter so that decoding does not fail on an
// unexpected element.
type rdCleanPathPdu struct {
Version int64 `asn1:"explicit,tag:0"`
Destination string `asn1:"explicit,tag:2,optional,utf8"`
ProxyAuth string `asn1:"explicit,tag:3,optional,utf8"`
ServerAuth string `asn1:"explicit,tag:4,optional,utf8"`
PreconnectionBlob string `asn1:"explicit,tag:5,optional,utf8"`
X224 []byte `asn1:"explicit,tag:6,optional"`
ServerCertChain [][]byte `asn1:"explicit,tag:7,optional"`
ServerAddr string `asn1:"explicit,tag:9,optional,utf8"`
}
// decodeRDCleanPathRequest parses a client-to-proxy RDCleanPath PDU.
func decodeRDCleanPathRequest(buf []byte) (*rdCleanPathPdu, error) {
var pdu rdCleanPathPdu
rest, err := asn1.Unmarshal(buf, &pdu)
if err != nil {
return nil, fmt.Errorf("asn1 unmarshal: %w", err)
}
if len(rest) != 0 {
return nil, fmt.Errorf("trailing data after RDCleanPath PDU: %d bytes", len(rest))
}
if pdu.Version != rdCleanPathVersion {
return nil, fmt.Errorf("unexpected RDCleanPath version: %d", pdu.Version)
}
return &pdu, nil
}
// encodeRDCleanPathResponse builds a proxy-to-client RDCleanPath response PDU
// containing the server address, X.224 connection confirm and server TLS chain.
func encodeRDCleanPathResponse(serverAddr string, x224Rsp []byte, certChain [][]byte) ([]byte, error) {
pdu := rdCleanPathPdu{
Version: rdCleanPathVersion,
X224: x224Rsp,
ServerCertChain: certChain,
ServerAddr: serverAddr,
}
return asn1.Marshal(pdu)
}
// detectRDCleanPathLength returns the total DER length of an RDCleanPath PDU
// if enough bytes are available, otherwise -1.
//
// The PDU is a DER SEQUENCE, which begins with the universal SEQUENCE tag
// (0x30) followed by a length octet/octets. We parse just enough to know the
// total length so we can buffer accordingly.
func detectRDCleanPathLength(buf []byte) int {
if len(buf) < 2 {
return -1
}
if buf[0] != 0x30 {
// Not a SEQUENCE: cannot be RDCleanPath.
return -2
}
l := buf[1]
if l < 0x80 {
return 2 + int(l)
}
n := int(l & 0x7f)
if n == 0 || n > 4 {
return -2
}
if len(buf) < 2+n {
return -1
}
total := 0
for i := 0; i < n; i++ {
total = (total << 8) | int(buf[2+i])
}
return 2 + n + total
}

271
browsergateway/rdp.go Normal file
View File

@@ -0,0 +1,271 @@
package browsergateway
import (
"context"
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"time"
"github.com/coder/websocket"
"github.com/fosrl/newt/logger"
)
// HandleRDP is an http.HandlerFunc for RDP-over-WebSocket connections.
func (g *Gateway) HandleRDP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true, // any-origin: minimal dev proxy with no auth
Subprotocols: []string{"binary"},
})
if err != nil {
logger.Debug("websocket upgrade failed: %v", err)
return
}
// Disable per-message read size cap (default is 32 KiB which would break
// large RDP graphics messages).
ws.SetReadLimit(-1)
defer ws.CloseNow() //nolint:errcheck
if err := g.serveSession(ctx, ws); err != nil {
logger.Debug("session error: %v", err)
}
}
func (g *Gateway) serveSession(ctx context.Context, ws *websocket.Conn) error {
// Expose the WebSocket as a streaming net.Conn. Binary messages are
// concatenated into a byte stream and writes become single binary frames.
// This is a thin wrapper with no per-message goroutine, unlike Gorilla.
stream := websocket.NetConn(ctx, ws, websocket.MessageBinary)
defer stream.Close() //nolint:errcheck
// -- Read the initial RDCleanPath request from the client --
pdu, err := readCleanPath(stream)
if err != nil {
return fmt.Errorf("read RDCleanPath: %w", err)
}
if pdu.Destination == "" {
return errors.New("RDCleanPath missing destination")
}
if len(pdu.X224) == 0 {
return errors.New("RDCleanPath missing X224 connection PDU")
}
target := pdu.Destination
// Default port for RDP if not specified.
if _, _, splitErr := net.SplitHostPort(target); splitErr != nil {
target = net.JoinHostPort(target, "3389")
}
// Validate destination against the registered target allowlist,
// including per-target auth token.
rdpHost, rdpPortStr, _ := net.SplitHostPort(target)
rdpPort, _ := strconv.Atoi(rdpPortStr)
if !g.isAllowed("rdp", rdpHost, rdpPort, pdu.ProxyAuth) {
return fmt.Errorf("RDP destination %s is not in the allowed target list or auth token mismatch", target)
}
logger.Debug("Connecting to RDP server %s", target)
// -- Open TCP connection to the destination RDP server --
serverTCP, err := net.DialTimeout("tcp", target, 15*time.Second)
if err != nil {
return fmt.Errorf("dial %s: %w", target, err)
}
defer serverTCP.Close()
if tcp, ok := serverTCP.(*net.TCPConn); ok {
// NoDelay is Go's default; set explicitly. RDP wants low latency for
// input echo, and the bulk path is naturally chunked by TLS records.
_ = tcp.SetNoDelay(true)
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
_ = tcp.SetReadBuffer(forwardBufSize)
_ = tcp.SetWriteBuffer(forwardBufSize)
}
serverAddr := serverTCP.RemoteAddr().String()
// Forward the optional pre-connection blob, then the X.224 connection request.
if pdu.PreconnectionBlob != "" {
if _, err := serverTCP.Write([]byte(pdu.PreconnectionBlob)); err != nil {
return fmt.Errorf("send PCB: %w", err)
}
}
if _, err := serverTCP.Write(pdu.X224); err != nil {
return fmt.Errorf("send X224: %w", err)
}
// -- Read the X.224 connection confirm from the server --
x224Rsp, err := readX224(serverTCP)
if err != nil {
return fmt.Errorf("read X224 response: %w", err)
}
logX224Negotiation(x224Rsp)
// -- Upgrade the server connection to TLS (skip verification) --
//
// Windows RDP hosts are picky: only set SNI when the target is a hostname
// (Go would skip SNI for IP literals anyway, but be explicit), and accept
// the full range of TLS versions / ciphers since some servers only
// negotiate TLS 1.0 or legacy suites.
host, _, _ := net.SplitHostPort(target)
tlsCfg := &tls.Config{
InsecureSkipVerify: true, //nolint:gosec // proxy intentionally skips verification
MinVersion: tls.VersionTLS10,
// Cap at TLS 1.2: Windows RDP servers commonly send a TLS "internal_error"
// alert when CredSSP/NLA is layered on top of a TLS 1.3 session.
MaxVersion: tls.VersionTLS12,
}
if net.ParseIP(host) == nil {
tlsCfg.ServerName = host
}
tlsConn := tls.Client(serverTCP, tlsCfg)
if err := tlsConn.Handshake(); err != nil {
return fmt.Errorf("TLS handshake with server: %w", err)
}
logger.Debug("Server TLS handshake OK (version=0x%04x cipher=0x%04x)",
tlsConn.ConnectionState().Version, tlsConn.ConnectionState().CipherSuite)
// Collect the raw DER server certificate chain to return to the client.
state := tlsConn.ConnectionState()
if len(state.PeerCertificates) == 0 {
return errors.New("server did not present any certificates")
}
chain := make([][]byte, 0, len(state.PeerCertificates))
for _, c := range state.PeerCertificates {
chain = append(chain, c.Raw)
}
// -- Send the RDCleanPath response back to the client --
rsp, err := encodeRDCleanPathResponse(serverAddr, x224Rsp, chain)
if err != nil {
return fmt.Errorf("encode RDCleanPath response: %w", err)
}
if _, err := stream.Write(rsp); err != nil {
return fmt.Errorf("write RDCleanPath response: %w", err)
}
logger.Debug("RDCleanPath handshake complete, forwarding traffic to %s", serverAddr)
// -- Two-way blind forwarding of the (now TLS-encrypted) RDP stream --
return forward(stream, tlsConn)
}
// readCleanPath buffers bytes from the stream until a full RDCleanPath PDU has
// been received, then decodes it.
func readCleanPath(r io.Reader) (*rdCleanPathPdu, error) {
buf := make([]byte, 0, 1024)
tmp := make([]byte, 1024)
for {
total := detectRDCleanPathLength(buf)
switch {
case total == -2:
return nil, errors.New("invalid RDCleanPath PDU")
case total > 0 && len(buf) >= total:
return decodeRDCleanPathRequest(buf[:total])
}
n, err := r.Read(tmp)
if n > 0 {
buf = append(buf, tmp[:n]...)
}
if err != nil {
return nil, err
}
}
}
// readX224 reads exactly one TPKT-framed X.224 PDU from the server.
//
// The TPKT header is 4 bytes: version (0x03), reserved (0x00), and a u16
// big-endian total length that includes the header itself.
func readX224(r io.Reader) ([]byte, error) {
hdr := make([]byte, 4)
if _, err := io.ReadFull(r, hdr); err != nil {
return nil, err
}
if hdr[0] != 0x03 {
return nil, fmt.Errorf("unexpected TPKT version 0x%02x", hdr[0])
}
total := int(binary.BigEndian.Uint16(hdr[2:4]))
if total < 4 || total > 4096 {
return nil, fmt.Errorf("unreasonable TPKT length %d", total)
}
out := make([]byte, total)
copy(out, hdr)
if _, err := io.ReadFull(r, out[4:]); err != nil {
return nil, err
}
return out, nil
}
// logX224Negotiation prints which RDP security protocol the server selected
// (or the failure code), to help diagnose handshake issues such as the server
// requiring NLA/CredSSP.
//
// X.224 Connection Confirm layout (RFC 1006 / [MS-RDPBCGR]):
//
// bytes 0..3 TPKT header (03 00 LL LL)
// byte 4 X.224 length indicator
// byte 5 X.224 code (0xD0 = CC)
// bytes 6..10 DST-REF, SRC-REF, class
// byte 11 optional RDP Negotiation type (0x02 = response, 0x03 = failure)
// byte 12 flags
// bytes 13..14 length (little-endian, =8)
// bytes 15..18 selected protocol / failure code (u32 little-endian)
func logX224Negotiation(pdu []byte) {
if len(pdu) < 19 {
logger.Debug("X.224 response too short (%d bytes) to contain RDP negotiation", len(pdu))
return
}
switch pdu[11] {
case 0x02:
proto := uint32(pdu[15]) | uint32(pdu[16])<<8 | uint32(pdu[17])<<16 | uint32(pdu[18])<<24
name := "unknown"
switch proto {
case 0:
name = "RDP (standard)"
case 1:
name = "SSL/TLS"
case 2:
name = "HYBRID (CredSSP/NLA)"
case 8:
name = "HYBRID_EX"
}
logger.Debug("Server selected RDP protocol 0x%x (%s)", proto, name)
case 0x03:
code := uint32(pdu[15]) | uint32(pdu[16])<<8 | uint32(pdu[17])<<16 | uint32(pdu[18])<<24
logger.Debug("Server returned RDP negotiation failure code 0x%x", code)
default:
logger.Debug("X.224 response has no RDP negotiation block (type=0x%02x)", pdu[11])
}
}
// forward shuttles bytes between the two streams until either side closes.
func forward(a, b io.ReadWriteCloser) error {
errc := make(chan error, 2)
go func() {
buf := make([]byte, forwardBufSize)
_, err := io.CopyBuffer(a, b, buf)
_ = a.Close()
_ = b.Close()
errc <- err
}()
go func() {
buf := make([]byte, forwardBufSize)
_, err := io.CopyBuffer(b, a, buf)
_ = a.Close()
_ = b.Close()
errc <- err
}()
// Wait for one side to finish, then return.
err := <-errc
if errors.Is(err, io.EOF) || err == nil {
return nil
}
return err
}

282
browsergateway/ssh.go Normal file
View File

@@ -0,0 +1,282 @@
package browsergateway
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"time"
"github.com/coder/websocket"
"github.com/fosrl/newt/logger"
"golang.org/x/crypto/ssh"
)
// sshClientMsg is a JSON message sent from the browser to the proxy.
type sshClientMsg struct {
// type: "auth" | "data" | "resize"
Type string `json:"type"`
Password string `json:"password,omitempty"` // used when type="auth"
PrivateKey string `json:"privateKey,omitempty"` // used when type="auth"
Certificate string `json:"certificate,omitempty"` // used when type="auth"
Data string `json:"data,omitempty"` // used when type="data"
Cols uint32 `json:"cols,omitempty"` // used when type="resize"
Rows uint32 `json:"rows,omitempty"` // used when type="resize"
}
// sshServerMsg is a JSON message sent from the proxy back to the browser.
type sshServerMsg struct {
// type: "data" | "error"
Type string `json:"type"`
Data string `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// HandleSSH is an http.HandlerFunc for SSH-over-WebSocket connections.
func (g *Gateway) HandleSSH(w http.ResponseWriter, r *http.Request) {
logger.Debug("SSH connection request from %s", r.RemoteAddr)
ctx := r.Context()
token := r.URL.Query().Get("authToken")
// "mode=native" (default) connects to the local SSH daemon on this host.
// "mode=proxy" connects to an arbitrary host+port supplied in query params.
nativeSSH := r.URL.Query().Get("mode") != "proxy"
// In proxy mode we also need host + username from query params.
var target, username string
if !nativeSSH {
host := r.URL.Query().Get("host")
port := r.URL.Query().Get("port")
username = r.URL.Query().Get("username")
if host == "" || username == "" {
http.Error(w, "missing host or username", http.StatusBadRequest)
return
}
if port == "" {
port = "22"
}
sshPort, _ := strconv.Atoi(port)
if !g.isAllowed("ssh", host, sshPort, token) {
http.Error(w, "destination not allowed or auth token mismatch", http.StatusForbidden)
return
}
target = net.JoinHostPort(host, port)
} else {
// Native SSH mode: validate the token against any registered ssh target.
if !g.isTokenValid("ssh", token) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
username = r.URL.Query().Get("username")
if username == "" {
http.Error(w, "missing username", http.StatusBadRequest)
return
}
}
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true,
Subprotocols: []string{"ssh"},
})
if err != nil {
logger.Debug("SSH websocket upgrade failed: %v", err)
return
}
ws.SetReadLimit(-1)
defer ws.CloseNow() //nolint:errcheck
if nativeSSH {
if err := serveNativeSSHSession(ctx, ws, username, g.sshCreds); err != nil {
logger.Debug("SSH native session error: %v", err)
}
} else {
if err := serveSSHSession(ctx, ws, target, username, g.authToken); err != nil {
logger.Debug("SSH session error: %v", err)
}
}
}
func serveSSHSession(ctx context.Context, ws *websocket.Conn, target, username, _ string) error {
// -- Wait for the auth message from the client to get the password --
_, authBytes, err := ws.Read(ctx)
if err != nil {
return fmt.Errorf("read auth message: %w", err)
}
var authMsg sshClientMsg
if err := json.Unmarshal(authBytes, &authMsg); err != nil || authMsg.Type != "auth" {
return fmt.Errorf("expected auth message, got: %s", authBytes)
}
password := authMsg.Password
privateKey := authMsg.PrivateKey
certificate := authMsg.Certificate
// Build the list of auth methods. Certificate+private key takes priority
// when both are provided, then private key, then password.
var authMethods []ssh.AuthMethod
if privateKey != "" {
signer, err := ssh.ParsePrivateKey([]byte(privateKey))
if err != nil {
sendSSHError(ctx, ws, fmt.Sprintf("Failed to parse private key: %v", err))
return fmt.Errorf("parse private key: %w", err)
}
if certificate != "" {
certPubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certificate))
if err != nil {
sendSSHError(ctx, ws, fmt.Sprintf("Failed to parse certificate: %v", err))
return fmt.Errorf("parse certificate: %w", err)
}
cert, ok := certPubKey.(*ssh.Certificate)
if !ok {
sendSSHError(ctx, ws, "Provided certificate is not a valid SSH certificate")
return fmt.Errorf("provided certificate is not ssh certificate")
}
certSigner, err := ssh.NewCertSigner(cert, signer)
if err != nil {
sendSSHError(ctx, ws, fmt.Sprintf("Failed to combine private key and certificate: %v", err))
return fmt.Errorf("new cert signer: %w", err)
}
authMethods = append(authMethods, ssh.PublicKeys(certSigner))
} else {
authMethods = append(authMethods, ssh.PublicKeys(signer))
}
}
if password != "" {
authMethods = append(authMethods, ssh.Password(password))
}
if len(authMethods) == 0 {
sendSSHError(ctx, ws, "No authentication credentials provided")
return fmt.Errorf("no auth credentials")
}
logger.Debug("SSH: connecting to %s as %s", target, username)
sshCfg := &ssh.ClientConfig{
User: username,
Auth: authMethods,
// HostKeyCallback is intentionally InsecureIgnoreHostKey for this dev
// proxy. In production, verify against a known-hosts store.
HostKeyCallback: ssh.InsecureIgnoreHostKey(), //nolint:gosec
Timeout: 15 * time.Second,
}
sshClient, err := ssh.Dial("tcp", target, sshCfg)
if err != nil {
sendSSHError(ctx, ws, fmt.Sprintf("SSH dial failed: %v", err))
return fmt.Errorf("ssh dial %s: %w", target, err)
}
defer sshClient.Close()
// -- Open an interactive session --
sess, err := sshClient.NewSession()
if err != nil {
sendSSHError(ctx, ws, fmt.Sprintf("Failed to open SSH session: %v", err))
return fmt.Errorf("ssh new session: %w", err)
}
defer sess.Close()
// Request a PTY.
if err := sess.RequestPty("xterm-256color", 24, 80, ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 38400,
ssh.TTY_OP_OSPEED: 38400,
}); err != nil {
sendSSHError(ctx, ws, fmt.Sprintf("Failed to request PTY: %v", err))
return fmt.Errorf("ssh request pty: %w", err)
}
stdinPipe, err := sess.StdinPipe()
if err != nil {
return fmt.Errorf("ssh stdin pipe: %w", err)
}
stdoutPipe, err := sess.StdoutPipe()
if err != nil {
return fmt.Errorf("ssh stdout pipe: %w", err)
}
stderrPipe, err := sess.StderrPipe()
if err != nil {
return fmt.Errorf("ssh stderr pipe: %w", err)
}
if err := sess.Shell(); err != nil {
sendSSHError(ctx, ws, fmt.Sprintf("Failed to start shell: %v", err))
return fmt.Errorf("ssh shell: %w", err)
}
logger.Debug("SSH: session established with %s", target)
// -- Pump SSH stdout/stderr → WebSocket --
sessCtx, cancelSess := context.WithCancel(ctx)
defer cancelSess()
go func() {
buf := make([]byte, 4096)
for {
n, readErr := stdoutPipe.Read(buf)
if n > 0 {
msg := sshServerMsg{Type: "data", Data: string(buf[:n])}
b, _ := json.Marshal(msg)
if writeErr := ws.Write(sessCtx, websocket.MessageText, b); writeErr != nil {
return
}
}
if readErr != nil {
cancelSess()
return
}
}
}()
go func() {
buf := make([]byte, 4096)
for {
n, readErr := stderrPipe.Read(buf)
if n > 0 {
msg := sshServerMsg{Type: "data", Data: string(buf[:n])}
b, _ := json.Marshal(msg)
if writeErr := ws.Write(sessCtx, websocket.MessageText, b); writeErr != nil {
return
}
}
if readErr != nil {
return
}
}
}()
// -- Pump WebSocket input → SSH stdin / resize --
for {
_, msgBytes, readErr := ws.Read(sessCtx)
if readErr != nil {
break
}
var msg sshClientMsg
if err := json.Unmarshal(msgBytes, &msg); err != nil {
continue
}
switch msg.Type {
case "data":
if _, err := stdinPipe.Write([]byte(msg.Data)); err != nil {
return fmt.Errorf("write ssh stdin: %w", err)
}
case "resize":
if msg.Cols > 0 && msg.Rows > 0 {
_ = sess.WindowChange(int(msg.Rows), int(msg.Cols))
}
}
}
return nil
}
// sendSSHError sends an error message to the browser and logs it.
func sendSSHError(ctx context.Context, ws *websocket.Conn, msg string) {
logger.Debug("SSH error: %s", msg)
b, _ := json.Marshal(sshServerMsg{Type: "error", Error: msg})
_ = ws.Write(ctx, websocket.MessageText, b)
}

View File

@@ -0,0 +1,94 @@
//go:build !windows
package browsergateway
import (
"context"
"encoding/json"
"fmt"
"github.com/coder/websocket"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/nativessh"
)
// serveNativeSSHSession handles a WebSocket SSH session by authenticating the
// user against the host OS (authorized_keys then PAM password) and then
// spawning a PTY+shell running as that user.
//
// The auth frame from the browser must be a JSON sshClientMsg with type="auth"
// carrying the same password/privateKey fields used by the proxy SSH path.
// The target username is passed in from the HTTP layer (query param).
func serveNativeSSHSession(ctx context.Context, ws *websocket.Conn, username string, creds *nativessh.CredentialStore) error {
// Read the auth frame.
_, authBytes, err := ws.Read(ctx)
if err != nil {
return fmt.Errorf("read auth message: %w", err)
}
var authMsg sshClientMsg
if err := json.Unmarshal(authBytes, &authMsg); err != nil || authMsg.Type != "auth" {
return fmt.Errorf("expected auth message, got: %s", authBytes)
}
// Authenticate using host authorized_keys or PAM password.
if err := nativessh.AuthenticateWithCertificate(creds, username, authMsg.Password, authMsg.PrivateKey, authMsg.Certificate); err != nil {
sendSSHError(ctx, ws, "Authentication failed")
return fmt.Errorf("auth for user %q: %w", username, err)
}
logger.Debug("SSH native: spawning shell as user %q", username)
sess, err := nativessh.NewPTYSessionAs(username)
if err != nil {
sendSSHError(ctx, ws, fmt.Sprintf("Failed to spawn shell: %v", err))
return fmt.Errorf("pty session as %q: %w", username, err)
}
defer sess.Close()
// Cancel context to unblock the WebSocket read loop when the shell exits.
sessCtx, cancelSess := context.WithCancel(ctx)
defer cancelSess()
// Pump PTY output → WebSocket.
go func() {
defer cancelSess()
buf := make([]byte, 4096)
for {
n, readErr := sess.Read(buf)
if n > 0 {
msg := sshServerMsg{Type: "data", Data: string(buf[:n])}
b, _ := json.Marshal(msg)
if writeErr := ws.Write(sessCtx, websocket.MessageText, b); writeErr != nil {
return
}
}
if readErr != nil {
return
}
}
}()
// Pump WebSocket input → PTY stdin / resize.
for {
_, msgBytes, readErr := ws.Read(sessCtx)
if readErr != nil {
break
}
var msg sshClientMsg
if err := json.Unmarshal(msgBytes, &msg); err != nil {
continue
}
switch msg.Type {
case "data":
if _, writeErr := sess.Write([]byte(msg.Data)); writeErr != nil {
return fmt.Errorf("write pty: %w", writeErr)
}
case "resize":
if msg.Cols > 0 && msg.Rows > 0 {
_ = sess.Resize(uint16(msg.Cols), uint16(msg.Rows))
}
}
}
return nil
}

View File

@@ -0,0 +1,16 @@
//go:build windows
package browsergateway
import (
"context"
"errors"
"github.com/coder/websocket"
"github.com/fosrl/newt/nativessh"
)
// serveNativeSSHSession is not supported on Windows.
func serveNativeSSHSession(_ context.Context, _ *websocket.Conn, _ string, _ *nativessh.CredentialStore) error {
return errors.New("native SSH is not supported on Windows")
}

125
browsergateway/vnc.go Normal file
View File

@@ -0,0 +1,125 @@
package browsergateway
import (
"context"
"fmt"
"io"
"net"
"net/http"
"strconv"
"time"
"github.com/coder/websocket"
"github.com/fosrl/newt/logger"
)
const (
vncDialTimeout = 10 * time.Second
vncKeepAlive = 30 * time.Second
vncForwardBufSize = 32 * 1024
)
// handleVNC proxies a noVNC WebSocket connection to a raw TCP VNC backend.
// It follows the same auth-token-in-query-param pattern as handleSSH.
//
// Query parameters:
//
// authToken shared secret matching the -auth-token flag
// host VNC backend hostname or IP
// port VNC backend port (default: 5900)
func (g *Gateway) handleVNC(w http.ResponseWriter, r *http.Request) {
host := r.URL.Query().Get("host")
port := r.URL.Query().Get("port")
if host == "" {
http.Error(w, "missing host", http.StatusBadRequest)
return
}
if port == "" {
port = "5900"
}
vncPort, _ := strconv.Atoi(port)
authToken := r.URL.Query().Get("authToken")
if !g.isAllowed("vnc", host, vncPort, authToken) {
http.Error(w, "destination not allowed or auth token mismatch", http.StatusForbidden)
return
}
target := net.JoinHostPort(host, port)
// Optional HTTP probe used by the web UI to surface backend reachability
// failures before attempting a WebSocket session.
if r.URL.Query().Get("checkOnly") == "1" {
if err := dialVNCBackend(r.Context(), target); err != nil {
logger.Debug("vnc: preflight failed (%s): %v", target, err)
http.Error(w, fmt.Sprintf("failed to connect to VNC backend: %v", err), http.StatusBadGateway)
return
}
w.WriteHeader(http.StatusNoContent)
return
}
// Accept the WebSocket. noVNC negotiates the "binary" subprotocol;
// fall back gracefully when the client sends "base64" as well.
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true,
Subprotocols: []string{"binary", "base64"},
})
if err != nil {
logger.Debug("vnc: websocket upgrade failed: %v", err)
return
}
ws.SetReadLimit(-1)
defer ws.CloseNow() //nolint:errcheck
ctx := r.Context()
if err := serveVNC(ctx, ws, target); err != nil {
logger.Debug("vnc: session error (%s): %v", target, err)
}
}
func dialVNCBackend(ctx context.Context, target string) error {
dialer := &net.Dialer{
Timeout: vncDialTimeout,
KeepAlive: vncKeepAlive,
}
conn, err := dialer.DialContext(ctx, "tcp", target)
if err != nil {
return err
}
defer conn.Close() //nolint:errcheck
return nil
}
func serveVNC(ctx context.Context, ws *websocket.Conn, target string) error {
// Dial the VNC backend TCP server.
dialer := &net.Dialer{
Timeout: vncDialTimeout,
KeepAlive: vncKeepAlive,
}
conn, err := dialer.DialContext(ctx, "tcp", target)
if err != nil {
return err
}
defer conn.Close() //nolint:errcheck
// Expose the WebSocket as a plain net.Conn byte stream (binary frames).
stream := websocket.NetConn(ctx, ws, websocket.MessageBinary)
defer stream.Close() //nolint:errcheck
// Proxy bidirectionally: VNC backend <-> browser.
errc := make(chan error, 2)
go func() {
buf := make([]byte, vncForwardBufSize)
_, err := io.CopyBuffer(conn, stream, buf)
errc <- err
}()
go func() {
buf := make([]byte, vncForwardBufSize)
_, err := io.CopyBuffer(stream, conn, buf)
errc <- err
}()
// Return when either direction closes.
err = <-errc
return err
}

View File

@@ -1,104 +0,0 @@
package main
import (
"strings"
"github.com/fosrl/newt/clients"
wgnetstack "github.com/fosrl/newt/clients"
"github.com/fosrl/newt/clients/permissions"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/websocket"
"golang.zx2c4.com/wireguard/tun/netstack"
)
var wgService *clients.WireGuardService
var ready bool
func setupClients(client *websocket.Client) {
var host = endpoint
if strings.HasPrefix(host, "http://") {
host = strings.TrimPrefix(host, "http://")
} else if strings.HasPrefix(host, "https://") {
host = strings.TrimPrefix(host, "https://")
}
host = strings.TrimSuffix(host, "/")
logger.Debug("Setting up clients with netstack2...")
// if useNativeInterface is true make sure we have permission to use native interface
if useNativeInterface {
logger.Debug("Checking permissions for native interface")
err := permissions.CheckNativeInterfacePermissions()
if err != nil {
logger.Fatal("Insufficient permissions to create native TUN interface: %v", err)
return
}
}
// Create WireGuard service
wgService, err = wgnetstack.NewWireGuardService(interfaceName, port, mtuInt, host, id, client, dns, useNativeInterface)
if err != nil {
logger.Fatal("Failed to create WireGuard service: %v", err)
}
client.OnTokenUpdate(func(token string) {
wgService.SetToken(token)
})
ready = true
}
func setDownstreamTNetstack(tnet *netstack.Net) {
if wgService != nil {
wgService.SetOthertnet(tnet)
}
}
func closeClients() {
logger.Info("Closing clients...")
if wgService != nil {
wgService.Close()
wgService = nil
}
}
func clientsHandleNewtConnection(publicKey string, endpoint string, relayPort uint16) {
if !ready {
return
}
// split off the port from the endpoint
parts := strings.Split(endpoint, ":")
if len(parts) < 2 {
logger.Error("Invalid endpoint format: %s", endpoint)
return
}
endpoint = strings.Join(parts[:len(parts)-1], ":")
if wgService != nil {
wgService.StartHolepunch(publicKey, endpoint, relayPort)
}
}
func clientsOnConnect() {
if !ready {
return
}
if wgService != nil {
wgService.LoadRemoteConfig()
}
}
// clientsStartDirectRelay starts a direct UDP relay from the main tunnel netstack
// to the clients' WireGuard, bypassing the proxy for better performance.
func clientsStartDirectRelay(tunnelIP string) {
if !ready {
return
}
if wgService != nil {
if err := wgService.StartDirectUDPRelay(tunnelIP); err != nil {
logger.Error("Failed to start direct UDP relay: %v", err)
}
}
}

View File

@@ -13,12 +13,14 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/fosrl/newt/bind"
newtDevice "github.com/fosrl/newt/device"
"github.com/fosrl/newt/holepunch"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/nativessh"
"github.com/fosrl/newt/netstack2"
"github.com/fosrl/newt/network"
"github.com/fosrl/newt/util"
@@ -108,12 +110,18 @@ type WireGuardService struct {
sharedBind *bind.SharedBind
holePunchManager *holepunch.Manager
useNativeInterface bool
// SSH server running on the clients' netstack
sshServer *sshServerHandle
credStore *nativessh.CredentialStore
// Direct UDP relay from main tunnel to clients' WireGuard
directRelayStop chan struct{}
directRelayWg sync.WaitGroup
netstackListener net.PacketConn
netstackListenerMu sync.Mutex
wgTesterServer *wgtester.Server
// connection blocking: when true, all new incoming connections are dropped
blocked atomic.Bool
}
// generateChainId generates a random chain ID for deduplicating round-trip messages.
@@ -188,11 +196,17 @@ 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
}
// SetCredentialStore sets the in-memory SSH credential store used by the
// native SSH server. It must be called before the netstack is configured
// (i.e. before the first newt/wg/receive-config message is processed).
func (s *WireGuardService) SetCredentialStore(store *nativessh.CredentialStore) {
s.credStore = store
}
// ReportRTT allows reporting native RTTs to telemetry, rate-limited externally.
func (s *WireGuardService) ReportRTT(seconds float64) {
if s.serverPubKey == "" {
@@ -211,6 +225,12 @@ func (s *WireGuardService) Close() {
s.stopGetConfig = nil
}
// Stop SSH server before tearing down the netstack
if s.sshServer != nil {
s.sshServer.stop()
s.sshServer = nil
}
// Flush access logs before tearing down the tunnel
if s.tnet != nil {
if ph := s.tnet.GetProxyHandler(); ph != nil {
@@ -286,6 +306,21 @@ func (s *WireGuardService) GetPublicKey() wgtypes.Key {
return s.key.PublicKey()
}
// SetBlocked enables or disables connection blocking for this WireGuard service.
// The state is persisted and applied immediately to any active proxy handler.
func (s *WireGuardService) SetBlocked(v bool) {
s.blocked.Store(v)
s.mu.Lock()
tnet := s.tnet
s.mu.Unlock()
if tnet == nil {
return
}
if ph := tnet.GetProxyHandler(); ph != nil {
ph.SetBlocked(v)
}
}
// SetOnNetstackReady sets a callback function to be called when the netstack interface is ready
func (s *WireGuardService) SetOnNetstackReady(callback func(*netstack2.Net)) {
s.onNetstackReady = callback
@@ -532,37 +567,15 @@ 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
// Sync synchronizes the clients WireGuard peers and targets with the desired state
// received as part of the main newt/sync message.
func (s *WireGuardService) Sync(peers []Peer, targets []Target) {
if err := s.syncPeers(peers); err != nil {
logger.Error("Failed to sync client peers: %v", err)
}
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)
if err := s.syncTargets(targets); err != nil {
logger.Error("Failed to sync client targets: %v", err)
}
}
@@ -629,8 +642,12 @@ func (s *WireGuardService) syncPeers(desiredPeers []Peer) error {
return nil
}
// syncTargets synchronizes the current targets with the desired state
// It removes targets not in the desired list and adds missing ones
// syncTargets synchronizes the current targets with the desired state.
// A sync represents the full authoritative state from the server, so rather
// than diffing against the currently installed rules (which can miss
// changes to a rule's contents when its source/dest prefix key is
// unchanged - e.g. a RewriteTo update), we just rebuild the entire rule set
// from scratch on every sync. This guarantees no stale rule can survive.
func (s *WireGuardService) syncTargets(desiredTargets []Target) error {
if s.tnet == nil {
// Native interface mode - proxy features not available, skip silently
@@ -638,70 +655,31 @@ func (s *WireGuardService) syncTargets(desiredTargets []Target) error {
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)
var rules []netstack2.SubnetRule
for _, target := range desiredTargets {
key := ruleKey{
sourcePrefix: target.SourcePrefix,
destPrefix: target.DestPrefix,
destPrefix, err := netip.ParsePrefix(target.DestPrefix)
if err != nil {
logger.Warn("Invalid dest prefix %s during sync: %v", target.DestPrefix, err)
continue
}
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(),
var portRanges []netstack2.PortRange
for _, pr := range target.PortRange {
portRanges = append(portRanges, netstack2.PortRange{
Min: pr.Min,
Max: pr.Max,
Protocol: pr.Protocol,
})
}
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)
for _, sp := range resolveSourcePrefixes(target) {
sourcePrefix, err := netip.ParsePrefix(sp)
if err != nil {
logger.Warn("Invalid source prefix %s during sync: %v", target.SourcePrefix, err)
logger.Warn("Invalid source prefix %s during sync: %v", sp, 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{
rules = append(rules, netstack2.SubnetRule{
SourcePrefix: sourcePrefix,
DestPrefix: destPrefix,
RewriteTo: target.RewriteTo,
@@ -713,10 +691,12 @@ func (s *WireGuardService) syncTargets(desiredTargets []Target) error {
TLSCert: target.TLSCert,
TLSKey: target.TLSKey,
})
logger.Info("Added target %s -> %s during sync", target.SourcePrefix, target.DestPrefix)
}
}
s.tnet.ReplaceProxySubnetRules(rules)
logger.Info("Synced targets: %d rules installed", len(rules))
return nil
}
@@ -890,7 +870,25 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error {
logger.Error("Failed to start WireGuard tester server: %v", err)
}
// Start the SSH server on the clients' netstack (port 22).
// A nil credStore means SSH is disabled (--disable-ssh), so skip starting the server.
if s.credStore != nil {
if h, sshErr := startSSHOnNetstack(s.tnet, s.credStore); sshErr != nil {
logger.Warn("nativessh: not starting SSH server on clients netstack: %v", sshErr)
} else {
s.sshServer = h
}
}
// Note: we already unlocked above, so don't use defer unlock
// Apply any pending blocked state to the newly created proxy handler
if s.blocked.Load() {
if ph := s.tnet.GetProxyHandler(); ph != nil {
ph.SetBlocked(true)
}
}
return nil
}
@@ -1563,3 +1561,33 @@ func (s *WireGuardService) filterReadOnlyFields(config string) string {
return strings.Join(filteredLines, "\n")
}
// sshServerHandle holds the listener so the SSH server can be stopped by
// closing it.
type sshServerHandle struct {
ln net.Listener
}
func (h *sshServerHandle) stop() {
_ = h.ln.Close()
}
// startSSHOnNetstack creates a TCP listener on port 22 of the clients' netstack
// and starts serving SSH connections on it in the background. The returned
// handle can be used to stop the server by closing the listener.
func startSSHOnNetstack(tnet *netstack2.Net, creds *nativessh.CredentialStore) (*sshServerHandle, error) {
srv := nativessh.NewServer(nativessh.ServerConfig{
Credentials: creds,
})
ln, err := tnet.ListenTCP(&net.TCPAddr{Port: 22})
if err != nil {
return nil, fmt.Errorf("listen on netstack port 22: %w", err)
}
h := &sshServerHandle{ln: ln}
go func() {
if err := srv.Serve(ln); err != nil {
logger.Debug("nativessh: clients netstack server stopped: %v", err)
}
}()
return h, nil
}

610
common.go
View File

@@ -1,610 +0,0 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"os"
"os/exec"
"regexp"
"strings"
"time"
"math/rand"
"github.com/fosrl/newt/internal/telemetry"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/proxy"
"github.com/fosrl/newt/websocket"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.zx2c4.com/wireguard/tun/netstack"
"gopkg.in/yaml.v3"
)
const msgHealthFileWriteFailed = "Failed to write health file: %v"
func ping(tnet *netstack.Net, dst string, timeout time.Duration) (time.Duration, error) {
// logger.Debug("Pinging %s", dst)
socket, err := tnet.Dial("ping4", dst)
if err != nil {
return 0, fmt.Errorf("failed to create ICMP socket: %w", err)
}
defer socket.Close()
// Set socket buffer sizes to handle high bandwidth scenarios
if tcpConn, ok := socket.(interface{ SetReadBuffer(int) error }); ok {
tcpConn.SetReadBuffer(64 * 1024)
}
if tcpConn, ok := socket.(interface{ SetWriteBuffer(int) error }); ok {
tcpConn.SetWriteBuffer(64 * 1024)
}
requestPing := icmp.Echo{
Seq: rand.Intn(1 << 16),
Data: []byte("newtping"),
}
icmpBytes, err := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil)
if err != nil {
return 0, fmt.Errorf("failed to marshal ICMP message: %w", err)
}
if err := socket.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return 0, fmt.Errorf("failed to set read deadline: %w", err)
}
start := time.Now()
_, err = socket.Write(icmpBytes)
if err != nil {
return 0, fmt.Errorf("failed to write ICMP packet: %w", err)
}
// Use larger buffer for reading to handle potential network congestion
readBuffer := make([]byte, 1500)
n, err := socket.Read(readBuffer)
if err != nil {
return 0, fmt.Errorf("failed to read ICMP packet: %w", err)
}
replyPacket, err := icmp.ParseMessage(1, readBuffer[:n])
if err != nil {
return 0, fmt.Errorf("failed to parse ICMP packet: %w", err)
}
replyPing, ok := replyPacket.Body.(*icmp.Echo)
if !ok {
return 0, fmt.Errorf("invalid reply type: got %T, want *icmp.Echo", replyPacket.Body)
}
if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq {
return 0, fmt.Errorf("invalid ping reply: got seq=%d data=%q, want seq=%d data=%q",
replyPing.Seq, replyPing.Data, requestPing.Seq, requestPing.Data)
}
latency := time.Since(start)
// logger.Debug("Ping to %s successful, latency: %v", dst, latency)
return latency, nil
}
// reliablePing performs multiple ping attempts with adaptive timeout
func reliablePing(tnet *netstack.Net, dst string, baseTimeout time.Duration, maxAttempts int) (time.Duration, error) {
var lastErr error
var totalLatency time.Duration
successCount := 0
for attempt := 1; attempt <= maxAttempts; attempt++ {
// Adaptive timeout: increase timeout for later attempts
timeout := baseTimeout + time.Duration(attempt-1)*500*time.Millisecond
// Add jitter to prevent thundering herd
jitter := time.Duration(rand.Intn(100)) * time.Millisecond
timeout += jitter
latency, err := ping(tnet, dst, timeout)
if err != nil {
lastErr = err
logger.Debug("Ping attempt %d/%d failed: %v", attempt, maxAttempts, err)
// Brief pause between attempts with exponential backoff
if attempt < maxAttempts {
backoff := time.Duration(attempt) * 50 * time.Millisecond
time.Sleep(backoff)
}
continue
}
totalLatency += latency
successCount++
// If we get at least one success, we can return early for health checks
if successCount > 0 {
avgLatency := totalLatency / time.Duration(successCount)
// logger.Debug("Reliable ping succeeded after %d attempts, avg latency: %v", attempt, avgLatency)
return avgLatency, nil
}
}
if successCount == 0 {
return 0, fmt.Errorf("all %d ping attempts failed, last error: %v", maxAttempts, lastErr)
}
return totalLatency / time.Duration(successCount), nil
}
func pingWithRetry(tnet *netstack.Net, dst string, timeout time.Duration) (stopChan chan struct{}, err error) {
if healthFile != "" {
err = os.Remove(healthFile)
if err != nil {
logger.Error("Failed to remove health file: %v", err)
}
}
const (
initialMaxAttempts = 5
initialRetryDelay = 2 * time.Second
maxRetryDelay = 60 * time.Second // Cap the maximum delay
)
stopChan = make(chan struct{})
attempt := 1
retryDelay := initialRetryDelay
// First try with the initial parameters
logger.Debug("Ping attempt %d", attempt)
if latency, err := ping(tnet, dst, timeout); err == nil {
// Successful ping
logger.Debug("Ping latency: %v", latency)
logger.Info("Tunnel connection to server established successfully!")
if healthFile != "" {
err := os.WriteFile(healthFile, []byte("ok"), 0644)
if err != nil {
logger.Warn(msgHealthFileWriteFailed, err)
}
}
return stopChan, nil
} else {
logger.Warn("Ping attempt %d failed: %v", attempt, err)
}
// Start a goroutine that will attempt pings indefinitely with increasing delays
go func() {
attempt = 2 // Continue from attempt 2
for {
select {
case <-stopChan:
return
default:
logger.Debug("Ping attempt %d", attempt)
if latency, err := ping(tnet, dst, timeout); err != nil {
logger.Warn("Ping attempt %d failed: %v", attempt, err)
// Increase delay after certain thresholds but cap it
if attempt%5 == 0 && retryDelay < maxRetryDelay {
retryDelay = time.Duration(float64(retryDelay) * 1.5)
if retryDelay > maxRetryDelay {
retryDelay = maxRetryDelay
}
logger.Info("Increasing ping retry delay to %v", retryDelay)
}
time.Sleep(retryDelay)
attempt++
} else {
// Successful ping
logger.Debug("Ping succeeded after %d attempts", attempt)
logger.Debug("Ping latency: %v", latency)
logger.Info("Tunnel connection to server established successfully!")
if healthFile != "" {
err := os.WriteFile(healthFile, []byte("ok"), 0644)
if err != nil {
logger.Warn(msgHealthFileWriteFailed, err)
}
}
return
}
case <-pingStopChan:
// Stop the goroutine when signaled
return
}
}
}()
// Return an error for the first batch of attempts (to maintain compatibility with existing code)
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
consecutiveFailures := 0
connectionLost := false
// Track recent latencies for adaptive timeout calculation
recentLatencies := make([]time.Duration, 0, 10)
pingStopChan := make(chan struct{})
go func() {
ticker := time.NewTicker(currentInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Calculate adaptive timeout based on recent latencies
adaptiveTimeout := pingTimeout
if len(recentLatencies) > 0 {
var sum time.Duration
for _, lat := range recentLatencies {
sum += lat
}
avgLatency := sum / time.Duration(len(recentLatencies))
// Use 3x average latency as timeout, with minimum of pingTimeout
adaptiveTimeout = avgLatency * 3
if adaptiveTimeout < pingTimeout {
adaptiveTimeout = pingTimeout
}
if adaptiveTimeout > 15*time.Second {
adaptiveTimeout = 15 * time.Second
}
}
// Use reliable ping with multiple attempts
maxAttempts := 2
if consecutiveFailures > 4 {
maxAttempts = 4 // More attempts when connection is unstable
}
latency, err := reliablePing(tnet, serverIP, adaptiveTimeout, maxAttempts)
if err != nil {
consecutiveFailures++
// Track recent latencies (add a high value for failures)
recentLatencies = append(recentLatencies, adaptiveTimeout)
if len(recentLatencies) > 10 {
recentLatencies = recentLatencies[1:]
}
if consecutiveFailures < 2 {
logger.Debug("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err)
} else {
logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err)
}
// More lenient threshold for declaring connection lost under load
failureThreshold := 4
if 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 remove health file: %v", err)
}
}
}
// 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
}
}
} else {
// Track recent latencies
recentLatencies = append(recentLatencies, latency)
// Record tunnel latency (limit sampling to this periodic check)
if tunnelID != "" {
telemetry.ObserveTunnelLatency(context.Background(), tunnelID, "wireguard", latency.Seconds())
}
if len(recentLatencies) > 10 {
recentLatencies = recentLatencies[1:]
}
if connectionLost {
connectionLost = false
logger.Info("Connection to server restored after %d failures!", consecutiveFailures)
if healthFile != "" {
err := os.WriteFile(healthFile, []byte("ok"), 0644)
if err != nil {
logger.Warn("Failed to write health file: %v", err)
}
}
}
if currentInterval > pingInterval {
currentInterval = time.Duration(float64(currentInterval) * 0.9) // Slower decrease
if currentInterval < pingInterval {
currentInterval = pingInterval
}
ticker.Reset(currentInterval)
logger.Debug("Decreased ping check interval to %v after successful ping", currentInterval)
}
consecutiveFailures = 0
}
case <-pingStopChan:
logger.Info("Stopping ping check")
return
}
}
}()
return pingStopChan
}
func parseTargetData(data interface{}) (TargetData, error) {
var targetData TargetData
jsonData, err := json.Marshal(data)
if err != nil {
logger.Info("Error marshaling data: %v", err)
return targetData, err
}
if err := json.Unmarshal(jsonData, &targetData); err != nil {
logger.Info("Error unmarshaling target data: %v", err)
return targetData, err
}
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 {
// Parse the target string, handling both IPv4 and IPv6 addresses
port, target, err := parseTargetString(t)
if err != nil {
logger.Info("Invalid target format: %s (%v)", t, err)
continue
}
switch action {
case "add":
// Call updown script if provided
processedTarget := target
if updownScript != "" {
newTarget, err := executeUpdownScript(action, proto, target)
if err != nil {
logger.Warn("Updown script error: %v", err)
} else if newTarget != "" {
processedTarget = newTarget
}
}
// Only remove the specific target if it exists
err := pm.RemoveTarget(proto, tunnelIP, port)
if err != nil {
// Ignore "target not found" errors as this is expected for new targets
if !strings.Contains(err.Error(), "target not found") {
logger.Error("Failed to remove existing target: %v", err)
}
}
// Add the new target
pm.AddTarget(proto, tunnelIP, port, processedTarget)
case "remove":
logger.Info("Removing target with port %d", port)
// Call updown script if provided
if updownScript != "" {
_, err := executeUpdownScript(action, proto, target)
if err != nil {
logger.Warn("Updown script error: %v", err)
}
}
err = pm.RemoveTarget(proto, tunnelIP, port)
if err != nil {
logger.Error("Failed to remove target: %v", err)
return err
}
default:
logger.Info("Unknown action: %s", action)
}
}
return nil
}
func executeUpdownScript(action, proto, target string) (string, error) {
if updownScript == "" {
return target, nil
}
// Split the updownScript in case it contains spaces (like "/usr/bin/python3 script.py")
parts := strings.Fields(updownScript)
if len(parts) == 0 {
return target, fmt.Errorf("invalid updown script command")
}
var cmd *exec.Cmd
if len(parts) == 1 {
// If it's a single executable
logger.Info("Executing updown script: %s %s %s %s", updownScript, action, proto, target)
cmd = exec.Command(parts[0], action, proto, target)
} else {
// If it includes interpreter and script
args := append(parts[1:], action, proto, target)
logger.Info("Executing updown script: %s %s %s %s %s", parts[0], strings.Join(parts[1:], " "), action, proto, target)
cmd = exec.Command(parts[0], args...)
}
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("updown script execution failed (exit code %d): %s",
exitErr.ExitCode(), string(exitErr.Stderr))
}
return "", fmt.Errorf("updown script execution failed: %v", err)
}
// If the script returns a new target, use it
newTarget := strings.TrimSpace(string(output))
if newTarget != "" {
logger.Info("Updown script returned new target: %s", newTarget)
return newTarget, nil
}
return target, nil
}
// 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(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
err = yaml.Unmarshal(blueprintData, &yamlObj)
if err != nil {
logger.Error("Failed to parse blueprint YAML: %v", err)
} else {
// convert to json
jsonBytes, err := json.Marshal(yamlObj)
if err != nil {
logger.Error("Failed to convert blueprint to JSON: %v", err)
} else {
blueprintJsonData = string(jsonBytes)
logger.Debug("Converted blueprint to JSON: %s", blueprintJsonData)
}
}
// if we have valid json data, we can send it to the server
if blueprintJsonData == "" {
logger.Error("No valid blueprint JSON data to send to server")
return nil
}
logger.Info("Sending blueprint to server for application")
// send the blueprint data to the server
err = client.SendMessage("newt/blueprint/apply", map[string]interface{}{
"blueprint": blueprintJsonData,
})
}
return nil
}

681
config.go Normal file
View File

@@ -0,0 +1,681 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/fosrl/newt/logger"
newtpkg "github.com/fosrl/newt/newt"
)
type stringSlice []string
func (s *stringSlice) String() string {
return strings.Join(*s, ",")
}
func (s *stringSlice) Set(value string) error {
*s = append(*s, value)
return nil
}
// configSource records where a resolved setting came from, for --show-config.
type configSource string
const (
sourceDefault configSource = "default"
sourceFile configSource = "file"
sourceEnv configSource = "environment"
sourceCLI configSource = "cli"
)
// fileSettings mirrors the on-disk JSON config file schema. Fields are
// pointers (or, for slices, nil-vs-non-empty) so that "absent from the file"
// can be distinguished from an explicit zero value.
type fileSettings struct {
Endpoint *string `json:"endpoint"`
ID *string `json:"id"`
Secret *string `json:"secret"`
ProvisioningKey *string `json:"provisioningKey"`
Name *string `json:"name"`
DNS *string `json:"dns"`
LogLevel *string `json:"logLevel"`
UpdownScript *string `json:"updownScript"`
InterfaceName *string `json:"interface"`
MTU *int `json:"mtu"`
Port *int `json:"port"`
UseNativeInterface *bool `json:"native"`
UseNativeMainInterface *bool `json:"nativeMain"`
NativeMainInterfaceName *string `json:"interfaceMain"`
NoCloud *bool `json:"noCloud"`
PreferEndpoint *string `json:"preferEndpoint"`
PingInterval *string `json:"pingInterval"`
PingTimeout *string `json:"pingTimeout"`
UDPProxyIdleTimeout *string `json:"udpProxyIdleTimeout"`
DisableClients *bool `json:"disableClients"`
DisableSSH *bool `json:"disableSsh"`
EnforceHealthcheckCert *bool `json:"enforceHcCert"`
HealthFile *string `json:"healthFile"`
BlueprintFile *string `json:"blueprintFile"`
ProvisioningBlueprintFile *string `json:"provisioningBlueprintFile"`
DockerSocket *string `json:"dockerSocket"`
DockerEnforceNetworkValidation *bool `json:"dockerEnforceNetworkValidation"`
AuthDaemonKey *string `json:"adPreSharedKey"`
AuthDaemonPrincipalsFile *string `json:"adPrincipalsFile"`
AuthDaemonCACertPath *string `json:"adCaCertPath"`
AuthDaemonGenerateRandomPassword *bool `json:"adGenerateRandomPassword"`
TLSClientCert *string `json:"tlsClientCertFile"`
TLSClientKey *string `json:"tlsClientKey"`
TLSClientCAs []string `json:"tlsClientCa"`
TLSPrivateKey *string `json:"tlsClientCert"` // legacy PKCS12 path; matches the key already written by the credential-save path
MetricsEnabled *bool `json:"metrics"`
OTLPEnabled *bool `json:"otlp"`
AdminAddr *string `json:"metricsAdminAddr"`
Region *string `json:"region"`
MetricsAsyncBytes *bool `json:"metricsAsyncBytes"`
PprofEnabled *bool `json:"pprof"`
}
// resolveConfigFilePath determines the settings/credentials file path using
// the same precedence as every other setting: CLI > env > OS default.
// It has to run before flag.Parse (which needs the file-resolved defaults),
// so it scans os.Args directly instead of using the flag package.
func resolveConfigFilePath(args []string) string {
for i, a := range args {
if a == "--config-file" || a == "-config-file" {
if i+1 < len(args) {
return args[i+1]
}
}
if v, ok := strings.CutPrefix(a, "--config-file="); ok {
return v
}
if v, ok := strings.CutPrefix(a, "-config-file="); ok {
return v
}
}
if v := os.Getenv("CONFIG_FILE"); v != "" {
return v
}
var configDir string
switch runtime.GOOS {
case "darwin":
configDir = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "newt-client")
case "windows":
configDir = filepath.Join(os.Getenv("PROGRAMDATA"), "newt", "newt-client")
default: // linux and others
configDir = filepath.Join(os.Getenv("HOME"), ".config", "newt-client")
}
if err := os.MkdirAll(configDir, 0755); err != nil {
fmt.Printf("Warning: Failed to create config directory: %v\n", err)
}
return filepath.Join(configDir, "config.json")
}
// loadFileSettings reads and parses the config file. A missing or empty file
// is not an error (returns nil, nil) since the file may not exist yet.
func loadFileSettings(path string) (*fileSettings, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
if len(strings.TrimSpace(string(data))) == 0 {
return nil, nil
}
var fs fileSettings
if err := json.Unmarshal(data, &fs); err != nil {
return nil, fmt.Errorf("failed to parse config file %s: %w", path, err)
}
return &fs, nil
}
func applyStr(dst *string, v *string, key string, sources map[string]string, src configSource) {
if v != nil {
*dst = *v
sources[key] = string(src)
}
}
func applyBool(dst *bool, v *bool, key string, sources map[string]string, src configSource) {
if v != nil {
*dst = *v
sources[key] = string(src)
}
}
func applyEnvStr(dst *string, envName, key string, sources map[string]string) {
if v := os.Getenv(envName); v != "" {
*dst = v
sources[key] = string(sourceEnv)
}
}
func applyEnvBool(dst *bool, envName, key string, sources map[string]string) {
if v := os.Getenv(envName); v != "" {
*dst = v == "true"
sources[key] = string(sourceEnv)
}
}
// validateTLSConfig validates that TLS config fields are consistent and that
// referenced files exist.
func validateTLSConfig(cfg newtpkg.Config) error {
pkcs12Specified := cfg.TLSPrivateKey != ""
separateFilesSpecified := cfg.TLSClientCert != "" || cfg.TLSClientKey != "" || len(cfg.TLSClientCAs) > 0
if pkcs12Specified && separateFilesSpecified {
return fmt.Errorf("cannot use both PKCS12 format (--tls-client-cert) and separate certificate files (--tls-client-cert-file, --tls-client-key, --tls-client-ca)")
}
if (cfg.TLSClientCert != "" && cfg.TLSClientKey == "") || (cfg.TLSClientCert == "" && cfg.TLSClientKey != "") {
return fmt.Errorf("both --tls-client-cert-file and --tls-client-key must be specified together")
}
if cfg.TLSClientCert != "" {
if _, err := os.Stat(cfg.TLSClientCert); os.IsNotExist(err) {
return fmt.Errorf("client certificate file does not exist: %s", cfg.TLSClientCert)
}
}
if cfg.TLSClientKey != "" {
if _, err := os.Stat(cfg.TLSClientKey); os.IsNotExist(err) {
return fmt.Errorf("client key file does not exist: %s", cfg.TLSClientKey)
}
}
for _, caFile := range cfg.TLSClientCAs {
if _, err := os.Stat(caFile); os.IsNotExist(err) {
return fmt.Errorf("CA certificate file does not exist: %s", caFile)
}
}
if cfg.TLSPrivateKey != "" {
if _, err := os.Stat(cfg.TLSPrivateKey); os.IsNotExist(err) {
return fmt.Errorf("PKCS12 certificate file does not exist: %s", cfg.TLSPrivateKey)
}
}
return nil
}
// parseDurationEnvOrFlag parses s as a duration, using defaultVal on failure.
func parseDurationEnvOrFlag(s string, defaultVal time.Duration, label string) time.Duration {
if s == "" {
return defaultVal
}
d, err := time.ParseDuration(s)
if err != nil || d <= 0 {
fmt.Printf("Invalid %s value: %s, using default %v\n", label, s, defaultVal)
return defaultVal
}
return d
}
// loadNewtConfig resolves configuration with priority cli > env > file >
// default, then returns a populated newtpkg.Config. This function calls
// flag.Parse internally and will exit the process if --version or
// --show-config is passed.
func loadNewtConfig() newtpkg.Config {
sources := make(map[string]string)
configPath := resolveConfigFilePath(os.Args[1:])
fileCfg, err := loadFileSettings(configPath)
if err != nil {
logger.Fatal("Failed to load config file: %v", err)
}
// ---- defaults ----
cfg := newtpkg.Config{
Version: newtVersion,
Platform: newtPlatform,
DNS: "9.9.9.9",
LogLevel: "INFO",
InterfaceName: "newt",
NativeMainInterfaceName: "newt",
AuthDaemonPrincipalsFile: "/var/run/auth-daemon/principals",
AuthDaemonCACertPath: "/etc/ssh/ca.pem",
AdminAddr: "127.0.0.1:2112",
}
mtuStr := "1280"
portStr := ""
pingIntervalStr := "15s"
pingTimeoutStr := "7s"
udpProxyIdleTimeoutStr := "90s"
dockerEnforceStr := "false"
// ---- layer 1: config file ----
if fileCfg != nil {
applyStr(&cfg.Endpoint, fileCfg.Endpoint, "endpoint", sources, sourceFile)
applyStr(&cfg.ID, fileCfg.ID, "id", sources, sourceFile)
applyStr(&cfg.Secret, fileCfg.Secret, "secret", sources, sourceFile)
applyStr(&cfg.ProvisioningKey, fileCfg.ProvisioningKey, "provisioning-key", sources, sourceFile)
applyStr(&cfg.NewtName, fileCfg.Name, "name", sources, sourceFile)
applyStr(&cfg.DNS, fileCfg.DNS, "dns", sources, sourceFile)
applyStr(&cfg.LogLevel, fileCfg.LogLevel, "log-level", sources, sourceFile)
applyStr(&cfg.UpdownScript, fileCfg.UpdownScript, "updown", sources, sourceFile)
applyStr(&cfg.InterfaceName, fileCfg.InterfaceName, "interface", sources, sourceFile)
if fileCfg.MTU != nil {
mtuStr = strconv.Itoa(*fileCfg.MTU)
sources["mtu"] = string(sourceFile)
}
if fileCfg.Port != nil {
portStr = strconv.Itoa(*fileCfg.Port)
sources["port"] = string(sourceFile)
}
applyBool(&cfg.UseNativeInterface, fileCfg.UseNativeInterface, "native", sources, sourceFile)
applyBool(&cfg.UseNativeMainInterface, fileCfg.UseNativeMainInterface, "native-main", sources, sourceFile)
applyStr(&cfg.NativeMainInterfaceName, fileCfg.NativeMainInterfaceName, "interface-main", sources, sourceFile)
applyBool(&cfg.NoCloud, fileCfg.NoCloud, "no-cloud", sources, sourceFile)
applyStr(&cfg.PreferEndpoint, fileCfg.PreferEndpoint, "prefer-endpoint", sources, sourceFile)
applyStr(&pingIntervalStr, fileCfg.PingInterval, "ping-interval", sources, sourceFile)
applyStr(&pingTimeoutStr, fileCfg.PingTimeout, "ping-timeout", sources, sourceFile)
applyStr(&udpProxyIdleTimeoutStr, fileCfg.UDPProxyIdleTimeout, "udp-proxy-idle-timeout", sources, sourceFile)
applyBool(&cfg.DisableClients, fileCfg.DisableClients, "disable-clients", sources, sourceFile)
applyBool(&cfg.DisableSSH, fileCfg.DisableSSH, "disable-ssh", sources, sourceFile)
applyBool(&cfg.EnforceHealthcheckCert, fileCfg.EnforceHealthcheckCert, "enforce-hc-cert", sources, sourceFile)
applyStr(&cfg.HealthFile, fileCfg.HealthFile, "health-file", sources, sourceFile)
applyStr(&cfg.BlueprintFile, fileCfg.BlueprintFile, "blueprint-file", sources, sourceFile)
applyStr(&cfg.ProvisioningBlueprintFile, fileCfg.ProvisioningBlueprintFile, "provisioning-blueprint-file", sources, sourceFile)
applyStr(&cfg.DockerSocket, fileCfg.DockerSocket, "docker-socket", sources, sourceFile)
if fileCfg.DockerEnforceNetworkValidation != nil {
dockerEnforceStr = strconv.FormatBool(*fileCfg.DockerEnforceNetworkValidation)
sources["docker-enforce-network-validation"] = string(sourceFile)
}
applyStr(&cfg.AuthDaemonKey, fileCfg.AuthDaemonKey, "ad-pre-shared-key", sources, sourceFile)
applyStr(&cfg.AuthDaemonPrincipalsFile, fileCfg.AuthDaemonPrincipalsFile, "ad-principals-file", sources, sourceFile)
applyStr(&cfg.AuthDaemonCACertPath, fileCfg.AuthDaemonCACertPath, "ad-ca-cert-path", sources, sourceFile)
applyBool(&cfg.AuthDaemonGenerateRandomPassword, fileCfg.AuthDaemonGenerateRandomPassword, "ad-generate-random-password", sources, sourceFile)
applyStr(&cfg.TLSClientCert, fileCfg.TLSClientCert, "tls-client-cert-file", sources, sourceFile)
applyStr(&cfg.TLSClientKey, fileCfg.TLSClientKey, "tls-client-key", sources, sourceFile)
if len(fileCfg.TLSClientCAs) > 0 {
cfg.TLSClientCAs = append(cfg.TLSClientCAs, fileCfg.TLSClientCAs...)
sources["tls-client-ca"] = string(sourceFile)
}
applyStr(&cfg.TLSPrivateKey, fileCfg.TLSPrivateKey, "tls-client-cert", sources, sourceFile)
applyBool(&cfg.MetricsEnabled, fileCfg.MetricsEnabled, "metrics", sources, sourceFile)
applyBool(&cfg.OTLPEnabled, fileCfg.OTLPEnabled, "otlp", sources, sourceFile)
applyStr(&cfg.AdminAddr, fileCfg.AdminAddr, "metrics-admin-addr", sources, sourceFile)
applyStr(&cfg.Region, fileCfg.Region, "region", sources, sourceFile)
applyBool(&cfg.MetricsAsyncBytes, fileCfg.MetricsAsyncBytes, "metrics-async-bytes", sources, sourceFile)
applyBool(&cfg.PprofEnabled, fileCfg.PprofEnabled, "pprof", sources, sourceFile)
}
// ---- layer 2: environment variables ----
applyEnvStr(&cfg.Endpoint, "PANGOLIN_ENDPOINT", "endpoint", sources)
applyEnvStr(&cfg.ID, "NEWT_ID", "id", sources)
applyEnvStr(&cfg.Secret, "NEWT_SECRET", "secret", sources)
applyEnvStr(&cfg.ProvisioningKey, "NEWT_PROVISIONING_KEY", "provisioning-key", sources)
applyEnvStr(&cfg.NewtName, "NEWT_NAME", "name", sources)
applyEnvStr(&cfg.DNS, "DNS", "dns", sources)
applyEnvStr(&cfg.LogLevel, "LOG_LEVEL", "log-level", sources)
applyEnvStr(&cfg.UpdownScript, "UPDOWN_SCRIPT", "updown", sources)
applyEnvStr(&cfg.InterfaceName, "INTERFACE", "interface", sources)
applyEnvStr(&mtuStr, "MTU", "mtu", sources)
applyEnvStr(&portStr, "PORT", "port", sources)
applyEnvBool(&cfg.UseNativeInterface, "USE_NATIVE_INTERFACE", "native", sources)
applyEnvBool(&cfg.UseNativeMainInterface, "USE_NATIVE_MAIN_INTERFACE", "native-main", sources)
applyEnvStr(&cfg.NativeMainInterfaceName, "INTERFACE_MAIN", "interface-main", sources)
applyEnvBool(&cfg.NoCloud, "NO_CLOUD", "no-cloud", sources)
applyEnvStr(&pingIntervalStr, "PING_INTERVAL", "ping-interval", sources)
applyEnvStr(&pingTimeoutStr, "PING_TIMEOUT", "ping-timeout", sources)
applyEnvStr(&udpProxyIdleTimeoutStr, "NEWT_UDP_PROXY_IDLE_TIMEOUT", "udp-proxy-idle-timeout", sources)
applyEnvBool(&cfg.DisableClients, "DISABLE_CLIENTS", "disable-clients", sources)
applyEnvBool(&cfg.DisableSSH, "DISABLE_SSH", "disable-ssh", sources)
applyEnvBool(&cfg.EnforceHealthcheckCert, "ENFORCE_HC_CERT", "enforce-hc-cert", sources)
applyEnvStr(&cfg.HealthFile, "HEALTH_FILE", "health-file", sources)
applyEnvStr(&cfg.BlueprintFile, "BLUEPRINT_FILE", "blueprint-file", sources)
applyEnvStr(&cfg.ProvisioningBlueprintFile, "PROVISIONING_BLUEPRINT_FILE", "provisioning-blueprint-file", sources)
applyEnvStr(&cfg.DockerSocket, "DOCKER_SOCKET", "docker-socket", sources)
applyEnvStr(&dockerEnforceStr, "DOCKER_ENFORCE_NETWORK_VALIDATION", "docker-enforce-network-validation", sources)
applyEnvStr(&cfg.AuthDaemonKey, "AD_KEY", "ad-pre-shared-key", sources)
applyEnvStr(&cfg.AuthDaemonPrincipalsFile, "AD_PRINCIPALS_FILE", "ad-principals-file", sources)
applyEnvStr(&cfg.AuthDaemonCACertPath, "AD_CA_CERT_PATH", "ad-ca-cert-path", sources)
if v, err := strconv.ParseBool(os.Getenv("AD_GENERATE_RANDOM_PASSWORD")); err == nil {
cfg.AuthDaemonGenerateRandomPassword = v
sources["ad-generate-random-password"] = string(sourceEnv)
}
applyEnvStr(&cfg.TLSClientCert, "TLS_CLIENT_CERT", "tls-client-cert-file", sources)
applyEnvStr(&cfg.TLSClientKey, "TLS_CLIENT_KEY", "tls-client-key", sources)
if tlsClientCAsEnv := os.Getenv("TLS_CLIENT_CAS"); tlsClientCAsEnv != "" {
for _, ca := range strings.Split(tlsClientCAsEnv, ",") {
cfg.TLSClientCAs = append(cfg.TLSClientCAs, strings.TrimSpace(ca))
}
sources["tls-client-ca"] = string(sourceEnv)
}
applyEnvStr(&cfg.TLSPrivateKey, "TLS_CLIENT_CERT_PKCS12", "tls-client-cert", sources)
// Legacy PKCS12 backward-compat: fall back to the (already layered)
// separate-cert-file value for PKCS12 when the newer fields are unset.
if cfg.TLSPrivateKey == "" && cfg.TLSClientKey == "" && len(cfg.TLSClientCAs) == 0 && cfg.TLSClientCert != "" {
cfg.TLSPrivateKey = cfg.TLSClientCert
sources["tls-client-cert"] = sources["tls-client-cert-file"]
}
if metricsEnabledEnv := os.Getenv("NEWT_METRICS_PROMETHEUS_ENABLED"); metricsEnabledEnv != "" {
if v, err := strconv.ParseBool(metricsEnabledEnv); err == nil {
cfg.MetricsEnabled = v
} else {
cfg.MetricsEnabled = true
}
sources["metrics"] = string(sourceEnv)
}
applyEnvBool(&cfg.OTLPEnabled, "NEWT_METRICS_OTLP_ENABLED", "otlp", sources)
applyEnvStr(&cfg.AdminAddr, "NEWT_ADMIN_ADDR", "metrics-admin-addr", sources)
applyEnvStr(&cfg.Region, "NEWT_REGION", "region", sources)
applyEnvBool(&cfg.MetricsAsyncBytes, "NEWT_METRICS_ASYNC_BYTES", "metrics-async-bytes", sources)
applyEnvBool(&cfg.PprofEnabled, "NEWT_PPROF_ENABLED", "pprof", sources)
// ---- layer 3: CLI flags (always registered; default = file/env-resolved value) ----
origEndpoint, origID, origSecret := cfg.Endpoint, cfg.ID, cfg.Secret
origMTU, origDNS, origLogLevel := mtuStr, cfg.DNS, cfg.LogLevel
origUpdown, origInterface, origPort := cfg.UpdownScript, cfg.InterfaceName, portStr
origNative, origNativeMain, origInterfaceMain := cfg.UseNativeInterface, cfg.UseNativeMainInterface, cfg.NativeMainInterfaceName
origDisableClients, origDisableSSH, origEnforceHC := cfg.DisableClients, cfg.DisableSSH, cfg.EnforceHealthcheckCert
origDockerSocket, origPingInterval, origPingTimeout := cfg.DockerSocket, pingIntervalStr, pingTimeoutStr
origUDPIdle, origProvisioningKey, origName := udpProxyIdleTimeoutStr, cfg.ProvisioningKey, cfg.NewtName
origTLSCert, origTLSKey, origDockerEnforce := cfg.TLSClientCert, cfg.TLSClientKey, dockerEnforceStr
origHealthFile, origBlueprintFile, origProvBlueprintFile := cfg.HealthFile, cfg.BlueprintFile, cfg.ProvisioningBlueprintFile
origNoCloud, origTLSPrivateKey := cfg.NoCloud, cfg.TLSPrivateKey
origMetrics, origOTLP, origAdminAddr := cfg.MetricsEnabled, cfg.OTLPEnabled, cfg.AdminAddr
origMetricsAsync, origPprof, origRegion := cfg.MetricsAsyncBytes, cfg.PprofEnabled, cfg.Region
origADKey, origADPrincipals, origADCACert := cfg.AuthDaemonKey, cfg.AuthDaemonPrincipalsFile, cfg.AuthDaemonCACertPath
origADRandomPass := cfg.AuthDaemonGenerateRandomPassword
flag.StringVar(&cfg.Endpoint, "endpoint", cfg.Endpoint, "Endpoint of your pangolin server")
flag.StringVar(&cfg.ID, "id", cfg.ID, "Newt ID")
flag.StringVar(&cfg.Secret, "secret", cfg.Secret, "Newt secret")
flag.StringVar(&mtuStr, "mtu", mtuStr, "MTU to use")
flag.StringVar(&cfg.DNS, "dns", cfg.DNS, "DNS server to use")
flag.StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
flag.StringVar(&cfg.UpdownScript, "updown", cfg.UpdownScript, "Path to updown script to be called when targets are added or removed")
flag.StringVar(&cfg.InterfaceName, "interface", cfg.InterfaceName, "Name of the WireGuard interface")
flag.StringVar(&portStr, "port", portStr, "Port for client WireGuard interface")
flag.BoolVar(&cfg.UseNativeInterface, "native", cfg.UseNativeInterface, "Use native WireGuard interface for client tunnels")
flag.BoolVar(&cfg.UseNativeMainInterface, "native-main", cfg.UseNativeMainInterface, "Use native WireGuard interface for the main tunnel (instead of netstack)")
// making this the same as above should prevent them from running together
flag.StringVar(&cfg.NativeMainInterfaceName, "interface-main", cfg.NativeMainInterfaceName, "Name of the native main tunnel WireGuard interface (used with --native-main)")
flag.BoolVar(&cfg.DisableClients, "disable-clients", cfg.DisableClients, "Disable clients on the WireGuard interface")
flag.BoolVar(&cfg.DisableSSH, "disable-ssh", cfg.DisableSSH, "Disable SSH auth daemon and native SSH mode (remote auth daemon still works)")
flag.BoolVar(&cfg.EnforceHealthcheckCert, "enforce-hc-cert", cfg.EnforceHealthcheckCert, "Enforce certificate validation for health checks (default: false, accepts any cert)")
flag.StringVar(&cfg.DockerSocket, "docker-socket", cfg.DockerSocket, "Path or address to Docker socket (typically unix:///var/run/docker.sock)")
flag.StringVar(&pingIntervalStr, "ping-interval", pingIntervalStr, "Interval for pinging the server (default 15s)")
flag.StringVar(&pingTimeoutStr, "ping-timeout", pingTimeoutStr, "Timeout for each ping (default 7s)")
flag.StringVar(&udpProxyIdleTimeoutStr, "udp-proxy-idle-timeout", udpProxyIdleTimeoutStr, "Idle timeout for UDP proxied client flows before cleanup")
flag.StringVar(&cfg.PreferEndpoint, "prefer-endpoint", cfg.PreferEndpoint, "Prefer this endpoint for the connection (if set, will override the endpoint from the server)")
flag.StringVar(&cfg.ProvisioningKey, "provisioning-key", cfg.ProvisioningKey, "One-time provisioning key used to obtain a newt ID and secret from the server")
flag.StringVar(&cfg.NewtName, "name", cfg.NewtName, "Name for the site created during provisioning (supports {{env.VAR}} interpolation)")
flag.StringVar(&cfg.ConfigFile, "config-file", configPath, "Path to config file (overrides CONFIG_FILE env var and default location)")
flag.StringVar(&cfg.TLSClientCert, "tls-client-cert-file", cfg.TLSClientCert, "Path to client certificate file (PEM/DER format)")
flag.StringVar(&cfg.TLSClientKey, "tls-client-key", cfg.TLSClientKey, "Path to client private key file (PEM/DER format)")
// Backward-compat dummy flag (auth daemon is always enabled now)
flag.Bool("auth-daemon", false, "Enable auth daemon mode (deprecated, always enabled)")
var tlsClientCAsFlag stringSlice
flag.Var(&tlsClientCAsFlag, "tls-client-ca", "Path to CA certificate file for validating remote certificates (can be specified multiple times)")
flag.StringVar(&cfg.TLSPrivateKey, "tls-client-cert", cfg.TLSPrivateKey, "Path to client certificate (PKCS12 format) - DEPRECATED: use --tls-client-cert-file and --tls-client-key instead")
flag.StringVar(&dockerEnforceStr, "docker-enforce-network-validation", dockerEnforceStr, "Enforce validation of container on newt network (true or false)")
flag.StringVar(&cfg.HealthFile, "health-file", cfg.HealthFile, "Path to health file (if unset, health file won't be written)")
flag.StringVar(&cfg.BlueprintFile, "blueprint-file", cfg.BlueprintFile, "Path to blueprint file (if unset, no blueprint will be applied)")
flag.StringVar(&cfg.ProvisioningBlueprintFile, "provisioning-blueprint-file", cfg.ProvisioningBlueprintFile, "Path to blueprint file applied once after a provisioning credential exchange (if unset, no provisioning blueprint will be applied)")
flag.BoolVar(&cfg.NoCloud, "no-cloud", cfg.NoCloud, "Disable cloud failover")
flag.BoolVar(&cfg.MetricsEnabled, "metrics", cfg.MetricsEnabled, "Enable Prometheus metrics exporter")
flag.BoolVar(&cfg.OTLPEnabled, "otlp", cfg.OTLPEnabled, "Enable OTLP exporters (metrics/traces) to OTEL_EXPORTER_OTLP_ENDPOINT")
flag.StringVar(&cfg.AdminAddr, "metrics-admin-addr", cfg.AdminAddr, "Admin/metrics bind address")
flag.BoolVar(&cfg.MetricsAsyncBytes, "metrics-async-bytes", cfg.MetricsAsyncBytes, "Enable async bytes counting (background flush; lower hot path overhead)")
flag.BoolVar(&cfg.PprofEnabled, "pprof", cfg.PprofEnabled, "Enable pprof debug endpoints on admin server")
flag.StringVar(&cfg.Region, "region", cfg.Region, "Optional region resource attribute (also NEWT_REGION)")
flag.StringVar(&cfg.AuthDaemonKey, "ad-pre-shared-key", cfg.AuthDaemonKey, "Pre-shared key for auth daemon authentication")
flag.StringVar(&cfg.AuthDaemonPrincipalsFile, "ad-principals-file", cfg.AuthDaemonPrincipalsFile, "Path to the principals file for auth daemon")
flag.StringVar(&cfg.AuthDaemonCACertPath, "ad-ca-cert-path", cfg.AuthDaemonCACertPath, "Path to the CA certificate file for auth daemon")
flag.BoolVar(&cfg.AuthDaemonGenerateRandomPassword, "ad-generate-random-password", cfg.AuthDaemonGenerateRandomPassword, "Generate a random password for authenticated users")
version := flag.Bool("version", false, "Print the version")
showConfig := flag.Bool("show-config", false, "Show configuration values and their sources, then exit")
flag.Parse()
// ---- post-parse processing ----
// Merge CLI CA files onto whatever file/env already contributed.
if len(tlsClientCAsFlag) > 0 {
cfg.TLSClientCAs = append(cfg.TLSClientCAs, tlsClientCAsFlag...)
sources["tls-client-ca"] = string(sourceCLI)
}
markCLI := func(key string, changed bool) {
if changed {
sources[key] = string(sourceCLI)
}
}
markCLI("endpoint", cfg.Endpoint != origEndpoint)
markCLI("id", cfg.ID != origID)
markCLI("secret", cfg.Secret != origSecret)
markCLI("mtu", mtuStr != origMTU)
markCLI("dns", cfg.DNS != origDNS)
markCLI("log-level", cfg.LogLevel != origLogLevel)
markCLI("updown", cfg.UpdownScript != origUpdown)
markCLI("interface", cfg.InterfaceName != origInterface)
markCLI("port", portStr != origPort)
markCLI("native", cfg.UseNativeInterface != origNative)
markCLI("native-main", cfg.UseNativeMainInterface != origNativeMain)
markCLI("interface-main", cfg.NativeMainInterfaceName != origInterfaceMain)
markCLI("disable-clients", cfg.DisableClients != origDisableClients)
markCLI("disable-ssh", cfg.DisableSSH != origDisableSSH)
markCLI("enforce-hc-cert", cfg.EnforceHealthcheckCert != origEnforceHC)
markCLI("docker-socket", cfg.DockerSocket != origDockerSocket)
markCLI("ping-interval", pingIntervalStr != origPingInterval)
markCLI("ping-timeout", pingTimeoutStr != origPingTimeout)
markCLI("udp-proxy-idle-timeout", udpProxyIdleTimeoutStr != origUDPIdle)
markCLI("provisioning-key", cfg.ProvisioningKey != origProvisioningKey)
markCLI("name", cfg.NewtName != origName)
markCLI("tls-client-cert-file", cfg.TLSClientCert != origTLSCert)
markCLI("tls-client-key", cfg.TLSClientKey != origTLSKey)
markCLI("tls-client-cert", cfg.TLSPrivateKey != origTLSPrivateKey)
markCLI("docker-enforce-network-validation", dockerEnforceStr != origDockerEnforce)
markCLI("health-file", cfg.HealthFile != origHealthFile)
markCLI("blueprint-file", cfg.BlueprintFile != origBlueprintFile)
markCLI("provisioning-blueprint-file", cfg.ProvisioningBlueprintFile != origProvBlueprintFile)
markCLI("no-cloud", cfg.NoCloud != origNoCloud)
markCLI("metrics", cfg.MetricsEnabled != origMetrics)
markCLI("otlp", cfg.OTLPEnabled != origOTLP)
markCLI("metrics-admin-addr", cfg.AdminAddr != origAdminAddr)
markCLI("metrics-async-bytes", cfg.MetricsAsyncBytes != origMetricsAsync)
markCLI("pprof", cfg.PprofEnabled != origPprof)
markCLI("region", cfg.Region != origRegion)
markCLI("ad-pre-shared-key", cfg.AuthDaemonKey != origADKey)
markCLI("ad-principals-file", cfg.AuthDaemonPrincipalsFile != origADPrincipals)
markCLI("ad-ca-cert-path", cfg.AuthDaemonCACertPath != origADCACert)
markCLI("ad-generate-random-password", cfg.AuthDaemonGenerateRandomPassword != origADRandomPass)
if cfg.ConfigFile != configPath {
sources["config-file"] = string(sourceCLI)
}
// Version check (exits process)
if *version {
fmt.Println("Newt version " + newtVersion)
os.Exit(0)
}
if *showConfig {
printShowConfig(cfg, sources, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr)
os.Exit(0)
}
logger.Info("Newt version %s", newtVersion)
// Parse port
if portStr != "" {
portInt, err := strconv.Atoi(portStr)
if err != nil {
logger.Warn("Failed to parse PORT, choosing a random port")
} else {
cfg.Port = uint16(portInt)
}
}
// Parse MTU
if mtuStr == "" {
mtuStr = "1280"
}
mtuInt, err := strconv.Atoi(mtuStr)
if err != nil {
logger.Fatal("Failed to parse MTU: %v", err)
}
cfg.MTU = mtuInt
// Parse docker network validation flag
if v, err := strconv.ParseBool(dockerEnforceStr); err == nil {
cfg.DockerEnforceNetworkValidation = v
} else {
logger.Info("Docker enforce network validation cannot be parsed. Defaulting to 'false'")
cfg.DockerEnforceNetworkValidation = false
}
// Parse durations (after flag.Parse so CLI flags take effect)
cfg.PingInterval = parseDurationEnvOrFlag(pingIntervalStr, 15*time.Second, "PING_INTERVAL")
cfg.PingTimeout = parseDurationEnvOrFlag(pingTimeoutStr, 7*time.Second, "PING_TIMEOUT")
cfg.UDPProxyIdleTimeout = parseDurationEnvOrFlag(udpProxyIdleTimeoutStr, 90*time.Second, "NEWT_UDP_PROXY_IDLE_TIMEOUT")
return cfg
}
// printShowConfig prints the resolved configuration and the source of each value
func printShowConfig(cfg newtpkg.Config, sources map[string]string, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr string) {
getSource := func(key string) string {
if s, ok := sources[key]; ok && s != "" {
return s
}
return string(sourceDefault)
}
mask := func(key, value string) string {
if key == "secret" && value != "" {
if len(value) > 8 {
return value[:4] + "****" + value[len(value)-4:]
}
return "****"
}
if value == "" {
return "(not set)"
}
return value
}
fmt.Print("\n=== Newt Configuration ===\n\n")
fmt.Printf("Config File: %s\n", configPath)
if _, err := os.Stat(configPath); err == nil {
fmt.Printf("Config File Status: exists\n")
} else {
fmt.Printf("Config File Status: not found\n")
}
fmt.Println("\n--- Configuration Values ---")
fmt.Print("(Format: Setting = Value [source])\n\n")
fmt.Println("Connection:")
fmt.Printf(" endpoint = %s [%s]\n", mask("endpoint", cfg.Endpoint), getSource("endpoint"))
fmt.Printf(" id = %s [%s]\n", mask("id", cfg.ID), getSource("id"))
fmt.Printf(" secret = %s [%s]\n", mask("secret", cfg.Secret), getSource("secret"))
fmt.Printf(" provisioning-key = %s [%s]\n", mask("provisioning-key", cfg.ProvisioningKey), getSource("provisioning-key"))
fmt.Printf(" name = %s [%s]\n", mask("name", cfg.NewtName), getSource("name"))
fmt.Printf(" prefer-endpoint = %s [%s]\n", mask("prefer-endpoint", cfg.PreferEndpoint), getSource("prefer-endpoint"))
fmt.Println("\nNetwork:")
fmt.Printf(" mtu = %s [%s]\n", mtuStr, getSource("mtu"))
fmt.Printf(" dns = %s [%s]\n", cfg.DNS, getSource("dns"))
fmt.Printf(" interface = %s [%s]\n", cfg.InterfaceName, getSource("interface"))
fmt.Printf(" port = %s [%s]\n", mask("port", portStr), getSource("port"))
fmt.Printf(" native = %v [%s]\n", cfg.UseNativeInterface, getSource("native"))
fmt.Printf(" native-main = %v [%s]\n", cfg.UseNativeMainInterface, getSource("native-main"))
fmt.Printf(" interface-main = %s [%s]\n", cfg.NativeMainInterfaceName, getSource("interface-main"))
fmt.Printf(" no-cloud = %v [%s]\n", cfg.NoCloud, getSource("no-cloud"))
fmt.Println("\nLogging:")
fmt.Printf(" log-level = %s [%s]\n", cfg.LogLevel, getSource("log-level"))
fmt.Println("\nTiming:")
fmt.Printf(" ping-interval = %s [%s]\n", pingIntervalStr, getSource("ping-interval"))
fmt.Printf(" ping-timeout = %s [%s]\n", pingTimeoutStr, getSource("ping-timeout"))
fmt.Printf(" udp-proxy-idle-timeout = %s [%s]\n", udpProxyIdleTimeoutStr, getSource("udp-proxy-idle-timeout"))
fmt.Println("\nFeatures:")
fmt.Printf(" disable-clients = %v [%s]\n", cfg.DisableClients, getSource("disable-clients"))
fmt.Printf(" disable-ssh = %v [%s]\n", cfg.DisableSSH, getSource("disable-ssh"))
fmt.Printf(" enforce-hc-cert = %v [%s]\n", cfg.EnforceHealthcheckCert, getSource("enforce-hc-cert"))
fmt.Printf(" health-file = %s [%s]\n", mask("health-file", cfg.HealthFile), getSource("health-file"))
fmt.Printf(" blueprint-file = %s [%s]\n", mask("blueprint-file", cfg.BlueprintFile), getSource("blueprint-file"))
fmt.Printf(" provisioning-blueprint-file = %s [%s]\n", mask("provisioning-blueprint-file", cfg.ProvisioningBlueprintFile), getSource("provisioning-blueprint-file"))
fmt.Printf(" updown = %s [%s]\n", mask("updown", cfg.UpdownScript), getSource("updown"))
fmt.Println("\nDocker:")
fmt.Printf(" docker-socket = %s [%s]\n", mask("docker-socket", cfg.DockerSocket), getSource("docker-socket"))
fmt.Printf(" docker-enforce-network-validation = %s [%s]\n", dockerEnforceStr, getSource("docker-enforce-network-validation"))
fmt.Println("\nAuth Daemon:")
fmt.Printf(" ad-pre-shared-key = %s [%s]\n", mask("ad-pre-shared-key", cfg.AuthDaemonKey), getSource("ad-pre-shared-key"))
fmt.Printf(" ad-principals-file = %s [%s]\n", cfg.AuthDaemonPrincipalsFile, getSource("ad-principals-file"))
fmt.Printf(" ad-ca-cert-path = %s [%s]\n", cfg.AuthDaemonCACertPath, getSource("ad-ca-cert-path"))
fmt.Printf(" ad-generate-random-password = %v [%s]\n", cfg.AuthDaemonGenerateRandomPassword, getSource("ad-generate-random-password"))
fmt.Println("\nTLS:")
fmt.Printf(" tls-client-cert-file = %s [%s]\n", mask("tls-client-cert-file", cfg.TLSClientCert), getSource("tls-client-cert-file"))
fmt.Printf(" tls-client-key = %s [%s]\n", mask("tls-client-key", cfg.TLSClientKey), getSource("tls-client-key"))
fmt.Printf(" tls-client-ca = %v [%s]\n", cfg.TLSClientCAs, getSource("tls-client-ca"))
fmt.Printf(" tls-client-cert = %s [%s] (deprecated PKCS12 path)\n", mask("tls-client-cert", cfg.TLSPrivateKey), getSource("tls-client-cert"))
fmt.Println("\nMetrics/Observability:")
fmt.Printf(" metrics = %v [%s]\n", cfg.MetricsEnabled, getSource("metrics"))
fmt.Printf(" otlp = %v [%s]\n", cfg.OTLPEnabled, getSource("otlp"))
fmt.Printf(" metrics-admin-addr = %s [%s]\n", cfg.AdminAddr, getSource("metrics-admin-addr"))
fmt.Printf(" metrics-async-bytes = %v [%s]\n", cfg.MetricsAsyncBytes, getSource("metrics-async-bytes"))
fmt.Printf(" pprof = %v [%s]\n", cfg.PprofEnabled, getSource("pprof"))
fmt.Printf(" region = %s [%s]\n", cfg.Region, getSource("region"))
fmt.Println("\n--- Source Legend ---")
fmt.Println(" default = Built-in default value")
fmt.Println(" file = Loaded from config file")
fmt.Println(" environment = Set via environment variable")
fmt.Println(" cli = Provided as command-line argument")
fmt.Println("\nPriority: cli > environment > file > default")
fmt.Println()
}

157
config_test.go Normal file
View File

@@ -0,0 +1,157 @@
package main
import (
"flag"
"os"
"path/filepath"
"testing"
)
// resetFlags allows flag.Parse() to be called again in each test, since
// loadNewtConfig registers flags on the global flag.CommandLine.
func resetFlags(t *testing.T) {
t.Helper()
oldArgs := os.Args
oldCommandLine := flag.CommandLine
t.Cleanup(func() {
os.Args = oldArgs
flag.CommandLine = oldCommandLine
})
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
}
func clearNewtEnv(t *testing.T) {
t.Helper()
for _, k := range []string{
"PANGOLIN_ENDPOINT", "NEWT_ID", "NEWT_SECRET", "DNS", "LOG_LEVEL",
"MTU", "CONFIG_FILE", "NEWT_PROVISIONING_KEY", "NEWT_NAME",
"DISABLE_SSH", "DISABLE_CLIENTS",
} {
t.Setenv(k, "")
}
}
func TestLoadNewtConfig_Defaults(t *testing.T) {
resetFlags(t)
clearNewtEnv(t)
os.Args = []string{"newt", "--config-file", filepath.Join(t.TempDir(), "missing.json")}
cfg := loadNewtConfig()
if cfg.DNS != "9.9.9.9" {
t.Errorf("expected default dns, got %q", cfg.DNS)
}
if cfg.MTU != 1280 {
t.Errorf("expected default mtu 1280, got %d", cfg.MTU)
}
if cfg.LogLevel != "INFO" {
t.Errorf("expected default log level INFO, got %q", cfg.LogLevel)
}
}
func TestLoadNewtConfig_FileOverridesDefault(t *testing.T) {
resetFlags(t)
clearNewtEnv(t)
configPath := filepath.Join(t.TempDir(), "config.json")
if err := os.WriteFile(configPath, []byte(`{"dns":"1.1.1.1","mtu":1300,"disableSsh":true}`), 0o644); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
os.Args = []string{"newt", "--config-file", configPath}
cfg := loadNewtConfig()
if cfg.DNS != "1.1.1.1" {
t.Errorf("expected dns from file, got %q", cfg.DNS)
}
if cfg.MTU != 1300 {
t.Errorf("expected mtu from file, got %d", cfg.MTU)
}
if !cfg.DisableSSH {
t.Errorf("expected disableSsh from file to be true")
}
}
func TestLoadNewtConfig_EnvOverridesFile(t *testing.T) {
resetFlags(t)
clearNewtEnv(t)
configPath := filepath.Join(t.TempDir(), "config.json")
if err := os.WriteFile(configPath, []byte(`{"dns":"1.1.1.1"}`), 0o644); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
t.Setenv("DNS", "8.8.4.4")
os.Args = []string{"newt", "--config-file", configPath}
cfg := loadNewtConfig()
if cfg.DNS != "8.8.4.4" {
t.Errorf("expected env to override file dns, got %q", cfg.DNS)
}
}
func TestLoadNewtConfig_CLIOverridesEnv(t *testing.T) {
resetFlags(t)
clearNewtEnv(t)
configPath := filepath.Join(t.TempDir(), "config.json")
if err := os.WriteFile(configPath, []byte(`{"dns":"1.1.1.1"}`), 0o644); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
t.Setenv("DNS", "8.8.4.4")
os.Args = []string{"newt", "--config-file", configPath, "--dns", "4.2.2.2"}
cfg := loadNewtConfig()
if cfg.DNS != "4.2.2.2" {
t.Errorf("expected cli to override env dns, got %q", cfg.DNS)
}
}
func TestLoadNewtConfig_TLSClientCAMergesAcrossSources(t *testing.T) {
resetFlags(t)
clearNewtEnv(t)
tmpDir := t.TempDir()
caFromFile := filepath.Join(tmpDir, "file-ca.pem")
caFromEnv := filepath.Join(tmpDir, "env-ca.pem")
caFromCLI := filepath.Join(tmpDir, "cli-ca.pem")
configPath := filepath.Join(tmpDir, "config.json")
if err := os.WriteFile(configPath, []byte(`{"tlsClientCa":["`+caFromFile+`"]}`), 0o644); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
t.Setenv("TLS_CLIENT_CAS", caFromEnv)
os.Args = []string{"newt", "--config-file", configPath, "--tls-client-ca", caFromCLI}
cfg := loadNewtConfig()
want := map[string]bool{caFromFile: true, caFromEnv: true, caFromCLI: true}
if len(cfg.TLSClientCAs) != len(want) {
t.Fatalf("expected %d CA entries, got %v", len(want), cfg.TLSClientCAs)
}
for _, ca := range cfg.TLSClientCAs {
if !want[ca] {
t.Errorf("unexpected CA entry: %s", ca)
}
}
}
func TestResolveConfigFilePath_Precedence(t *testing.T) {
t.Setenv("CONFIG_FILE", "")
t.Setenv("HOME", t.TempDir())
// CLI flag wins over env.
t.Setenv("CONFIG_FILE", "/env/path/config.json")
if got := resolveConfigFilePath([]string{"--config-file", "/cli/path/config.json"}); got != "/cli/path/config.json" {
t.Errorf("expected cli path to win, got %q", got)
}
if got := resolveConfigFilePath([]string{"--config-file=/cli/eq/config.json"}); got != "/cli/eq/config.json" {
t.Errorf("expected cli = path to win, got %q", got)
}
// Env wins over default when no CLI flag given.
if got := resolveConfigFilePath([]string{}); got != "/env/path/config.json" {
t.Errorf("expected env path, got %q", got)
}
}

View File

@@ -9,10 +9,9 @@ import (
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/client"
"github.com/fosrl/newt/logger"
)
@@ -170,7 +169,7 @@ func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Contain
}
// Used to filter down containers returned to Pangolin
containerFilters := filters.NewArgs()
containerFilters := make(client.Filters)
// Used to determine if we will send IP addresses or hostnames to Pangolin
useContainerIpAddresses := true
@@ -215,21 +214,28 @@ func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Contain
}
// List containers
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true, Filters: containerFilters})
containerListResult, err := cli.ContainerList(ctx, client.ContainerListOptions{All: true, Filters: containerFilters})
if err != nil {
return nil, fmt.Errorf("failed to list containers: %v", err)
}
addrToString := func(addr interface{ IsValid() bool; String() string }) string {
if addr.IsValid() {
return addr.String()
}
return ""
}
var dockerContainers []Container
for _, c := range containers {
for _, c := range containerListResult.Items {
// Short ID like docker ps
shortId := c.ID[:12]
// Inspect container to get hostname
hostname := ""
containerInfo, err := cli.ContainerInspect(ctx, c.ID)
if err == nil && containerInfo.Config != nil {
hostname = containerInfo.Config.Hostname
containerInfo, err := cli.ContainerInspect(ctx, c.ID, client.ContainerInspectOptions{})
if err == nil && containerInfo.Container.Config != nil {
hostname = containerInfo.Container.Config.Hostname
}
// Skip host container if set
@@ -253,8 +259,8 @@ func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Contain
if port.PublicPort != 0 {
dockerPort.PublicPort = int(port.PublicPort)
}
if port.IP != "" {
dockerPort.IP = port.IP
if port.IP.IsValid() {
dockerPort.IP = port.IP.String()
}
ports = append(ports, dockerPort)
}
@@ -268,19 +274,19 @@ func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Contain
dockerNetwork := Network{
NetworkID: endpoint.NetworkID,
EndpointID: endpoint.EndpointID,
Gateway: endpoint.Gateway,
Gateway: addrToString(endpoint.Gateway),
IPPrefixLen: endpoint.IPPrefixLen,
IPv6Gateway: endpoint.IPv6Gateway,
GlobalIPv6Address: endpoint.GlobalIPv6Address,
IPv6Gateway: addrToString(endpoint.IPv6Gateway),
GlobalIPv6Address: addrToString(endpoint.GlobalIPv6Address),
GlobalIPv6PrefixLen: endpoint.GlobalIPv6PrefixLen,
MacAddress: endpoint.MacAddress,
MacAddress: endpoint.MacAddress.String(),
Aliases: endpoint.Aliases,
DNSNames: endpoint.DNSNames,
}
// Use IPs over hostnames/containers as we're on the bridge network
if useContainerIpAddresses {
dockerNetwork.IPAddress = endpoint.IPAddress
dockerNetwork.IPAddress = addrToString(endpoint.IPAddress)
}
networks[networkName] = dockerNetwork
@@ -291,7 +297,7 @@ func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Contain
ID: shortId,
Name: name,
Image: c.Image,
State: c.State,
State: string(c.State),
Status: c.Status,
Ports: ports,
Labels: c.Labels,
@@ -315,12 +321,12 @@ func getHostContainer(dockerContext context.Context, dockerClient *client.Client
}
// Get host container from the docker socket
hostContainer, err := dockerClient.ContainerInspect(dockerContext, hostContainerName)
hostContainer, err := dockerClient.ContainerInspect(dockerContext, hostContainerName, client.ContainerInspectOptions{})
if err != nil {
return nil, fmt.Errorf("failed to find host container")
}
return &hostContainer, nil
return &hostContainer.Container, nil
}
// EventCallback defines the function signature for handling Docker events
@@ -371,7 +377,7 @@ func (em *EventMonitor) Start() error {
logger.Debug("Starting Docker event monitoring")
// Filter for container events we care about
eventFilters := filters.NewArgs()
eventFilters := make(client.Filters)
eventFilters.Add("type", "container")
// eventFilters.Add("event", "create")
eventFilters.Add("event", "start")
@@ -382,9 +388,10 @@ func (em *EventMonitor) Start() error {
// eventFilters.Add("event", "unpause")
// Start listening for events
eventCh, errCh := em.client.Events(em.ctx, events.ListOptions{
eventsResult := em.client.Events(em.ctx, client.EventsListOptions{
Filters: eventFilters,
})
eventCh, errCh := eventsResult.Messages, eventsResult.Err
go func() {
defer func() {
@@ -408,9 +415,10 @@ func (em *EventMonitor) Start() error {
time.Sleep(5 * time.Second)
if em.ctx.Err() == nil {
logger.Info("Attempting to reconnect to Docker event stream")
eventCh, errCh = em.client.Events(em.ctx, events.ListOptions{
eventsResult = em.client.Events(em.ctx, client.EventsListOptions{
Filters: eventFilters,
})
eventCh, errCh = eventsResult.Messages, eventsResult.Err
}
}
return

View File

@@ -35,7 +35,7 @@
inherit version;
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
vendorHash = "sha256-WfIK+Q8WQ372NzLw6DRapv1nYPduShi4KnVJBPk0Oz0=";
vendorHash = "sha256-JhNBJhj5YX3Wurv7r/JDu6YtHizOMLk+NCob7ISx+3c=";
nativeInstallCheckInputs = [ pkgs.versionCheckHook ];

View File

@@ -273,7 +273,7 @@ main() {
CUSTOM_PATH=$(parse_path_arg "$@")
if [ -n "$CUSTOM_PATH" ]; then
print_status "Installing latest version of newt to ${CUSTOM_PATH} (--path override)..."
print_status "Installing latest version of newt to ${CUSTOM_PATH}..."
else
print_status "Installing latest version of newt..."
fi
@@ -331,4 +331,4 @@ main() {
}
# Run main function
main "$@"
main "$@"

70
go.mod
View File

@@ -3,75 +3,73 @@ module github.com/fosrl/newt
go 1.25.0
require (
github.com/docker/docker v28.5.2+incompatible
github.com/gaissmai/bart v0.26.1
github.com/coder/websocket v1.8.15
github.com/creack/pty v1.1.24
github.com/fsnotify/fsnotify v1.9.0
github.com/gaissmai/bart v0.28.0
github.com/go-crypt/crypt v0.14.15
github.com/go-crypt/x v0.4.16
github.com/gorilla/websocket v1.5.3
github.com/moby/moby/api v1.55.0
github.com/moby/moby/client v0.5.0
github.com/prometheus/client_golang v1.23.2
github.com/vishvananda/netlink v1.3.1
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
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
go.opentelemetry.io/otel/exporters/prometheus v0.66.0
go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/metric v1.44.0
golang.org/x/crypto v0.53.0
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
golang.org/x/net v0.53.0
golang.org/x/sys v0.43.0
golang.org/x/net v0.56.0
golang.org/x/sys v0.46.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.81.0
golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.82.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
software.sslmate.com/src/go-pkcs12 v0.7.3
)
require (
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.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
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/otlptranslator v1.0.0 // 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.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/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.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/text v0.38.0 // indirect
golang.org/x/time v0.12.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-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

158
go.sum
View File

@@ -1,33 +1,37 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gaissmai/bart v0.28.0 h1:89yZLo8NmyqD0RYgJ3QO9HhqqGGw+oWhf90cZm69Lko=
github.com/gaissmai/bart v0.28.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/go-crypt/crypt v0.14.15 h1:q1i5OMpL05r935IxWmXgpDAVF0nvi4SMoHhGXLBQUEQ=
github.com/go-crypt/crypt v0.14.15/go.mod h1:0n/to1VqIZPENj2yEUa/sLLYYnmupma6cp+QMX4zfF0=
github.com/go-crypt/x v0.4.16 h1:WXdY28H/0MsXnH+gwerxuCcvBTJPkBG90u6oS4gIPZI=
github.com/go-crypt/x v0.4.16/go.mod h1:vmVFA/d/oLrEaCbqsLcjBMlTqF8u8pvH/c4+EJ/ped8=
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=
@@ -43,8 +47,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.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
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=
@@ -55,22 +59,16 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
@@ -85,8 +83,6 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy
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=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@@ -95,72 +91,68 @@ 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.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/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0 h1:MtkMsuRo3zEXTTMALfyrszwCDZTkB6wolyPjbwFAdq0=
go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0/go.mod h1:FYTxnpsm+UPD0erZNq20GvnM8T2YQHiHtT2vokdpoac=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
go.opentelemetry.io/otel/exporters/prometheus v0.66.0 h1:vkrK8PAznv2NKt2r+kdu252ccGzkEqLc2aSXbQIALYQ=
go.opentelemetry.io/otel/exporters/prometheus v0.66.0/go.mod h1:V/UB6D3vMF/UBOL5igAsAYnk1nG/bzYYTzvsB16cy7o=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
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.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/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
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.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/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
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.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/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
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.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=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
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/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
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=
@@ -168,9 +160,11 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0=
software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
software.sslmate.com/src/go-pkcs12 v0.7.3 h1:JBQD3FDqYjTeyDAeZQklj2ar88ykBLtALloPJHyAauU=
software.sslmate.com/src/go-pkcs12 v0.7.3/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

View File

@@ -212,6 +212,10 @@ func (m *Monitor) addTargetUnsafe(config Config) error {
return nil
}(),
Transport: &http.Transport{
// Disable keep-alives so each health check uses a fresh connection.
// Reusing idle connections causes spurious timeouts when the remote
// server closes the connection between long check intervals.
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{
// Configure TLS settings based on certificate enforcement
InsecureSkipVerify: !m.enforceCert,
@@ -432,7 +436,9 @@ func (m *Monitor) performTCPCheck(target *Target) (bool, string) {
logger.Debug("Target %d: performing TCP health check to %s (timeout: %v)",
target.Config.ID, address, timeout)
conn, err := net.DialTimeout("tcp", address, timeout)
ctx, cancel := context.WithTimeout(target.ctx, timeout)
defer cancel()
conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", address)
if err != nil {
msg := fmt.Sprintf("TCP dial failed: %v", err)
logger.Warn("Target %d: %s", target.Config.ID, msg)
@@ -467,8 +473,9 @@ func (m *Monitor) performHTTPCheck(target *Target) (bool, string) {
target.Config.ID, m.enforceCert)
}
// Create request with timeout context
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(target.Config.Timeout)*time.Second)
// Create request with timeout context, derived from the target's context so
// that in-flight requests are cancelled immediately when the target is replaced.
ctx, cancel := context.WithTimeout(target.ctx, time.Duration(target.Config.Timeout)*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, target.Config.Method, url, nil)

View File

@@ -27,17 +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
publicDNS []string
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
@@ -56,7 +56,7 @@ func NewManager(sharedBind *bind.SharedBind, ID string, clientType string, publi
ID: ID,
clientType: clientType,
publicKey: publicKey,
publicDNS: publicDNS,
publicDNS: publicDNS,
exitNodes: make(map[string]ExitNode),
sendHolepunchInterval: defaultSendHolepunchIntervalMin,
sendHolepunchIntervalMin: defaultSendHolepunchIntervalMin,
@@ -73,6 +73,14 @@ func (m *Manager) SetToken(token string) {
m.token = token
}
// SetPublicDNS replaces the list of DNS servers used to resolve exit-node
// endpoints. The servers must be in "host:port" format (e.g. "8.8.8.8:53").
func (m *Manager) SetPublicDNS(servers []string) {
m.mu.Lock()
defer m.mu.Unlock()
m.publicDNS = servers
}
// IsRunning returns whether hole punching is currently active
func (m *Manager) IsRunning() bool {
m.mu.Lock()

View File

@@ -43,17 +43,17 @@ func DefaultTestOptions() TestConnectionOptions {
// cachedAddr holds a cached resolved UDP address
type cachedAddr struct {
addr *net.UDPAddr
addr *net.UDPAddr
resolvedAt time.Time
}
// HolepunchTester monitors holepunch connectivity using magic packets
type HolepunchTester struct {
sharedBind *bind.SharedBind
publicDNS []string
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
@@ -88,12 +88,24 @@ type pendingRequest struct {
func NewHolepunchTester(sharedBind *bind.SharedBind, publicDNS []string) *HolepunchTester {
return &HolepunchTester{
sharedBind: sharedBind,
publicDNS: publicDNS,
publicDNS: publicDNS,
addrCache: make(map[string]*cachedAddr),
addrCacheTTL: 5 * time.Minute, // Cache addresses for 5 minutes
}
}
// SetPublicDNS replaces the list of DNS servers used to resolve peer endpoints.
// The servers must be in "host:port" format (e.g. "8.8.8.8:53").
func (t *HolepunchTester) SetPublicDNS(servers []string) {
t.mu.Lock()
defer t.mu.Unlock()
t.publicDNS = servers
// Invalidate the address cache so endpoints are re-resolved with the new DNS.
t.addrCacheMu.Lock()
t.addrCache = make(map[string]*cachedAddr)
t.addrCacheMu.Unlock()
}
// SetCallback sets the callback for connection status changes
func (t *HolepunchTester) SetCallback(callback HolepunchStatusCallback) {
t.mu.Lock()

2007
main.go

File diff suppressed because it is too large Load Diff

158
nativessh/auth.go Normal file
View File

@@ -0,0 +1,158 @@
package nativessh
import (
"bufio"
"fmt"
"net"
"os"
"os/user"
"path/filepath"
"strings"
"github.com/fosrl/newt/logger"
"golang.org/x/crypto/ssh"
)
type staticConnMeta struct {
user string
}
func (m staticConnMeta) User() string { return m.user }
func (m staticConnMeta) SessionID() []byte { return nil }
func (m staticConnMeta) ClientVersion() []byte { return nil }
func (m staticConnMeta) ServerVersion() []byte { return nil }
func (m staticConnMeta) RemoteAddr() net.Addr { return nil }
func (m staticConnMeta) LocalAddr() net.Addr { return nil }
// CheckAuthorizedKeys reports whether key matches any entry in the system
// user's ~/.ssh/authorized_keys file. Returns false (not an error) when the
// user or file does not exist.
func CheckAuthorizedKeys(username string, key ssh.PublicKey) bool {
u, err := user.Lookup(username)
if err != nil {
return false
}
f, err := os.Open(filepath.Join(u.HomeDir, ".ssh", "authorized_keys"))
if err != nil {
return false
}
defer f.Close()
want := ssh.FingerprintSHA256(key)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(line))
if err != nil {
continue
}
if ssh.FingerprintSHA256(parsed) == want {
return true
}
}
return false
}
// SystemUserExists reports whether a user account with the given name exists
// on the host OS.
func SystemUserExists(username string) bool {
_, err := user.Lookup(username)
return err == nil
}
// Authenticate authenticates a user for a browser-based native SSH session.
// It tries, in order:
// 1. Private key — parses privateKeyPEM and checks it against the user's
// ~/.ssh/authorized_keys.
// 2. Password — verifies password via the host OS PAM stack (Linux only).
//
// Returns nil on the first method that succeeds, or an error if all fail.
func Authenticate(username, password, privateKeyPEM string) error {
return AuthenticateWithCertificate(nil, username, password, privateKeyPEM, "")
}
// AuthenticateWithCertificate authenticates a user for a browser-based native
// SSH session using the same method ordering as the native SSH server:
// 1. Private key in host ~/.ssh/authorized_keys.
// 2. SSH certificate signed by the configured CA (when provided).
// 3. Password via host PAM stack.
func AuthenticateWithCertificate(store *CredentialStore, username, password, privateKeyPEM, certificate string) error {
logger.Debug("nativessh: authenticating user %q (hasPassword=%v, hasPrivateKey=%v)", username, password != "", privateKeyPEM != "")
if !SystemUserExists(username) {
logger.Debug("nativessh: user %q not found on system", username)
return fmt.Errorf("user %q does not exist", username)
}
var signer ssh.Signer
if privateKeyPEM != "" {
parsedSigner, err := ssh.ParsePrivateKey([]byte(privateKeyPEM))
if err != nil {
logger.Debug("nativessh: failed to parse private key for %q: %v", username, err)
} else if CheckAuthorizedKeys(username, parsedSigner.PublicKey()) {
logger.Debug("nativessh: private key auth succeeded for %q", username)
return nil
} else {
signer = parsedSigner
logger.Debug("nativessh: private key not in authorized_keys for %q", username)
}
}
if store != nil && certificate != "" {
if signer == nil {
logger.Debug("nativessh: certificate provided for %q but no matching private key was provided", username)
} else {
pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certificate))
if err != nil {
logger.Debug("nativessh: failed to parse certificate for %q: %v", username, err)
} else {
cert, ok := pub.(*ssh.Certificate)
if !ok {
logger.Debug("nativessh: provided cert data for %q is not an SSH certificate", username)
} else if ssh.FingerprintSHA256(cert.Key) != ssh.FingerprintSHA256(signer.PublicKey()) {
logger.Debug("nativessh: certificate key mismatch for %q", username)
} else {
caKey, userPrincipals := store.get(username)
if caKey == nil {
logger.Debug("nativessh: CA key is not set for certificate auth user %q", username)
} else if len(userPrincipals) == 0 {
logger.Debug("nativessh: no allowed principals found for certificate auth user %q", username)
} else {
checker := &ssh.CertChecker{
IsUserAuthority: func(auth ssh.PublicKey) bool {
return ssh.FingerprintSHA256(auth) == ssh.FingerprintSHA256(caKey)
},
}
var lastErr error
for principal := range userPrincipals {
_, authErr := checker.Authenticate(staticConnMeta{user: principal}, cert)
if authErr == nil {
logger.Debug("nativessh: certificate auth succeeded for %q (principal=%q)", username, principal)
return nil
}
lastErr = authErr
}
if lastErr != nil {
logger.Debug("nativessh: certificate auth failed for %q: %v", username, lastErr)
}
}
}
}
}
}
if password != "" {
if err := VerifySystemPassword(username, password); err != nil {
logger.Debug("nativessh: password auth failed for %q: %v", username, err)
} else {
logger.Debug("nativessh: password auth succeeded for %q", username)
return nil
}
} else {
logger.Debug("nativessh: no password provided for %q", username)
}
return fmt.Errorf("authentication failed for user %q", username)
}

99
nativessh/pam_linux.go Normal file
View File

@@ -0,0 +1,99 @@
//go:build linux
package nativessh
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"strings"
"github.com/fosrl/newt/logger"
"github.com/go-crypt/crypt"
"github.com/go-crypt/x/yescrypt"
)
// VerifySystemPassword authenticates username/password by reading /etc/shadow.
// Supports yescrypt ($y$), bcrypt ($2b$/$2a$/$2y$), SHA-512 ($6$), SHA-256
// ($5$), argon2, scrypt, and other schemes handled by go-crypt/crypt.
func VerifySystemPassword(username, password string) error {
hash, err := readShadowHash(username)
if err != nil {
logger.Debug("nativessh/pam: readShadowHash for %q failed: %v", username, err)
return fmt.Errorf("shadow: %w", err)
}
// Log the scheme prefix only (never the full hash).
scheme := "unknown"
for _, prefix := range []string{"$y$", "$2a$", "$2b$", "$2y$", "$6$", "$5$", "$1$"} {
if strings.HasPrefix(hash, prefix) {
scheme = prefix
break
}
}
logger.Debug("nativessh/pam: verifying password for %q using scheme %s", username, scheme)
// Yescrypt ($y$) is not in go-crypt/crypt's default decoder; handle it directly.
if strings.HasPrefix(hash, "$y$") {
computed, err := yescrypt.Hash([]byte(password), []byte(hash))
if err != nil {
logger.Debug("nativessh/pam: yescrypt.Hash for %q failed: %v", username, err)
return fmt.Errorf("yescrypt: %w", err)
}
if !bytes.Equal(computed, []byte(hash)) {
logger.Debug("nativessh/pam: yescrypt mismatch for %q", username)
return errors.New("authentication failed")
}
return nil
}
decoder, err := crypt.NewDefaultDecoder()
if err != nil {
return fmt.Errorf("crypt decoder: %w", err)
}
digest, err := decoder.Decode(hash)
if err != nil {
logger.Debug("nativessh/pam: failed to decode hash for %q: %v", username, err)
return fmt.Errorf("unsupported password hash scheme %q: %w", scheme, err)
}
match, err := digest.MatchAdvanced(password)
if err != nil {
logger.Debug("nativessh/pam: MatchAdvanced for %q failed: %v", username, err)
return err
}
if !match {
logger.Debug("nativessh/pam: password mismatch for %q", username)
return errors.New("authentication failed")
}
return nil
}
// readShadowHash reads /etc/shadow and returns the password hash for username.
func readShadowHash(username string) (string, error) {
f, err := os.Open("/etc/shadow")
if err != nil {
return "", err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fields := strings.SplitN(scanner.Text(), ":", 3)
if len(fields) < 2 || fields[0] != username {
continue
}
h := fields[1]
if h == "" || h == "*" || strings.HasPrefix(h, "!") || h == "x" {
return "", errors.New("account locked or has no password")
}
return h, nil
}
if err := scanner.Err(); err != nil {
return "", err
}
return "", errors.New("user not found in shadow database")
}

11
nativessh/pam_other.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build !linux
package nativessh
import "errors"
// VerifySystemPassword is not supported on non-Linux platforms; it always
// returns an error so that password authentication is never accepted.
func VerifySystemPassword(username, password string) error {
return errors.New("password authentication not supported on this platform")
}

130
nativessh/pty.go Normal file
View File

@@ -0,0 +1,130 @@
//go:build !windows
package nativessh
import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"strings"
"sync"
"github.com/creack/pty"
)
// PTYSession is a running shell process attached to a PTY.
// It implements io.ReadWriteCloser so it can be bridged to any transport.
type PTYSession struct {
ptmx *os.File
cmd *exec.Cmd
waitOnce sync.Once
exitCode int
}
// findShell returns the path to the best available interactive shell by
// checking preferred shells in order, falling back to /bin/sh.
func findShell() string {
preferred := []string{"zsh", "bash", "fish", "ksh", "sh"}
for _, name := range preferred {
if path, err := exec.LookPath(name); err == nil {
return path
}
}
return "/bin/sh"
}
// userShell returns the login shell configured for u in /etc/passwd.
// If the field is empty or the binary does not exist, it falls back to
// findShell so there is always a usable shell.
func userShell(u *user.User) string {
if shell := passwdShell(u.Username); shell != "" {
if _, err := exec.LookPath(shell); err == nil {
return shell
}
}
return findShell()
}
// passwdShell reads /etc/passwd and returns the login shell for the named user.
// Returns "" if the user is not found or the file cannot be read.
func passwdShell(username string) string {
f, err := os.Open("/etc/passwd")
if err != nil {
return ""
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if line == "" || line[0] == '#' {
continue
}
// Fields: username:password:uid:gid:gecos:home:shell
fields := strings.SplitN(line, ":", 7)
if len(fields) == 7 && fields[0] == username {
return fields[6]
}
}
_ = scanner.Err()
return ""
}
// NewPTYSession spawns the best available shell in a PTY.
func NewPTYSession() (*PTYSession, error) {
shell := findShell()
cmd := exec.Command(shell)
cmd.Env = append(os.Environ(), "TERM=xterm-256color")
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, fmt.Errorf("pty start: %w", err)
}
return &PTYSession{ptmx: ptmx, cmd: cmd}, nil
}
// Read reads output from the PTY.
func (p *PTYSession) Read(b []byte) (int, error) {
return p.ptmx.Read(b)
}
// Write writes input to the PTY.
func (p *PTYSession) Write(b []byte) (int, error) {
return p.ptmx.Write(b)
}
// Resize changes the PTY window size.
func (p *PTYSession) Resize(cols, rows uint16) error {
return pty.Setsize(p.ptmx, &pty.Winsize{Cols: cols, Rows: rows})
}
// wait waits for the child process to exit exactly once and records its exit
// code. Safe to call concurrently or multiple times.
func (p *PTYSession) wait() {
p.waitOnce.Do(func() {
err := p.cmd.Wait()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
p.exitCode = exitErr.ExitCode()
return
}
p.exitCode = 1
}
})
}
// ExitCode waits for the shell process to exit and returns its exit code.
// It is safe to call before or after Close.
func (p *PTYSession) ExitCode() int {
p.wait()
return p.exitCode
}
// Close closes the PTY and waits for the child process to exit.
func (p *PTYSession) Close() error {
err := p.ptmx.Close()
p.wait()
return err
}

78
nativessh/pty_unix.go Normal file
View File

@@ -0,0 +1,78 @@
//go:build !windows
package nativessh
import (
"fmt"
"os"
"os/exec"
"os/user"
"strconv"
"syscall"
"github.com/creack/pty"
)
// NewPTYSessionAs spawns an interactive shell in a PTY running as the given
// system user. The calling process must have sufficient privileges (typically
// root / CAP_SETUID) to switch to a different UID/GID.
func NewPTYSessionAs(username string) (*PTYSession, error) {
u, err := user.Lookup(username)
if err != nil {
return nil, fmt.Errorf("user lookup %q: %w", username, err)
}
uid, err := strconv.ParseUint(u.Uid, 10, 32)
if err != nil {
return nil, fmt.Errorf("parse uid: %w", err)
}
gid, err := strconv.ParseUint(u.Gid, 10, 32)
if err != nil {
return nil, fmt.Errorf("parse gid: %w", err)
}
// Collect supplementary group IDs.
groupIDs, err := u.GroupIds()
if err != nil {
groupIDs = []string{}
}
var groups []uint32
for _, g := range groupIDs {
gval, err := strconv.ParseUint(g, 10, 32)
if err == nil {
groups = append(groups, uint32(gval))
}
}
shell := userShell(u)
// Prefer the user's home directory as the working directory, but fall back
// to / if it does not exist (e.g. useradd was run without -m).
homeDir := u.HomeDir
if _, err := os.Stat(homeDir); err != nil {
homeDir = "/"
}
cmd := exec.Command(shell, "--login")
cmd.Env = []string{
"TERM=xterm-256color",
"HOME=" + u.HomeDir,
"USER=" + username,
"LOGNAME=" + username,
"SHELL=" + shell,
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
}
cmd.Dir = homeDir
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
Groups: groups,
},
}
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, fmt.Errorf("pty start: %w", err)
}
return &PTYSession{ptmx: ptmx, cmd: cmd}, nil
}

467
nativessh/server.go Normal file
View File

@@ -0,0 +1,467 @@
//go:build !windows
package nativessh
import (
"crypto/ed25519"
"crypto/rand"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"os/user"
"strconv"
"strings"
"sync"
"syscall"
"github.com/fosrl/newt/logger"
"golang.org/x/crypto/ssh"
)
// CredentialStore holds in-memory SSH credentials that can be updated at runtime.
// It is safe for concurrent use.
type CredentialStore struct {
mu sync.RWMutex
caKey ssh.PublicKey
principals map[string]map[string]struct{} // username -> set of allowed principals
}
// connMetaWithUser wraps ConnMetadata while overriding User() for cert checks.
type connMetaWithUser struct {
ssh.ConnMetadata
user string
}
func (m connMetaWithUser) User() string { return m.user }
// NewCredentialStore returns an empty, ready-to-use CredentialStore.
func NewCredentialStore() *CredentialStore {
return &CredentialStore{
principals: make(map[string]map[string]struct{}),
}
}
// SetCAKey parses and stores the CA public key from authorized_keys-format data.
func (s *CredentialStore) SetCAKey(authorizedKeyData string) error {
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(authorizedKeyData))
if err != nil {
return fmt.Errorf("parse CA key: %w", err)
}
s.mu.Lock()
s.caKey = key
s.mu.Unlock()
return nil
}
// AddPrincipals records username and niceId as allowed principals for username.
// Both values are stored; either can appear in the certificate's ValidPrincipals
// field to satisfy the standard cert-auth principal check.
func (s *CredentialStore) AddPrincipals(username, niceId string) {
username = strings.TrimSpace(username)
niceId = strings.TrimSpace(niceId)
if username == "" {
return
}
s.mu.Lock()
defer s.mu.Unlock()
if s.principals[username] == nil {
s.principals[username] = make(map[string]struct{})
}
s.principals[username][username] = struct{}{}
if niceId != "" {
s.principals[username][niceId] = struct{}{}
}
}
// get returns the CA key and the principal set for username under a read lock.
func (s *CredentialStore) get(username string) (ssh.PublicKey, map[string]struct{}) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.caKey, s.principals[username]
}
// ServerConfig holds configuration for the native SSH server.
type ServerConfig struct {
// ListenAddr is the TCP address to listen on. Defaults to ":2222".
ListenAddr string
// Credentials provides in-memory CA key and per-user principals.
// Updates to the store are reflected immediately for new connections.
// If nil or the store has no CA key set, all connections are rejected.
Credentials *CredentialStore
}
// Server is a simple SSH server that authenticates clients via SSH certificate
// auth only. Certificates must be signed by the configured CA and the
// connecting username must appear in both the certificate's principal list and
// the local principals file.
type Server struct {
cfg ServerConfig
}
// NewServer creates a new Server. The ListenAddr defaults to ":2222" when empty.
func NewServer(cfg ServerConfig) *Server {
if cfg.ListenAddr == "" {
cfg.ListenAddr = ":2222"
}
return &Server{cfg: cfg}
}
// buildSSHConfig builds the ssh.ServerConfig with multi-method authentication:
// 1. Public key: host ~/.ssh/authorized_keys, then CA certificate.
// 2. Password: system PAM stack (Linux only).
func (s *Server) buildSSHConfig() (*ssh.ServerConfig, error) {
hostSigner, err := generateHostKey()
if err != nil {
return nil, fmt.Errorf("host key: %w", err)
}
cfg := &ssh.ServerConfig{
PublicKeyCallback: makePublicKeyCallback(s.cfg.Credentials),
PasswordCallback: makePasswordCallback(),
}
cfg.AddHostKey(hostSigner)
return cfg, nil
}
// Serve accepts connections on ln and handles them. It returns when ln is
// closed or a non-temporary Accept error occurs.
func (s *Server) Serve(ln net.Listener) error {
sshCfg, err := s.buildSSHConfig()
if err != nil {
return err
}
logger.Debug("nativessh: server listening on %s", ln.Addr())
for {
conn, err := ln.Accept()
if err != nil {
return err
}
go s.handleConn(conn, sshCfg)
}
}
// ListenAndServe starts the SSH server on the host network and blocks until
// the listener is closed.
func (s *Server) ListenAndServe() error {
ln, err := net.Listen("tcp", s.cfg.ListenAddr)
if err != nil {
return fmt.Errorf("listen %s: %w", s.cfg.ListenAddr, err)
}
defer ln.Close()
return s.Serve(ln)
}
func (s *Server) handleConn(conn net.Conn, cfg *ssh.ServerConfig) {
defer conn.Close()
sshConn, chans, reqs, err := ssh.NewServerConn(conn, cfg)
if err != nil {
logger.Debug("nativessh: handshake failed from %s: %v", conn.RemoteAddr(), err)
return
}
defer sshConn.Close()
logger.Debug("nativessh: connection from %s user=%s", conn.RemoteAddr(), sshConn.User())
go ssh.DiscardRequests(reqs)
for newChan := range chans {
if newChan.ChannelType() != "session" {
_ = newChan.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
ch, requests, err := newChan.Accept()
if err != nil {
logger.Debug("nativessh: channel accept error: %v", err)
return
}
go s.handleSession(ch, requests, sshConn.User())
}
}
// handleSession drives a single SSH session channel. It waits for a pty-req
// followed by a shell request and then bridges the PTY to the channel.
func (s *Server) handleSession(ch ssh.Channel, requests <-chan *ssh.Request, username string) {
defer ch.Close()
var (
sess *PTYSession
started bool
)
for req := range requests {
switch req.Type {
case "pty-req":
var err error
if sess == nil {
sess, err = NewPTYSessionAs(username)
if err != nil {
logger.Debug("nativessh: PTY start error: %v", err)
if req.WantReply {
_ = req.Reply(false, nil)
}
return
}
}
cols, rows := parsePTYReq(req.Payload)
_ = sess.Resize(cols, rows)
if req.WantReply {
_ = req.Reply(true, nil)
}
case "shell":
if req.WantReply {
_ = req.Reply(true, nil)
}
if started || sess == nil {
continue
}
started = true
// PTY output → SSH channel.
go func() {
_, _ = io.Copy(ch, sess)
// Notify the client of the shell's exit status so it can
// disconnect cleanly instead of requiring a manual disconnect.
exitCode := sess.ExitCode()
exitStatusPayload := ssh.Marshal(struct{ Status uint32 }{uint32(exitCode)})
_, _ = ch.SendRequest("exit-status", false, exitStatusPayload)
_ = ch.CloseWrite()
sess.Close() //nolint:errcheck
// Close the channel so the ssh library closes the requests
// channel, which unblocks the for-range loop in handleSession
// and allows the deferred ch.Close() to run. Without this,
// handleSession blocks forever waiting for requests to drain.
_ = ch.Close()
}()
// SSH channel input → PTY stdin.
go func() {
_, _ = io.Copy(sess, ch)
}()
case "exec":
if req.WantReply {
_ = req.Reply(true, nil)
}
cmd := parseExecPayload(req.Payload)
go s.runExecAs(ch, username, cmd)
// Drain remaining requests; the goroutine owns the channel now.
go ssh.DiscardRequests(requests)
return
case "window-change":
if sess != nil {
cols, rows := parseWindowChange(req.Payload)
_ = sess.Resize(cols, rows)
}
if req.WantReply {
_ = req.Reply(true, nil)
}
default:
if req.WantReply {
_ = req.Reply(false, nil)
}
}
}
if sess != nil && !started {
sess.Close() //nolint:errcheck
}
}
// runExecAs runs command as username, wiring stdin/stdout/stderr directly to
// the SSH channel (no PTY). This supports scp, rsync, and plain exec requests.
func (s *Server) runExecAs(ch ssh.Channel, username, command string) {
defer ch.Close()
u, err := user.Lookup(username)
if err != nil {
logger.Debug("nativessh: exec user lookup %q: %v", username, err)
sendExitStatus(ch, 1)
return
}
uid, err := strconv.ParseUint(u.Uid, 10, 32)
if err != nil {
logger.Debug("nativessh: exec parse uid for %q: %v", username, err)
sendExitStatus(ch, 1)
return
}
gid, err := strconv.ParseUint(u.Gid, 10, 32)
if err != nil {
logger.Debug("nativessh: exec parse gid for %q: %v", username, err)
sendExitStatus(ch, 1)
return
}
groupIDs, _ := u.GroupIds()
var groups []uint32
for _, g := range groupIDs {
gval, err := strconv.ParseUint(g, 10, 32)
if err == nil {
groups = append(groups, uint32(gval))
}
}
homeDir := u.HomeDir
if _, err := os.Stat(homeDir); err != nil {
homeDir = "/"
}
cmd := exec.Command("/bin/sh", "-c", command)
cmd.Env = []string{
"HOME=" + u.HomeDir,
"USER=" + username,
"LOGNAME=" + username,
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
}
cmd.Dir = homeDir
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
Groups: groups,
},
}
cmd.Stdin = ch
cmd.Stdout = ch
cmd.Stderr = ch.Stderr()
exitCode := 0
if err := cmd.Run(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
exitCode = exitErr.ExitCode()
} else {
exitCode = 1
}
logger.Debug("nativessh: exec %q as %q exited: %v", command, username, err)
}
sendExitStatus(ch, exitCode)
_ = ch.CloseWrite()
}
// sendExitStatus sends an SSH exit-status request on ch.
func sendExitStatus(ch ssh.Channel, code int) {
payload := ssh.Marshal(struct{ Status uint32 }{uint32(code)})
_, _ = ch.SendRequest("exit-status", false, payload)
}
// parseExecPayload decodes the command string from an SSH exec request payload.
func parseExecPayload(payload []byte) string {
var msg struct{ Command string }
if err := ssh.Unmarshal(payload, &msg); err != nil {
return ""
}
return msg.Command
}
// makePublicKeyCallback returns a PublicKeyCallback that tries, in order:
// 1. Host authorized_keys matches any key in the OS user's
// ~/.ssh/authorized_keys file.
// 2. CA certificate validates an SSH certificate signed by the
// configured CA and checks that the user appears in the principals map.
//
// store may be nil or empty; those paths are simply skipped.
func makePublicKeyCallback(store *CredentialStore) func(ssh.ConnMetadata, ssh.PublicKey) (*ssh.Permissions, error) {
return func(meta ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
// 1. Host authorized_keys.
if CheckAuthorizedKeys(meta.User(), key) {
logger.Debug("nativessh: authorized_keys auth for user %q", meta.User())
return &ssh.Permissions{}, nil
}
// 2. CA certificate.
if store != nil {
caKey, userPrincipals := store.get(meta.User())
if caKey != nil {
checker := &ssh.CertChecker{
IsUserAuthority: func(auth ssh.PublicKey) bool {
return ssh.FingerprintSHA256(auth) == ssh.FingerprintSHA256(caKey)
},
}
if len(userPrincipals) == 0 {
return nil, fmt.Errorf("user %q not in allowed principals list", meta.User())
}
var lastErr error
for principal := range userPrincipals {
perms, err := checker.Authenticate(connMetaWithUser{ConnMetadata: meta, user: principal}, key)
if err == nil {
logger.Debug("nativessh: CA cert auth for user %q principal=%q", meta.User(), principal)
return perms, nil
}
lastErr = err
}
if lastErr != nil {
logger.Debug("nativessh: CA cert rejected for user %q: %v", meta.User(), lastErr)
}
}
}
return nil, fmt.Errorf("public key not authorized for user %q", meta.User())
}
}
// makePasswordCallback returns a PasswordCallback that validates the supplied
// password via the host OS PAM stack. On non-Linux platforms this always
// fails (see pam_other.go).
func makePasswordCallback() func(ssh.ConnMetadata, []byte) (*ssh.Permissions, error) {
return func(meta ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
if err := VerifySystemPassword(meta.User(), string(password)); err != nil {
// Return a generic message to the client; log the real reason.
logger.Debug("nativessh: password auth failed for user %q: %v", meta.User(), err)
return nil, fmt.Errorf("permission denied")
}
logger.Debug("nativessh: password auth for user %q", meta.User())
return &ssh.Permissions{}, nil
}
}
// generateHostKey generates a fresh ephemeral Ed25519 host key in memory.
// A new key is created on every server start; nothing is written to disk.
func generateHostKey() (ssh.Signer, error) {
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate host key: %w", err)
}
logger.Debug("nativessh: generated ephemeral Ed25519 host key")
return ssh.NewSignerFromKey(priv)
}
// ptyRequestMsg mirrors the SSH wire format for pty-req (RFC 4254 §6.2).
type ptyRequestMsg struct {
Term string
Columns uint32
Rows uint32
Width uint32
Height uint32
Modelist string
}
func parsePTYReq(payload []byte) (cols, rows uint16) {
var req ptyRequestMsg
if err := ssh.Unmarshal(payload, &req); err != nil {
return 80, 24
}
return uint16(req.Columns), uint16(req.Rows)
}
// windowChangeMsg mirrors the SSH wire format for window-change (RFC 4254 §6.7).
type windowChangeMsg struct {
Columns uint32
Rows uint32
Width uint32
Height uint32
}
func parseWindowChange(payload []byte) (cols, rows uint16) {
var msg windowChangeMsg
if err := ssh.Unmarshal(payload, &msg); err != nil {
return 80, 24
}
return uint16(msg.Columns), uint16(msg.Rows)
}

View File

@@ -0,0 +1,64 @@
//go:build windows
package nativessh
import (
"errors"
"log"
"net"
"sync"
"golang.org/x/crypto/ssh"
)
// CredentialStore is a stub on Windows. Native SSH is not supported on Windows.
type CredentialStore struct {
mu sync.RWMutex
principals map[string]map[string]struct{}
}
// NewCredentialStore returns an empty CredentialStore stub.
// Native SSH is not supported on Windows; a warning is logged.
func NewCredentialStore() *CredentialStore {
log.Println("WARNING: native SSH is not supported on Windows and will be disabled")
return &CredentialStore{
principals: make(map[string]map[string]struct{}),
}
}
// SetCAKey is a no-op stub on Windows.
func (s *CredentialStore) SetCAKey(_ string) error {
return errors.New("native SSH not supported on Windows")
}
// AddPrincipals is a no-op stub on Windows.
func (s *CredentialStore) AddPrincipals(_, _ string) {}
// get returns nil CA key and empty principals on Windows.
func (s *CredentialStore) get(_ string) (ssh.PublicKey, map[string]struct{}) {
return nil, nil
}
// ServerConfig holds configuration for the native SSH server (stub on Windows).
type ServerConfig struct {
ListenAddr string
Credentials *CredentialStore
}
// Server is a stub on Windows.
type Server struct{}
// NewServer returns a stub Server and logs a warning.
func NewServer(cfg ServerConfig) *Server {
return &Server{}
}
// ListenAndServe always returns an error on Windows.
func (s *Server) ListenAndServe() error {
return errors.New("native SSH not supported on Windows")
}
// Serve always returns an error on Windows.
func (s *Server) Serve(_ net.Listener) error {
return errors.New("native SSH not supported on Windows")
}

View File

@@ -1,8 +1,3 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/
package netstack2
import (
@@ -144,6 +139,13 @@ func (h *TCPHandler) handleTCPConn(netstackConn *gonet.TCPConn, id stack.Transpo
dstIP := id.LocalAddress.String()
dstPort := id.LocalPort
// Drop connection if blocking is enabled
if h.proxyHandler != nil && h.proxyHandler.IsBlocked() {
logger.Debug("TCP Forwarder: connection blocked: %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort)
netstackConn.Close()
return
}
// 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
@@ -164,6 +166,13 @@ func (h *TCPHandler) handleTCPConn(netstackConn *gonet.TCPConn, id stack.Transpo
defer netstackConn.Close()
// Release this connection's NAT state once it fully closes, so a rule
// change (e.g. RewriteTo) takes effect for the next connection on this
// tuple instead of being masked by a stale cached resolution forever.
if h.proxyHandler != nil {
defer h.proxyHandler.releaseConnectionState(srcIP, srcPort, dstIP, dstPort, uint8(tcp.ProtocolNumber))
}
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)
@@ -313,8 +322,21 @@ func (h *UDPHandler) handleUDPConn(netstackConn *gonet.UDPConn, id stack.Transpo
dstIP := id.LocalAddress.String()
dstPort := id.LocalPort
// Release this session's NAT state once it fully closes (session end or
// idle timeout), so a rule change takes effect for the next session on
// this tuple instead of being masked by a stale cached resolution.
if h.proxyHandler != nil {
defer h.proxyHandler.releaseConnectionState(srcIP, srcPort, dstIP, dstPort, uint8(udp.ProtocolNumber))
}
logger.Info("UDP Forwarder: Handling connection %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort)
// Drop connection if blocking is enabled
if h.proxyHandler != nil && h.proxyHandler.IsBlocked() {
logger.Debug("UDP Forwarder: connection blocked: %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort)
return
}
// Check if there's a destination rewrite for this connection (e.g., localhost targets)
actualDstIP := dstIP
if h.proxyHandler != nil {

View File

@@ -1,8 +1,3 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/
package netstack2
import (
@@ -315,6 +310,13 @@ func (h *HTTPHandler) getProxy(target HTTPTarget) *httputil.ReverseProxy {
// Director means the proxy does not append its own automatic
// X-Forwarded-For entry, so the header is set exactly once.
pr.SetXForwarded()
// SetXForwarded derives X-Forwarded-Proto from pr.In.TLS,
// which is nil because httpConnCtx wraps *tls.Conn behind
// net.Conn. Override using the context flag set by ConnContext.
if isTLS, _ := pr.In.Context().Value(connTLSKey{}).(bool); isTLS {
pr.Out.Header.Set("X-Forwarded-Proto", "https")
}
},
Transport: transport,
}

View File

@@ -1,48 +0,0 @@
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

@@ -6,6 +6,7 @@ import (
"net"
"net/netip"
"sync"
"sync/atomic"
"time"
"github.com/fosrl/newt/logger"
@@ -134,6 +135,7 @@ type ProxyHandler struct {
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
blocked atomic.Bool // when true, all new connections are dropped
}
// ProxyHandlerOptions configures the proxy handler
@@ -240,6 +242,28 @@ func (p *ProxyHandler) AddSubnetRule(rule SubnetRule) {
p.subnetLookup.AddSubnet(rule)
}
// SetBlocked enables or disables connection blocking on this proxy handler.
// When enabled, all new TCP/UDP connections from the tunnel are dropped immediately.
func (p *ProxyHandler) SetBlocked(v bool) {
if p == nil {
return
}
p.blocked.Store(v)
if v {
logger.Debug("ProxyHandler: connection blocking enabled")
} else {
logger.Debug("ProxyHandler: connection blocking disabled")
}
}
// IsBlocked returns true if connection blocking is currently enabled.
func (p *ProxyHandler) IsBlocked() bool {
if p == nil {
return false
}
return p.blocked.Load()
}
// RemoveSubnetRule removes a subnet from the proxy handler
func (p *ProxyHandler) RemoveSubnetRule(sourcePrefix, destPrefix netip.Prefix) {
if p == nil || !p.enabled {
@@ -248,6 +272,18 @@ func (p *ProxyHandler) RemoveSubnetRule(sourcePrefix, destPrefix netip.Prefix) {
p.subnetLookup.RemoveSubnet(sourcePrefix, destPrefix)
}
// ReplaceAllSubnetRules atomically replaces the full set of subnet rules.
// Intended for full-state syncs where the desired rule set is authoritative,
// so any stale rule is guaranteed to be cleared even if its key
// (SourcePrefix, DestPrefix) matches a still-desired rule but its contents
// (e.g. RewriteTo) have changed.
func (p *ProxyHandler) ReplaceAllSubnetRules(rules []SubnetRule) {
if p == nil || !p.enabled {
return
}
p.subnetLookup.ReplaceAll(rules)
}
// GetAllRules returns all subnet rules from the proxy handler
func (p *ProxyHandler) GetAllRules() []SubnetRule {
if p == nil || !p.enabled {
@@ -311,6 +347,51 @@ func (p *ProxyHandler) SetHTTPRequestLogSender(fn SendFunc) {
p.httpRequestLogger.SetSendFunc(fn)
}
// releaseConnectionState removes the per-connection NAT state for a single
// (srcIP, srcPort, dstIP, dstPort, proto) tuple. Callers must only invoke
// this once that exact connection has fully closed (both directions torn
// down), since a new connection can never be accepted on the same 5-tuple
// before then.
//
// This intentionally does NOT touch destRewriteTable/resourceTable: those
// are keyed without srcPort (destKey), so they are shared across every
// concurrent connection from the same source to the same destination
// service. They don't need connection-scoped cleanup - each new connection's
// first packet already refreshes them via HandleIncomingPacket - and
// deleting them here on a single connection's close could break other
// connections still in flight to the same destination.
func (p *ProxyHandler) releaseConnectionState(srcIP string, srcPort uint16, dstIP string, dstPort uint16, proto uint8) {
if p == nil || !p.enabled {
return
}
key := connKey{
srcIP: srcIP,
srcPort: srcPort,
dstIP: dstIP,
dstPort: dstPort,
proto: proto,
}
p.natMu.Lock()
defer p.natMu.Unlock()
entry, ok := p.natTable[key]
if !ok {
return
}
delete(p.natTable, key)
reverseKey := reverseConnKey{
rewrittenTo: entry.rewrittenTo.String(),
originalSrcIP: srcIP,
originalSrcPort: srcPort,
originalDstPort: dstPort,
proto: proto,
}
delete(p.reverseNatTable, reverseKey)
}
// 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) {
@@ -612,7 +693,7 @@ func (p *ProxyHandler) HandleIncomingPacket(packet []byte) bool {
}
// logger.Debug("HandleIncomingPacket: No matching rule for %s -> %s (proto=%d, port=%d)",
// srcAddr, dstAddr, protocol, dstPort)
// srcAddr, dstAddr, protocol, dstPort)
return false
}

View File

@@ -52,6 +52,13 @@ func (sl *SubnetLookup) AddSubnet(rule SubnetRule) {
sl.mu.Lock()
defer sl.mu.Unlock()
sl.addSubnetLocked(rule)
}
// addSubnetLocked is the lock-free body of AddSubnet, factored out so
// ReplaceAll can insert many rules under a single lock acquisition.
// Callers must hold sl.mu for writing.
func (sl *SubnetLookup) addSubnetLocked(rule SubnetRule) {
rulePtr := &rule
// Canonicalize source prefix to handle host bits correctly
@@ -89,6 +96,21 @@ func (sl *SubnetLookup) AddSubnet(rule SubnetRule) {
destTriePtr.rules = newRules
}
// ReplaceAll atomically replaces the entire rule set with the given rules.
// This guarantees no stale rule can survive a sync, even when a rule's key
// (SourcePrefix, DestPrefix) is unchanged but other fields (e.g. RewriteTo)
// differ - a case that an add/remove diff keyed only on prefixes would miss.
func (sl *SubnetLookup) ReplaceAll(rules []SubnetRule) {
sl.mu.Lock()
defer sl.mu.Unlock()
sl.sourceTrie = &bart.Table[*destTrie]{}
for _, rule := range rules {
sl.addSubnetLocked(rule)
}
}
// RemoveSubnet removes a subnet rule from the lookup table
func (sl *SubnetLookup) RemoveSubnet(sourcePrefix, destPrefix netip.Prefix) {
sl.mu.Lock()

View File

@@ -1,8 +1,3 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/
package netstack2
import (
@@ -369,6 +364,15 @@ func (net *Net) RemoveProxySubnetRule(sourcePrefix, destPrefix netip.Prefix) {
}
}
// ReplaceProxySubnetRules atomically replaces the full set of subnet rules
// on the proxy handler with the given rules.
func (net *Net) ReplaceProxySubnetRules(rules []SubnetRule) {
tun := (*netTun)(net)
if tun.proxyHandler != nil {
tun.proxyHandler.ReplaceAllSubnetRules(rules)
}
}
// GetProxySubnetRules returns all subnet rules from the proxy handler
func (net *Net) GetProxySubnetRules() []SubnetRule {
tun := (*netTun)(net)

60
newt/authdaemon.go Normal file
View File

@@ -0,0 +1,60 @@
package newt
import (
"context"
"fmt"
"os"
"runtime"
"github.com/fosrl/newt/authdaemon"
"github.com/fosrl/newt/logger"
)
const (
defaultPrincipalsPath = "/var/run/auth-daemon/principals"
defaultCACertPath = "/etc/ssh/ca.pem"
)
func (n *Newt) startAuthDaemon(ctx context.Context) error {
if runtime.GOOS != "linux" {
return fmt.Errorf("auth-daemon is only supported on Linux, not %s", runtime.GOOS)
}
if os.Geteuid() != 0 {
return fmt.Errorf("auth-daemon must be run as root (use sudo)")
}
principalsFile := n.config.AuthDaemonPrincipalsFile
if principalsFile == "" {
principalsFile = defaultPrincipalsPath
}
caCertPath := n.config.AuthDaemonCACertPath
if caCertPath == "" {
caCertPath = defaultCACertPath
}
cfg := authdaemon.Config{
DisableHTTPS: true,
PresharedKey: "this-key-is-not-used",
PrincipalsFilePath: principalsFile,
CACertPath: caCertPath,
Force: true,
GenerateRandomPassword: n.config.AuthDaemonGenerateRandomPassword,
}
srv, err := authdaemon.NewServer(cfg)
if err != nil {
return fmt.Errorf("create auth daemon server: %w", err)
}
n.authDaemonServer = srv
go func() {
logger.Debug("Auth daemon starting (native mode, no HTTP server)")
if err := srv.Run(ctx); err != nil {
logger.Error("Auth daemon error: %v", err)
}
logger.Info("Auth daemon stopped")
}()
return nil
}

127
newt/blueprint.go Normal file
View File

@@ -0,0 +1,127 @@
package newt
import (
"context"
"encoding/json"
"os"
"regexp"
"strings"
"time"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/websocket"
"github.com/fsnotify/fsnotify"
"gopkg.in/yaml.v3"
)
// interpolateBlueprint replaces {{env.VAR}} tokens with their environment variable values.
func interpolateBlueprint(data []byte) []byte {
re := regexp.MustCompile(`\{\{([^}]+)\}\}`)
return re.ReplaceAllFunc(data, func(match []byte) []byte {
inner := strings.TrimSpace(string(match[2 : len(match)-2]))
if strings.HasPrefix(inner, "env.") {
varName := strings.TrimPrefix(inner, "env.")
return []byte(os.Getenv(varName))
}
return match
})
}
func sendBlueprint(client *websocket.Client, file string) error {
if file == "" {
return nil
}
blueprintData, err := os.ReadFile(file)
if err != nil {
logger.Error("Failed to read blueprint file: %v", err)
return nil
}
blueprintData = interpolateBlueprint(blueprintData)
var yamlObj interface{}
if err := yaml.Unmarshal(blueprintData, &yamlObj); err != nil {
logger.Error("Failed to parse blueprint YAML: %v", err)
return nil
}
jsonBytes, err := json.Marshal(yamlObj)
if err != nil {
logger.Error("Failed to convert blueprint to JSON: %v", err)
return nil
}
blueprintJsonData := string(jsonBytes)
logger.Debug("Converted blueprint to JSON: %s", blueprintJsonData)
if blueprintJsonData == "" {
logger.Error("No valid blueprint JSON data to send to server")
return nil
}
logger.Info("Sending blueprint to server for application")
return client.SendMessage("newt/blueprint/apply", map[string]interface{}{
"blueprint": blueprintJsonData,
})
}
func watchBlueprintFile(ctx context.Context, filePath string, send func() error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
logger.Error("blueprint watcher: failed to create: %v", err)
return
}
defer watcher.Close()
if err := watcher.Add(filePath); err != nil {
logger.Error("blueprint watcher: failed to watch %s: %v", filePath, err)
return
}
logger.Info("Watching blueprint file for changes: %s", filePath)
var debounce *time.Timer
scheduleSend := func() {
if debounce != nil {
debounce.Stop()
}
debounce = time.AfterFunc(500*time.Millisecond, func() {
logger.Info("Blueprint file changed, resending...")
if err := send(); err != nil {
logger.Error("blueprint watcher: resend failed: %v", err)
}
})
}
for {
select {
case <-ctx.Done():
if debounce != nil {
debounce.Stop()
}
return
case event, ok := <-watcher.Events:
if !ok {
return
}
switch {
case event.Has(fsnotify.Write) || event.Has(fsnotify.Create):
if event.Has(fsnotify.Create) {
_ = watcher.Add(filePath)
}
scheduleSend()
case event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename):
_ = watcher.Add(filePath)
scheduleSend()
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
logger.Error("blueprint watcher: %v", err)
}
}
}

114
newt/clients.go Normal file
View File

@@ -0,0 +1,114 @@
package newt
import (
"strings"
wgnetstack "github.com/fosrl/newt/clients"
"github.com/fosrl/newt/clients/permissions"
"github.com/fosrl/newt/logger"
"golang.zx2c4.com/wireguard/tun/netstack"
)
func checkNativeMainPermissions() error {
return permissions.CheckNativeInterfacePermissions()
}
func (n *Newt) setupClients() {
host := n.config.Endpoint
if strings.HasPrefix(host, "http://") {
host = strings.TrimPrefix(host, "http://")
} else if strings.HasPrefix(host, "https://") {
host = strings.TrimPrefix(host, "https://")
}
host = strings.TrimSuffix(host, "/")
logger.Debug("Setting up clients with netstack2...")
if n.config.UseNativeInterface {
logger.Debug("Checking permissions for native interface")
if err := permissions.CheckNativeInterfacePermissions(); err != nil {
logger.Fatal("Insufficient permissions to create native TUN interface: %v", err)
return
}
}
var err error
n.wgService, err = wgnetstack.NewWireGuardService(
n.config.InterfaceName,
n.config.Port,
n.config.MTU,
host,
n.config.ID,
n.client,
n.config.DNS,
n.config.UseNativeInterface,
)
if err != nil {
logger.Fatal("Failed to create WireGuard service: %v", err)
}
n.wgService.SetCredentialStore(n.sshCredStore)
n.client.OnTokenUpdate(func(token string) {
n.wgService.SetToken(token)
})
n.ready = true
}
func (n *Newt) setDownstreamTNetstack(tnet *netstack.Net) {
if n.wgService != nil {
n.wgService.SetOthertnet(tnet)
}
}
func (n *Newt) closeClients() {
logger.Info("Closing clients...")
if n.wgService != nil {
n.wgService.Close()
n.wgService = nil
}
}
func (n *Newt) setClientsBlocked(v bool) {
if n.wgService != nil {
n.wgService.SetBlocked(v)
}
}
func (n *Newt) clientsHandleNewtConnection(publicKey string, endpoint string, relayPort uint16) {
if !n.ready {
return
}
parts := strings.Split(endpoint, ":")
if len(parts) < 2 {
logger.Error("Invalid endpoint format: %s", endpoint)
return
}
endpoint = strings.Join(parts[:len(parts)-1], ":")
if n.wgService != nil {
n.wgService.StartHolepunch(publicKey, endpoint, relayPort)
}
}
func (n *Newt) clientsOnConnect() {
if !n.ready {
return
}
if n.wgService != nil {
n.wgService.LoadRemoteConfig()
}
}
func (n *Newt) clientsStartDirectRelay(tunnelIP string) {
if !n.ready {
return
}
if n.wgService != nil {
if err := n.wgService.StartDirectUDPRelay(tunnelIP); err != nil {
logger.Error("Failed to start direct UDP relay: %v", err)
}
}
}

73
newt/config.go Normal file
View File

@@ -0,0 +1,73 @@
package newt
import "time"
// Config holds all runtime configuration for a Newt instance.
type Config struct {
// Build info
Version string
Platform string
// Logging
LogLevel string
// Connection
Endpoint string
ID string
Secret string
ProvisioningKey string
NewtName string
ConfigFile string
// Network
MTU int
DNS string
InterfaceName string
Port uint16
UseNativeInterface bool
UseNativeMainInterface bool
NativeMainInterfaceName string
NoCloud bool
PreferEndpoint string
// Timing
PingInterval time.Duration
PingTimeout time.Duration
UDPProxyIdleTimeout time.Duration
// Features
DisableClients bool
DisableSSH bool
EnforceHealthcheckCert bool
HealthFile string
BlueprintFile string
ProvisioningBlueprintFile string
UpdownScript string
// Docker
DockerSocket string
DockerEnforceNetworkValidation bool
// Auth daemon
AuthDaemonKey string
AuthDaemonPrincipalsFile string
AuthDaemonCACertPath string
AuthDaemonGenerateRandomPassword bool
// TLS (mTLS)
TLSClientCert string
TLSClientKey string
TLSClientCAs []string
TLSPrivateKey string
// Metrics/observability
MetricsEnabled bool
OTLPEnabled bool
AdminAddr string
Region string
MetricsAsyncBytes bool
PprofEnabled bool
// Callbacks
OnRestart func() error
}

351
newt/connect.go Normal file
View File

@@ -0,0 +1,351 @@
package newt
import (
"context"
"encoding/json"
"fmt"
"net"
"net/netip"
"runtime"
"time"
"github.com/fosrl/newt/browsergateway"
newtDevice "github.com/fosrl/newt/device"
"github.com/fosrl/newt/internal/telemetry"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/network"
"github.com/fosrl/newt/proxy"
"github.com/fosrl/newt/util"
"github.com/fosrl/newt/websocket"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
wtun "golang.zx2c4.com/wireguard/tun"
"golang.zx2c4.com/wireguard/tun/netstack"
)
func (n *Newt) handleConnect(ctx context.Context, msg websocket.WSMessage) {
logger.Debug("Received registration message")
regResult := "success"
defer func() {
telemetry.IncSiteRegistration(ctx, regResult)
}()
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 != n.pendingRegisterChainId {
logger.Debug("Discarding duplicate/stale newt/wg/connect (chainId=%s, expected=%s)", chainData.ChainId, n.pendingRegisterChainId)
return
}
n.pendingRegisterChainId = ""
}
if n.stopFunc != nil {
n.stopFunc()
n.stopFunc = nil
}
if n.connected {
n.closeWgTunnel()
n.connected = false
}
logger.Debug("Received registration message data: %+v", msg.Data)
jsonData, err := json.Marshal(msg.Data)
if err != nil {
logger.Info(fmtErrMarshaling, err)
regResult = "failure"
return
}
if err := json.Unmarshal(jsonData, &n.wgData); err != nil {
logger.Info("Error unmarshaling target data: %v", err)
regResult = "failure"
return
}
logger.Debug(fmtReceivedMsg, msg)
if n.config.UseNativeMainInterface {
mainIfName := n.config.NativeMainInterfaceName
if runtime.GOOS == "darwin" {
mainIfName, err = network.FindUnusedUTUN()
if err != nil {
logger.Error("Failed to find unused utun for main tunnel: %v", err)
regResult = "failure"
return
}
}
n.tun, err = wtun.CreateTUN(mainIfName, n.config.MTU)
if err != nil {
logger.Error("Failed to create native main TUN device: %v", err)
regResult = "failure"
return
}
if realName, nameErr := n.tun.Name(); nameErr == nil {
mainIfName = realName
}
n.tnet = nil
n.config.NativeMainInterfaceName = mainIfName
} else {
n.tun, n.tnet, err = netstack.CreateNetTUN(
[]netip.Addr{netip.MustParseAddr(n.wgData.TunnelIP)},
[]netip.Addr{netip.MustParseAddr(n.config.DNS)},
n.config.MTU)
if err != nil {
logger.Error("Failed to create TUN device: %v", err)
regResult = "failure"
}
}
n.setDownstreamTNetstack(n.tnet)
n.dev = device.NewDevice(n.tun, conn.NewDefaultBind(), device.NewLogger(
util.MapToWireGuardLogLevel(n.loggerLevel),
"gerbil-wireguard: ",
))
host, _, err := net.SplitHostPort(n.wgData.Endpoint)
if err != nil {
logger.Error("Failed to split endpoint: %v", err)
regResult = "failure"
return
}
logger.Info("Connecting to endpoint: %s", host)
resolvedEndpoint, err := util.ResolveDomain(n.wgData.Endpoint)
if err != nil {
logger.Error("Failed to resolve endpoint: %v", err)
regResult = "failure"
return
}
relayPort := n.wgData.RelayPort
if relayPort == 0 {
relayPort = 21820
}
n.clientsHandleNewtConnection(n.wgData.PublicKey, resolvedEndpoint, relayPort)
wgConfig := fmt.Sprintf(`private_key=%s
public_key=%s
allowed_ip=%s/32
endpoint=%s
persistent_keepalive_interval=5`, util.FixKey(n.privateKey.String()), util.FixKey(n.wgData.PublicKey), n.wgData.ServerIP, resolvedEndpoint)
if err = n.dev.IpcSet(wgConfig); err != nil {
logger.Error("Failed to configure WireGuard device: %v", err)
regResult = "failure"
}
if err = n.dev.Up(); err != nil {
logger.Error("Failed to bring up WireGuard device: %v", err)
regResult = "failure"
}
if n.config.UseNativeMainInterface {
if cfgErr := network.ConfigureInterface(n.config.NativeMainInterfaceName, n.wgData.TunnelIP+"/32", n.config.MTU); cfgErr != nil {
logger.Error("Failed to configure native main tunnel interface: %v", cfgErr)
}
if routeErr := network.AddRoutes([]string{n.wgData.ServerIP + "/32"}, n.config.NativeMainInterfaceName); routeErr != nil {
logger.Warn("Failed to add route for main tunnel server IP: %v", routeErr)
}
if fileUAPI, uapiErr := newtDevice.UapiOpen(n.config.NativeMainInterfaceName); uapiErr != nil {
logger.Warn("Main tunnel UAPI open error: %v", uapiErr)
} else if uapiListener, uapiListenErr := newtDevice.UapiListen(n.config.NativeMainInterfaceName, fileUAPI); uapiListenErr != nil {
logger.Warn("Main tunnel UAPI listen error: %v", uapiListenErr)
} else {
go func() {
for {
c, aErr := uapiListener.Accept()
if aErr != nil {
return
}
go n.dev.IpcHandle(c)
}
}()
logger.Debug("Main tunnel UAPI listener started on %s", n.config.NativeMainInterfaceName)
}
}
n.activeRemoteSubnets = nil
if len(n.wgData.RemoteExitNodeSubnets) > 0 {
for _, subnet := range n.wgData.RemoteExitNodeSubnets {
subnetCfg := fmt.Sprintf("public_key=%s\nallowed_ip=%s", util.FixKey(n.wgData.PublicKey), subnet)
if err := n.dev.IpcSet(subnetCfg); err != nil {
logger.Warn("Failed to add AllowedIP %s to main tunnel: %v", subnet, err)
}
}
if n.config.UseNativeMainInterface {
if routeErr := network.AddRoutes(n.wgData.RemoteExitNodeSubnets, n.config.NativeMainInterfaceName); routeErr != nil {
logger.Warn("Failed to add routes for remote exit node subnets: %v", routeErr)
}
}
n.activeRemoteSubnets = append([]string{}, n.wgData.RemoteExitNodeSubnets...)
logger.Debug("Added %d remote exit node subnets", len(n.wgData.RemoteExitNodeSubnets))
}
logger.Debug("WireGuard device created. Lets ping the server now...")
if n.pingWithRetryStopChan != nil {
close(n.pingWithRetryStopChan)
n.pingWithRetryStopChan = nil
}
var pinger pingFunc
if n.config.UseNativeMainInterface {
pinger = pingNative
} else {
pinger = func(dst string, timeout time.Duration) (time.Duration, error) {
return ping(n.tnet, dst, timeout)
}
}
logger.Debug("Testing initial connection with reliable ping...")
lat, err := reliablePing(pinger, n.wgData.ServerIP, n.config.PingTimeout, 5)
if err == nil && n.wgData.PublicKey != "" {
telemetry.ObserveTunnelLatency(ctx, n.wgData.PublicKey, "wireguard", lat.Seconds())
}
if err != nil {
logger.Warn("Initial reliable ping failed, but continuing: %v", err)
regResult = "failure"
} else {
logger.Debug("Initial connection test successful")
}
n.pingWithRetryStopChan, _ = n.pingWithRetry(pinger, n.wgData.ServerIP, n.config.PingTimeout)
if !n.connected {
logger.Debug("Starting ping check")
n.pingStopChan = n.startPingCheck(pinger, n.wgData.ServerIP, n.wgData.PublicKey)
}
if n.config.UseNativeMainInterface {
n.pm = proxy.NewProxyManagerNative(n.wgData.TunnelIP)
} else {
n.pm = proxy.NewProxyManager(n.tnet)
}
n.pm.SetAsyncBytes(n.config.MetricsAsyncBytes)
n.pm.SetUDPIdleTimeout(n.config.UDPProxyIdleTimeout)
n.pm.SetTunnelID(n.wgData.PublicKey)
n.pm.SetBlocked(n.connectionBlocked.Load())
n.currentPM.Store(n.pm)
n.connected = true
if len(n.wgData.Targets.TCP) > 0 {
n.updateTargets(n.pm, "add", n.wgData.TunnelIP, "tcp", TargetData{Targets: n.wgData.Targets.TCP})
}
if len(n.wgData.Targets.UDP) > 0 {
n.updateTargets(n.pm, "add", n.wgData.TunnelIP, "udp", TargetData{Targets: n.wgData.Targets.UDP})
}
if !n.config.UseNativeMainInterface {
n.clientsStartDirectRelay(n.wgData.TunnelIP)
}
if err := n.healthMonitor.AddTargets(n.wgData.HealthCheckTargets); err != nil {
logger.Error("Failed to bulk add health check targets: %v", err)
} else {
logger.Debug("Successfully added %d health check targets", len(n.wgData.HealthCheckTargets))
}
if err = n.pm.Start(); err != nil {
logger.Error("Failed to start proxy manager: %v", err)
}
if len(n.wgData.BrowserGatewayTargets) > 0 {
// The netstack is fresh on (re)connect, so any previously running
// gateway listener is bound to a now-defunct interface - tear it down.
if n.browserGatewayStop != nil {
n.browserGatewayStop()
n.browserGatewayStop = nil
n.browserGateway = nil
}
if err := n.startBrowserGateway(); err != nil {
logger.Error("Failed to start browser gateway listener: %v", err)
} else {
n.browserGateway.SetTargets(toBrowserGatewayTargets(n.wgData.BrowserGatewayTargets))
}
}
}
// startBrowserGateway creates the browser gateway and its listener if one
// isn't already running. Callers that need to rebind to a fresh netstack
// (e.g. on reconnect) must stop and clear any existing gateway first.
func (n *Newt) startBrowserGateway() error {
if n.browserGateway != nil {
return nil
}
if n.tnet == nil && !n.config.UseNativeMainInterface {
return fmt.Errorf("netstack not ready")
}
gateway := browsergateway.New(browsergateway.Config{SSHCredentials: n.sshCredStore})
var ln net.Listener
var err error
if n.config.UseNativeMainInterface {
ln, err = net.Listen("tcp", fmt.Sprintf("%s:%d", n.wgData.TunnelIP, browsergateway.ListenPort))
} else {
ln, err = n.tnet.ListenTCP(&net.TCPAddr{Port: browsergateway.ListenPort})
}
if err != nil {
return err
}
n.browserGateway = gateway
n.browserGatewayStop = func() { _ = ln.Close() }
go func() {
logger.Debug("Browser gateway started on port %d", browsergateway.ListenPort)
if startErr := gateway.Start(ln); startErr != nil {
logger.Error("Browser gateway stopped with error: %v", startErr)
}
}()
return nil
}
// syncBrowserGatewayTargets reconciles the browser gateway's allowed
// destinations with the desired state received from a sync message.
// It lazily starts the gateway if targets are present and it isn't running
// yet, and clears the allow-list (without tearing down the listener) when
// no targets are desired.
func (n *Newt) syncBrowserGatewayTargets(targets []BrowserGatewayTarget) {
bgTargets := toBrowserGatewayTargets(targets)
if len(bgTargets) == 0 {
if n.browserGateway != nil {
n.browserGateway.SetTargets(nil)
}
return
}
if err := n.startBrowserGateway(); err != nil {
logger.Error("Failed to start browser gateway: %v", err)
return
}
n.browserGateway.SetTargets(bgTargets)
}
func toBrowserGatewayTargets(targets []BrowserGatewayTarget) []browsergateway.Target {
bgTargets := make([]browsergateway.Target, 0, len(targets))
for _, t := range targets {
bgTargets = append(bgTargets, browsergateway.Target{
ID: t.ID,
Type: t.Type,
Destination: t.Destination,
DestinationPort: t.DestinationPort,
AuthToken: t.AuthToken,
})
}
return bgTargets
}

161
newt/data.go Normal file
View File

@@ -0,0 +1,161 @@
package newt
import (
"encoding/json"
"fmt"
"strings"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/websocket"
)
func (n *Newt) handleSync(msg websocket.WSMessage) {
logger.Info("Received sync message")
// if there is no wgData or pm, we can't sync targets
if n.wgData.TunnelIP == "" || n.pm == nil {
logger.Info(msgNoTunnelOrProxy)
return
}
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))
// 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 := n.pm.GetTargets()
// Sync TCP targets
// Remove TCP targets not in desired set
if tcpForIP, ok := currentTCP[n.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])
n.updateTargets(n.pm, "remove", n.wgData.TunnelIP, "tcp", TargetData{Targets: []string{targetStr}})
}
}
}
// Add TCP targets that are missing
for port, target := range desiredTCP {
needsAdd := true
if tcpForIP, ok := currentTCP[n.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)
n.updateTargets(n.pm, "remove", n.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)
n.updateTargets(n.pm, "add", n.wgData.TunnelIP, "tcp", TargetData{Targets: []string{targetStr}})
}
}
// Sync UDP targets
// Remove UDP targets not in desired set
if udpForIP, ok := currentUDP[n.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])
n.updateTargets(n.pm, "remove", n.wgData.TunnelIP, "udp", TargetData{Targets: []string{targetStr}})
}
}
}
// Add UDP targets that are missing
for port, target := range desiredUDP {
needsAdd := true
if udpForIP, ok := currentUDP[n.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)
n.updateTargets(n.pm, "remove", n.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)
n.updateTargets(n.pm, "add", n.wgData.TunnelIP, "udp", TargetData{Targets: []string{targetStr}})
}
}
// Sync remote exit node subnets
if n.dev != nil {
n.updateRemoteExitNodeSubnets(syncData.RemoteExitNodeSubnets)
}
// Sync clients WireGuard peers and targets, if clients are set up
if n.wgService != nil {
n.wgService.Sync(syncData.Peers, syncData.ClientTargets)
}
// Sync browser gateway targets
n.syncBrowserGatewayTargets(syncData.BrowserGatewayTargets)
// Sync health check targets
if err := n.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")
}

1082
newt/handlers.go Normal file

File diff suppressed because it is too large Load Diff

291
newt/newt.go Normal file
View File

@@ -0,0 +1,291 @@
package newt
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"sync/atomic"
"time"
"github.com/fosrl/newt/authdaemon"
"github.com/fosrl/newt/browsergateway"
wgclients "github.com/fosrl/newt/clients"
"github.com/fosrl/newt/docker"
"github.com/fosrl/newt/healthcheck"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/nativessh"
"github.com/fosrl/newt/proxy"
"github.com/fosrl/newt/util"
"github.com/fosrl/newt/websocket"
"golang.zx2c4.com/wireguard/device"
wtun "golang.zx2c4.com/wireguard/tun"
"golang.zx2c4.com/wireguard/tun/netstack"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
// Newt holds all runtime state for a newt tunnel instance.
type Newt struct {
config Config
client *websocket.Client
privateKey wgtypes.Key
publicKey wgtypes.Key
loggerLevel logger.LogLevel
tlsOpt websocket.ClientOption
// WireGuard tunnel state
tun wtun.Device
tnet *netstack.Net
dev *device.Device
// Proxy / networking
pm *proxy.ProxyManager
currentPM atomic.Pointer[proxy.ProxyManager]
connectionBlocked atomic.Bool
activeRemoteSubnets []string
// Ping state
pingStopChan chan struct{}
pingWithRetryStopChan chan struct{}
// Connection / messaging state
connected bool
stopFunc func()
pendingRegisterChainId string
pendingPingChainId string
// Browser gateway
browserGateway *browsergateway.Gateway
browserGatewayStop func()
// Health monitoring
healthMonitor *healthcheck.Monitor
// Downstream WireGuard client management
wgService *wgclients.WireGuardService
ready bool
sshCredStore *nativessh.CredentialStore
// Auth daemon (Linux only)
authDaemonServer *authdaemon.Server
// Docker monitoring
dockerEventMonitor *docker.EventMonitor
// Current tunnel data
wgData WgData
}
// Init creates and initialises a Newt instance. It sets up the websocket
// client, generates WireGuard keys, and starts the auth daemon if enabled.
// Callers should invoke Start after any additional setup (telemetry, etc.).
func Init(ctx context.Context, cfg Config) (*Newt, error) {
n := &Newt{config: cfg}
n.loggerLevel = util.ParseLogLevel(cfg.LogLevel)
if !cfg.DisableSSH {
if err := n.startAuthDaemon(ctx); err != nil {
logger.Warn("Did not start on site auth daemon: %v", err)
}
}
logger.GetLogger().SetLevel(n.loggerLevel)
if cfg.TLSPrivateKey != "" {
logger.Warn("Using deprecated PKCS12 format for mTLS. Consider migrating to separate certificate files using --tls-client-cert-file, --tls-client-key, and --tls-client-ca")
}
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, fmt.Errorf("generate private key: %w", err)
}
n.privateKey = privateKey
if cfg.TLSClientCert != "" && cfg.TLSClientKey != "" {
n.tlsOpt = websocket.WithTLSConfig(websocket.TLSConfig{
ClientCertFile: cfg.TLSClientCert,
ClientKeyFile: cfg.TLSClientKey,
CAFiles: cfg.TLSClientCAs,
})
logger.Debug("Using separate certificate files for mTLS")
logger.Debug("Client cert: %s", cfg.TLSClientCert)
logger.Debug("Client key: %s", cfg.TLSClientKey)
logger.Debug("CA files: %v", cfg.TLSClientCAs)
} else if cfg.TLSPrivateKey != "" {
n.tlsOpt = websocket.WithTLSConfig(websocket.TLSConfig{
PKCS12File: cfg.TLSPrivateKey,
})
logger.Debug("Using PKCS12 file for mTLS: %s", cfg.TLSPrivateKey)
}
client, err := websocket.NewClient(
"newt",
cfg.ID,
cfg.Secret,
cfg.Endpoint,
30*time.Second,
n.tlsOpt,
websocket.WithConfigFile(cfg.ConfigFile),
)
if err != nil {
return nil, fmt.Errorf("create websocket client: %w", err)
}
n.client = client
client.GetConfig().ProvisioningKey = cfg.ProvisioningKey
client.GetConfig().Name = cfg.NewtName
// Resolve provisioning synchronously so ID/Secret are final before
// setupClients() bakes them into the WireGuard service / hole-punch
// manager. Connect() only provisions lazily in the background, which
// would otherwise race setupClients() on first run.
if err := client.EnsureProvisioned(); err != nil {
return nil, fmt.Errorf("provision newt credentials: %w", err)
}
// Update config from resolved client values (provisioning / config file).
n.config.Endpoint = client.GetConfig().Endpoint
n.config.ID = client.GetConfig().ID
n.config.Secret = client.GetConfig().Secret
if !cfg.DisableSSH {
n.sshCredStore = nativessh.NewCredentialStore()
}
return n, nil
}
// GetConfig returns the (potentially resolved) configuration.
func (n *Newt) GetConfig() Config {
return n.config
}
// GetTLSClientOpt returns the websocket TLS option, so callers can reuse the
// same TLS configuration for other HTTP clients (e.g. self-update).
func (n *Newt) GetTLSClientOpt() websocket.ClientOption {
return n.tlsOpt
}
// Start sets up all WebSocket handlers, connects to the server, and blocks
// until ctx is cancelled.
func (n *Newt) Start(ctx context.Context) {
if !n.config.DisableClients {
n.setupClients()
}
n.connectionBlocked.Store(n.client.GetConfig().Blocked)
if n.connectionBlocked.Load() {
logger.Info("Connection blocking is enabled (from config)")
n.setClientsBlocked(true)
}
n.healthMonitor = healthcheck.NewMonitor(func(targets map[int]*healthcheck.Target) {
logger.Debug("Health check status update for %d targets", len(targets))
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("Health check status: %+v", healthStatuses)
if err := n.client.SendMessage("newt/healthcheck/status", map[string]interface{}{
"targets": healthStatuses,
}); err != nil {
logger.Error("Failed to send health check status update: %v", err)
}
}, n.config.EnforceHealthcheckCert)
n.registerHandlers(ctx)
if err := n.client.Connect(); err != nil {
logger.Fatal("Failed to connect to server: %v", err)
}
defer n.client.Close()
if n.config.DockerSocket != "" {
logger.Debug("Initializing Docker event monitoring")
var err error
n.dockerEventMonitor, err = docker.NewEventMonitor(
n.config.DockerSocket,
n.config.DockerEnforceNetworkValidation,
func(containers []docker.Container) {
logger.Debug("Docker event detected, sending updated container list (%d containers)", len(containers))
if err := n.client.SendMessage("newt/socket/containers", map[string]interface{}{
"containers": containers,
}); err != nil {
logger.Error("Failed to send updated container list after Docker event: %v", err)
} else {
logger.Debug("Updated container list sent successfully")
}
})
if err != nil {
logger.Error("Failed to create Docker event monitor: %v", err)
} else {
if err := n.dockerEventMonitor.Start(); err != nil {
logger.Error("Failed to start Docker event monitoring: %v", err)
} else {
logger.Debug("Docker event monitoring started successfully")
}
}
}
if n.config.BlueprintFile != "" {
go watchBlueprintFile(ctx, n.config.BlueprintFile, func() error {
return sendBlueprint(n.client, n.config.BlueprintFile)
})
}
<-ctx.Done()
n.closeClients()
if n.dockerEventMonitor != nil {
n.dockerEventMonitor.Stop()
}
if n.healthMonitor != nil {
n.healthMonitor.Stop()
}
if n.dev != nil {
n.dev.Close()
}
if n.pm != nil {
n.pm.Stop()
}
n.client.SendMessage("newt/disconnecting", map[string]any{})
if n.client != nil {
n.client.Close()
}
logger.Info("Exiting...")
}
// Close performs an emergency shutdown: closes the tunnel, clients, health
// monitor, and websocket connection. Typically used before re-exec.
func (n *Newt) Close() {
n.closeWgTunnel()
n.closeClients()
if n.healthMonitor != nil {
n.healthMonitor.Stop()
}
if n.client != nil {
n.client.Close()
}
}
func generateChainId() string {
b := make([]byte, 8)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}

View File

@@ -1,10 +1,151 @@
package main
package newt
import (
"context"
"net"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
)
func TestWatchBlueprintFile_WriteTriggersSend(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "blueprint-*.yaml")
if err != nil {
t.Fatal(err)
}
f.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var calls atomic.Int32
go watchBlueprintFile(ctx, f.Name(), func() error {
calls.Add(1)
return nil
})
time.Sleep(50 * time.Millisecond)
if err := os.WriteFile(f.Name(), []byte("content"), 0644); err != nil {
t.Fatal(err)
}
time.Sleep(700 * time.Millisecond)
if calls.Load() != 1 {
t.Errorf("expected 1 send call, got %d", calls.Load())
}
}
func TestWatchBlueprintFile_DebounceCoalescesEvents(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "blueprint-*.yaml")
if err != nil {
t.Fatal(err)
}
f.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var calls atomic.Int32
go watchBlueprintFile(ctx, f.Name(), func() error {
calls.Add(1)
return nil
})
time.Sleep(50 * time.Millisecond)
for i := 0; i < 5; i++ {
if err := os.WriteFile(f.Name(), []byte("change"), 0644); err != nil {
t.Fatal(err)
}
time.Sleep(50 * time.Millisecond)
}
time.Sleep(700 * time.Millisecond)
if calls.Load() != 1 {
t.Errorf("expected 1 send call after debounce, got %d", calls.Load())
}
}
func TestWatchBlueprintFile_ContextCancellationStops(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "blueprint-*.yaml")
if err != nil {
t.Fatal(err)
}
f.Close()
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
watchBlueprintFile(ctx, f.Name(), func() error { return nil })
close(done)
}()
time.Sleep(50 * time.Millisecond)
cancel()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Error("watchBlueprintFile did not exit after context cancellation")
}
}
func TestWatchBlueprintFile_AtomicWriteTriggersSend(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "blueprint.yaml")
if err := os.WriteFile(target, []byte("initial"), 0644); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var calls atomic.Int32
go watchBlueprintFile(ctx, target, func() error {
calls.Add(1)
return nil
})
time.Sleep(50 * time.Millisecond)
tmp := filepath.Join(dir, "blueprint.yaml.tmp")
if err := os.WriteFile(tmp, []byte("updated"), 0644); err != nil {
t.Fatal(err)
}
if err := os.Rename(tmp, target); err != nil {
t.Fatal(err)
}
time.Sleep(700 * time.Millisecond)
if calls.Load() < 1 {
t.Errorf("expected at least 1 send call after atomic write, got %d", calls.Load())
}
}
func TestWatchBlueprintFile_MissingFileReturnsGracefully(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan struct{})
go func() {
watchBlueprintFile(ctx, "/nonexistent/path/blueprint.yaml", func() error { return nil })
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Error("watchBlueprintFile did not return for missing file")
}
}
func TestParseTargetString(t *testing.T) {
tests := []struct {
name string
@@ -13,7 +154,6 @@ func TestParseTargetString(t *testing.T) {
wantTargetAddr string
wantErr bool
}{
// IPv4 test cases
{
name: "valid IPv4 basic",
input: "3001:192.168.1.1:80",
@@ -35,8 +175,6 @@ func TestParseTargetString(t *testing.T) {
wantTargetAddr: "10.0.0.1:443",
wantErr: false,
},
// IPv6 test cases
{
name: "valid IPv6 loopback",
input: "3001:[::1]:8080",
@@ -72,8 +210,6 @@ func TestParseTargetString(t *testing.T) {
wantTargetAddr: "[::ffff:192.168.1.1]:6000",
wantErr: false,
},
// Hostname test cases
{
name: "valid hostname",
input: "8080:example.com:80",
@@ -95,8 +231,6 @@ func TestParseTargetString(t *testing.T) {
wantTargetAddr: "localhost:3000",
wantErr: false,
},
// Error cases
{
name: "invalid - no colons",
input: "invalid",
@@ -169,7 +303,7 @@ func TestParseTargetString(t *testing.T) {
}
if tt.wantErr {
return // Don't check other values if we expected an error
return
}
if listenPort != tt.wantListenPort {
@@ -183,7 +317,6 @@ func TestParseTargetString(t *testing.T) {
}
}
// TestParseTargetStringNetDialCompatibility verifies that the output is compatible with net.Dial
func TestParseTargetStringNetDialCompatibility(t *testing.T) {
tests := []struct {
name string
@@ -201,8 +334,6 @@ func TestParseTargetStringNetDialCompatibility(t *testing.T) {
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)
@@ -213,18 +344,7 @@ func TestParseTargetStringNetDialCompatibility(t *testing.T) {
// 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.
// (fosrl/newt#284, #310, pangolin#1004).
func TestShouldFireRecovery(t *testing.T) {
const threshold = 4
cases := []struct {

339
newt/ping.go Normal file
View File

@@ -0,0 +1,339 @@
package newt
import (
"bytes"
"context"
"fmt"
"math/rand"
"os"
"os/exec"
"runtime"
"time"
"github.com/fosrl/newt/internal/telemetry"
"github.com/fosrl/newt/logger"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.zx2c4.com/wireguard/tun/netstack"
)
type pingFunc func(dst string, timeout time.Duration) (time.Duration, error)
const msgHealthFileWriteFailed = "Failed to write health file: %v"
func pingNative(dst string, timeout time.Duration) (time.Duration, error) {
timeoutSecs := int(timeout.Seconds())
if timeoutSecs < 1 {
timeoutSecs = 1
}
ctx, cancel := context.WithTimeout(context.Background(), timeout+time.Second)
defer cancel()
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.CommandContext(ctx, "ping", "-n", "1", "-w", fmt.Sprintf("%d", int(timeout.Milliseconds())), dst)
case "darwin":
cmd = exec.CommandContext(ctx, "ping", "-c", "1", "-W", fmt.Sprintf("%d", int(timeout.Milliseconds())), dst)
default:
cmd = exec.CommandContext(ctx, "ping", "-c", "1", "-W", fmt.Sprintf("%d", timeoutSecs), dst)
}
start := time.Now()
if err := cmd.Run(); err != nil {
return 0, fmt.Errorf("native ping to %s failed: %w", dst, err)
}
return time.Since(start), nil
}
func ping(tnet *netstack.Net, dst string, timeout time.Duration) (time.Duration, error) {
socket, err := tnet.Dial("ping4", dst)
if err != nil {
return 0, fmt.Errorf("failed to create ICMP socket: %w", err)
}
defer socket.Close()
if tcpConn, ok := socket.(interface{ SetReadBuffer(int) error }); ok {
tcpConn.SetReadBuffer(64 * 1024)
}
if tcpConn, ok := socket.(interface{ SetWriteBuffer(int) error }); ok {
tcpConn.SetWriteBuffer(64 * 1024)
}
requestPing := icmp.Echo{
Seq: rand.Intn(1 << 16),
Data: []byte("newtping"),
}
icmpBytes, err := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil)
if err != nil {
return 0, fmt.Errorf("failed to marshal ICMP message: %w", err)
}
if err := socket.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return 0, fmt.Errorf("failed to set read deadline: %w", err)
}
start := time.Now()
_, err = socket.Write(icmpBytes)
if err != nil {
return 0, fmt.Errorf("failed to write ICMP packet: %w", err)
}
readBuffer := make([]byte, 1500)
n, err := socket.Read(readBuffer)
if err != nil {
return 0, fmt.Errorf("failed to read ICMP packet: %w", err)
}
replyPacket, err := icmp.ParseMessage(1, readBuffer[:n])
if err != nil {
return 0, fmt.Errorf("failed to parse ICMP packet: %w", err)
}
replyPing, ok := replyPacket.Body.(*icmp.Echo)
if !ok {
return 0, fmt.Errorf("invalid reply type: got %T, want *icmp.Echo", replyPacket.Body)
}
if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq {
return 0, fmt.Errorf("invalid ping reply: got seq=%d data=%q, want seq=%d data=%q",
replyPing.Seq, replyPing.Data, requestPing.Seq, requestPing.Data)
}
return time.Since(start), nil
}
func reliablePing(fn pingFunc, dst string, baseTimeout time.Duration, maxAttempts int) (time.Duration, error) {
var lastErr error
var totalLatency time.Duration
successCount := 0
for attempt := 1; attempt <= maxAttempts; attempt++ {
timeout := baseTimeout + time.Duration(attempt-1)*500*time.Millisecond
jitter := time.Duration(rand.Intn(100)) * time.Millisecond
timeout += jitter
latency, err := fn(dst, timeout)
if err != nil {
lastErr = err
logger.Debug("Ping attempt %d/%d failed: %v", attempt, maxAttempts, err)
if attempt < maxAttempts {
backoff := time.Duration(attempt) * 50 * time.Millisecond
time.Sleep(backoff)
}
continue
}
totalLatency += latency
successCount++
return totalLatency / time.Duration(successCount), nil
}
return 0, fmt.Errorf("all %d ping attempts failed, last error: %v", maxAttempts, lastErr)
}
// shouldFireRecovery decides whether the data-plane recovery flow should run on
// this tick. See startPingCheck for the rationale behind separating recovery
// from the backoff ramp.
func shouldFireRecovery(consecutiveFailures, failureThreshold int, connectionLost bool) bool {
return consecutiveFailures >= failureThreshold && !connectionLost
}
func (n *Newt) pingWithRetry(fn pingFunc, dst string, timeout time.Duration) (stopChan chan struct{}, err error) {
if n.config.HealthFile != "" {
err = os.Remove(n.config.HealthFile)
if err != nil {
logger.Error("Failed to remove health file: %v", err)
}
}
const (
initialRetryDelay = 2 * time.Second
maxRetryDelay = 60 * time.Second
)
stopChan = make(chan struct{})
attempt := 1
retryDelay := initialRetryDelay
logger.Debug("Ping attempt %d", attempt)
if latency, err := fn(dst, timeout); err == nil {
logger.Debug("Ping latency: %v", latency)
logger.Info("Tunnel connection to server established successfully!")
if n.config.HealthFile != "" {
if err := os.WriteFile(n.config.HealthFile, []byte("ok"), 0644); err != nil {
logger.Warn(msgHealthFileWriteFailed, err)
}
}
return stopChan, nil
} else {
logger.Warn("Ping attempt %d failed: %v", attempt, err)
}
go func() {
attempt = 2
for {
select {
case <-stopChan:
return
default:
logger.Debug("Ping attempt %d", attempt)
if latency, err := fn(dst, timeout); err != nil {
logger.Warn("Ping attempt %d failed: %v", attempt, err)
if attempt%5 == 0 && retryDelay < maxRetryDelay {
retryDelay = time.Duration(float64(retryDelay) * 1.5)
if retryDelay > maxRetryDelay {
retryDelay = maxRetryDelay
}
logger.Info("Increasing ping retry delay to %v", retryDelay)
}
time.Sleep(retryDelay)
attempt++
} else {
logger.Debug("Ping succeeded after %d attempts", attempt)
logger.Debug("Ping latency: %v", latency)
logger.Info("Tunnel connection to server established successfully!")
if n.config.HealthFile != "" {
if err := os.WriteFile(n.config.HealthFile, []byte("ok"), 0644); err != nil {
logger.Warn(msgHealthFileWriteFailed, err)
}
}
return
}
case <-n.pingStopChan:
return
}
}
}()
return stopChan, fmt.Errorf("initial ping attempts failed, continuing in background")
}
func (n *Newt) startPingCheck(fn pingFunc, serverIP, tunnelID string) chan struct{} {
maxInterval := 6 * time.Second
currentInterval := n.config.PingInterval
consecutiveFailures := 0
connectionLost := false
recentLatencies := make([]time.Duration, 0, 10)
pingStopChan := make(chan struct{})
go func() {
ticker := time.NewTicker(currentInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
adaptiveTimeout := n.config.PingTimeout
if len(recentLatencies) > 0 {
var sum time.Duration
for _, lat := range recentLatencies {
sum += lat
}
avgLatency := sum / time.Duration(len(recentLatencies))
adaptiveTimeout = avgLatency * 3
if adaptiveTimeout < n.config.PingTimeout {
adaptiveTimeout = n.config.PingTimeout
}
if adaptiveTimeout > 15*time.Second {
adaptiveTimeout = 15 * time.Second
}
}
maxAttempts := 2
if consecutiveFailures > 4 {
maxAttempts = 4
}
latency, err := reliablePing(fn, serverIP, adaptiveTimeout, maxAttempts)
if err != nil {
consecutiveFailures++
recentLatencies = append(recentLatencies, adaptiveTimeout)
if len(recentLatencies) > 10 {
recentLatencies = recentLatencies[1:]
}
if consecutiveFailures < 2 {
logger.Debug("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err)
} else {
logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err)
}
failureThreshold := 4
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()
n.pendingPingChainId = pingChainId
n.stopFunc = n.client.SendMessageInterval("newt/ping/request", map[string]interface{}{
"chainId": pingChainId,
}, 3*time.Second)
bcChainId := generateChainId()
n.pendingRegisterChainId = bcChainId
if err := n.client.SendMessage("newt/wg/register", map[string]interface{}{
"publicKey": n.publicKey.String(),
"backwardsCompatible": true,
"chainId": bcChainId,
}); err != nil {
logger.Error("Failed to send registration message: %v", err)
}
if n.config.HealthFile != "" {
if err := os.Remove(n.config.HealthFile); err != nil {
logger.Error("Failed to remove health file: %v", err)
}
}
}
if consecutiveFailures >= failureThreshold && currentInterval < maxInterval {
currentInterval = time.Duration(float64(currentInterval) * 1.3)
if currentInterval > maxInterval {
currentInterval = maxInterval
}
}
} else {
recentLatencies = append(recentLatencies, latency)
if tunnelID != "" {
telemetry.ObserveTunnelLatency(context.Background(), tunnelID, "wireguard", latency.Seconds())
}
if len(recentLatencies) > 10 {
recentLatencies = recentLatencies[1:]
}
if connectionLost {
connectionLost = false
logger.Info("Connection to server restored after %d failures!", consecutiveFailures)
if n.config.HealthFile != "" {
if err := os.WriteFile(n.config.HealthFile, []byte("ok"), 0644); err != nil {
logger.Warn("Failed to write health file: %v", err)
}
}
}
if currentInterval > n.config.PingInterval {
currentInterval = time.Duration(float64(currentInterval) * 0.9)
if currentInterval < n.config.PingInterval {
currentInterval = n.config.PingInterval
}
ticker.Reset(currentInterval)
logger.Debug("Decreased ping check interval to %v after successful ping", currentInterval)
}
consecutiveFailures = 0
}
case <-pingStopChan:
logger.Info("Stopping ping check")
return
}
}
}()
return pingStopChan
}

150
newt/targets.go Normal file
View File

@@ -0,0 +1,150 @@
package newt
import (
"encoding/json"
"fmt"
"net"
"os/exec"
"strings"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/proxy"
)
func parseTargetData(data interface{}) (TargetData, error) {
var targetData TargetData
jsonData, err := json.Marshal(data)
if err != nil {
logger.Info("Error marshaling data: %v", err)
return targetData, err
}
if err := json.Unmarshal(jsonData, &targetData); err != nil {
logger.Info("Error unmarshaling target data: %v", err)
return targetData, err
}
return targetData, nil
}
// parseTargetString parses "listenPort:host:targetPort", handling IPv6 brackets.
func parseTargetString(target string) (int, string, error) {
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)
}
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)
}
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)
}
return listenPort, net.JoinHostPort(host, targetPort), nil
}
func (n *Newt) updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto string, targetData TargetData) error {
for _, t := range targetData.Targets {
port, target, err := parseTargetString(t)
if err != nil {
logger.Info("Invalid target format: %s (%v)", t, err)
continue
}
switch action {
case "add":
processedTarget := target
if n.config.UpdownScript != "" {
newTarget, err := n.executeUpdownScript(action, proto, target)
if err != nil {
logger.Warn("Updown script error: %v", err)
} else if newTarget != "" {
processedTarget = newTarget
}
}
err := pm.RemoveTarget(proto, tunnelIP, port)
if err != nil {
if !strings.Contains(err.Error(), "target not found") {
logger.Error("Failed to remove existing target: %v", err)
}
}
pm.AddTarget(proto, tunnelIP, port, processedTarget)
case "remove":
logger.Info("Removing target with port %d", port)
if n.config.UpdownScript != "" {
_, err := n.executeUpdownScript(action, proto, target)
if err != nil {
logger.Warn("Updown script error: %v", err)
}
}
err = pm.RemoveTarget(proto, tunnelIP, port)
if err != nil {
logger.Error("Failed to remove target: %v", err)
return err
}
default:
logger.Info("Unknown action: %s", action)
}
}
return nil
}
func (n *Newt) executeUpdownScript(action, proto, target string) (string, error) {
if n.config.UpdownScript == "" {
return target, nil
}
parts := strings.Fields(n.config.UpdownScript)
if len(parts) == 0 {
return target, fmt.Errorf("invalid updown script command")
}
var cmd *exec.Cmd
if len(parts) == 1 {
logger.Info("Executing updown script: %s %s %s %s", n.config.UpdownScript, action, proto, target)
cmd = exec.Command(parts[0], action, proto, target)
} else {
args := append(parts[1:], action, proto, target)
logger.Info("Executing updown script: %s %s %s %s %s", parts[0], strings.Join(parts[1:], " "), action, proto, target)
cmd = exec.Command(parts[0], args...)
}
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("updown script execution failed (exit code %d): %s",
exitErr.ExitCode(), string(exitErr.Stderr))
}
return "", fmt.Errorf("updown script execution failed: %v", err)
}
newTarget := strings.TrimSpace(string(output))
if newTarget != "" {
logger.Info("Updown script returned new target: %s", newTarget)
return newTarget, nil
}
return target, nil
}

109
newt/tunnel.go Normal file
View File

@@ -0,0 +1,109 @@
package newt
import (
"fmt"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/network"
"github.com/fosrl/newt/util"
)
// updateRemoteExitNodeSubnets replaces the set of active remote exit node
// subnets with the given list, updating WireGuard AllowedIPs and native
// routes to match.
func (n *Newt) updateRemoteExitNodeSubnets(subnets []string) {
if n.config.UseNativeMainInterface && len(n.activeRemoteSubnets) > 0 {
toRemove := make([]string, 0)
newSet := make(map[string]bool, len(subnets))
for _, s := range subnets {
newSet[s] = true
}
for _, s := range n.activeRemoteSubnets {
if !newSet[s] {
toRemove = append(toRemove, s)
}
}
if len(toRemove) > 0 {
if err := network.RemoveRoutes(toRemove); err != nil {
logger.Warn("Failed to remove old subnet routes: %v", err)
}
}
}
if n.dev != nil && n.wgData.PublicKey != "" {
lines := fmt.Sprintf("public_key=%s\nreplace_allowed_ips=true\nallowed_ip=%s/32",
util.FixKey(n.wgData.PublicKey), n.wgData.ServerIP)
for _, s := range subnets {
lines += "\nallowed_ip=" + s
}
if err := n.dev.IpcSet(lines); err != nil {
logger.Warn("Failed to update WireGuard AllowedIPs: %v", err)
}
}
if n.config.UseNativeMainInterface && len(subnets) > 0 {
existing := make(map[string]bool, len(n.activeRemoteSubnets))
for _, s := range n.activeRemoteSubnets {
existing[s] = true
}
toAdd := make([]string, 0)
for _, s := range subnets {
if !existing[s] {
toAdd = append(toAdd, s)
}
}
if len(toAdd) > 0 {
if err := network.AddRoutes(toAdd, n.config.NativeMainInterfaceName); err != nil {
logger.Warn("Failed to add new subnet routes: %v", err)
}
}
}
n.activeRemoteSubnets = append([]string{}, subnets...)
logger.Info("Updated remote exit node subnets: %d total", len(subnets))
}
func (n *Newt) closeWgTunnel() {
if n.pingStopChan != nil {
close(n.pingStopChan)
n.pingStopChan = nil
}
if n.browserGatewayStop != nil {
n.browserGatewayStop()
n.browserGatewayStop = nil
n.browserGateway = nil
}
if n.pm != nil {
n.pm.Stop()
n.currentPM.Store(nil)
n.pm = nil
}
if n.config.UseNativeMainInterface {
toRemove := make([]string, 0, len(n.activeRemoteSubnets)+1)
if n.wgData.ServerIP != "" {
toRemove = append(toRemove, n.wgData.ServerIP+"/32")
}
toRemove = append(toRemove, n.activeRemoteSubnets...)
if len(toRemove) > 0 {
if err := network.RemoveRoutes(toRemove); err != nil {
logger.Warn("Failed to remove native main tunnel routes: %v", err)
}
}
n.activeRemoteSubnets = nil
}
if n.dev != nil {
n.dev.Close()
n.dev = nil
}
if n.tnet != nil {
n.tnet = nil
}
if n.tun != nil {
n.tun = nil
}
}

74
newt/types.go Normal file
View File

@@ -0,0 +1,74 @@
package newt
import (
wgclients "github.com/fosrl/newt/clients"
"github.com/fosrl/newt/healthcheck"
)
type BrowserGatewayTarget struct {
ID int `json:"id"`
Type string `json:"type"`
Destination string `json:"destination"`
DestinationPort int `json:"destinationPort"`
AuthToken string `json:"authToken"`
}
type WgData struct {
Endpoint string `json:"endpoint"`
RelayPort uint16 `json:"relayPort"`
PublicKey string `json:"publicKey"`
ServerIP string `json:"serverIP"`
TunnelIP string `json:"tunnelIP"`
Targets TargetsByType `json:"targets"`
HealthCheckTargets []healthcheck.Config `json:"healthCheckTargets"`
BrowserGatewayTargets []BrowserGatewayTarget `json:"browserGatewayTargets"`
RemoteExitNodeSubnets []string `json:"remoteExitNodeSubnets"`
ChainId string `json:"chainId"`
}
type TargetsByType struct {
UDP []string `json:"udp"`
TCP []string `json:"tcp"`
}
type TargetData struct {
Targets []string `json:"targets"`
}
type ExitNodeData struct {
ExitNodes []ExitNode `json:"exitNodes"`
ChainId string `json:"chainId"`
}
type ExitNode struct {
ID int `json:"exitNodeId"`
Name string `json:"exitNodeName"`
Endpoint string `json:"endpoint"`
Weight float64 `json:"weight"`
WasPreviouslyConnected bool `json:"wasPreviouslyConnected"`
}
type ExitNodePingResult struct {
ExitNodeID int `json:"exitNodeId"`
LatencyMs int64 `json:"latencyMs"`
Weight float64 `json:"weight"`
Error string `json:"error,omitempty"`
Name string `json:"exitNodeName"`
Endpoint string `json:"endpoint"`
WasPreviouslyConnected bool `json:"wasPreviouslyConnected"`
}
type BlueprintResult struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
// Define the sync data structure
type SyncData struct {
Targets TargetsByType `json:"proxyTargets"`
HealthCheckTargets []healthcheck.Config `json:"healthCheckTargets"`
RemoteExitNodeSubnets []string `json:"remoteExitNodeSubnets"`
Peers []wgclients.Peer `json:"peers"`
ClientTargets []wgclients.Target `json:"clientTargets"`
BrowserGatewayTargets []BrowserGatewayTarget `json:"browserGatewayTargets"`
}

3
packages/advantech/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.build
*.tgz
bin

View File

@@ -0,0 +1,54 @@
MODNAME := newt
VERSION := 1.12.5
RELEASE_URL := https://github.com/fosrl/newt/releases/download/$(VERSION)
PLATFORM ?= v4
# Map Advantech platform to newt release binary name
arch_v4 := arm64
arch_v4i := arm64
arch_v3 := arm32
arch_v2 := arm32
arch_v2i := arm32
NEWT_ARCH := $(arch_$(PLATFORM))
ifeq ($(NEWT_ARCH),)
$(error Unknown platform '$(PLATFORM)'. Supported: v4, v4i, v3, v2, v2i)
endif
BINARY := newt_linux_$(NEWT_ARCH)
BINDIR := bin
OUTFILE := $(MODNAME).$(PLATFORM).tgz
STAGEDIR := .build/$(MODNAME)
.PHONY: all clean
all: $(OUTFILE)
# Cache the downloaded binary in bin/ (re-download only if missing)
$(BINDIR)/$(BINARY):
mkdir -p $(BINDIR)
@echo "Downloading $(BINARY) $(VERSION) for platform $(PLATFORM)..."
wget -q -O $@ "$(RELEASE_URL)/$(BINARY)" 2>/dev/null || \
curl -fsSL -o $@ "$(RELEASE_URL)/$(BINARY)"
chmod +x $@
@echo "Binary ready: $@"
# Build the package
$(OUTFILE): $(BINDIR)/$(BINARY) $(shell find merge -type f 2>/dev/null)
@rm -rf $(STAGEDIR)
@mkdir -p $(STAGEDIR)/bin
@cp $(BINDIR)/$(BINARY) $(STAGEDIR)/bin/newt
@chmod +x $(STAGEDIR)/bin/newt
@cp -r merge/. $(STAGEDIR)/
@chmod +x \
$(STAGEDIR)/etc/init \
$(STAGEDIR)/etc/install \
$(STAGEDIR)/etc/uninstall \
$(STAGEDIR)/www/index.cgi
tar -c --owner=0 --group=0 --mtime="2001-01-01 UTC" \
-C .build $(MODNAME) | gzip -n > $@
@echo "Created: $@"
clean:
rm -rf .build $(MODNAME).*.tgz
@echo "Cleaned."

View File

@@ -0,0 +1,61 @@
MOD_PANGOLIN_SITE_ENABLED=0
MOD_PANGOLIN_SITE_ENDPOINT=
MOD_PANGOLIN_SITE_ID=
MOD_PANGOLIN_SITE_SECRET=
# Core networking
MOD_PANGOLIN_SITE_MTU=
MOD_PANGOLIN_SITE_DNS=
MOD_PANGOLIN_SITE_INTERFACE=
MOD_PANGOLIN_SITE_PORT=
MOD_PANGOLIN_SITE_UPDOWN_SCRIPT=
MOD_PANGOLIN_SITE_PREFER_ENDPOINT=
# Logging
MOD_PANGOLIN_SITE_LOG_LEVEL=
# Behavior toggles (true/false)
MOD_PANGOLIN_SITE_DISABLE_CLIENTS=
MOD_PANGOLIN_SITE_USE_NATIVE_INTERFACE=
MOD_PANGOLIN_SITE_ENFORCE_HC_CERT=
MOD_PANGOLIN_SITE_NO_CLOUD=
# Timers
MOD_PANGOLIN_SITE_PING_INTERVAL=
MOD_PANGOLIN_SITE_PING_TIMEOUT=
MOD_PANGOLIN_SITE_UDP_PROXY_IDLE_TIMEOUT=
# Docker integration
MOD_PANGOLIN_SITE_DOCKER_SOCKET=
MOD_PANGOLIN_SITE_DOCKER_ENFORCE_NETWORK_VALIDATION=
# Health / files
MOD_PANGOLIN_SITE_HEALTH_FILE=
MOD_PANGOLIN_SITE_BLUEPRINT_FILE=
MOD_PANGOLIN_SITE_PROVISIONING_BLUEPRINT_FILE=
MOD_PANGOLIN_SITE_CONFIG_FILE=
# Provisioning
MOD_PANGOLIN_SITE_PROVISIONING_KEY=
MOD_PANGOLIN_SITE_NAME=
# Auth daemon
MOD_PANGOLIN_SITE_AUTH_DAEMON_ENABLED=
MOD_PANGOLIN_SITE_AD_GENERATE_RANDOM_PASSWORD=
MOD_PANGOLIN_SITE_AD_KEY=
MOD_PANGOLIN_SITE_AD_PRINCIPALS_FILE=
MOD_PANGOLIN_SITE_AD_CA_CERT_PATH=
# Metrics / observability
MOD_PANGOLIN_SITE_METRICS_PROMETHEUS_ENABLED=
MOD_PANGOLIN_SITE_METRICS_OTLP_ENABLED=
MOD_PANGOLIN_SITE_ADMIN_ADDR=
MOD_PANGOLIN_SITE_REGION=
MOD_PANGOLIN_SITE_METRICS_ASYNC_BYTES=
MOD_PANGOLIN_SITE_PPROF_ENABLED=
# mTLS
MOD_PANGOLIN_SITE_TLS_CLIENT_CERT=
MOD_PANGOLIN_SITE_TLS_CLIENT_KEY=
MOD_PANGOLIN_SITE_TLS_CLIENT_CAS=
MOD_PANGOLIN_SITE_TLS_CLIENT_CERT_PKCS12=

View File

@@ -0,0 +1,4 @@
Pangolin Site (newt) is a WireGuard-based tunnel client that connects this router to a
Pangolin server, enabling secure remote access to local resources without opening
firewall ports. Configure the server endpoint, ID, and secret via the web
interface to establish the tunnel automatically on startup.

View File

@@ -0,0 +1,124 @@
#!/bin/sh
MODNAME=newt
PIDFILE=/tmp/newt.pid
LOGFILE=/tmp/newt.log
set_setting_raw() {
key="$1"
value="$2"
[ -f "/opt/$MODNAME/etc/settings" ] || cp "/opt/$MODNAME/etc/defaults" "/opt/$MODNAME/etc/settings" 2>/dev/null
awk -v k="$key" -v v="$value" '
BEGIN { done=0 }
index($0, k "=") == 1 { print k "=" v; done=1; next }
{ print }
END { if (!done) print k "=" v }
' "/opt/$MODNAME/etc/settings" > "/tmp/${MODNAME}.settings.tmp" && mv "/tmp/${MODNAME}.settings.tmp" "/opt/$MODNAME/etc/settings"
}
/usr/bin/logger -t $MODNAME "DEBUG: $0 $@"
case "$1" in
start)
. /opt/$MODNAME/etc/settings
if [ "$MOD_PANGOLIN_SITE_ENABLED" != "1" ]; then
echo "$MODNAME is disabled in settings, skipping start"
exit 0
fi
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then
echo "$MODNAME is already running (PID: $(cat "$PIDFILE"))"
exit 0
fi
# Starting newt implies enable at boot.
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "1"
# Map module settings to Newt environment variables.
[ -n "$MOD_PANGOLIN_SITE_ENDPOINT" ] && export PANGOLIN_ENDPOINT="$MOD_PANGOLIN_SITE_ENDPOINT"
[ -n "$MOD_PANGOLIN_SITE_ID" ] && export NEWT_ID="$MOD_PANGOLIN_SITE_ID"
[ -n "$MOD_PANGOLIN_SITE_SECRET" ] && export NEWT_SECRET="$MOD_PANGOLIN_SITE_SECRET"
[ -n "$MOD_PANGOLIN_SITE_MTU" ] && export MTU="$MOD_PANGOLIN_SITE_MTU"
[ -n "$MOD_PANGOLIN_SITE_DNS" ] && export DNS="$MOD_PANGOLIN_SITE_DNS"
[ -n "$MOD_PANGOLIN_SITE_LOG_LEVEL" ] && export LOG_LEVEL="$MOD_PANGOLIN_SITE_LOG_LEVEL"
[ -n "$MOD_PANGOLIN_SITE_UPDOWN_SCRIPT" ] && export UPDOWN_SCRIPT="$MOD_PANGOLIN_SITE_UPDOWN_SCRIPT"
[ -n "$MOD_PANGOLIN_SITE_INTERFACE" ] && export INTERFACE="$MOD_PANGOLIN_SITE_INTERFACE"
[ -n "$MOD_PANGOLIN_SITE_PORT" ] && export PORT="$MOD_PANGOLIN_SITE_PORT"
[ -n "$MOD_PANGOLIN_SITE_DISABLE_CLIENTS" ] && export DISABLE_CLIENTS="$MOD_PANGOLIN_SITE_DISABLE_CLIENTS"
[ -n "$MOD_PANGOLIN_SITE_USE_NATIVE_INTERFACE" ] && export USE_NATIVE_INTERFACE="$MOD_PANGOLIN_SITE_USE_NATIVE_INTERFACE"
[ -n "$MOD_PANGOLIN_SITE_ENFORCE_HC_CERT" ] && export ENFORCE_HC_CERT="$MOD_PANGOLIN_SITE_ENFORCE_HC_CERT"
[ -n "$MOD_PANGOLIN_SITE_DOCKER_SOCKET" ] && export DOCKER_SOCKET="$MOD_PANGOLIN_SITE_DOCKER_SOCKET"
[ -n "$MOD_PANGOLIN_SITE_PING_INTERVAL" ] && export PING_INTERVAL="$MOD_PANGOLIN_SITE_PING_INTERVAL"
[ -n "$MOD_PANGOLIN_SITE_PING_TIMEOUT" ] && export PING_TIMEOUT="$MOD_PANGOLIN_SITE_PING_TIMEOUT"
[ -n "$MOD_PANGOLIN_SITE_UDP_PROXY_IDLE_TIMEOUT" ] && export NEWT_UDP_PROXY_IDLE_TIMEOUT="$MOD_PANGOLIN_SITE_UDP_PROXY_IDLE_TIMEOUT"
[ -n "$MOD_PANGOLIN_SITE_DOCKER_ENFORCE_NETWORK_VALIDATION" ] && export DOCKER_ENFORCE_NETWORK_VALIDATION="$MOD_PANGOLIN_SITE_DOCKER_ENFORCE_NETWORK_VALIDATION"
[ -n "$MOD_PANGOLIN_SITE_HEALTH_FILE" ] && export HEALTH_FILE="$MOD_PANGOLIN_SITE_HEALTH_FILE"
[ -n "$MOD_PANGOLIN_SITE_AUTH_DAEMON_ENABLED" ] && export AUTH_DAEMON_ENABLED="$MOD_PANGOLIN_SITE_AUTH_DAEMON_ENABLED"
[ -n "$MOD_PANGOLIN_SITE_AD_GENERATE_RANDOM_PASSWORD" ] && export AD_GENERATE_RANDOM_PASSWORD="$MOD_PANGOLIN_SITE_AD_GENERATE_RANDOM_PASSWORD"
[ -n "$MOD_PANGOLIN_SITE_AD_KEY" ] && export AD_KEY="$MOD_PANGOLIN_SITE_AD_KEY"
[ -n "$MOD_PANGOLIN_SITE_AD_PRINCIPALS_FILE" ] && export AD_PRINCIPALS_FILE="$MOD_PANGOLIN_SITE_AD_PRINCIPALS_FILE"
[ -n "$MOD_PANGOLIN_SITE_AD_CA_CERT_PATH" ] && export AD_CA_CERT_PATH="$MOD_PANGOLIN_SITE_AD_CA_CERT_PATH"
[ -n "$MOD_PANGOLIN_SITE_METRICS_PROMETHEUS_ENABLED" ] && export NEWT_METRICS_PROMETHEUS_ENABLED="$MOD_PANGOLIN_SITE_METRICS_PROMETHEUS_ENABLED"
[ -n "$MOD_PANGOLIN_SITE_METRICS_OTLP_ENABLED" ] && export NEWT_METRICS_OTLP_ENABLED="$MOD_PANGOLIN_SITE_METRICS_OTLP_ENABLED"
[ -n "$MOD_PANGOLIN_SITE_ADMIN_ADDR" ] && export NEWT_ADMIN_ADDR="$MOD_PANGOLIN_SITE_ADMIN_ADDR"
[ -n "$MOD_PANGOLIN_SITE_REGION" ] && export NEWT_REGION="$MOD_PANGOLIN_SITE_REGION"
[ -n "$MOD_PANGOLIN_SITE_METRICS_ASYNC_BYTES" ] && export NEWT_METRICS_ASYNC_BYTES="$MOD_PANGOLIN_SITE_METRICS_ASYNC_BYTES"
[ -n "$MOD_PANGOLIN_SITE_PPROF_ENABLED" ] && export NEWT_PPROF_ENABLED="$MOD_PANGOLIN_SITE_PPROF_ENABLED"
[ -n "$MOD_PANGOLIN_SITE_TLS_CLIENT_CERT" ] && export TLS_CLIENT_CERT="$MOD_PANGOLIN_SITE_TLS_CLIENT_CERT"
[ -n "$MOD_PANGOLIN_SITE_TLS_CLIENT_KEY" ] && export TLS_CLIENT_KEY="$MOD_PANGOLIN_SITE_TLS_CLIENT_KEY"
[ -n "$MOD_PANGOLIN_SITE_TLS_CLIENT_CAS" ] && export TLS_CLIENT_CAS="$MOD_PANGOLIN_SITE_TLS_CLIENT_CAS"
[ -n "$MOD_PANGOLIN_SITE_TLS_CLIENT_CERT_PKCS12" ] && export TLS_CLIENT_CERT_PKCS12="$MOD_PANGOLIN_SITE_TLS_CLIENT_CERT_PKCS12"
[ -n "$MOD_PANGOLIN_SITE_BLUEPRINT_FILE" ] && export BLUEPRINT_FILE="$MOD_PANGOLIN_SITE_BLUEPRINT_FILE"
[ -n "$MOD_PANGOLIN_SITE_PROVISIONING_BLUEPRINT_FILE" ] && export PROVISIONING_BLUEPRINT_FILE="$MOD_PANGOLIN_SITE_PROVISIONING_BLUEPRINT_FILE"
[ -n "$MOD_PANGOLIN_SITE_NO_CLOUD" ] && export NO_CLOUD="$MOD_PANGOLIN_SITE_NO_CLOUD"
[ -n "$MOD_PANGOLIN_SITE_PROVISIONING_KEY" ] && export NEWT_PROVISIONING_KEY="$MOD_PANGOLIN_SITE_PROVISIONING_KEY"
[ -n "$MOD_PANGOLIN_SITE_NAME" ] && export NEWT_NAME="$MOD_PANGOLIN_SITE_NAME"
[ -n "$MOD_PANGOLIN_SITE_CONFIG_FILE" ] && export CONFIG_FILE="$MOD_PANGOLIN_SITE_CONFIG_FILE"
export NEWT_SYSTEM_SUBSTRATE="ADVANTECH_ROUTER_APP"
export NEWT_PID_FILE="$PIDFILE"
echo "Starting $MODNAME..."
if [ -n "$MOD_PANGOLIN_SITE_PREFER_ENDPOINT" ]; then
/opt/$MODNAME/bin/newt --prefer-endpoint "$MOD_PANGOLIN_SITE_PREFER_ENDPOINT" >> "$LOGFILE" 2>&1 &
else
/opt/$MODNAME/bin/newt >> "$LOGFILE" 2>&1 &
fi
echo $! > "$PIDFILE"
echo "Started $MODNAME (PID: $(cat "$PIDFILE"))"
exit 0
;;
stop)
echo "Stopping $MODNAME..."
if [ -f "$PIDFILE" ]; then
kill "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null
rm -f "$PIDFILE"
fi
killall newt 2>/dev/null
# Stopping newt implies disable at boot.
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "0"
echo "Stopped $MODNAME"
exit 0
;;
restart)
$0 stop
sleep 1
# Restart should remain enabled after reboot.
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "1"
$0 start
;;
status)
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then
echo "$MODNAME is running (PID: $(cat "$PIDFILE"))"
exit 0
else
echo "$MODNAME is not running"
exit 1
fi
;;
defaults)
cp /opt/$MODNAME/etc/defaults /opt/$MODNAME/etc/settings 2>/dev/null
;;
*)
echo "Usage: $0 {start|stop|restart|status|defaults}"
exit 1
esac

View File

@@ -0,0 +1,15 @@
#!/bin/sh
MODNAME=newt
# Ensure scripts are executable
chmod +x /opt/$MODNAME/etc/init
chmod +x /opt/$MODNAME/etc/install
chmod +x /opt/$MODNAME/etc/uninstall
chmod +x /opt/$MODNAME/bin/newt
chmod +x /opt/$MODNAME/www/index.cgi
# Secure the web interface with router authentication
ln -sf /etc/htpasswd /opt/$MODNAME/www/.htpasswd
exit 0

View File

@@ -0,0 +1 @@
Pangolin Site

View File

@@ -0,0 +1 @@
Tunnel client for Pangolin secure remote access.

View File

@@ -0,0 +1,9 @@
#!/bin/sh
MODNAME=newt
rm -f /opt/$MODNAME/www/.htpasswd
rm -f /tmp/newt.pid
rm -f /tmp/newt.log
exit 0

View File

@@ -0,0 +1 @@
1.12.5

View File

@@ -0,0 +1,282 @@
#!/bin/sh
MODNAME=newt
SETTINGS=/opt/$MODNAME/etc/settings
LOGFILE=/tmp/newt.log
PIDFILE=/tmp/newt.pid
# Escape special HTML characters
htmlesc() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g' \
-e 's/"/\&quot;/g'
}
# Decode URL-encoded form values (handles common chars in endpoints/ids/secrets)
urldecode() {
printf '%s' "$1" | sed \
-e 's/+/ /g' \
-e 's/%3[Aa]/:/g' -e 's/%3[aa]/:/g' \
-e 's/%2[Ff]/\//g' -e 's/%2[ff]/\//g' \
-e 's/%40/@/g' \
-e 's/%2[Ee]/./g' \
-e 's/%2[Dd]/-/g' \
-e 's/%5[Ff]/_/g' \
-e 's/%3[Dd]/=/g' \
-e 's/%3[Ff]/?/g' \
-e 's/%23/#/g' \
-e 's/%25/%/g'
}
# Extract a named field from URL-encoded POST data (awk splits on & without tr)
get_field() {
printf '%s' "$2" | awk -v f="${1}=" 'BEGIN{RS="&"} index($0,f)==1 {print substr($0,length(f)+1); exit}'
}
# Check whether newt process is alive
is_running() {
[ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null
}
ensure_settings_file() {
[ -f "$SETTINGS" ] && return
if [ -f "/opt/$MODNAME/etc/defaults" ]; then
cp "/opt/$MODNAME/etc/defaults" "$SETTINGS" 2>/dev/null
else
printf 'MOD_PANGOLIN_SITE_ENABLED=0\n' > "$SETTINGS"
fi
}
set_setting_raw() {
key="$1"
value="$2"
ensure_settings_file
awk -v k="$key" -v v="$value" '
BEGIN { done=0 }
index($0, k "=") == 1 { print k "=" v; done=1; next }
{ print }
END { if (!done) print k "=" v }
' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
}
quote_sh_value() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
# ── Read POST body ──────────────────────────────────────────────────────────
POST_DATA=""
if [ "$REQUEST_METHOD" = "POST" ] && [ -n "$CONTENT_LENGTH" ] && [ "$CONTENT_LENGTH" -gt 0 ] 2>/dev/null; then
POST_DATA=$(dd bs="$CONTENT_LENGTH" count=1 2>/dev/null)
fi
# ── Load current settings ───────────────────────────────────────────────────
MOD_PANGOLIN_SITE_ENABLED=0
MOD_PANGOLIN_SITE_ENDPOINT=""
MOD_PANGOLIN_SITE_ID=""
MOD_PANGOLIN_SITE_SECRET=""
[ -f "$SETTINGS" ] && . "$SETTINGS"
# ── Handle actions ──────────────────────────────────────────────────────────
if [ -n "$POST_DATA" ]; then
ACTION=$(get_field "action" "$POST_DATA")
case "$ACTION" in
save)
EP=$(urldecode "$(get_field "endpoint" "$POST_DATA")")
ID=$(urldecode "$(get_field "id" "$POST_DATA")")
SEC=$(urldecode "$(get_field "secret" "$POST_DATA")")
set_setting_raw "MOD_PANGOLIN_SITE_ENDPOINT" "\"$(quote_sh_value "$EP")\""
set_setting_raw "MOD_PANGOLIN_SITE_ID" "\"$(quote_sh_value "$ID")\""
set_setting_raw "MOD_PANGOLIN_SITE_SECRET" "\"$(quote_sh_value "$SEC")\""
;;
start)
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "1"
/opt/$MODNAME/etc/init start >/dev/null 2>&1
;;
stop)
/opt/$MODNAME/etc/init stop >/dev/null 2>&1
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "0"
;;
restart)
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "1"
/opt/$MODNAME/etc/init restart >/dev/null 2>&1
;;
clearlog)
printf '' > "$LOGFILE"
;;
esac
fi
# Reload settings after actions.
MOD_PANGOLIN_SITE_ENABLED=0
MOD_PANGOLIN_SITE_ENDPOINT=""
MOD_PANGOLIN_SITE_ID=""
MOD_PANGOLIN_SITE_SECRET=""
[ -f "$SETTINGS" ] && . "$SETTINGS"
# ── Status ──────────────────────────────────────────────────────────────────
if is_running; then
STATUS_TEXT="Running"
STATUS_CLASS="running"
DISABLED="disabled"
SETTINGS_HINT='<div class="notice">Stop Newt first to edit settings.</div>'
else
STATUS_TEXT="Stopped"
STATUS_CLASS="stopped"
DISABLED=""
SETTINGS_HINT=""
fi
EP_ESC=$(htmlesc "$MOD_PANGOLIN_SITE_ENDPOINT")
ID_ESC=$(htmlesc "$MOD_PANGOLIN_SITE_ID")
SEC_ESC=$(htmlesc "$MOD_PANGOLIN_SITE_SECRET")
EP_INPUT_ESC="$EP_ESC"
[ -z "$MOD_PANGOLIN_SITE_ENDPOINT" ] && EP_INPUT_ESC="https://app.pangolin.net"
# ── Log (escape HTML and dollar signs to prevent shell expansion in heredoc) ─
LOG_HTML=""
if [ -f "$LOGFILE" ]; then
LOG_HTML=$(tail -100 "$LOGFILE" 2>/dev/null | sed \
-e 's/&/\&amp;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g' \
-e 's/\$/\&#36;/g')
fi
# ── Output ──────────────────────────────────────────────────────────────────
printf 'Content-type: text/html\r\n\r\n'
# Static head — split around the conditional refresh meta tag
cat << 'HEAD_START'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
HEAD_START
# Only auto-refresh while newt is running (avoids wiping settings form mid-edit)
[ "$STATUS_CLASS" = "running" ] && printf '<meta http-equiv="refresh" content="10">\n'
cat << 'STATIC_HEAD'
<title>Pangolin Site</title>
<style>
*{box-sizing:border-box}
body{margin:0;padding:8px;border-top:3px solid #79BD28;background:#fff;color:#000}
body,table,tr,td,a,input,select,textarea,button{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:12px}
a{color:#004280;text-decoration:none}
a:hover{text-decoration:underline}
.topline{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:6px}
.app-title{background:#004280;color:#fff;font-weight:bold;padding:6px 8px;border:1px solid #00315f;border-bottom:none}
.window{background:#f4f4f4;border:2px solid #004280;padding:0}
.section{border-top:1px solid #9fb7d5}
.section:first-child{border-top:none}
.section-head{background:#c0e0ff;color:#000;font-weight:bold;padding:5px 8px;border-bottom:1px solid #9fb7d5;display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
.section-body{padding:10px 8px}
.badge{display:inline-block;padding:2px 10px;border-radius:2px;color:#fff;font-weight:bold;line-height:1.4}
.running{background:#008000}
.stopped{background:#800000}
.row{display:flex;align-items:center;gap:8px;margin:6px 0}
.row label{width:90px;font-weight:bold}
.row input{width:360px;max-width:100%;padding:2px 4px;border:1px solid #7f9db9;background:#fff;height:23px}
.btn{height:24px;padding:0 10px;border:1px solid #7f9db9;background:#efefef;color:#000;cursor:pointer}
.btn:disabled{color:#777;background:#e5e5e5}
.btn + .btn{margin-left:6px}
.btn-start{border-color:#2d7a2d;background:#dff0df}
.btn-stop{border-color:#8a2c2c;background:#f7dddd}
.btn-restart{border-color:#9a6a1f;background:#f7ecd8}
.btn-save{border-color:#2e5f92;background:#dce9f8}
.hint{color:#808080;font-weight:normal}
.notice{margin:0 0 10px 0;color:#ff0000;font-style:italic}
.log{background:#fff;border:1px solid #7f9db9;color:#000;font-family:monospace;font-size:12px;line-height:1.35;padding:6px;height:300px;overflow-y:auto;white-space:pre-wrap;word-break:break-word}
</style>
</head>
<body>
<div class="topline">
<a href="/">&laquo; Back to Router</a>
</div>
<div class="app-title">Router Apps - Pangolin Site</div>
<div class="window">
STATIC_HEAD
# Status card (double-quoted heredoc — variables expand)
cat << STATUS_CARD
<div class="section">
<div class="section-head">Status</div>
<div class="section-body">
<div><span class="badge ${STATUS_CLASS}">${STATUS_TEXT}</span></div>
<div style="margin-top:8px">
<form method="post" style="display:inline">
<input type="hidden" name="action" value="start">
<button class="btn btn-start" type="submit">Start</button>
</form>
<form method="post" style="display:inline">
<input type="hidden" name="action" value="stop">
<button class="btn btn-stop" type="submit">Stop</button>
</form>
<form method="post" style="display:inline">
<input type="hidden" name="action" value="restart">
<button class="btn btn-restart" type="submit">Restart</button>
</form>
</div>
</div>
</div>
STATUS_CARD
# Settings card
cat << SETTINGS_CARD
<div class="section">
<div class="section-head">Settings</div>
<div class="section-body">
${SETTINGS_HINT}<form method="post">
<input type="hidden" name="action" value="save">
<div class="row">
<label>Endpoint</label>
<input type="text" name="endpoint" value="${EP_INPUT_ESC}" ${DISABLED}>
</div>
<div class="row">
<label>ID</label>
<input type="text" name="id" value="${ID_ESC}" ${DISABLED}>
</div>
<div class="row">
<label>Secret</label>
<input type="password" name="secret" value="${SEC_ESC}" ${DISABLED}>
</div>
<div style="margin-top:12px">
<button class="btn btn-save" type="submit" ${DISABLED}>Save</button>
</div>
</form>
</div>
</div>
SETTINGS_CARD
# Log card header
cat << 'LOG_HEADER'
<div class="section">
<div class="section-head">
<span>Log <span class="hint">(last 100 lines, auto-refreshes every 10s while running)</span></span>
<span style="display:inline-flex;gap:6px">
<form method="post" style="margin:0">
<input type="hidden" name="action" value="clearlog">
<button class="btn" type="submit">Clear Log</button>
</form>
<form method="get" style="margin:0">
<button class="btn" type="submit">Refresh</button>
</form>
</span>
</div>
<div class="section-body">
<div class="log">
LOG_HEADER
# Log content — printed with printf to prevent any shell expansion
printf '%s' "$LOG_HTML"
# Close tags (static)
cat << 'STATIC_FOOTER'
</div>
</div>
</div>
</body>
</html>
STATIC_FOOTER

View File

View File

View File

@@ -18,7 +18,6 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"golang.zx2c4.com/wireguard/tun/netstack"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
)
const (
@@ -54,15 +53,60 @@ type Target struct {
Port int
}
// managedListener wraps a net.Listener so an intentional Close() can be
// detected reliably by the accept loop. gVisor's netstack (unlike the
// stdlib) does not return net.ErrClosed from Accept() after Close() - it
// returns a generic "endpoint is in invalid state" error - so relying on
// errors.Is(err, net.ErrClosed) leaves the accept loop spinning forever.
type managedListener struct {
net.Listener
closed chan struct{}
}
func newManagedListener(l net.Listener) *managedListener {
return &managedListener{Listener: l, closed: make(chan struct{})}
}
func (m *managedListener) Close() error {
err := m.Listener.Close()
select {
case <-m.closed:
default:
close(m.closed)
}
return err
}
// managedPacketConn is the net.PacketConn equivalent of managedListener.
type managedPacketConn struct {
net.PacketConn
closed chan struct{}
}
func newManagedPacketConn(c net.PacketConn) *managedPacketConn {
return &managedPacketConn{PacketConn: c, closed: make(chan struct{})}
}
func (m *managedPacketConn) Close() error {
err := m.PacketConn.Close()
select {
case <-m.closed:
default:
close(m.closed)
}
return err
}
// ProxyManager handles the creation and management of proxy connections
type ProxyManager struct {
tnet *netstack.Net
tcpTargets map[string]map[int]string // map[listenIP]map[port]targetAddress
udpTargets map[string]map[int]string
listeners []*gonet.TCPListener
udpConns []*gonet.UDPConn
running bool
mutex sync.RWMutex
tnet *netstack.Net
tcpTargets map[string]map[int]string // map[listenIP]map[port]targetAddress
udpTargets map[string]map[int]string
listeners []net.Listener
udpConns []net.PacketConn
running bool
mutex sync.RWMutex
nativeListenIP string // when non-empty, use native OS listeners instead of netstack
// telemetry (multi-tunnel)
currentTunnelID string
@@ -70,6 +114,9 @@ type ProxyManager struct {
asyncBytes bool
flushStop chan struct{}
udpIdleTimeout time.Duration
// connection blocking
blocked atomic.Bool
}
// tunnelEntry holds per-tunnel attributes and (optional) async counters.
@@ -146,15 +193,29 @@ func classifyProxyError(err error) string {
}
}
// NewProxyManager creates a new proxy manager instance
// NewProxyManager creates a new proxy manager instance backed by a netstack.
func NewProxyManager(tnet *netstack.Net) *ProxyManager {
return &ProxyManager{
tnet: tnet,
tcpTargets: make(map[string]map[int]string),
udpTargets: make(map[string]map[int]string),
listeners: make([]*gonet.TCPListener, 0),
udpConns: make([]*gonet.UDPConn, 0),
tunnels: make(map[string]*tunnelEntry),
tnet: tnet,
tcpTargets: make(map[string]map[int]string),
udpTargets: make(map[string]map[int]string),
listeners: make([]net.Listener, 0),
udpConns: make([]net.PacketConn, 0),
tunnels: make(map[string]*tunnelEntry),
udpIdleTimeout: defaultUDPIdleTimeout,
}
}
// NewProxyManagerNative creates a proxy manager that binds listeners directly
// to the host network stack on the given IP address.
func NewProxyManagerNative(listenIP string) *ProxyManager {
return &ProxyManager{
nativeListenIP: listenIP,
tcpTargets: make(map[string]map[int]string),
udpTargets: make(map[string]map[int]string),
listeners: make([]net.Listener, 0),
udpConns: make([]net.PacketConn, 0),
tunnels: make(map[string]*tunnelEntry),
udpIdleTimeout: defaultUDPIdleTimeout,
}
}
@@ -226,13 +287,14 @@ func (pm *ProxyManager) ClearTunnelID() {
pm.currentTunnelID = ""
}
// init function without tnet
// NewProxyManagerWithoutTNet creates a proxy manager with no backing network.
// Call SetTNet before starting.
func NewProxyManagerWithoutTNet() *ProxyManager {
return &ProxyManager{
tcpTargets: make(map[string]map[int]string),
udpTargets: make(map[string]map[int]string),
listeners: make([]*gonet.TCPListener, 0),
udpConns: make([]*gonet.UDPConn, 0),
tcpTargets: make(map[string]map[int]string),
udpTargets: make(map[string]map[int]string),
listeners: make([]net.Listener, 0),
udpConns: make([]net.PacketConn, 0),
udpIdleTimeout: defaultUDPIdleTimeout,
}
}
@@ -244,6 +306,18 @@ func (pm *ProxyManager) SetTNet(tnet *netstack.Net) {
pm.tnet = tnet
}
// SetBlocked enables or disables connection blocking.
// When enabled, all new incoming TCP connections are immediately closed
// and all incoming UDP packets are silently dropped.
func (pm *ProxyManager) SetBlocked(v bool) {
pm.blocked.Store(v)
if v {
logger.Debug("ProxyManager: connection blocking enabled, new connections will be dropped")
} else {
logger.Debug("ProxyManager: connection blocking disabled, accepting connections")
}
}
// AddTarget adds as new target for proxying
func (pm *ProxyManager) AddTarget(proto, listenIP string, port int, targetAddr string) error {
pm.mutex.Lock()
@@ -481,23 +555,46 @@ func (pm *ProxyManager) Stop() error {
func (pm *ProxyManager) startTarget(proto, listenIP string, port int, targetAddr string) error {
switch proto {
case "tcp":
listener, err := pm.tnet.ListenTCP(&net.TCPAddr{Port: port})
if err != nil {
return fmt.Errorf("failed to create TCP listener: %v", err)
var listener net.Listener
if pm.tnet != nil {
l, err := pm.tnet.ListenTCP(&net.TCPAddr{Port: port})
if err != nil {
return fmt.Errorf("failed to create TCP listener: %v", err)
}
listener = l
} else if pm.nativeListenIP != "" {
l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP(pm.nativeListenIP), Port: port})
if err != nil {
return fmt.Errorf("failed to create native TCP listener on %s:%d: %v", pm.nativeListenIP, port, err)
}
listener = l
} else {
return fmt.Errorf("proxy manager has no tnet or native IP configured")
}
pm.listeners = append(pm.listeners, listener)
go pm.handleTCPProxy(listener, targetAddr)
ml := newManagedListener(listener)
pm.listeners = append(pm.listeners, ml)
go pm.handleTCPProxy(ml, targetAddr)
case "udp":
addr := &net.UDPAddr{Port: port}
conn, err := pm.tnet.ListenUDP(addr)
if err != nil {
return fmt.Errorf("failed to create UDP listener: %v", err)
var conn net.PacketConn
if pm.tnet != nil {
c, err := pm.tnet.ListenUDP(&net.UDPAddr{Port: port})
if err != nil {
return fmt.Errorf("failed to create UDP listener: %v", err)
}
conn = c
} else if pm.nativeListenIP != "" {
c, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP(pm.nativeListenIP), Port: port})
if err != nil {
return fmt.Errorf("failed to create native UDP listener on %s:%d: %v", pm.nativeListenIP, port, err)
}
conn = c
} else {
return fmt.Errorf("proxy manager has no tnet or native IP configured")
}
pm.udpConns = append(pm.udpConns, conn)
go pm.handleUDPProxy(conn, targetAddr)
mc := newManagedPacketConn(conn)
pm.udpConns = append(pm.udpConns, mc)
go pm.handleUDPProxy(mc, targetAddr)
default:
return fmt.Errorf(errUnsupportedProtoFmt, proto)
@@ -517,11 +614,17 @@ func (pm *ProxyManager) getEntry(id string) *tunnelEntry {
return e
}
func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string) {
func (pm *ProxyManager) handleTCPProxy(listener *managedListener, targetAddr string) {
for {
conn, err := listener.Accept()
if err != nil {
telemetry.IncProxyAccept(context.Background(), pm.currentTunnelID, "tcp", "failure", classifyProxyError(err))
select {
case <-listener.closed:
logger.Info("TCP listener closed, stopping proxy handler for %v", listener.Addr())
return
default:
}
if !pm.running {
return
}
@@ -535,6 +638,12 @@ func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string)
}
tunnelID := pm.currentTunnelID
// Drop connection if blocking is enabled
if pm.blocked.Load() {
conn.Close()
logger.Debug("TCP proxy: connection dropped (blocking enabled)")
continue
}
telemetry.IncProxyAccept(context.Background(), tunnelID, "tcp", "success", "")
telemetry.IncProxyConnectionEvent(context.Background(), tunnelID, "tcp", telemetry.ProxyConnectionOpened)
if tunnelID != "" {
@@ -590,7 +699,7 @@ func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string)
}
}
func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
func (pm *ProxyManager) handleUDPProxy(conn *managedPacketConn, targetAddr string) {
bufPtr := getUDPBuffer()
defer putUDPBuffer(bufPtr)
buffer := *bufPtr
@@ -600,37 +709,50 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
for {
n, remoteAddr, err := conn.ReadFrom(buffer)
if err != nil {
if !pm.running {
// Clean up all connections when stopping
closeAllClients := func() {
clientsMutex.Lock()
for _, targetConn := range clientConns {
targetConn.Close()
}
clientConns = nil
clientsMutex.Unlock()
}
// Check for intentional closure first: netstack does not
// surface net.ErrClosed/io.EOF from ReadFrom() after Close(),
// so this channel is the only reliable signal.
select {
case <-conn.closed:
logger.Info("UDP connection closed, stopping proxy handler")
closeAllClients()
return
default:
}
if !pm.running {
closeAllClients()
return
}
// Check for connection closed conditions
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
logger.Info("UDP connection closed, stopping proxy handler")
// Clean up existing client connections
clientsMutex.Lock()
for _, targetConn := range clientConns {
targetConn.Close()
}
clientConns = nil
clientsMutex.Unlock()
closeAllClients()
return
}
logger.Error("Error reading UDP packet: %v", err)
// Avoid a tight busy-loop if this error persists.
time.Sleep(100 * time.Millisecond)
continue
}
clientKey := remoteAddr.String()
// Drop packet if blocking is enabled
if pm.blocked.Load() {
logger.Debug("UDP proxy: packet dropped (blocking enabled)")
continue
}
// bytes from client -> target (direction=in)
if pm.currentTunnelID != "" && n > 0 {
if pm.asyncBytes {

87
proxy/manager_test.go Normal file
View File

@@ -0,0 +1,87 @@
package proxy
import (
"context"
"net/netip"
"os"
"strings"
"testing"
"time"
"github.com/fosrl/newt/internal/telemetry"
"github.com/fosrl/newt/logger"
"golang.zx2c4.com/wireguard/tun/netstack"
)
// TestRemoveTargetStopsAcceptLoop verifies that removing a TCP target on a
// netstack-backed ProxyManager causes the accept loop goroutine to actually
// stop retrying, instead of spinning forever logging
// "Error accepting TCP connection: ... endpoint is in invalid state".
func TestRemoveTargetStopsAcceptLoop(t *testing.T) {
if _, err := telemetry.Init(context.Background(), telemetry.Config{ServiceName: "test"}); err != nil {
t.Fatalf("telemetry.Init: %v", err)
}
logFile, err := os.CreateTemp(t.TempDir(), "newt-proxy-test-*.log")
if err != nil {
t.Fatalf("CreateTemp: %v", err)
}
defer logFile.Close()
logger.SetOutput(logFile)
defer logger.SetOutput(os.Stdout)
_, tnet, err := netstack.CreateNetTUN(
[]netip.Addr{netip.MustParseAddr("100.64.0.1")},
[]netip.Addr{},
1420,
)
if err != nil {
t.Fatalf("CreateNetTUN: %v", err)
}
pm := NewProxyManager(tnet)
const listenIP = "100.64.0.1"
const port = 53405
if err := pm.AddTarget("tcp", listenIP, port, "127.0.0.1:9999"); err != nil {
t.Fatalf("AddTarget: %v", err)
}
if err := pm.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
if err := pm.RemoveTarget("tcp", listenIP, port); err != nil {
t.Fatalf("RemoveTarget: %v", err)
}
// If the bug is present, the accept loop spins every 100ms logging an
// error forever. Sample the log twice, 400ms apart; a healthy accept
// loop logs the error/close message once (or zero times) and then goes
// silent, while the buggy loop keeps appending.
time.Sleep(200 * time.Millisecond)
countAt1 := countAcceptErrors(t, logFile.Name())
time.Sleep(400 * time.Millisecond)
countAt2 := countAcceptErrors(t, logFile.Name())
t.Logf("accept-error-ish log lines: at 200ms=%d, at 600ms=%d", countAt1, countAt2)
if countAt2 > countAt1 {
t.Fatalf("accept loop kept logging after RemoveTarget (200ms=%d, 600ms=%d) -- it is spinning forever on the closed netstack listener instead of exiting", countAt1, countAt2)
}
}
func countAcceptErrors(t *testing.T, path string) int {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
count := 0
for _, line := range strings.Split(string(data), "\n") {
if strings.Contains(line, "Error accepting TCP connection") {
count++
}
}
return count
}

21
reexec_unix.go Normal file
View File

@@ -0,0 +1,21 @@
//go:build !windows
package main
import (
"fmt"
"os"
"syscall"
)
// reexec replaces the current process image with a fresh copy of itself,
// preserving all arguments and environment variables. On success it never
// returns (execve replaces the process in-place). On failure it returns an
// error describing why the exec could not be performed.
func reexec() error {
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
return syscall.Exec(exe, os.Args, os.Environ())
}

40
reexec_windows.go Normal file
View File

@@ -0,0 +1,40 @@
//go:build windows
package main
import (
"fmt"
"os"
"os/exec"
)
// reexec restarts newt. On Windows, execve is not available, so outside of
// service mode we start a child process and exit (on success this never
// returns since os.Exit terminates the current process).
//
// When running as a Windows service, spawning a detached child would orphan
// it from the Service Control Manager (the SCM only tracks processes it
// started itself), and os.Exit would end the process without ever reporting
// a clean status transition, making the service look like it crashed. So in
// that case we instead delegate to requestServiceRestart, which asks the SCM
// itself to relaunch the service.
func reexec() error {
if isWindowsService() {
return requestServiceRestart()
}
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
cmd := exec.Command(exe, os.Args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Env = os.Environ()
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start new process: %w", err)
}
os.Exit(0)
return nil // unreachable
}

View File

@@ -7,11 +7,11 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/signal"
"path/filepath"
"strings"
"sync/atomic"
"syscall"
"time"
@@ -86,10 +86,44 @@ type newtService struct {
args []string
}
// activeService points at the newtService currently running under the SCM,
// so that requestServiceRestart (called from deep inside the app via
// cfg.OnRestart) can signal it without threading a reference through the
// newt package. restartRequested tells Execute's completion path whether the
// tunnel shut down because of a real Stop/Shutdown control request (report
// success) or because the app asked to be restarted (report a non-zero exit
// code so the SCM's configured recovery action relaunches the service as a
// fresh, SCM-tracked process).
var (
activeService *newtService
restartRequested atomic.Bool
)
// requestServiceRestart asks the Windows Service Control Manager to relaunch
// the service. Unlike reexec on other platforms, a Windows service cannot
// replace its own process image or spawn a detached child that the SCM would
// recognize - the SCM only respawns processes it started itself. So instead
// we gracefully stop the current run (same as a real Stop control request)
// and report a failure exit code on the way out, which - combined with the
// recovery actions configured in configureRecoveryActions - causes the SCM
// to start a brand new, properly tracked instance of the service.
func requestServiceRestart() error {
if activeService == nil || activeService.stop == nil {
return fmt.Errorf("newt service is not running")
}
restartRequested.Store(true)
activeService.elog.Info(1, "Restart requested; stopping tunnel and asking the service control manager to relaunch")
activeService.stop()
return nil
}
func (s *newtService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
changes <- svc.Status{State: svc.StartPending}
activeService = s
restartRequested.Store(false)
s.elog.Info(1, fmt.Sprintf("Service Execute called with args: %v", args))
// Load saved service arguments
@@ -170,8 +204,12 @@ func (s *newtService) Execute(args []string, r <-chan svc.ChangeRequest, changes
s.elog.Error(1, fmt.Sprintf("Unexpected control request #%d", c))
}
case <-newtDone:
s.elog.Info(1, "Main newt logic completed, stopping service")
changes <- svc.Status{State: svc.StopPending}
if restartRequested.Load() {
s.elog.Info(1, "Main newt logic stopped for restart, reporting failure exit code so the SCM relaunches the service")
return false, 1
}
s.elog.Info(1, "Main newt logic completed, stopping service")
return false, 0
}
}
@@ -208,6 +246,52 @@ func (s *newtService) runNewt() {
}
}
// configureRecoveryActions tells the Service Control Manager to relaunch the
// service automatically when it stops with a non-zero exit code. This is
// what makes requestServiceRestart's approach (report failure, let the SCM
// respawn us) actually result in a restart.
func configureRecoveryActions(s *mgr.Service) error {
actions := []mgr.RecoveryAction{
{Type: mgr.ServiceRestart, Delay: 5 * time.Second},
{Type: mgr.ServiceRestart, Delay: 5 * time.Second},
{Type: mgr.ServiceRestart, Delay: 30 * time.Second},
}
if err := s.SetRecoveryActions(actions, 24*60*60); err != nil {
return fmt.Errorf("failed to set recovery actions: %v", err)
}
// By default the SCM only runs recovery actions when a service crashes.
// A restart-via-OnRestart is a controlled stop that merely reports a
// non-zero exit code, so this flag must be enabled for it to count.
if err := s.SetRecoveryActionsOnNonCrashFailures(true); err != nil {
return fmt.Errorf("failed to enable recovery actions on non-crash failures: %v", err)
}
return nil
}
// ensureRecoveryActionsConfigured is a best-effort check run at service
// startup so that services installed by older versions of newt (before
// recovery actions existed) get them configured on their next start, without
// requiring a reinstall.
func ensureRecoveryActionsConfigured(name string, elog debug.Log) {
m, err := mgr.Connect()
if err != nil {
elog.Warning(1, fmt.Sprintf("Could not connect to service manager to verify recovery actions: %v", err))
return
}
defer m.Disconnect()
s, err := m.OpenService(name)
if err != nil {
elog.Warning(1, fmt.Sprintf("Could not open service to verify recovery actions: %v", err))
return
}
defer s.Close()
if err := configureRecoveryActions(s); err != nil {
elog.Warning(1, fmt.Sprintf("Could not configure recovery actions: %v", err))
}
}
func runService(name string, isDebug bool, args []string) {
var err error
var elog debug.Log
@@ -225,6 +309,7 @@ func runService(name string, isDebug bool, args []string) {
defer elog.Close()
elog.Info(1, fmt.Sprintf("Starting %s service", name))
ensureRecoveryActionsConfigured(name, elog)
run := svc.Run
if isDebug {
run = debug.Run
@@ -284,6 +369,13 @@ func installService() error {
return fmt.Errorf("failed to install event log: %v", err)
}
if err := configureRecoveryActions(s); err != nil {
// Non-fatal: the service is installed and usable, it just won't
// auto-relaunch on an app-initiated restart until this is retried
// (ensureRecoveryActionsConfigured does so on every service start).
fmt.Printf("Warning: failed to configure service recovery actions: %v\n", err)
}
return nil
}
@@ -649,7 +741,7 @@ func setupWindowsEventLog() {
// Set the custom logger output
logger.GetLogger().SetOutput(file)
log.Printf("Newt service logging initialized - log file: %s", logFile)
logger.Debug("Newt service logging initialized - log file: %s", logFile)
}
// handleServiceCommand checks for service management commands and returns true if handled

48
updates/advantech.go Normal file
View File

@@ -0,0 +1,48 @@
//go:build !windows
package updates
import (
"fmt"
"os"
"path/filepath"
"github.com/fosrl/newt/logger"
)
const advantechVersionFile = "/opt/newt/etc/version"
// postUpdateAdvantech performs Advantech Router App-specific post-update steps:
// - Writes the new version string to /opt/newt/etc/version so that the
// router firmware's package management reflects the installed version.
// - Updates the PID file at pidFile (if non-empty) with the current process
// PID, in case the re-exec lands with a new PID on platforms where
// syscall.Exec behaviour differs.
func postUpdateAdvantech(newVersion, pidFile string) error {
// --- Write version file ---
versionDir := filepath.Dir(advantechVersionFile)
if err := os.MkdirAll(versionDir, 0755); err != nil {
return fmt.Errorf("advantech: failed to create version directory %s: %w", versionDir, err)
}
if err := os.WriteFile(advantechVersionFile, []byte(newVersion+"\n"), 0644); err != nil {
return fmt.Errorf("advantech: failed to write version file %s: %w", advantechVersionFile, err)
}
logger.Debug("postUpdateAdvantech: wrote version %s to %s", newVersion, advantechVersionFile)
// --- Update PID file ---
// syscall.Exec replaces the process image in-place so the PID is preserved.
// We update the PID file here anyway so that any race between the old and
// new binary is covered (e.g. on platforms that fork before exec).
if pidFile != "" {
pid := fmt.Sprintf("%d\n", os.Getpid())
if err := os.WriteFile(pidFile, []byte(pid), 0644); err != nil {
// Non-fatal: log and continue so the update still proceeds.
logger.Debug("postUpdateAdvantech: warning: failed to update PID file %s: %v", pidFile, err)
} else {
logger.Debug("postUpdateAdvantech: updated PID file %s with PID %d", pidFile, os.Getpid())
}
}
return nil
}

View File

@@ -0,0 +1,8 @@
//go:build windows
package updates
// postUpdateAdvantech is not supported on Windows.
func postUpdateAdvantech(newVersion, pidFile string) error {
return nil
}

14
updates/reexec_unix.go Normal file
View File

@@ -0,0 +1,14 @@
//go:build !windows
package updates
import (
"os"
"syscall"
)
// reexec replaces the current process image with the binary at exePath,
// forwarding all original arguments and environment variables.
func reexec(exePath string) error {
return syscall.Exec(exePath, os.Args, os.Environ())
}

28
updates/reexec_windows.go Normal file
View File

@@ -0,0 +1,28 @@
//go:build windows
package updates
import (
"fmt"
"os"
"os/exec"
)
// reexec on Windows cannot use syscall.Exec (there is no exec syscall that
// replaces the process image). Instead we start a new process and exit the
// current one.
func reexec(exePath string) error {
cmd := exec.Command(exePath, os.Args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start updated binary: %w", err)
}
// Exit the current process so the new binary takes over.
os.Exit(0)
return nil // unreachable
}

300
updates/selfupdate.go Normal file
View File

@@ -0,0 +1,300 @@
package updates
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/fosrl/newt/logger"
)
// SelfUpdateConfig holds the configuration required to perform a self-update.
type SelfUpdateConfig struct {
// Endpoint is the base URL of the pangolin server (e.g. "https://app.pangolin.net")
Endpoint string
// NewtID is the newt client identifier used for authentication.
NewtID string
// Secret is the newt client secret used for authentication.
Secret string
// CurrentVersion is the version of the currently running binary.
CurrentVersion string
// Platform is the OS+arch string embedded at build time via ldflags
// (e.g. "linux_amd64", "darwin_arm64"). When non-empty it is used
// directly; when empty the value is derived from runtime.GOOS/GOARCH.
Platform string
// TLSConfig is an optional TLS configuration for the HTTP client (may be nil).
TLSConfig *tls.Config
}
// versionResponse mirrors the JSON returned by POST /api/v1/auth/newt/version
type versionResponse struct {
Data struct {
LatestVersion string `json:"latestVersion"`
CurrentIsLatest bool `json:"currentIsLatest"`
DownloadUrl string `json:"downloadUrl"`
Sha256 string `json:"sha256"`
} `json:"data"`
Success bool `json:"success"`
Message string `json:"message"`
}
// ErrAutoUpdateUnsupportedInOfficialContainer indicates auto-update is not
// available when running inside official Fossorial container images.
var ErrAutoUpdateUnsupportedInOfficialContainer = errors.New("auto-update unsupported in official Fossorial container images")
// isOfficialContainer returns true when the process is running inside an
// official Fossorial-built container image. The image sets
// NEWT_SYSTEM_SUBSTRATE="CONTAINER" at build time; users running newt in their own
// containers (or bare-metal) will not have this variable set.
func isOfficialContainer() bool {
return os.Getenv("NEWT_SYSTEM_SUBSTRATE") == "CONTAINER"
}
// platform returns the OS+arch string used in the newt release binary names,
// e.g. "linux_amd64", "darwin_arm64", "windows_amd64".
// It is used as a fallback when no platform was embedded at build time.
func platform() string {
goarch := runtime.GOARCH
goos := runtime.GOOS
// Map Go arch names to the names used by newt releases
archMap := map[string]string{
"amd64": "amd64",
"arm64": "arm64",
"arm": "arm32",
"riscv64": "riscv64",
}
arch, ok := archMap[goarch]
if !ok {
arch = goarch
}
return fmt.Sprintf("%s_%s", goos, arch)
}
// verifySHA256 checks that the file at path has the expected SHA-256 hex digest.
func verifySHA256(path, expected string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to open file for hashing: %w", err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return fmt.Errorf("failed to hash file: %w", err)
}
got := hex.EncodeToString(h.Sum(nil))
if !strings.EqualFold(got, expected) {
return fmt.Errorf("sha256 mismatch: expected %s, got %s", expected, got)
}
return nil
}
// CheckAndSelfUpdate contacts the pangolin server, checks whether a newer
// version of newt is available, downloads it if so, replaces the running
// binary on disk, and re-executes from the new binary.
//
// It returns an error when the check or update fails. On a successful update
// the function does not return the process is replaced by the new binary via
// syscall.Exec.
func CheckAndSelfUpdate(cfg SelfUpdateConfig) error {
logger.Debug("checkAndSelfUpdate: starting update check (currentVersion=%s)", cfg.CurrentVersion)
if isOfficialContainer() {
logger.Debug("checkAndSelfUpdate: running inside official container, skipping auto-update")
return fmt.Errorf("%w; pull a new image tag instead", ErrAutoUpdateUnsupportedInOfficialContainer)
}
if cfg.CurrentVersion == "version_replaceme" {
logger.Debug("checkAndSelfUpdate: development build detected, skipping auto-update")
return fmt.Errorf("cannot auto-update a development build (version_replaceme)")
}
baseEndpoint := strings.TrimRight(cfg.Endpoint, "/")
// Build the HTTP client.
httpClient := &http.Client{Timeout: 30 * time.Second}
if cfg.TLSConfig != nil {
httpClient.Transport = &http.Transport{TLSClientConfig: cfg.TLSConfig}
}
// Check the current binary path before we do anything else so we can fail
// fast if the executable path cannot be determined.
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to determine current executable path: %w", err)
}
exePath, err = filepath.EvalSymlinks(exePath)
if err != nil {
return fmt.Errorf("failed to resolve symlinks for executable path: %w", err)
}
logger.Debug("checkAndSelfUpdate: current executable path: %s", exePath)
// --- Step 1: Ask the server for the latest version ---
plat := cfg.Platform
if plat == "" {
plat = platform()
}
logger.Debug("checkAndSelfUpdate: querying server for latest version (platform=%s, endpoint=%s)", plat, baseEndpoint)
reqBody, err := json.Marshal(map[string]string{
"newtId": cfg.NewtID,
"secret": cfg.Secret,
"platform": plat,
})
if err != nil {
return fmt.Errorf("failed to marshal version request: %w", err)
}
versionURL, err := url.JoinPath(baseEndpoint, "/api/v1/auth/newt/version")
if err != nil {
return fmt.Errorf("failed to build version URL: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", versionURL, bytes.NewBuffer(reqBody))
if err != nil {
return fmt.Errorf("failed to create version request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to request version info: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent { // updates are disabled
logger.Debug("checkAndSelfUpdate: server indicated updates are disabled (204 No Content)")
return nil
}
if resp.StatusCode == http.StatusNotFound { // older server without version endpoint
logger.Debug("checkAndSelfUpdate: server does not support version endpoint (404 Not Found), skipping")
return nil
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body))
}
var verResp versionResponse
if err := json.NewDecoder(resp.Body).Decode(&verResp); err != nil {
return fmt.Errorf("failed to parse version response: %w", err)
}
if !verResp.Success {
return fmt.Errorf("server error: %s", verResp.Message)
}
if verResp.Data.CurrentIsLatest {
logger.Debug("checkAndSelfUpdate: already up to date (%s)", cfg.CurrentVersion)
return nil
}
logger.Debug("checkAndSelfUpdate: update available %s → %s", cfg.CurrentVersion, verResp.Data.LatestVersion)
// --- Pre-download: verify we can write to the binary's directory ---
// Do this before downloading so a permission failure doesn't waste bandwidth.
exeDir := filepath.Dir(exePath)
logger.Debug("checkAndSelfUpdate: verifying write access to %s", exeDir)
writeTestFile, err := os.CreateTemp(exeDir, ".newt-write-test-*")
if err != nil {
return fmt.Errorf("cannot write to %s (you may need to run as root or with elevated permissions): %w", exeDir, err)
}
writeTestFile.Close()
_ = os.Remove(writeTestFile.Name())
// --- Step 2: Download the new binary ---
logger.Debug("checkAndSelfUpdate: beginning download of new binary")
dlCtx, dlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer dlCancel()
dlReq, err := http.NewRequestWithContext(dlCtx, "GET", verResp.Data.DownloadUrl, nil)
if err != nil {
return fmt.Errorf("failed to create download request: %w", err)
}
dlResp, err := httpClient.Do(dlReq)
if err != nil {
return fmt.Errorf("failed to download new binary: %w", err)
}
defer dlResp.Body.Close()
if dlResp.StatusCode != http.StatusOK {
return fmt.Errorf("download failed with status %d", dlResp.StatusCode)
}
// Write to a temp file in the same directory as the current binary so that
// an atomic rename works even across filesystem boundaries.
tmpFile, err := os.CreateTemp(exeDir, ".newt-update-*")
tmpPath := tmpFile.Name()
// Ensure the temp file is cleaned up on any error path.
defer func() {
_ = os.Remove(tmpPath)
}()
if _, err := io.Copy(tmpFile, dlResp.Body); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("failed to write downloaded binary: %w", err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp file: %w", err)
}
// --- Verify SHA256 checksum if the server provided one ---
if verResp.Data.Sha256 != "" {
logger.Debug("checkAndSelfUpdate: verifying SHA256 checksum")
if err := verifySHA256(tmpPath, verResp.Data.Sha256); err != nil {
return fmt.Errorf("binary integrity check failed: %w", err)
}
logger.Debug("checkAndSelfUpdate: SHA256 checksum verified")
} else {
logger.Debug("checkAndSelfUpdate: no SHA256 checksum provided by server, skipping verification")
}
// Make the new binary executable.
if err := os.Chmod(tmpPath, 0755); err != nil {
return fmt.Errorf("failed to set executable permission: %w", err)
}
// --- Step 3: Replace the running binary ---
// On Unix an atomic rename works even while the file is running.
logger.Debug("checkAndSelfUpdate: replacing binary at %s", exePath)
if err := os.Rename(tmpPath, exePath); err != nil {
return fmt.Errorf("failed to replace binary (you may need to run as root): %w", err)
}
systemSubstrate := os.Getenv("NEWT_SYSTEM_SUBSTRATE")
logger.Debug("checkAndSelfUpdate: system substrate: %s", systemSubstrate)
if systemSubstrate == "ADVANTECH_ROUTER_APP" {
pidFile := os.Getenv("NEWT_PID_FILE")
if err := postUpdateAdvantech(verResp.Data.LatestVersion, pidFile); err != nil {
logger.Debug("checkAndSelfUpdate: advantech post-update steps failed: %v", err)
}
}
// --- Step 4: Re-exec ---
logger.Debug("checkAndSelfUpdate: re-executing new binary")
return reexec(exePath)
}

View File

@@ -9,11 +9,19 @@ import (
"time"
)
// GitHubRelease represents the GitHub API response for a release
type GitHubRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
HTMLURL string `json:"html_url"`
// VersionsAPIEntry represents one component's version payload.
type VersionsAPIEntry struct {
LatestVersion string `json:"latestVersion"`
ReleaseNotes string `json:"releaseNotes"`
}
// VersionsAPIResponse represents the versions API response.
type VersionsAPIResponse struct {
Data map[string]VersionsAPIEntry `json:"data"`
Success bool `json:"success"`
Error bool `json:"error"`
Message string `json:"message"`
Status int `json:"status"`
}
// Version represents a semantic version
@@ -75,14 +83,15 @@ func (v Version) String() string {
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
}
// CheckForUpdate checks GitHub for a newer version and prints an update banner if found
// CheckForUpdate checks the Fossorial versions API for a newer version and prints an update banner if found.
func CheckForUpdate(owner, repo, currentVersion string) error {
if currentVersion == "version_replaceme" {
return nil
}
_ = owner
// GitHub API URL for latest release
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
// Versions API URL
url := "https://api.fossorial.io/api/v1/versions"
// Create HTTP client with timeout
client := &http.Client{
@@ -97,13 +106,18 @@ func CheckForUpdate(owner, repo, currentVersion string) error {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("GitHub API returned status: %d", resp.StatusCode)
return fmt.Errorf("versions API returned status: %d", resp.StatusCode)
}
// Parse the JSON response
var release GitHubRelease
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return fmt.Errorf("failed to parse release info: %w", err)
// Parse the JSON response.
var versionsResp VersionsAPIResponse
if err := json.NewDecoder(resp.Body).Decode(&versionsResp); err != nil {
return fmt.Errorf("failed to parse versions response: %w", err)
}
componentVersion, ok := versionsResp.Data[repo]
if !ok {
return fmt.Errorf("component %q not found in versions API response", repo)
}
// Parse current and latest versions
@@ -112,14 +126,18 @@ func CheckForUpdate(owner, repo, currentVersion string) error {
return fmt.Errorf("invalid current version: %w", err)
}
latestVer, err := parseVersion(release.TagName)
latestVer, err := parseVersion(componentVersion.LatestVersion)
if err != nil {
return fmt.Errorf("invalid latest version: %w", err)
}
// Check if update is available
if currentVer.isNewer(latestVer) {
printUpdateBanner(currentVer.String(), latestVer.String(), "curl -fsSL https://static.pangolin.net/get-newt.sh | bash")
releaseNotes := componentVersion.ReleaseNotes
if releaseNotes == "" {
releaseNotes = "curl -fsSL https://static.pangolin.net/get-newt.sh | bash"
}
printUpdateBanner(currentVer.String(), latestVer.String(), releaseNotes)
}
return nil

View File

@@ -14,6 +14,7 @@ import (
"os"
"strings"
"sync"
"sync/atomic"
"time"
"software.sslmate.com/src/go-pkcs12"
@@ -38,6 +39,7 @@ type Client struct {
isConnected bool
reconnectMux sync.RWMutex
pingInterval time.Duration
pongWait time.Duration // read deadline window; if no pong/message arrives within it, the connection is considered dead
onConnect func() error
onTokenUpdate func(token string)
writeMux sync.Mutex
@@ -50,10 +52,11 @@ type Client struct {
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
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
consecutiveFailures atomic.Int32 // Counts consecutive connection failures for log suppression
}
type ClientOption func(*Client)
@@ -141,6 +144,14 @@ func NewClient(clientType string, ID, secret string, endpoint string, pingInterv
Endpoint: endpoint,
}
// Read deadline window: must exceed pingInterval so a healthy connection
// (which gets a pong/message at least every pingInterval) is never torn
// down, but a dead/half-open one is detected within ~2 ping cycles.
pongWait := pingInterval * 2
if pongWait < 20*time.Second {
pongWait = 20 * time.Second
}
client := &Client{
config: config,
baseURL: endpoint, // default value
@@ -149,6 +160,7 @@ func NewClient(clientType string, ID, secret string, endpoint string, pingInterv
reconnectInterval: 3 * time.Second,
isConnected: false,
pingInterval: pingInterval,
pongWait: pongWait,
clientType: clientType,
}
@@ -172,6 +184,11 @@ func (c *Client) GetConfig() *Config {
return c.config
}
// GetConfigFilePath returns the resolved path to the config file used by this client.
func (c *Client) GetConfigFilePath() string {
return getConfigPath(c.clientType, c.configFilePath)
}
func (c *Client) GetServerVersion() string {
return c.serverVersion
}
@@ -404,7 +421,7 @@ func (c *Client) getToken() (string, error) {
logger.Debug("Token response body: %s", string(body))
if resp.StatusCode != http.StatusOK {
logger.Error("Failed to get token with status code: %d", resp.StatusCode)
logger.Debug("Failed to get token with status code: %d", resp.StatusCode)
telemetry.IncConnAttempt(ctx, "auth", "failure")
etype := "io_error"
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
@@ -487,6 +504,12 @@ func classifyWSDisconnect(err error) (result, reason string) {
}
}
// consecutiveFailureThreshold is the number of consecutive failures before
// connection errors are promoted from Debug to Error. This suppresses the noisy
// transient errors that occur during routine server updates while still surfacing
// genuine connectivity problems.
const consecutiveFailureThreshold = 3
func (c *Client) connectWithRetry() {
for {
select {
@@ -495,10 +518,16 @@ func (c *Client) connectWithRetry() {
default:
err := c.establishConnection()
if err != nil {
logger.Error("Failed to connect: %v. Retrying in %v...", err, c.reconnectInterval)
n := c.consecutiveFailures.Add(1)
if n >= consecutiveFailureThreshold {
logger.Error("Failed to connect: %v. Retrying in %v...", err, c.reconnectInterval)
} else {
logger.Debug("Failed to connect (attempt %d): %v. Retrying in %v...", n, err, c.reconnectInterval)
}
time.Sleep(c.reconnectInterval)
continue
}
c.consecutiveFailures.Store(0)
return
}
}
@@ -602,16 +631,28 @@ func (c *Client) establishConnection() error {
telemetry.SetWSConnectionState(true)
c.setMetricsContext(ctx)
sessionStart := time.Now()
// Wire up pong handler for metrics
// Per-connection lifecycle channel. The read pump closes it when this
// connection ends so the matching ping monitor stops (avoids leaking a
// ping-monitor goroutine on every reconnect).
connClosed := make(chan struct{})
// Arm a read deadline and refresh it whenever a pong arrives. Combined with
// the protocol-level pings sent by the ping monitor, this detects a dead or
// half-open connection (e.g. a cloud load balancer keeping the socket open
// after the backend API server died/restarted) instead of blocking on
// ReadMessage forever — which previously required restarting newt by hand.
_ = c.conn.SetReadDeadline(time.Now().Add(c.pongWait))
c.conn.SetPongHandler(func(appData string) error {
_ = c.conn.SetReadDeadline(time.Now().Add(c.pongWait))
telemetry.IncWSMessage(c.metricsContext(), "in", "pong")
return nil
})
// Start the ping monitor
go c.pingMonitor()
go c.pingMonitor(connClosed)
// Start the read pump with disconnect detection
go c.readPumpWithDisconnectDetection(sessionStart)
go c.readPumpWithDisconnectDetection(sessionStart, connClosed)
if c.onConnect != nil {
err := c.saveConfig()
@@ -722,11 +763,16 @@ func (c *Client) sendPing() {
err := c.conn.WriteJSON(pingMsg)
if err == nil {
telemetry.IncWSMessage(c.metricsContext(), "out", "ping")
// Protocol-level ping: a standards-compliant server replies with a PONG,
// which refreshes the read deadline. This is what lets us notice a
// half-open connection where writes still succeed (buffered) but the
// peer is gone.
_ = c.conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(10*time.Second))
}
c.writeMux.Unlock()
if err != nil {
// Check if we're shutting down before logging error and reconnecting
// Check if we're shutting down before logging error
select {
case <-c.done:
// Expected during shutdown
@@ -735,13 +781,19 @@ func (c *Client) sendPing() {
logger.Error("Ping failed: %v", err)
telemetry.IncWSKeepaliveFailure(c.metricsContext(), "ping_write")
telemetry.IncWSReconnect(c.metricsContext(), "ping_write")
c.reconnect()
// Close the connection and let the read pump trigger a single
// reconnect (avoids racing two reconnects from here and the pump).
c.writeMux.Lock()
if c.conn != nil {
_ = c.conn.Close()
}
c.writeMux.Unlock()
return
}
}
}
func (c *Client) pingMonitor() {
func (c *Client) pingMonitor(connClosed <-chan struct{}) {
// Send an immediate ping as soon as we connect
c.sendPing()
@@ -752,6 +804,10 @@ func (c *Client) pingMonitor() {
select {
case <-c.done:
return
case <-connClosed:
// This connection ended; stop pinging it. A new monitor is started
// for the next connection by establishConnection.
return
case <-ticker.C:
c.sendPing()
}
@@ -759,12 +815,14 @@ func (c *Client) pingMonitor() {
}
// readPumpWithDisconnectDetection reads messages and triggers reconnect on error
func (c *Client) readPumpWithDisconnectDetection(started time.Time) {
func (c *Client) readPumpWithDisconnectDetection(started time.Time, connClosed chan struct{}) {
ctx := c.metricsContext()
disconnectReason := "shutdown"
disconnectResult := "success"
defer func() {
// Signal the ping monitor for this connection to stop.
close(connClosed)
if c.conn != nil {
c.conn.Close()
}
@@ -792,6 +850,10 @@ func (c *Client) readPumpWithDisconnectDetection(started time.Time) {
default:
msgType, p, err := c.conn.ReadMessage()
if err == nil {
// Any inbound traffic means the peer is alive — extend the
// read deadline (also covers servers that answer the app-level
// "newt/ping" with a message rather than a protocol pong).
_ = c.conn.SetReadDeadline(time.Now().Add(c.pongWait))
if msgType == websocket.BinaryMessage {
telemetry.IncWSMessage(c.metricsContext(), "in", "binary")
} else {
@@ -931,3 +993,19 @@ func loadClientCertificate(p12Path string) (*tls.Config, error) {
RootCAs: rootCAs,
}, nil
}
// BuildTLSConfig creates a *tls.Config from the provided certificate files.
// Pass empty strings / nil slice for fields that are not used.
// Returns nil, nil when no TLS credentials are provided.
func BuildTLSConfig(certFile, keyFile string, caFiles []string, pkcs12File string) (*tls.Config, error) {
c := &Client{
tlsConfig: TLSConfig{
ClientCertFile: certFile,
ClientKeyFile: keyFile,
CAFiles: caFiles,
PKCS12File: pkcs12File,
},
config: &Config{},
}
return c.setupTLS()
}

View File

@@ -7,7 +7,6 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
@@ -20,6 +19,10 @@ import (
"github.com/fosrl/newt/logger"
)
// getConfigPath returns the resolved config/credentials file path. Path
// resolution (CLI flag / env var / OS default) is now owned by the calling
// binary's root config loader; this is a fallback used only when a caller
// (e.g. a test) doesn't supply an explicit path via WithConfigFile.
func getConfigPath(clientType string, overridePath string) string {
if overridePath != "" {
return overridePath
@@ -38,7 +41,7 @@ func getConfigPath(clientType string, overridePath string) string {
}
if err := os.MkdirAll(configDir, 0755); err != nil {
log.Printf("Failed to create config directory: %v", err)
logger.Debug("Failed to create config directory: %v", err)
}
return filepath.Join(configDir, "config.json")
@@ -47,21 +50,14 @@ func getConfigPath(clientType string, overridePath string) string {
return configFile
}
// loadConfig no longer merges credentials in from the file: the caller
// resolves ID/Secret/Endpoint/TlsClientCert/ProvisioningKey/Name (from its
// own settings file, env, and CLI flags) before constructing the Client. This
// only figures out whether the file needs to be (re)written so that the
// caller-provided values get cached to disk on first successful connect.
func (c *Client) loadConfig() error {
originalConfig := *c.config // Store original config to detect changes
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")
// Check if config file exists, if not, we should save it
if _, err := os.Stat(configPath); os.IsNotExist(err) {
logger.Info("Config file does not exist at %s, will create it", configPath)
c.configNeedsSave = true
}
return nil
}
logger.Info("Loading config from: %s", configPath)
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
@@ -74,57 +70,16 @@ func (c *Client) loadConfig() error {
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 {
return err
}
// Track what was loaded from file vs provided by CLI
fileHadID := c.config.ID == ""
fileHadSecret := c.config.Secret == ""
fileHadCert := c.config.TlsClientCert == ""
fileHadEndpoint := c.config.Endpoint == ""
if c.config.ID == "" {
c.config.ID = config.ID
}
if c.config.Secret == "" {
c.config.Secret = config.Secret
}
if c.config.TlsClientCert == "" {
c.config.TlsClientCert = config.TlsClientCert
}
if c.config.Endpoint == "" {
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 != "") ||
(!fileHadSecret && originalConfig.Secret != "") ||
(!fileHadCert && originalConfig.TlsClientCert != "") ||
(!fileHadEndpoint && originalConfig.Endpoint != "") {
logger.Info("CLI arguments provided, config will be updated")
c.configNeedsSave = true
}
logger.Debug("Loaded config from %s", configPath)
logger.Debug("Config: %+v", c.config)
return nil
}
// saveConfig persists the credential fields (id, secret, endpoint,
// tlsClientCert, provisioningKey, name) into the config file. It's a
// read-modify-write against the raw JSON so that any other settings a user
// has placed in the same file (mtu, dns, etc.) are preserved rather than
// clobbered.
func (c *Client) saveConfig() error {
if !c.configNeedsSave {
logger.Debug("Config has not changed, skipping save")
@@ -132,7 +87,31 @@ func (c *Client) saveConfig() error {
}
configPath := getConfigPath(c.clientType, c.configFilePath)
data, err := json.MarshalIndent(c.config, "", " ")
existing := map[string]interface{}{}
if data, err := os.ReadFile(configPath); err == nil && len(bytes.TrimSpace(data)) > 0 {
if err := json.Unmarshal(data, &existing); err != nil {
logger.Warn("Existing config file at %s is not valid JSON, other settings in it will be lost: %v", configPath, err)
existing = map[string]interface{}{}
}
}
existing["id"] = c.config.ID
existing["secret"] = c.config.Secret
existing["endpoint"] = c.config.Endpoint
setOrDelete := func(key, value string) {
if value != "" {
existing[key] = value
} else {
delete(existing, key)
}
}
setOrDelete("tlsClientCert", c.config.TlsClientCert)
setOrDelete("provisioningKey", c.config.ProvisioningKey)
setOrDelete("name", c.config.Name)
data, err := json.MarshalIndent(existing, "", " ")
if err != nil {
return err
}
@@ -160,6 +139,15 @@ func interpolateString(s string) string {
})
}
// EnsureProvisioned exchanges a provisioning key for permanent credentials
// synchronously, if one is configured and credentials aren't already present.
// Callers that need the resolved ID/Secret before Connect() runs (which only
// provisions lazily, in the background, on first connection attempt) should
// call this first.
func (c *Client) EnsureProvisioned() error {
return c.provisionIfNeeded()
}
// 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
@@ -279,4 +267,4 @@ func (c *Client) provisionIfNeeded() error {
}
return nil
}
}

View File

@@ -1,6 +1,7 @@
package websocket
import (
"encoding/json"
"os"
"path/filepath"
"testing"
@@ -33,3 +34,63 @@ func TestLoadConfig_EmptyFileMarksConfigForSave(t *testing.T) {
}
}
func TestSaveConfig_PreservesUnrelatedSettings(t *testing.T) {
t.Setenv("CONFIG_FILE", "")
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config.json")
initial := `{
"mtu": 1300,
"dns": "1.1.1.1",
"provisioningKey": "spk-test"
}`
if err := os.WriteFile(configPath, []byte(initial), 0o644); err != nil {
t.Fatalf("failed to create config file: %v", err)
}
client := &Client{
config: &Config{
ID: "newt-id",
Secret: "newt-secret",
Endpoint: "https://example.com",
// ProvisioningKey cleared, simulating a completed provisioning exchange.
},
clientType: "newt",
configFilePath: configPath,
configNeedsSave: true,
}
if err := client.saveConfig(); err != nil {
t.Fatalf("saveConfig returned error: %v", err)
}
data, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("failed to read saved config: %v", err)
}
var saved map[string]interface{}
if err := json.Unmarshal(data, &saved); err != nil {
t.Fatalf("saved config is not valid JSON: %v", err)
}
if saved["mtu"] != float64(1300) {
t.Errorf("expected mtu to be preserved, got %v", saved["mtu"])
}
if saved["dns"] != "1.1.1.1" {
t.Errorf("expected dns to be preserved, got %v", saved["dns"])
}
if saved["id"] != "newt-id" {
t.Errorf("expected id to be updated, got %v", saved["id"])
}
if saved["secret"] != "newt-secret" {
t.Errorf("expected secret to be updated, got %v", saved["secret"])
}
if _, ok := saved["provisioningKey"]; ok {
t.Errorf("expected provisioningKey to be cleared after provisioning, got %v", saved["provisioningKey"])
}
if client.configNeedsSave {
t.Error("expected configNeedsSave to be reset after a successful save")
}
}

View File

@@ -7,6 +7,7 @@ type Config struct {
TlsClientCert string `json:"tlsClientCert"`
ProvisioningKey string `json:"provisioningKey,omitempty"`
Name string `json:"name,omitempty"`
Blocked bool `json:"blocked,omitempty"`
}
type TokenResponse struct {
@@ -31,4 +32,4 @@ type WSMessage struct {
Type string `json:"type"`
Data interface{} `json:"data"`
ConfigVersion int64 `json:"configVersion,omitempty"`
}
}