Compare commits

...

235 Commits

Author SHA1 Message Date
Owen
cd86e6b6de Dont ping if there is just 1 2025-07-17 15:01:02 -07:00
Owen
230c34e4e0 Make sure to only exclude if there is anouther 2025-07-16 21:15:03 -07:00
Owen
a038ce1458 Move docker messages to debug for #86 2025-07-14 10:16:06 -07:00
Owen Schwartz
cd83efd365 Merge pull request #85 from firecat53/main
Update flake to 1.3.2
2025-07-13 19:47:06 -07:00
Scott Hansen
702f39e870 Update flake to 1.3.2 2025-07-13 18:47:20 -07:00
Owen Schwartz
02b7ea51af Merge pull request #84 from fosrl/dev
Ping improvements, dependabot, hostname in docker
2025-07-13 16:26:11 -07:00
Owen
e8421364fc Merge branch 'woutervanelten-patch-2' into dev 2025-07-13 16:12:42 -07:00
Owen
7264bb7001 Merge branch 'patch-2' of github.com:woutervanelten/newt into woutervanelten-patch-2 2025-07-13 16:10:23 -07:00
Owen
86e262ac1e Merge branch 'main' into dev 2025-07-13 16:10:02 -07:00
Owen Schwartz
dcacc03e96 Merge pull request #81 from fosrl/dependabot/go_modules/prod-patch-updates-f7fa3bf88c
Bump github.com/vishvananda/netlink from 1.3.0 to 1.3.1 in the prod-patch-updates group
2025-07-13 16:09:48 -07:00
Owen
6f4469a5a4 Merge branch 'woutervanelten-patch-5' into dev 2025-07-13 16:09:33 -07:00
Owen
663e28329b Fix typo with _ 2025-07-13 16:08:32 -07:00
Owen
f513f97fc3 Working on better ping 2025-07-13 16:07:46 -07:00
dependabot[bot]
ce4f3e4cdf Bump github.com/vishvananda/netlink in the prod-patch-updates group
Bumps the prod-patch-updates group with 1 update: [github.com/vishvananda/netlink](https://github.com/vishvananda/netlink).


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

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

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


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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-11 09:32:02 +00:00
Wouter van Elten
4d343e3541 Update README.md for health check
Added explanation of health_file
2025-07-11 08:14:32 +02:00
Owen
71d1bbaaf2 Merge branch 'main' of github.com:fosrl/newt 2025-07-10 17:27:09 -07:00
Owen
9eb8e5122a Fix link again 2025-07-10 17:26:58 -07:00
Owen Schwartz
c593e2aa97 Merge pull request #72 from fosrl/dev
Dev
2025-07-10 17:25:54 -07:00
Owen
c3483ded8f Merge branch 'main' into dev 2025-07-10 17:25:25 -07:00
Owen Schwartz
5aa00a65c2 Merge pull request #78 from fosrl/dependabot/docker/patch-updates-6b6fe1e6e0
Bump golang from 1.24.4-alpine to 1.24.5-alpine in the patch-updates group
2025-07-10 17:24:32 -07:00
Owen Schwartz
675797c23f Merge pull request #79 from fosrl/dependabot/go_modules/prod-patch-updates-3aa09dd131
Bump github.com/docker/docker from 28.3.1+incompatible to 28.3.2+incompatible in the prod-patch-updates group
2025-07-10 17:24:15 -07:00
Owen
b1cfd3ba02 Update link 2025-07-10 17:23:26 -07:00
Owen
2d9b761de9 Get newt script 2025-07-10 17:17:12 -07:00
dependabot[bot]
99506a10f6 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.1+incompatible to 28.3.2+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.3.1...v28.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-10 10:01:34 +00:00
dependabot[bot]
9410b92169 Bump golang in the patch-updates group
Bumps the patch-updates group with 1 update: golang.


Updates `golang` from 1.24.4-alpine to 1.24.5-alpine

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.24.5-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-09 09:23:04 +00:00
Owen
53397663ef Adjust logging 2025-07-08 17:18:43 -07:00
Owen
221d5862fb Fix disconnect errors about closed connection 2025-07-08 08:48:39 -07:00
Owen
e4bdbbec7c Adjust logging 2025-07-08 08:48:27 -07:00
Owen
07bd283604 Remove dup code 2025-07-05 18:14:33 -07:00
Owen
b7d4ea0c84 Fix nil issues 2025-07-03 20:17:21 -07:00
Owen
d10c5e0366 Merge branch 'main' into dev 2025-07-03 10:11:30 -07:00
Owen
61a9097baf Make linux clients build correctly 2025-07-03 10:09:55 -07:00
Owen Schwartz
54416bbc92 Merge pull request #76 from fosrl/dependabot/go_modules/prod-patch-updates-b286305dda
Bump github.com/docker/docker from 28.3.0+incompatible to 28.3.1+incompatible in the prod-patch-updates group
2025-07-03 09:14:40 -07:00
Owen
a88d25f369 Fix missing netstack pack 2025-07-03 09:06:26 -07:00
Owen
c2a326c70a Working packages? 2025-07-03 09:05:09 -07:00
Owen Schwartz
be56550da4 Merge pull request #75 from woutervanelten/patch-4
Update go.mod
2025-07-03 09:01:39 -07:00
dependabot[bot]
2ecf3297cd 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.0+incompatible to 28.3.1+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.3.0...v28.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 09:36:37 +00:00
Wouter van Elten
a896291831 Update go.mod 2025-07-01 17:38:54 +02:00
Wouter van Elten
9348842e2c added use of hostname if available 2025-07-01 17:26:49 +02:00
Owen
ec8fc20438 Comment out WIP clients flags 2025-06-30 09:49:27 -07:00
Owen
a39d056725 Remove binary mistake 2025-06-30 09:04:58 -07:00
Owen
450fc6c20f Merge branch 'dev' into clients-pops 2025-06-30 09:01:25 -07:00
Owen
b7aa5d1396 Merge branch 'main' into dev 2025-06-30 09:00:52 -07:00
Owen
837f749cdb Merge branch 'dev' into clients-pops 2025-06-30 09:00:26 -07:00
Owen Schwartz
56b68ae3b0 Merge pull request #70 from fosrl/dependabot/go_modules/prod-minor-updates-dd8d89da05
Bump github.com/docker/docker from 28.2.2+incompatible to 28.3.0+incompatible in the prod-minor-updates group
2025-06-30 09:00:16 -07:00
Owen Schwartz
295cadc0b7 Merge pull request #71 from woutervanelten/add-healthcheck
Add healthcheck
2025-06-30 08:51:34 -07:00
Owen
0add2ec668 Merge branch 'add-healthcheck' of github.com:woutervanelten/newt into woutervanelten-add-healthcheck 2025-06-30 08:49:58 -07:00
Wouter van Elten
071a51afbc Update main.go
synced with dev
2025-06-30 13:09:11 +02:00
Wouter van Elten
9db3b78373 Update main.go
fixed some errors,
This file should be ok.
2025-06-30 13:03:06 +02:00
Wouter van Elten
700287163e Update main.go 2025-06-30 12:50:33 +02:00
Wouter van Elten
e357e7befb Update main.go
Added cli and env function
2025-06-30 12:45:42 +02:00
Wouter van Elten
a76e6c9637 added healthy check in main.go
added healthy check in main.go
extended the ping check that creates a /tmp/healthy file if ping successfull and removes that file if ping failes 3 times.

With this you can add the following to the newt docker compose to do the health check:
healthcheck:
  test: ["CMD-SHELL", "test -f /tmp/healthy"]
  interval: 30s
  timeout: 10s
  retries: 3
2025-06-25 19:43:27 +02:00
Wouter van Elten
678d82fa68 added healthy check in main.go
extended the ping check that creates a /tmp/healthy file if ping successfull and removes that file if ping failes 3 times.

With this you can add the following to the newt docker compose to do the health check:
healthcheck:
  test: ["CMD-SHELL", "test -f /tmp/healthy"]
  interval: 30s
  timeout: 10s
  retries: 3
2025-06-25 19:30:05 +02:00
dependabot[bot]
6dd9b44fd2 Bump github.com/docker/docker in the prod-minor-updates group
Bumps the prod-minor-updates group with 1 update: [github.com/docker/docker](https://github.com/docker/docker).


Updates `github.com/docker/docker` from 28.2.2+incompatible to 28.3.0+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.2.2...v28.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-25 10:18:22 +00:00
Owen
63aea704bd Change math random 2025-06-22 12:55:13 -04:00
Owen
c1a2a3208c Add log message 2025-06-22 12:54:13 -04:00
Owen
ac67df63fa Add way to stop initial ping 2025-06-22 12:50:55 -04:00
Owen
e642983b88 Move updates 2025-06-22 12:10:00 -04:00
Owen
e2dd965654 Merge branch 'dev' into clients-pops 2025-06-22 11:54:39 -04:00
Owen
a6849059b9 Merge branch 'JonnyBooker-docker-network-checking' into dev 2025-06-22 11:44:19 -04:00
Owen
5f14ff3a07 Resolve some basic comments 2025-06-22 11:38:43 -04:00
Owen
040f5375a6 Merge branch 'docker-network-checking' of github.com:JonnyBooker/newt into JonnyBooker-docker-network-checking 2025-06-22 11:34:30 -04:00
Owen
227631665e Properly check the http status code now 2025-06-19 22:58:27 -04:00
Owen
ca3ffa00ed Failover is working? 2025-06-19 17:38:21 -04:00
Owen
6d3938e14e Send version 2025-06-19 16:39:33 -04:00
Owen
37191924ee Change terminate to reconnect 2025-06-19 16:32:07 -04:00
Owen
a14f70dbaa Faster detection on ws side 2025-06-19 16:30:31 -04:00
Owen
bb1318278a Reorg and add timeout 2025-06-19 15:59:21 -04:00
Owen
1c75eb3bee New tunnel reconnect works 2025-06-19 15:55:47 -04:00
Owen
4b64b04603 Update ping check 2025-06-18 22:54:13 -04:00
miloschwartz
b397016da8 give preference to previously connected node 2025-06-18 15:57:15 -04:00
Owen
850c230c4a Print ping latency better and add connect message 2025-06-17 15:22:35 -04:00
Owen
c82b84194f Add mtu to readme 2025-06-17 10:23:43 -04:00
Owen
50df49e556 Tweak to call out when docker deployment 2025-06-17 10:23:43 -04:00
Rob
5f9c041c6b Update README.md
added docker socket mount example
2025-06-17 10:23:43 -04:00
Owen
7f9b700ec3 Add mtu to readme 2025-06-17 10:23:29 -04:00
miloschwartz
95d4cb2758 add exit node name to logs 2025-06-16 22:00:39 -04:00
miloschwartz
bbea9a91da adjust weight calculation 2025-06-16 15:54:46 -04:00
Jonny Booker
7c971d278c Revert the newt version placeholder text 2025-06-16 19:38:10 +01:00
Owen Schwartz
f6bfa70c10 Merge pull request #66 from robtec/patch-1
Update README.md
2025-06-15 10:49:17 -04:00
Owen
7f8e3d9ef9 Tweak to call out when docker deployment 2025-06-15 10:45:18 -04:00
Rob
6d7e7e7a93 Update README.md
added docker socket mount example
2025-06-14 15:31:16 +01:00
Jonny Booker
48cb0bf5a7 Minor README update for consistentcy 2025-06-14 15:22:56 +01:00
Jonny Booker
58f7835072 Revise README docs 2025-06-14 15:22:14 +01:00
Jonny Booker
6d9160ab5e Simplified based on PR feedback and support checking use of "bridge" network 2025-06-14 01:26:11 +01:00
Owen
8d4d8b91b9 Working on pining 2025-06-13 16:15:49 -04:00
Owen
d0e220511a First pass at pinging 2025-06-10 21:52:33 -04:00
Jonny Booker
5cb86f3e47 Update to readme with new configuration settings 2025-06-10 21:26:06 +01:00
Jonny Booker
e26552a5d7 Small refinement to how the docker enforcement setting is applied 2025-06-10 21:25:27 +01:00
Owen
22f44c860a Merge branch 'dev' into clients-pops 2025-06-10 09:38:53 -04:00
Owen Schwartz
64aef4ff34 Merge pull request #64 from Lokowitz/update-dependabot
fix - dependabot
2025-06-10 09:36:59 -04:00
Owen Schwartz
ef2cc34f02 Merge pull request #63 from Lokowitz/add-test-action
Add test action
2025-06-10 09:36:42 -04:00
Jonny Booker
5476a69963 Log the container name and id 2025-06-10 13:05:41 +01:00
Marvin
2e6ab2ba41 Update test.yml 2025-06-10 14:00:04 +02:00
Jonny Booker
126ced6d57 Merge branch 'docker-network-checking' of https://github.com/JonnyBooker/newt into docker-network-checking 2025-06-10 12:58:40 +01:00
Jonny Booker
cbbd5b0c76 Add extra pre-condition check for enforcing docker network 2025-06-10 12:58:37 +01:00
Jonny Booker
e335bb8a1f Rename added functions for docker client 2025-06-10 12:57:50 +01:00
Marvin
e053eff879 Update Makefile 2025-06-10 13:27:24 +02:00
Marvin
a2c22eff35 Update dependabot.yml 2025-06-10 13:24:14 +02:00
Marvin
52a8aabdb8 Update test.yml 2025-06-10 13:17:33 +02:00
Marvin
38b7f17e58 Update test.yml 2025-06-10 13:15:44 +02:00
Marvin
8941604f71 Update test.yml 2025-06-10 13:12:43 +02:00
Marvin
1820c8c019 Update test.yml 2025-06-10 13:10:48 +02:00
Marvin
1ecffab79a Update test.yml 2025-06-10 13:08:22 +02:00
Marvin
4ce2a656b8 Create test.yml 2025-06-10 13:07:08 +02:00
Jonny Booker
d4b88c3985 Merge branch 'fosrl:main' into docker-network-checking 2025-06-10 08:43:04 +01:00
Owen
a5f4d5fdf6 Resolve merge issues 2025-06-09 22:26:31 -04:00
Owen
ce6d340a8d Update go 2025-06-09 22:24:49 -04:00
Owen
eaf812a2a7 Merge branch 'dev' into clients-pops 2025-06-09 18:38:48 -04:00
Jonny Booker
a52260b49d Add an enforce network validation flag for docker to not break previous functionality 2025-06-09 23:06:29 +01:00
Jonny Booker
a4d4976103 Update to use docker network checking against newt networking 2025-06-09 22:54:10 +01:00
Owen Schwartz
acab633da6 Merge pull request #57 from fosrl/dependabot/go_modules/prod-minor-updates-cb91bdc677
Bump golang.org/x/net from 0.40.0 to 0.41.0 in the prod-minor-updates group
2025-06-09 09:57:41 -04:00
dependabot[bot]
036e255b47 Bump golang.org/x/net in the prod-minor-updates group
Bumps the prod-minor-updates group with 1 update: [golang.org/x/net](https://github.com/golang/net).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 13:56:53 +00:00
Owen Schwartz
faffaad926 Merge pull request #58 from fosrl/dependabot/docker/patch-updates-23ac2ce0c4
Bump golang from 1.24.3-alpine to 1.24.4-alpine in the patch-updates group
2025-06-09 09:55:01 -04:00
dependabot[bot]
157bb98fd3 Bump golang in the patch-updates group
Bumps the patch-updates group with 1 update: golang.


Updates `golang` from 1.24.3-alpine to 1.24.4-alpine

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.24.4-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 10:10:08 +00:00
Owen Schwartz
50b621f17c Merge pull request #60 from firecat53/main
Update Nix flake for 1.2.1
2025-06-06 10:45:44 -04:00
Scott Hansen
35d82ea15c Update flake.nix for 1.2.1 2025-06-06 07:36:48 -07:00
Scott Hansen
21f5aa906d Merge remote-tracking branch 'upstream/main' 2025-06-06 07:34:47 -07:00
Owen Schwartz
4a70af44bb Merge pull request #56 from fosrl/dev
Make Docker Socket Opt-In and Add Version Printout
2025-06-05 22:41:53 -04:00
Owen
5280c7ccda Add newt version on startup 2025-06-05 22:40:39 -04:00
Owen
ef2f25ef98 Update readme 2025-06-05 22:36:42 -04:00
Owen
eb8a12f290 Make docker socket opt in 2025-06-05 22:34:17 -04:00
Scott Hansen
a937027838 Update flake.nix for 1.2.0 2025-06-05 14:16:06 -07:00
Scott Hansen
f8653e245e Merge remote-tracking branch 'upstream/main' 2025-06-05 13:58:43 -07:00
Owen Schwartz
c423f6692a Merge pull request #53 from fosrl/dev
Docker socket integration & Go Updates
2025-06-05 11:50:29 -04:00
Owen
a9b96637b9 Merge branch 'main' into dev 2025-06-05 11:49:53 -04:00
Owen
f566f599d6 Remove link 2025-06-05 11:44:35 -04:00
Owen
918a9bdb84 Cap 2025-06-05 11:42:30 -04:00
Owen
315b6f3721 Update readme about docker socket 2025-06-05 11:41:44 -04:00
Owen
37940444c1 Package updates 2025-06-04 17:28:06 -04:00
Owen Schwartz
4e9aa30686 Merge pull request #49 from fosrl/dependabot/go_modules/golang.org/x/net-0.38.0
Bump golang.org/x/net from 0.30.0 to 0.38.0
2025-06-02 22:08:59 -04:00
Owen Schwartz
c2d3f00a6e Merge pull request #46 from improbableone/hub-dev
🐳 Add Docker Socket Integration
2025-06-02 20:34:37 -04:00
Owen
9f006b1cbd Update packages 2025-06-02 20:04:39 -04:00
dependabot[bot]
1ef61d7470 Bump golang.org/x/net from 0.30.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.30.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.30.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 02:55:17 +00:00
Owen Schwartz
6935c3b8db Merge pull request #48 from Lokowitz/main
Update deps and add dependabot.yml
2025-06-01 22:52:27 -04:00
Marvin
994d11b40c Merge pull request #6 from Lokowitz/dependabot/go_modules/prod-minor-updates-5e519fa3dd
Bump golang.org/x/net from 0.30.0 to 0.40.0 in the prod-minor-updates group
2025-06-01 11:28:00 +02:00
Marvin
f060306654 Merge pull request #5 from Lokowitz/dependabot/docker/minor-updates-462732f451
Bump the minor-updates group with 2 updates
2025-06-01 11:26:59 +02:00
dependabot[bot]
a3cfda9fc5 Bump golang.org/x/net in the prod-minor-updates group
Bumps the prod-minor-updates group with 1 update: [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/net` from 0.30.0 to 0.40.0
- [Commits](https://github.com/golang/net/compare/v0.30.0...v0.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-01 08:51:20 +00:00
dependabot[bot]
607d197b02 Bump the minor-updates group with 2 updates
Bumps the minor-updates group with 2 updates: golang and alpine.


Updates `golang` from 1.23.1-alpine to 1.24.3-alpine

Updates `alpine` from 3.19 to 3.22

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-01 08:51:11 +00:00
Marvin
78f31a56b0 Update dependabot.yml 2025-06-01 10:50:23 +02:00
Marvin
03988655b6 Create dependabot.yml 2025-06-01 10:47:06 +02:00
Rajesh V
4cf83f4cfc docker socket 2025-05-29 20:41:28 +05:30
Owen
494e30704b Set to 127 2025-05-13 11:42:06 -04:00
Owen
175718a48e Handle order of opertions of hole punch better 2025-04-22 22:11:37 -04:00
Owen
6a146ed371 Add more sensible controls 2025-04-13 21:28:38 -04:00
Owen
027d9a059f Remove redundant log message 2025-04-12 17:51:03 -04:00
Owen
0ced66e157 Relaying working 2025-04-11 20:52:29 -04:00
Scott Hansen
641c7f27a2 Update sha256 2025-04-07 21:10:45 -07:00
Owen
67abd239b6 Merge branch 'main' into dev 2025-04-07 21:38:12 -04:00
Owen Schwartz
3b166c465d Merge pull request #29 from firecat53/flake
Flake update for newt 1.1.3
2025-04-07 21:37:53 -04:00
Owen
e0d2349efa Add tzdata package
Resolves #23 again
2025-04-07 09:47:58 -04:00
Scott Hansen
7b7d7228a6 Flake update for newt 1.1.3 2025-04-06 16:40:32 -07:00
Owen Schwartz
a1a439c75c Merge pull request #28 from fosrl/dev
MTLS, Connection Monitoring, time zone logger
2025-04-06 14:09:59 -04:00
Owen
6b0ca9cab5 Adjust wgtester to work with bpf 2025-04-03 21:59:16 -04:00
Owen Schwartz
e7c8dbc1c8 Merge pull request #26 from progressive-kiwi/feat-mtls-support
Feat: mTLS support
2025-04-02 21:23:17 -04:00
progressive-kiwi
d28e3ca5e8 feat/mtls-support-cert: doc update, removing config.Endpoint loading duplicates, handling null-pointer case and some logging 2025-04-02 21:00:09 +02:00
progressive-kiwi
b41570eb2c feat/mtls-support-cert: config support 2025-04-01 20:43:42 +02:00
Owen
e47ddaa916 Merge branch 'holepunch' into hp-multi-client 2025-04-01 10:40:38 -04:00
Owen
65dc81ca8b Add wgtester 2025-03-31 18:10:16 -04:00
Owen
09d6829f8b Add update message 2025-03-31 15:46:01 -04:00
Owen
f677376fae Merge branch 'dev' into hp-multi-client 2025-03-31 15:17:34 -04:00
Owen
72e0adc1bf Monitor connection with pings and keep pining
Resolves #24
2025-03-30 19:31:55 -04:00
progressive-kiwi
435b638701 feat/mtls-support-cert-script 2025-03-31 00:52:48 +02:00
progressive-kiwi
9b3c82648b feat/mtls-support 2025-03-31 00:06:40 +02:00
Owen Schwartz
f713c294b2 Merge pull request #25 from firecat53/flake
Add flake for build and devshell.
2025-03-30 10:55:00 -04:00
Owen
b3e8bf7d12 Add LOGGER_TIMEZONE env to control the time zone
Closes #23

If the name is "" or "UTC", LoadLocation returns UTC. If the name is
"Local", LoadLocation returns Local.

Otherwise, the name is taken to be a location name corresponding to a
file in the IANA Time Zone database, such as "America/New_York".

LoadLocation looks for the IANA Time Zone database in the following
locations in order:

the directory or uncompressed zip file named by the ZONEINFO environment
variable
on a Unix system, the system standard installation location
$GOROOT/lib/time/zoneinfo.zip
the time/tzdata package, if it was imported
2025-03-30 10:52:07 -04:00
Owen
c5978d9c4e Handle port correctly and delete interface 2025-03-27 22:12:54 -04:00
Owen
7f9a31ac3e Remove listen port - unused 2025-03-26 10:54:52 -04:00
Owen
f08378b67e Fix segfault if no wgService created 2025-03-25 20:54:09 -04:00
Scott Hansen
7852f11e8d Add flake for build and devshell.
Package named newt-pangolin to avoid conflicts with existing package name
2025-03-25 15:46:12 -07:00
Owen
2ff8df9a8d Merge branch 'dev' 2025-03-22 12:54:31 -04:00
Owen
9d80161ab7 Increases ping attempts to 15
Might help #7
2025-03-21 17:24:04 -04:00
Owen
1501de691a Handle encrypted messages 2025-03-15 21:47:22 -04:00
Owen
2a19856556 Merge branch 'main' into holepunch 2025-03-15 11:33:34 -04:00
Owen Schwartz
f4e17a4dd7 Merge pull request #22 from fosrl/dev
Fix 51820 typo
2025-03-14 18:52:18 -04:00
Owen
ab544fc9ed Fix 51820 typo 2025-03-14 18:51:33 -04:00
Owen
f9e52c4d91 Working on encryption 2025-03-14 18:49:50 -04:00
Owen
14eff8e16c Merge branch 'holepunch' of github.com:fosrl/newt_dg into holepunch 2025-03-12 20:41:38 -04:00
Owen
067e079293 Handle / better 2025-03-12 20:37:57 -04:00
Owen Schwartz
623be5ea0d Merge pull request #20 from fosrl/dev
Cleanup & Updown Script
2025-03-09 23:28:44 -04:00
Owen
72d264d427 Add note about reference script 2025-03-09 11:41:58 -04:00
Owen
a19fc8c588 Go mod tidy 2025-03-09 11:41:12 -04:00
Owen Schwartz
dbc2a92456 Merge pull request #19 from fosrl/updown
Add updown script capabilities
2025-03-08 21:12:01 -05:00
Owen
437d8b67a4 Add documentation for updown 2025-03-08 21:11:36 -05:00
Owen
6f1d4752f0 Add updown script capabilities 2025-03-07 12:35:46 -05:00
Owen
683312c78e Setup qemu 2025-03-04 00:01:37 -05:00
Owen
29543aece3 Merge branch 'main' of github.com:fosrl/newt 2025-03-03 22:34:52 -05:00
Owen Schwartz
e68a38e929 Merge pull request #18 from fosrl/dev
Minor Updates & Improvements
2025-03-03 22:33:46 -05:00
miloschwartz
bc72c96b5e add arm/v7 to cicd 2025-03-03 21:29:11 -05:00
Owen
3d15ecb732 Log the token response to make it more clear
Helps resolve #16
2025-03-02 14:10:18 -05:00
Owen
a69618310b Also build armv6 2025-02-28 12:54:21 -05:00
Owen
ed8a2ccd23 Build riscv64 newt binary and use alpine in docker
Resolves #14
2025-02-26 18:52:05 -05:00
Owen
5e673c829b Clean up when wg is used 2025-02-24 10:05:35 -05:00
Owen
cd3ec0b259 Support relay switch 2025-02-23 20:18:25 -05:00
Owen
b68502de9e Basic relay working! 2025-02-23 16:49:24 -05:00
Owen
f6429b6eee Basic holepunch working 2025-02-23 01:00:16 -05:00
Owen
8795c57b2e HP works! 2025-02-22 12:53:23 -05:00
Owen
e8141a177b Fix typo 51820
Fixes #13
2025-02-22 11:46:52 -05:00
Owen
4aa718d55f Initial hp working but need to fix port issue 2025-02-22 11:21:13 -05:00
Owen
afa93d8a3f Add static port and udp hole punch 2025-02-21 22:27:24 -05:00
Owen
270ee9cd19 Fix panic 2025-02-21 20:33:31 -05:00
Owen
0affef401c Properly handle key 2025-02-21 18:04:36 -05:00
Owen
18d99de924 Handle messages correctly 2025-02-21 17:13:00 -05:00
Owen
bff6707577 Basic create wg seems to be working 2025-02-21 16:20:03 -05:00
Owen
95eab504fa Get wg working 2025-02-21 16:12:12 -05:00
Owen
56e75902e3 Adjust ws types 2025-02-21 12:44:52 -05:00
Owen
45a1ab91d7 Dont always do wg 2025-02-20 22:10:02 -05:00
Owen
fb199cc94b Tidy 2025-02-20 22:07:27 -05:00
Owen
66edae4288 Clean up implementation 2025-02-20 21:01:44 -05:00
Owen
f69a7f647d Move wg into more of a class 2025-02-20 20:37:31 -05:00
Owen
e8bd55bed9 Copy in gerbil wg config 2025-02-20 20:04:01 -05:00
Owen
b23eda9c06 Add arm32 go binary as well 2025-02-15 17:59:59 -05:00
Owen
92bc883b5b Add arm build 2025-02-15 17:53:08 -05:00
Owen
76503f3f2c Fix typo 2025-02-15 17:52:51 -05:00
Owen
9c3112f9bd Merge branch 'dev' 2025-02-10 21:42:29 -05:00
Owen
462af30d16 Add systemd service; Closes #12 2025-02-10 21:41:59 -05:00
Owen
fa6038eb38 Move message to debug to reduce confusion 2025-02-06 20:21:04 -05:00
Owen Schwartz
f346b6cc5d Bump actions/upload-artifact 2025-01-30 10:18:46 -05:00
Owen Schwartz
f20b9ebb14 Merge pull request #9 from fosrl/dev
CICD, --version & Bug Fixes
2025-01-30 10:14:31 -05:00
Owen Schwartz
39bfe5b230 Insert version CICD 2025-01-29 22:31:14 -05:00
Milo Schwartz
a1a3dd9ba2 Merge branch 'dev' of https://github.com/fosrl/newt into dev 2025-01-29 22:23:42 -05:00
Milo Schwartz
7b1492f327 add cicd 2025-01-29 22:23:03 -05:00
Owen Schwartz
4e50819785 Add --version check 2025-01-29 22:19:18 -05:00
Owen Schwartz
f8dccbec80 Fix save config 2025-01-29 22:15:28 -05:00
Owen Schwartz
0c5c59cf00 Fix removing udp sockets 2025-01-27 21:28:22 -05:00
Owen Schwartz
868bb55f87 Fix windows build in release 2025-01-20 21:40:55 -05:00
Owen Schwartz
5b4245402a Merge pull request #6 from fosrl/dev
Proxy Manager Rewrite
2025-01-20 21:15:31 -05:00
Owen Schwartz
f7a705e6f8 Remove starts 2025-01-20 21:13:09 -05:00
Owen Schwartz
3a63657822 Rewrite proxy manager 2025-01-20 21:11:06 -05:00
Owen Schwartz
759780508a Resolve TCP hanging but port is in use issue 2025-01-19 22:46:00 -05:00
Owen Schwartz
533886f2e4 Standarize makefile release 2025-01-16 07:41:56 -05:00
30 changed files with 4503 additions and 731 deletions

35
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
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"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
groups:
patch-updates:
update-types:
- "patch"
minor-updates:
update-types:
- "minor"

61
.github/workflows/cicd.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: CI/CD Pipeline
on:
push:
tags:
- "*"
jobs:
release:
name: Build and Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 1.23.1
- name: Update version in main.go
run: |
TAG=${{ env.TAG }}
if [ -f main.go ]; then
sed -i 's/version_replaceme/'"$TAG"'/' main.go
echo "Updated main.go with version $TAG"
else
echo "main.go not found"
fi
- name: Build and push Docker images
run: |
TAG=${{ env.TAG }}
make docker-build-release tag=$TAG
- name: Build binaries
run: |
make go-build-release
- name: Upload artifacts from /bin
uses: actions/upload-artifact@v4
with:
name: binaries
path: bin/

28
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Run Tests
on:
pull_request:
branches:
- main
- dev
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Build go
run: go build
- name: Build Docker image
run: make build
- name: Build binaries
run: make go-build-release

6
.gitignore vendored
View File

@@ -1,2 +1,8 @@
newt
.DS_Store
bin/
nohup.out
.idea
*.iml
certs/
newt_arm64

1
.go-version Normal file
View File

@@ -0,0 +1 @@
1.23.2

View File

@@ -1,4 +1,4 @@
FROM golang:1.23.1-alpine AS builder
FROM golang:1.24.5-alpine AS builder
# Set the working directory inside the container
WORKDIR /app
@@ -15,19 +15,13 @@ COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -o /newt
# Start a new stage from scratch
FROM ubuntu:22.04 AS runner
FROM alpine:3.22 AS runner
RUN apt-get update && apt-get install ca-certificates -y && rm -rf /var/lib/apt/lists/*
RUN apk --no-cache add ca-certificates tzdata
# Copy the pre-built binary file from the previous stage and the entrypoint script
COPY --from=builder /newt /usr/local/bin/
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
# Copy the entrypoint script
ENTRYPOINT ["/entrypoint.sh"]
# Command to run the executable
CMD ["newt"]

View File

@@ -1,6 +1,14 @@
all: build push
docker-build-release:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make docker-build-release tag=<tag>"; \
exit 1; \
fi
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t fosrl/newt:latest -f Dockerfile --push .
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t fosrl/newt:$(tag) -f Dockerfile --push .
build:
docker build -t fosrl/newt:latest .
@@ -13,14 +21,17 @@ test:
local:
CGO_ENABLED=0 go build -o newt
all_arches:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o newt_linux_arm64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o newt_linux_amd64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o newt_darwin_arm64
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o newt_darwin_amd64
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o newt_windows_amd64.exe
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o newt_freebsd_amd64
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o newt_freebsd_arm64
go-build-release:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/newt_linux_arm64
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o bin/newt_linux_arm32
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o bin/newt_linux_arm32v6
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/newt_linux_amd64
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -o bin/newt_linux_riscv64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/newt_darwin_arm64
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/newt_darwin_amd64
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/newt_windows_amd64.exe
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/newt_freebsd_amd64
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/newt_freebsd_arm64
clean:
rm newt

112
README.md
View File

@@ -6,7 +6,6 @@ Newt is a fully user space [WireGuard](https://www.wireguard.com/) tunnel client
Newt is used with Pangolin and Gerbil as part of the larger system. See documentation below:
- [Installation Instructions](https://docs.fossorial.io)
- [Full Documentation](https://docs.fossorial.io)
## Preview
@@ -34,10 +33,16 @@ When Newt receives WireGuard control messages, it will use the information encod
- `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket.
- `id`: Newt ID generated by Pangolin to identify the client.
- `secret`: A unique secret (not shared and kept private) used to authenticate the client ID with the websocket in order to receive commands.
- `mtu`: MTU for the internal WG interface. Default: 1280
- `dns`: DNS server to use to resolve the endpoint
- `log-level` (optional): The log level to use. Default: INFO
- `updown` (optional): A script to be called when targets are added or removed.
- `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls)
- `docker-socket` (optional): Set the Docker socket to use the container discovery integration
- `docker-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process
- `health-file` (optional): Check if connection to WG server (pangolin) is ok. creates a file if ok, removes it if not ok. Can be used with docker healtcheck to restart newt
Example:
- Example:
```bash
./newt \
@@ -57,7 +62,8 @@ services:
environment:
- PANGOLIN_ENDPOINT=https://example.com
- NEWT_ID=2ix2t8xk22ubpfy
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
- HEALTH_FILE=/tmp/healthy
```
You can also pass the CLI args to the container:
@@ -72,6 +78,96 @@ services:
- --id 31frd0uzbjvp721
- --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6
- --endpoint https://example.com
- --health-file /tmp/healthy
```
### Docker Socket Integration
Newt can integrate with the Docker socket to provide remote inspection of Docker containers. This allows Pangolin to query and retrieve detailed information about containers running on the Newt client, including metadata, network configuration, port mappings, and more.
**Configuration:**
You can specify the Docker socket path using the `--docker-socket` CLI argument or by setting the `DOCKER_SOCKET` environment variable. On most linux systems the socket is `/var/run/docker.sock`. When deploying newt as a container, you need to mount the host socket as a volume for the newt container to access it. If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation.
```yaml
services:
newt:
image: fosrl/newt
container_name: newt
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- PANGOLIN_ENDPOINT=https://example.com
- NEWT_ID=2ix2t8xk22ubpfy
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
- DOCKER_SOCKET=/var/run/docker.sock
```
#### Hostnames vs IPs
When the Docker Socket Integration is used, depending on the network which Newt is run with, either the hostname (generally considered the container name) or the IP address of the container will be sent to Pangolin. Here are some of the scenarios where IPs or hostname of the container will be utilised:
- **Running in Network Mode 'host'**: IP addresses will be used
- **Running in Network Mode 'bridge'**: IP addresses will be used
- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default, hostnames will be used
- **Running on docker-compose with defined network**: Hostnames will be used
### Docker Enforce Network Validation
When run as a Docker container, Newt can validate that the target being provided is on the same network as the Newt container and only return containers directly accessible by Newt. Validation will be carried out against either the hostname/IP Address and the Port number to ensure the running container is exposing the ports to Newt.
It is important to note that if the Newt container is run with a network mode of `host` that this feature will not work. Running in `host` mode causes the container to share its resources with the host machine, therefore making it so the specific host container information for Newt cannot be retrieved to be able to carry out network validation.
**Configuration:**
Validation is `false` by default. It can be enabled via setting the `--docker-enforce-network-validation` CLI argument or by setting the `DOCKER_ENFORCE_NETWORK_VALIDATION` environment variable.
If validation is enforced and the Docker socket is available, Newt will **not** add the target as it cannot be verified. A warning will be presented in the Newt logs.
### Updown
You can pass in a updown script for Newt to call when it is adding or removing a target:
`--updown "python3 test.py"`
It will get called with args when a target is added:
`python3 test.py add tcp localhost:8556`
`python3 test.py remove tcp localhost:8556`
Returning a string from the script in the format of a target (`ip:dst` so `10.0.0.1:8080`) it will override the target and use this value instead to proxy.
You can look at updown.py as a reference script to get started!
### mTLS
Newt supports mutual TLS (mTLS) authentication, if the server has been configured to request a client certificate.
* Only PKCS12 (.p12 or .pfx) file format is accepted
* The PKCS12 file must contain:
* Private key
* Public certificate
* CA certificate
* Encrypted PKCS12 files are currently not supported
Examples:
```bash
./newt \
--id 31frd0uzbjvp721 \
--secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
--endpoint https://example.com \
--tls-client-cert ./client.p12
```
```yaml
services:
newt:
image: fosrl/newt
container_name: newt
restart: unless-stopped
environment:
- PANGOLIN_ENDPOINT=https://example.com
- NEWT_ID=2ix2t8xk22ubpfy
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
- TLS_CLIENT_CERT=./client.p12
```
## Build
@@ -92,6 +188,16 @@ Make sure to have Go 1.23.1 installed.
make local
```
### Nix Flake
```bash
nix build
```
Binary will be at `./result/bin/newt`
Development shell available with `nix develop`
## Licensing
Newt is dual licensed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.

275
docker/client.go Normal file
View File

@@ -0,0 +1,275 @@
package docker
import (
"context"
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/fosrl/newt/logger"
)
// Container represents a Docker container
type Container struct {
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
State string `json:"state"`
Status string `json:"status"`
Ports []Port `json:"ports"`
Labels map[string]string `json:"labels"`
Created int64 `json:"created"`
Networks map[string]Network `json:"networks"`
Hostname string `json:"hostname"` // added to use hostname if available instead of network address
}
// Port represents a port mapping for a Docker container
type Port struct {
PrivatePort int `json:"privatePort"`
PublicPort int `json:"publicPort,omitempty"`
Type string `json:"type"`
IP string `json:"ip,omitempty"`
}
// Network represents network information for a Docker container
type Network struct {
NetworkID string `json:"networkId"`
EndpointID string `json:"endpointId"`
Gateway string `json:"gateway,omitempty"`
IPAddress string `json:"ipAddress,omitempty"`
IPPrefixLen int `json:"ipPrefixLen,omitempty"`
IPv6Gateway string `json:"ipv6Gateway,omitempty"`
GlobalIPv6Address string `json:"globalIPv6Address,omitempty"`
GlobalIPv6PrefixLen int `json:"globalIPv6PrefixLen,omitempty"`
MacAddress string `json:"macAddress,omitempty"`
Aliases []string `json:"aliases,omitempty"`
DNSNames []string `json:"dnsNames,omitempty"`
}
// CheckSocket checks if Docker socket is available
func CheckSocket(socketPath string) bool {
// Use the provided socket path or default to standard location
if socketPath == "" {
socketPath = "/var/run/docker.sock"
}
// Try to create a connection to the Docker socket
conn, err := net.Dial("unix", socketPath)
if err != nil {
logger.Debug("Docker socket not available at %s: %v", socketPath, err)
return false
}
defer conn.Close()
logger.Debug("Docker socket is available at %s", socketPath)
return true
}
// IsWithinHostNetwork checks if a provided target is within the host container network
func IsWithinHostNetwork(socketPath string, targetAddress string, targetPort int) (bool, error) {
// Always enforce network validation
containers, err := ListContainers(socketPath, true)
if err != nil {
return false, err
}
// Determine if given an IP address
var parsedTargetAddressIp = net.ParseIP(targetAddress)
// If we can find the passed hostname/IP address in the networks or as the container name, it is valid and can add it
for _, c := range containers {
for _, network := range c.Networks {
// If the target address is not an IP address, use the container name
if parsedTargetAddressIp == nil {
if c.Name == targetAddress {
for _, port := range c.Ports {
if port.PublicPort == targetPort || port.PrivatePort == targetPort {
return true, nil
}
}
}
} else {
//If the IP address matches, check the ports being mapped too
if network.IPAddress == targetAddress {
for _, port := range c.Ports {
if port.PublicPort == targetPort || port.PrivatePort == targetPort {
return true, nil
}
}
}
}
}
}
combinedTargetAddress := targetAddress + ":" + strconv.Itoa(targetPort)
return false, fmt.Errorf("target address not within host container network: %s", combinedTargetAddress)
}
// ListContainers lists all Docker containers with their network information
func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Container, error) {
// Use the provided socket path or default to standard location
if socketPath == "" {
socketPath = "/var/run/docker.sock"
}
// Used to filter down containers returned to Pangolin
containerFilters := filters.NewArgs()
// Used to determine if we will send IP addresses or hostnames to Pangolin
useContainerIpAddresses := true
hostContainerId := ""
// Create a new Docker client
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Create client with custom socket path
cli, err := client.NewClientWithOpts(
client.WithHost("unix://"+socketPath),
client.WithAPIVersionNegotiation(),
)
if err != nil {
return nil, fmt.Errorf("failed to create Docker client: %v", err)
}
defer cli.Close()
hostContainer, err := getHostContainer(ctx, cli)
if enforceNetworkValidation && err != nil {
return nil, fmt.Errorf("network validation enforced, cannot validate due to: %w", err)
}
// We may not be able to get back host container in scenarios like running the container in network mode 'host'
if hostContainer != nil {
// We can use the host container to filter out the list of returned containers
hostContainerId = hostContainer.ID
for hostContainerNetworkName := range hostContainer.NetworkSettings.Networks {
// If we're enforcing network validation, we'll filter on the host containers networks
if enforceNetworkValidation {
containerFilters.Add("network", hostContainerNetworkName)
}
// If the container is on the docker bridge network, we will use IP addresses over hostnames
if useContainerIpAddresses && hostContainerNetworkName != "bridge" {
useContainerIpAddresses = false
}
}
}
// List containers
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true, Filters: containerFilters})
if err != nil {
return nil, fmt.Errorf("failed to list containers: %v", err)
}
var dockerContainers []Container
for _, c := range containers {
// Short ID like docker ps
shortId := c.ID[:12]
// Inspect container to get hostname
hostname := ""
containerInfo, err := cli.ContainerInspect(ctx, c.ID)
if err == nil && containerInfo.Config != nil {
hostname = containerInfo.Config.Hostname
}
// Skip host container if set
if hostContainerId != "" && c.ID == hostContainerId {
continue
}
// Get container name (remove leading slash)
name := ""
if len(c.Names) > 0 {
name = strings.TrimPrefix(c.Names[0], "/")
}
// Convert ports
var ports []Port
for _, port := range c.Ports {
dockerPort := Port{
PrivatePort: int(port.PrivatePort),
Type: port.Type,
}
if port.PublicPort != 0 {
dockerPort.PublicPort = int(port.PublicPort)
}
if port.IP != "" {
dockerPort.IP = port.IP
}
ports = append(ports, dockerPort)
}
// Get network information by inspecting the container
networks := make(map[string]Network)
// Extract network information from inspection
if c.NetworkSettings != nil && c.NetworkSettings.Networks != nil {
for networkName, endpoint := range c.NetworkSettings.Networks {
dockerNetwork := Network{
NetworkID: endpoint.NetworkID,
EndpointID: endpoint.EndpointID,
Gateway: endpoint.Gateway,
IPPrefixLen: endpoint.IPPrefixLen,
IPv6Gateway: endpoint.IPv6Gateway,
GlobalIPv6Address: endpoint.GlobalIPv6Address,
GlobalIPv6PrefixLen: endpoint.GlobalIPv6PrefixLen,
MacAddress: endpoint.MacAddress,
Aliases: endpoint.Aliases,
DNSNames: endpoint.DNSNames,
}
// Use IPs over hostnames/containers as we're on the bridge network
if useContainerIpAddresses {
dockerNetwork.IPAddress = endpoint.IPAddress
}
networks[networkName] = dockerNetwork
}
}
dockerContainer := Container{
ID: shortId,
Name: name,
Image: c.Image,
State: c.State,
Status: c.Status,
Ports: ports,
Labels: c.Labels,
Created: c.Created,
Networks: networks,
Hostname: hostname, // added
}
dockerContainers = append(dockerContainers, dockerContainer)
}
return dockerContainers, nil
}
// getHostContainer gets the current container for the current host if possible
func getHostContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) {
// Get hostname from the os
hostContainerName, err := os.Hostname()
if err != nil {
return nil, fmt.Errorf("failed to find hostname for container")
}
// Get host container from the docker socket
hostContainer, err := dockerClient.ContainerInspect(dockerContext, hostContainerName)
if err != nil {
return nil, fmt.Errorf("failed to find host container")
}
return &hostContainer, nil
}

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1752308619,
"narHash": "sha256-pzrVLKRQNPrii06Rm09Q0i0dq3wt2t2pciT/GNq5EZQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "650e572363c091045cdbc5b36b0f4c1f614d3058",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

65
flake.nix Normal file
View File

@@ -0,0 +1,65 @@
{
description = "newt - A tunneling client for Pangolin";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
};
outputs =
{ self, nixpkgs }:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
pkgsFor = system: nixpkgs.legacyPackages.${system};
in
{
packages = forAllSystems (
system:
let
pkgs = pkgsFor system;
in
{
default = self.packages.${system}.pangolin-newt;
pangolin-newt = pkgs.buildGoModule {
pname = "pangolin-newt";
version = "1.3.2";
src = ./.;
vendorHash = "sha256-Y/f7GCO7Kf1iQiDR32DIEIGJdcN+PKS0OrhBvXiHvwo=";
meta = with pkgs.lib; {
description = "A tunneling client for Pangolin";
homepage = "https://github.com/fosrl/newt";
license = licenses.gpl3;
maintainers = [ ];
};
};
}
);
devShells = forAllSystems (
system:
let
pkgs = pkgsFor system;
in
{
default = pkgs.mkShell {
buildInputs = with pkgs; [
go
gopls
gotools
go-outline
gopkgs
godef
golint
];
};
}
);
};
}

235
get-newt.sh Normal file
View File

@@ -0,0 +1,235 @@
#!/bin/bash
# Get Newt - Cross-platform installation script
# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/newt/refs/heads/main/get-newt.sh | bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# GitHub repository info
REPO="fosrl/newt"
GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest"
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to get latest version from GitHub API
get_latest_version() {
local latest_info
if command -v curl >/dev/null 2>&1; then
latest_info=$(curl -fsSL "$GITHUB_API_URL" 2>/dev/null)
elif command -v wget >/dev/null 2>&1; then
latest_info=$(wget -qO- "$GITHUB_API_URL" 2>/dev/null)
else
print_error "Neither curl nor wget is available. Please install one of them." >&2
exit 1
fi
if [ -z "$latest_info" ]; then
print_error "Failed to fetch latest version information" >&2
exit 1
fi
# Extract version from JSON response (works without jq)
local version=$(echo "$latest_info" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
if [ -z "$version" ]; then
print_error "Could not parse version from GitHub API response" >&2
exit 1
fi
# Remove 'v' prefix if present
version=$(echo "$version" | sed 's/^v//')
echo "$version"
}
# Detect OS and architecture
detect_platform() {
local os arch
# Detect OS
case "$(uname -s)" in
Linux*) os="linux" ;;
Darwin*) os="darwin" ;;
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
FreeBSD*) os="freebsd" ;;
*)
print_error "Unsupported operating system: $(uname -s)"
exit 1
;;
esac
# Detect architecture
case "$(uname -m)" in
x86_64|amd64) arch="amd64" ;;
arm64|aarch64) arch="arm64" ;;
armv7l|armv6l)
if [ "$os" = "linux" ]; then
if [ "$(uname -m)" = "armv6l" ]; then
arch="arm32v6"
else
arch="arm32"
fi
else
arch="arm64" # Default for non-Linux ARM
fi
;;
riscv64)
if [ "$os" = "linux" ]; then
arch="riscv64"
else
print_error "RISC-V architecture only supported on Linux"
exit 1
fi
;;
*)
print_error "Unsupported architecture: $(uname -m)"
exit 1
;;
esac
echo "${os}_${arch}"
}
# Get installation directory
get_install_dir() {
if [ "$OS" = "windows" ]; then
echo "$HOME/bin"
else
# Try to use a directory in PATH, fallback to ~/.local/bin
if echo "$PATH" | grep -q "/usr/local/bin"; then
if [ -w "/usr/local/bin" ] 2>/dev/null; then
echo "/usr/local/bin"
else
echo "$HOME/.local/bin"
fi
else
echo "$HOME/.local/bin"
fi
fi
}
# Download and install newt
install_newt() {
local platform="$1"
local install_dir="$2"
local binary_name="newt_${platform}"
local exe_suffix=""
# Add .exe suffix for Windows
if [[ "$platform" == *"windows"* ]]; then
binary_name="${binary_name}.exe"
exe_suffix=".exe"
fi
local download_url="${BASE_URL}/${binary_name}"
local temp_file="/tmp/newt${exe_suffix}"
local final_path="${install_dir}/newt${exe_suffix}"
print_status "Downloading newt from ${download_url}"
# Download the binary
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$download_url" -o "$temp_file"
elif command -v wget >/dev/null 2>&1; then
wget -q "$download_url" -O "$temp_file"
else
print_error "Neither curl nor wget is available. Please install one of them."
exit 1
fi
# Create install directory if it doesn't exist
mkdir -p "$install_dir"
# Move binary to install directory
mv "$temp_file" "$final_path"
# Make executable (not needed on Windows, but doesn't hurt)
chmod +x "$final_path"
print_status "newt installed to ${final_path}"
# Check if install directory is in PATH
if ! echo "$PATH" | grep -q "$install_dir"; then
print_warning "Install directory ${install_dir} is not in your PATH."
print_warning "Add it to your PATH by adding this line to your shell profile:"
print_warning " export PATH=\"${install_dir}:\$PATH\""
fi
}
# Verify installation
verify_installation() {
local install_dir="$1"
local exe_suffix=""
if [[ "$PLATFORM" == *"windows"* ]]; then
exe_suffix=".exe"
fi
local newt_path="${install_dir}/newt${exe_suffix}"
if [ -f "$newt_path" ] && [ -x "$newt_path" ]; then
print_status "Installation successful!"
print_status "newt version: $("$newt_path" --version 2>/dev/null || echo "unknown")"
return 0
else
print_error "Installation failed. Binary not found or not executable."
return 1
fi
}
# Main installation process
main() {
print_status "Installing latest version of newt..."
# Get latest version
print_status "Fetching latest version from GitHub..."
VERSION=$(get_latest_version)
print_status "Latest version: v${VERSION}"
# Set base URL with the fetched version
BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
# Detect platform
PLATFORM=$(detect_platform)
print_status "Detected platform: ${PLATFORM}"
# Get install directory
INSTALL_DIR=$(get_install_dir)
print_status "Install directory: ${INSTALL_DIR}"
# Install newt
install_newt "$PLATFORM" "$INSTALL_DIR"
# Verify installation
if verify_installation "$INSTALL_DIR"; then
print_status "newt is ready to use!"
if [[ "$PLATFORM" == *"windows"* ]]; then
print_status "Run 'newt --help' to get started"
else
print_status "Run 'newt --help' to get started"
fi
else
exit 1
fi
}
# Run main function
main "$@"

53
go.mod
View File

@@ -4,16 +4,55 @@ go 1.23.1
toolchain go1.23.2
require golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
require (
github.com/docker/docker v28.3.2+incompatible
github.com/google/gopacket v1.1.19
github.com/gorilla/websocket v1.5.3
github.com/vishvananda/netlink v1.3.1
golang.org/x/crypto v0.40.0
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
golang.org/x/net v0.42.0
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c
software.sslmate.com/src/go-pkcs12 v0.5.0
)
require (
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // 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/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.30.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
)

179
go.sum
View File

@@ -1,20 +1,175 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA=
github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
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=
google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

83
linux.go Normal file
View File

@@ -0,0 +1,83 @@
//go:build linux
package main
import (
"fmt"
"strings"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/proxy"
"github.com/fosrl/newt/websocket"
"github.com/fosrl/newt/wg"
"github.com/fosrl/newt/wgtester"
)
var wgService *wg.WireGuardService
var wgTesterServer *wgtester.Server
func setupClients(client *websocket.Client) {
var host = endpoint
if strings.HasPrefix(host, "http://") {
host = strings.TrimPrefix(host, "http://")
} else if strings.HasPrefix(host, "https://") {
host = strings.TrimPrefix(host, "https://")
}
host = strings.TrimSuffix(host, "/")
// Create WireGuard service
wgService, err = wg.NewWireGuardService(interfaceName, mtuInt, generateAndSaveKeyTo, host, id, client)
if err != nil {
logger.Fatal("Failed to create WireGuard service: %v", err)
}
defer wgService.Close(rm)
wgTesterServer = wgtester.NewServer("0.0.0.0", wgService.Port, id) // TODO: maybe make this the same ip of the wg server?
err := wgTesterServer.Start()
if err != nil {
logger.Error("Failed to start WireGuard tester server: %v", err)
} else {
// Make sure to stop the server on exit
defer wgTesterServer.Stop()
}
client.OnTokenUpdate(func(token string) {
wgService.SetToken(token)
})
}
func closeClients() {
if wgService != nil {
wgService.Close(rm)
wgService = nil
}
if wgTesterServer != nil {
wgTesterServer.Stop()
wgTesterServer = nil
}
}
func clientsHandleNewtConnection(publicKey string) {
if wgService == nil {
return
}
wgService.SetServerPubKey(publicKey)
}
func clientsOnConnect() {
if wgService == nil {
return
}
wgService.LoadRemoteConfig()
}
func clientsAddProxyTarget(pm *proxy.ProxyManager, tunnelIp string) {
if wgService == nil {
return
}
// add a udp proxy for localost and the wgService port
// TODO: make sure this port is not used in a target
pm.AddTarget("udp", tunnelIp, int(wgService.Port), fmt.Sprintf("127.0.0.1:%d", wgService.Port))
}

View File

@@ -53,7 +53,23 @@ func (l *Logger) log(level LogLevel, format string, args ...interface{}) {
if level < l.level {
return
}
timestamp := time.Now().Format("2006/01/02 15:04:05")
// Get timezone from environment variable or use local timezone
timezone := os.Getenv("LOGGER_TIMEZONE")
var location *time.Location
var err error
if timezone != "" {
location, err = time.LoadLocation(timezone)
if err != nil {
// If invalid timezone, fall back to local
location = time.Local
}
} else {
location = time.Local
}
timestamp := time.Now().In(location).Format("2006/01/02 15:04:05")
message := fmt.Sprintf(format, args...)
l.logger.Printf("%s: %s %s", level.String(), timestamp, message)
}

811
main.go
View File

@@ -1,28 +1,26 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"math/rand"
"net"
"net/http"
"net/netip"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/fosrl/newt/docker"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/proxy"
"github.com/fosrl/newt/updates"
"github.com/fosrl/newt/websocket"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
@@ -47,216 +45,57 @@ type TargetData struct {
Targets []string `json:"targets"`
}
func fixKey(key string) string {
// Remove any whitespace
key = strings.TrimSpace(key)
// Decode from base64
decoded, err := base64.StdEncoding.DecodeString(key)
if err != nil {
logger.Fatal("Error decoding base64:", err)
}
// Convert to hex
return hex.EncodeToString(decoded)
type ExitNodeData struct {
ExitNodes []ExitNode `json:"exitNodes"`
}
func ping(tnet *netstack.Net, dst string) error {
logger.Info("Pinging %s", dst)
socket, err := tnet.Dial("ping4", dst)
if err != nil {
return fmt.Errorf("failed to create ICMP socket: %w", err)
}
defer socket.Close()
requestPing := icmp.Echo{
Seq: rand.Intn(1 << 16),
Data: []byte("gopher burrow"),
}
icmpBytes, err := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil)
if err != nil {
return fmt.Errorf("failed to marshal ICMP message: %w", err)
}
if err := socket.SetReadDeadline(time.Now().Add(time.Second * 10)); err != nil {
return fmt.Errorf("failed to set read deadline: %w", err)
}
start := time.Now()
_, err = socket.Write(icmpBytes)
if err != nil {
return fmt.Errorf("failed to write ICMP packet: %w", err)
}
n, err := socket.Read(icmpBytes[:])
if err != nil {
return fmt.Errorf("failed to read ICMP packet: %w", err)
}
replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n])
if err != nil {
return fmt.Errorf("failed to parse ICMP packet: %w", err)
}
replyPing, ok := replyPacket.Body.(*icmp.Echo)
if !ok {
return fmt.Errorf("invalid reply type: got %T, want *icmp.Echo", replyPacket.Body)
}
if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq {
return fmt.Errorf("invalid ping reply: got seq=%d data=%q, want seq=%d data=%q",
replyPing.Seq, replyPing.Data, requestPing.Seq, requestPing.Data)
}
logger.Info("Ping latency: %v", time.Since(start))
return nil
// ExitNode represents an exit node with an ID, endpoint, and weight.
type ExitNode struct {
ID int `json:"exitNodeId"`
Name string `json:"exitNodeName"`
Endpoint string `json:"endpoint"`
Weight float64 `json:"weight"`
WasPreviouslyConnected bool `json:"wasPreviouslyConnected"`
}
func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
go func() {
for {
select {
case <-ticker.C:
err := ping(tnet, serverIP)
if err != nil {
logger.Warn("Periodic ping failed: %v", err)
logger.Warn("HINT: Do you have UDP port 51280 (or the port in config.yml) open on your Pangolin server?")
}
case <-stopChan:
logger.Info("Stopping ping check")
return
}
}
}()
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"`
}
func pingWithRetry(tnet *netstack.Net, dst string) error {
const (
maxAttempts = 5
retryDelay = 2 * time.Second
)
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
logger.Info("Ping attempt %d of %d", attempt, maxAttempts)
if err := ping(tnet, dst); err != nil {
lastErr = err
logger.Warn("Ping attempt %d failed: %v", attempt, err)
if attempt < maxAttempts {
time.Sleep(retryDelay)
continue
}
return fmt.Errorf("all ping attempts failed after %d tries, last error: %w",
maxAttempts, lastErr)
}
// Successful ping
return nil
}
// This shouldn't be reached due to the return in the loop, but added for completeness
return fmt.Errorf("unexpected error: all ping attempts failed")
}
func parseLogLevel(level string) logger.LogLevel {
switch strings.ToUpper(level) {
case "DEBUG":
return logger.DEBUG
case "INFO":
return logger.INFO
case "WARN":
return logger.WARN
case "ERROR":
return logger.ERROR
case "FATAL":
return logger.FATAL
default:
return logger.INFO // default to INFO if invalid level provided
}
}
func mapToWireGuardLogLevel(level logger.LogLevel) int {
switch level {
case logger.DEBUG:
return device.LogLevelVerbose
// case logger.INFO:
// return device.LogLevel
case logger.WARN:
return device.LogLevelError
case logger.ERROR, logger.FATAL:
return device.LogLevelSilent
default:
return device.LogLevelSilent
}
}
func resolveDomain(domain string) (string, error) {
// Check if there's a port in the domain
host, port, err := net.SplitHostPort(domain)
if err != nil {
// No port found, use the domain as is
host = domain
port = ""
}
// Remove any protocol prefix if present
if strings.HasPrefix(host, "http://") {
host = strings.TrimPrefix(host, "http://")
} else if strings.HasPrefix(host, "https://") {
host = strings.TrimPrefix(host, "https://")
}
// Lookup IP addresses
ips, err := net.LookupIP(host)
if err != nil {
return "", fmt.Errorf("DNS lookup failed: %v", err)
}
if len(ips) == 0 {
return "", fmt.Errorf("no IP addresses found for domain %s", host)
}
// Get the first IPv4 address if available
var ipAddr string
for _, ip := range ips {
if ipv4 := ip.To4(); ipv4 != nil {
ipAddr = ipv4.String()
break
}
}
// If no IPv4 found, use the first IP (might be IPv6)
if ipAddr == "" {
ipAddr = ips[0].String()
}
// Add port back if it existed
if port != "" {
ipAddr = net.JoinHostPort(ipAddr, port)
}
return ipAddr, nil
}
var (
endpoint string
id string
secret string
mtu string
mtuInt int
dns string
privateKey wgtypes.Key
err error
logLevel string
interfaceName string
generateAndSaveKeyTo string
rm bool
acceptClients bool
updownScript string
tlsPrivateKey string
dockerSocket string
dockerEnforceNetworkValidation string
dockerEnforceNetworkValidationBool bool
pingInterval time.Duration
pingTimeout time.Duration
publicKey wgtypes.Key
pingStopChan chan struct{}
stopFunc func()
healthFile string
)
func main() {
var (
endpoint string
id string
secret string
mtu string
mtuInt int
dns string
privateKey wgtypes.Key
err error
logLevel string
)
// if PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET are set as environment variables, they will be used as default values
endpoint = os.Getenv("PANGOLIN_ENDPOINT")
id = os.Getenv("NEWT_ID")
@@ -264,6 +103,17 @@ func main() {
mtu = os.Getenv("MTU")
dns = os.Getenv("DNS")
logLevel = os.Getenv("LOG_LEVEL")
updownScript = os.Getenv("UPDOWN_SCRIPT")
// interfaceName = os.Getenv("INTERFACE")
// generateAndSaveKeyTo = os.Getenv("GENERATE_AND_SAVE_KEY_TO")
// rm = os.Getenv("RM") == "true"
// acceptClients = os.Getenv("ACCEPT_CLIENTS") == "true"
tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT")
dockerSocket = os.Getenv("DOCKER_SOCKET")
pingIntervalStr := os.Getenv("PING_INTERVAL")
pingTimeoutStr := os.Getenv("PING_TIMEOUT")
dockerEnforceNetworkValidation = os.Getenv("DOCKER_ENFORCE_NETWORK_VALIDATION")
healthFile = os.Getenv("HEALTH_FILE")
if endpoint == "" {
flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server")
@@ -283,15 +133,76 @@ func main() {
if logLevel == "" {
flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
}
if updownScript == "" {
flag.StringVar(&updownScript, "updown", "", "Path to updown script to be called when targets are added or removed")
}
// if interfaceName == "" {
// flag.StringVar(&interfaceName, "interface", "wg1", "Name of the WireGuard interface")
// }
// if generateAndSaveKeyTo == "" {
// flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "/tmp/newtkey", "Path to save generated private key")
// }
// flag.BoolVar(&rm, "rm", false, "Remove the WireGuard interface")
// flag.BoolVar(&acceptClients, "accept-clients", false, "Accept clients on the WireGuard interface")
if tlsPrivateKey == "" {
flag.StringVar(&tlsPrivateKey, "tls-client-cert", "", "Path to client certificate used for mTLS")
}
if dockerSocket == "" {
flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)")
}
if pingIntervalStr == "" {
flag.StringVar(&pingIntervalStr, "ping-interval", "3s", "Interval for pinging the server (default 3s)")
}
if pingTimeoutStr == "" {
flag.StringVar(&pingTimeoutStr, "ping-timeout", "5s", " Timeout for each ping (default 3s)")
}
if pingIntervalStr != "" {
pingInterval, err = time.ParseDuration(pingIntervalStr)
if err != nil {
fmt.Printf("Invalid PING_INTERVAL value: %s, using default 3 seconds\n", pingIntervalStr)
pingInterval = 3 * time.Second
}
} else {
pingInterval = 3 * time.Second
}
if pingTimeoutStr != "" {
pingTimeout, err = time.ParseDuration(pingTimeoutStr)
if err != nil {
fmt.Printf("Invalid PING_TIMEOUT value: %s, using default 5 seconds\n", pingTimeoutStr)
pingTimeout = 5 * time.Second
}
} else {
pingTimeout = 5 * time.Second
}
if dockerEnforceNetworkValidation == "" {
flag.StringVar(&dockerEnforceNetworkValidation, "docker-enforce-network-validation", "false", "Enforce validation of container on newt network (true or false)")
}
if healthFile == "" {
flag.StringVar(&healthFile, "health-file", "", "Path to health file (if unset, health file wont be written)")
}
// do a --version check
version := flag.Bool("version", false, "Print the version")
flag.Parse()
logger.Init()
loggerLevel := parseLogLevel(logLevel)
logger.GetLogger().SetLevel(parseLogLevel(logLevel))
// Validate required fields
if endpoint == "" || id == "" || secret == "" {
logger.Fatal("endpoint, id, and secret are required either via CLI flags or environment variables")
newtVersion := "version_replaceme"
if *version {
fmt.Println("Newt version " + newtVersion)
os.Exit(0)
} else {
logger.Info("Newt version " + newtVersion)
}
if err := updates.CheckForUpdate("fosrl", "newt", newtVersion); err != nil {
logger.Error("Error checking for updates: %v\n", err)
}
// parse the mtu string into an int
@@ -300,21 +211,52 @@ func main() {
logger.Fatal("Failed to parse MTU: %v", err)
}
// parse if we want to enforce container network validation
dockerEnforceNetworkValidationBool, err = strconv.ParseBool(dockerEnforceNetworkValidation)
if err != nil {
logger.Info("Docker enforce network validation cannot be parsed. Defaulting to 'false'")
dockerEnforceNetworkValidationBool = false
}
privateKey, err = wgtypes.GeneratePrivateKey()
if err != nil {
logger.Fatal("Failed to generate private key: %v", err)
}
var opt websocket.ClientOption
if tlsPrivateKey != "" {
opt = websocket.WithTLSConfig(tlsPrivateKey)
}
// Create a new client
client, err := websocket.NewClient(
id, // CLI arg takes precedence
secret, // CLI arg takes precedence
endpoint,
pingInterval,
pingTimeout,
opt,
)
if err != nil {
logger.Fatal("Failed to create client: %v", err)
}
// output env var values if set
logger.Debug("Endpoint: %v", endpoint)
logger.Debug("Log Level: %v", logLevel)
logger.Debug("Docker Network Validation Enabled: %v", dockerEnforceNetworkValidationBool)
logger.Debug("TLS Private Key Set: %v", tlsPrivateKey != "")
if dns != "" {
logger.Debug("Dns: %v", dns)
}
if dockerSocket != "" {
logger.Debug("Docker Socket: %v", dockerSocket)
}
if mtu != "" {
logger.Debug("MTU: %v", mtu)
}
if updownScript != "" {
logger.Debug("Up Down Script: %v", updownScript)
}
// Create TUN device and network stack
var tun tun.Device
var tnet *netstack.Net
@@ -323,34 +265,61 @@ func main() {
var connected bool
var wgData WgData
client.RegisterHandler("newt/terminate", func(msg websocket.WSMessage) {
logger.Info("Received terminate message")
if acceptClients {
// make sure we are running on linux
if runtime.GOOS != "linux" {
logger.Fatal("Tunnel management is only supported on Linux right now!")
os.Exit(1)
}
setupClients(client)
}
var pingWithRetryStopChan chan struct{}
closeWgTunnel := func() {
if pingStopChan != nil {
// Stop the ping check
close(pingStopChan)
pingStopChan = nil
}
// Stop proxy manager if running
if pm != nil {
pm.Stop()
pm = nil
}
// Close WireGuard device first - this will automatically close the TUN device
if dev != nil {
dev.Close()
dev = nil
}
client.Close()
})
pingStopChan := make(chan struct{})
defer close(pingStopChan)
// Clear references but don't manually close since dev.Close() already did it
if tnet != nil {
tnet = nil
}
if tun != nil {
tun = nil // Don't call tun.Close() here since dev.Close() already closed it
}
}
// Register handlers for different message types
client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) {
logger.Info("Received registration message")
if stopFunc != nil {
stopFunc() // stop the ws from sending more requests
stopFunc = nil // reset stopFunc to nil to avoid double stopping
}
if connected {
logger.Info("Already connected! But I will send a ping anyway...")
// ping(tnet, wgData.ServerIP)
err = pingWithRetry(tnet, wgData.ServerIP)
if err != nil {
// Handle complete failure after all retries
logger.Warn("Failed to ping %s: %v", wgData.ServerIP, err)
logger.Warn("HINT: Do you have UDP port 51280 (or the port in config.yml) open on your Pangolin server?")
}
return
// Mark as disconnected
closeWgTunnel()
connected = false
}
jsonData, err := json.Marshal(msg.Data)
@@ -364,7 +333,9 @@ func main() {
return
}
logger.Info("Received: %+v", msg)
clientsHandleNewtConnection(wgData.PublicKey)
logger.Debug("Received: %+v", msg)
tun, tnet, err = netstack.CreateNetTUN(
[]netip.Addr{netip.MustParseAddr(wgData.TunnelIP)},
[]netip.Addr{netip.MustParseAddr(dns)},
@@ -379,6 +350,14 @@ func main() {
"wireguard: ",
))
host, _, err := net.SplitHostPort(wgData.Endpoint)
if err != nil {
logger.Error("Failed to split endpoint: %v", err)
return
}
logger.Info("Connecting to endpoint: %s", host)
endpoint, err := resolveDomain(wgData.Endpoint)
if err != nil {
logger.Error("Failed to resolve endpoint: %v", err)
@@ -390,7 +369,7 @@ func main() {
public_key=%s
allowed_ip=%s/32
endpoint=%s
persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(wgData.PublicKey), wgData.ServerIP, endpoint)
persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.PublicKey), wgData.ServerIP, endpoint)
err = dev.IpcSet(config)
if err != nil {
@@ -403,18 +382,30 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
logger.Error("Failed to bring up WireGuard device: %v", err)
}
logger.Info("WireGuard device created. Lets ping the server now...")
// Ping to bring the tunnel up on the server side quickly
// ping(tnet, wgData.ServerIP)
err = pingWithRetry(tnet, wgData.ServerIP)
logger.Debug("WireGuard device created. Lets ping the server now...")
// Even if pingWithRetry returns an error, it will continue trying in the background
if pingWithRetryStopChan != nil {
// Stop the previous pingWithRetry if it exists
close(pingWithRetryStopChan)
pingWithRetryStopChan = nil
}
// Use reliable ping for initial connection test
logger.Debug("Testing initial connection with reliable ping...")
_, err = reliablePing(tnet, wgData.ServerIP, pingTimeout, 5)
if err != nil {
// Handle complete failure after all retries
logger.Error("Failed to ping %s: %v", wgData.ServerIP, err)
logger.Warn("Initial reliable ping failed, but continuing: %v", err)
} else {
logger.Info("Initial connection test successful!")
}
pingWithRetryStopChan, _ = pingWithRetry(tnet, wgData.ServerIP, pingTimeout)
// Always mark as connected and start the proxy manager regardless of initial ping result
// as the pings will continue in the background
if !connected {
logger.Info("Starting ping check")
startPingCheck(tnet, wgData.ServerIP, pingStopChan)
logger.Debug("Starting ping check")
pingStopChan = startPingCheck(tnet, wgData.ServerIP, client)
}
// Create proxy manager
@@ -431,14 +422,199 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
updateTargets(pm, "add", wgData.TunnelIP, "udp", TargetData{Targets: wgData.Targets.UDP})
}
clientsAddProxyTarget(pm, wgData.TunnelIP)
err = pm.Start()
if err != nil {
logger.Error("Failed to start proxy manager: %v", err)
}
})
client.RegisterHandler("newt/wg/reconnect", func(msg websocket.WSMessage) {
logger.Info("Received reconnect message")
// Close the WireGuard device and TUN
closeWgTunnel()
// Mark as disconnected
connected = false
if stopFunc != nil {
stopFunc() // stop the ws from sending more requests
stopFunc = nil // reset stopFunc to nil to avoid double stopping
}
// Request exit nodes from the server
stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second)
logger.Info("Tunnel destroyed, ready for reconnection")
})
client.RegisterHandler("newt/wg/terminate", func(msg websocket.WSMessage) {
logger.Info("Received termination message")
// Close the WireGuard device and TUN
closeWgTunnel()
// Mark as disconnected
connected = false
logger.Info("Tunnel destroyed")
})
client.RegisterHandler("newt/ping/exitNodes", func(msg websocket.WSMessage) {
logger.Info("Received ping message")
if stopFunc != nil {
stopFunc() // stop the ws from sending more requests
stopFunc = nil // reset stopFunc to nil to avoid double stopping
}
// Parse the incoming list of exit nodes
var exitNodeData ExitNodeData
jsonData, err := json.Marshal(msg.Data)
if err != nil {
logger.Info("Error marshaling data: %v", err)
return
}
if err := json.Unmarshal(jsonData, &exitNodeData); err != nil {
logger.Info("Error unmarshaling exit node data: %v", err)
return
}
exitNodes := exitNodeData.ExitNodes
if len(exitNodes) == 0 {
logger.Info("No exit nodes provided")
return
}
// If there is just one exit node, we can skip pinging it and use it directly
if len(exitNodes) == 1 {
logger.Debug("Only one exit node available, using it directly: %s", exitNodes[0].Endpoint)
// Prepare data to send to the cloud for selection
pingResults := []ExitNodePingResult{
{
ExitNodeID: exitNodes[0].ID,
LatencyMs: 0, // No ping latency since we are using it directly
Weight: exitNodes[0].Weight,
Error: "",
Name: exitNodes[0].Name,
Endpoint: exitNodes[0].Endpoint,
WasPreviouslyConnected: exitNodes[0].WasPreviouslyConnected,
},
}
stopFunc = client.SendMessageInterval("newt/wg/register", map[string]interface{}{
"publicKey": publicKey.String(),
"pingResults": pingResults,
"newtVersion": newtVersion,
}, 1*time.Second)
return
}
type nodeResult struct {
Node ExitNode
Latency time.Duration
Err error
}
results := make([]nodeResult, len(exitNodes))
const pingAttempts = 3
for i, node := range exitNodes {
var totalLatency time.Duration
var lastErr error
successes := 0
client := &http.Client{
Timeout: 5 * time.Second,
}
url := node.Endpoint
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url = "http://" + url
}
if !strings.HasSuffix(url, "/ping") {
url = strings.TrimRight(url, "/") + "/ping"
}
for j := 0; j < pingAttempts; j++ {
start := time.Now()
resp, err := client.Get(url)
latency := time.Since(start)
if err != nil {
lastErr = err
logger.Warn("Failed to ping exit node %d (%s) attempt %d: %v", node.ID, url, j+1, err)
continue
}
resp.Body.Close()
totalLatency += latency
successes++
}
var avgLatency time.Duration
if successes > 0 {
avgLatency = totalLatency / time.Duration(successes)
}
if successes == 0 {
results[i] = nodeResult{Node: node, Latency: 0, Err: lastErr}
} else {
results[i] = nodeResult{Node: node, Latency: avgLatency, Err: nil}
}
}
// Prepare data to send to the cloud for selection
var pingResults []ExitNodePingResult
for _, res := range results {
errMsg := ""
if res.Err != nil {
errMsg = res.Err.Error()
}
pingResults = append(pingResults, ExitNodePingResult{
ExitNodeID: res.Node.ID,
LatencyMs: res.Latency.Milliseconds(),
Weight: res.Node.Weight,
Error: errMsg,
Name: res.Node.Name,
Endpoint: res.Node.Endpoint,
WasPreviouslyConnected: res.Node.WasPreviouslyConnected,
})
}
// If we were previously connected and there is at least one other good node,
// exclude the previously connected node from pingResults sent to the cloud.
var filteredPingResults []ExitNodePingResult
previouslyConnectedNodeIdx := -1
for i, res := range pingResults {
if res.WasPreviouslyConnected {
previouslyConnectedNodeIdx = i
}
}
// Count good nodes (latency > 0, no error, not previously connected)
goodNodeCount := 0
for i, res := range pingResults {
if i != previouslyConnectedNodeIdx && res.LatencyMs > 0 && res.Error == "" {
goodNodeCount++
}
}
if previouslyConnectedNodeIdx != -1 && goodNodeCount > 0 {
for i, res := range pingResults {
if i != previouslyConnectedNodeIdx {
filteredPingResults = append(filteredPingResults, res)
}
}
pingResults = filteredPingResults
logger.Info("Excluding previously connected exit node from ping results due to other available nodes")
}
// Send the ping results to the cloud for selection
stopFunc = client.SendMessageInterval("newt/wg/register", map[string]interface{}{
"publicKey": publicKey.String(),
"pingResults": pingResults,
"newtVersion": newtVersion,
}, 1*time.Second)
logger.Debug("Sent exit node ping results to cloud for selection: pingResults=%+v", pingResults)
})
client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) {
logger.Info("Received: %+v", msg)
logger.Debug("Received: %+v", msg)
// if there is no wgData or pm, we can't add targets
if wgData.TunnelIP == "" || pm == nil {
@@ -455,11 +631,6 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
if len(targetData.Targets) > 0 {
updateTargets(pm, "add", wgData.TunnelIP, "tcp", targetData)
}
err = pm.Start()
if err != nil {
logger.Error("Failed to start proxy manager: %v", err)
}
})
client.RegisterHandler("newt/udp/add", func(msg websocket.WSMessage) {
@@ -480,11 +651,6 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
if len(targetData.Targets) > 0 {
updateTargets(pm, "add", wgData.TunnelIP, "udp", targetData)
}
err = pm.Start()
if err != nil {
logger.Error("Failed to start proxy manager: %v", err)
}
})
client.RegisterHandler("newt/udp/remove", func(msg websocket.WSMessage) {
@@ -527,19 +693,92 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
}
})
client.OnConnect(func() error {
publicKey := privateKey.PublicKey()
logger.Debug("Public key: %s", publicKey)
// Register handler for Docker socket check
client.RegisterHandler("newt/socket/check", func(msg websocket.WSMessage) {
logger.Debug("Received Docker socket check request")
err := client.SendMessage("newt/wg/register", map[string]interface{}{
"publicKey": fmt.Sprintf("%s", publicKey),
if dockerSocket == "" {
logger.Debug("Docker socket path is not set")
err := client.SendMessage("newt/socket/status", map[string]interface{}{
"available": false,
"socketPath": dockerSocket,
})
if err != nil {
logger.Error("Failed to send Docker socket check response: %v", err)
}
return
}
// Check if Docker socket is available
isAvailable := docker.CheckSocket(dockerSocket)
// Send response back to server
err := client.SendMessage("newt/socket/status", map[string]interface{}{
"available": isAvailable,
"socketPath": dockerSocket,
})
if err != nil {
logger.Error("Failed to send Docker socket check response: %v", err)
} else {
logger.Info("Docker socket check response sent: available=%t", isAvailable)
}
})
// Register handler for Docker container listing
client.RegisterHandler("newt/socket/fetch", func(msg websocket.WSMessage) {
logger.Debug("Received Docker container fetch request")
if dockerSocket == "" {
logger.Debug("Docker socket path is not set")
return
}
// List Docker containers
containers, err := docker.ListContainers(dockerSocket, dockerEnforceNetworkValidationBool)
if err != nil {
logger.Error("Failed to list Docker containers: %v", err)
return
}
// Send container list back to server
err = client.SendMessage("newt/socket/containers", map[string]interface{}{
"containers": containers,
})
if err != nil {
logger.Error("Failed to send registration message: %v", err)
}
if err != nil {
logger.Error("Failed to send Docker container list: %v", err)
} else {
logger.Info("Docker container list sent, count: %d", len(containers))
}
})
client.OnConnect(func() error {
publicKey = privateKey.PublicKey()
logger.Debug("Public key: %s", publicKey)
logger.Info("Websocket connected")
if !connected {
// request from the server the list of nodes to ping at newt/ping/request
stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second)
logger.Info("Requesting exit nodes from server")
clientsOnConnect()
}
// Send registration message to the server for backward compatibility
err := client.SendMessage("newt/wg/register", map[string]interface{}{
"publicKey": publicKey.String(),
"newtVersion": newtVersion,
"backwardsCompatible": true,
})
if err != nil {
logger.Error("Failed to send registration message: %v", err)
return err
}
logger.Info("Sent registration message")
return nil
})
@@ -554,65 +793,17 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
// Cleanup
dev.Close()
}
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
closeClients()
if pm != nil {
pm.Stop()
}
if err := json.Unmarshal(jsonData, &targetData); err != nil {
logger.Info("Error unmarshaling target data: %v", err)
return targetData, err
if client != nil {
client.Close()
}
return targetData, nil
}
func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto string, targetData TargetData) error {
for _, t := range targetData.Targets {
// Split the first number off of the target with : separator and use as the port
parts := strings.Split(t, ":")
if len(parts) != 3 {
logger.Info("Invalid target format: %s", t)
continue
}
// Get the port as an int
port := 0
_, err := fmt.Sscanf(parts[0], "%d", &port)
if err != nil {
logger.Info("Invalid port: %s", parts[0])
continue
}
if action == "add" {
target := parts[1] + ":" + parts[2]
// 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, target)
} else if action == "remove" {
logger.Info("Removing target with port %d", port)
err := pm.RemoveTarget(proto, tunnelIP, port)
if err != nil {
logger.Error("Failed to remove target: %v", err)
return err
}
}
}
return nil
logger.Info("Exiting...")
os.Exit(0)
}

195
network/network.go Normal file
View File

@@ -0,0 +1,195 @@
package network
import (
"encoding/binary"
"encoding/json"
"fmt"
"log"
"net"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/vishvananda/netlink"
"golang.org/x/net/bpf"
"golang.org/x/net/ipv4"
)
const (
udpProtocol = 17
// EmptyUDPSize is the size of an empty UDP packet
EmptyUDPSize = 28
timeout = time.Second * 10
)
// Server stores data relating to the server
type Server struct {
Hostname string
Addr *net.IPAddr
Port uint16
}
// PeerNet stores data about a peer's endpoint
type PeerNet struct {
Resolved bool
IP net.IP
Port uint16
NewtID string
}
// GetClientIP gets source ip address that will be used when sending data to dstIP
func GetClientIP(dstIP net.IP) net.IP {
routes, err := netlink.RouteGet(dstIP)
if err != nil {
log.Fatalln("Error getting route:", err)
}
return routes[0].Src
}
// HostToAddr resolves a hostname, whether DNS or IP to a valid net.IPAddr
func HostToAddr(hostStr string) *net.IPAddr {
remoteAddrs, err := net.LookupHost(hostStr)
if err != nil {
log.Fatalln("Error parsing remote address:", err)
}
for _, addrStr := range remoteAddrs {
if remoteAddr, err := net.ResolveIPAddr("ip4", addrStr); err == nil {
return remoteAddr
}
}
return nil
}
// SetupRawConn creates an ipv4 and udp only RawConn and applies packet filtering
func SetupRawConn(server *Server, client *PeerNet) *ipv4.RawConn {
packetConn, err := net.ListenPacket("ip4:udp", client.IP.String())
if err != nil {
log.Fatalln("Error creating packetConn:", err)
}
rawConn, err := ipv4.NewRawConn(packetConn)
if err != nil {
log.Fatalln("Error creating rawConn:", err)
}
ApplyBPF(rawConn, server, client)
return rawConn
}
// ApplyBPF constructs a BPF program and applies it to the RawConn
func ApplyBPF(rawConn *ipv4.RawConn, server *Server, client *PeerNet) {
const ipv4HeaderLen = 20
const srcIPOffset = 12
const srcPortOffset = ipv4HeaderLen + 0
const dstPortOffset = ipv4HeaderLen + 2
ipArr := []byte(server.Addr.IP.To4())
ipInt := uint32(ipArr[0])<<(3*8) + uint32(ipArr[1])<<(2*8) + uint32(ipArr[2])<<8 + uint32(ipArr[3])
bpfRaw, err := bpf.Assemble([]bpf.Instruction{
bpf.LoadAbsolute{Off: srcIPOffset, Size: 4},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: ipInt, SkipFalse: 5, SkipTrue: 0},
bpf.LoadAbsolute{Off: srcPortOffset, Size: 2},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(server.Port), SkipFalse: 3, SkipTrue: 0},
bpf.LoadAbsolute{Off: dstPortOffset, Size: 2},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(client.Port), SkipFalse: 1, SkipTrue: 0},
bpf.RetConstant{Val: 1<<(8*4) - 1},
bpf.RetConstant{Val: 0},
})
if err != nil {
log.Fatalln("Error assembling BPF:", err)
}
err = rawConn.SetBPF(bpfRaw)
if err != nil {
log.Fatalln("Error setting BPF:", err)
}
}
// MakePacket constructs a request packet to send to the server
func MakePacket(payload []byte, server *Server, client *PeerNet) []byte {
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}
ipHeader := layers.IPv4{
SrcIP: client.IP,
DstIP: server.Addr.IP,
Version: 4,
TTL: 64,
Protocol: layers.IPProtocolUDP,
}
udpHeader := layers.UDP{
SrcPort: layers.UDPPort(client.Port),
DstPort: layers.UDPPort(server.Port),
}
payloadLayer := gopacket.Payload(payload)
udpHeader.SetNetworkLayerForChecksum(&ipHeader)
gopacket.SerializeLayers(buf, opts, &ipHeader, &udpHeader, &payloadLayer)
return buf.Bytes()
}
// SendPacket sends packet to the Server
func SendPacket(packet []byte, conn *ipv4.RawConn, server *Server, client *PeerNet) error {
fullPacket := MakePacket(packet, server, client)
_, err := conn.WriteToIP(fullPacket, server.Addr)
return err
}
// SendDataPacket sends a JSON payload to the Server
func SendDataPacket(data interface{}, conn *ipv4.RawConn, server *Server, client *PeerNet) error {
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal payload: %v", err)
}
return SendPacket(jsonData, conn, server, client)
}
// RecvPacket receives a UDP packet from server
func RecvPacket(conn *ipv4.RawConn, server *Server, client *PeerNet) ([]byte, int, error) {
err := conn.SetReadDeadline(time.Now().Add(timeout))
if err != nil {
return nil, 0, err
}
response := make([]byte, 4096)
n, err := conn.Read(response)
if err != nil {
return nil, n, err
}
return response, n, nil
}
// RecvDataPacket receives and unmarshals a JSON packet from server
func RecvDataPacket(conn *ipv4.RawConn, server *Server, client *PeerNet) ([]byte, error) {
response, n, err := RecvPacket(conn, server, client)
if err != nil {
return nil, err
}
// Extract payload from UDP packet
payload := response[EmptyUDPSize:n]
return payload, nil
}
// ParseResponse takes a response packet and parses it into an IP and port
func ParseResponse(response []byte) (net.IP, uint16) {
ip := net.IP(response[:4])
port := binary.BigEndian.Uint16(response[4:6])
return ip, port
}

View File

@@ -9,326 +9,345 @@ import (
"time"
"github.com/fosrl/newt/logger"
"golang.zx2c4.com/wireguard/tun/netstack"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
)
// Target represents a proxy target with its address and port
type Target struct {
Address string
Port int
}
// 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
}
// NewProxyManager creates a new proxy manager instance
func NewProxyManager(tnet *netstack.Net) *ProxyManager {
return &ProxyManager{
tnet: tnet,
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),
}
}
func (pm *ProxyManager) AddTarget(protocol, listen string, port int, target string) {
pm.Lock()
defer pm.Unlock()
// AddTarget adds as new target for proxying
func (pm *ProxyManager) AddTarget(proto, listenIP string, port int, targetAddr string) error {
pm.mutex.Lock()
defer pm.mutex.Unlock()
logger.Info("Adding target: %s://%s:%d -> %s", protocol, listen, port, target)
newTarget := ProxyTarget{
Protocol: protocol,
Listen: listen,
Port: port,
Target: target,
cancel: make(chan struct{}),
done: make(chan struct{}),
switch proto {
case "tcp":
if pm.tcpTargets[listenIP] == nil {
pm.tcpTargets[listenIP] = make(map[int]string)
}
pm.tcpTargets[listenIP][port] = targetAddr
case "udp":
if pm.udpTargets[listenIP] == nil {
pm.udpTargets[listenIP] = make(map[int]string)
}
pm.udpTargets[listenIP][port] = targetAddr
default:
return fmt.Errorf("unsupported protocol: %s", proto)
}
pm.targets = append(pm.targets, newTarget)
if pm.running {
return pm.startTarget(proto, listenIP, port, targetAddr)
} else {
logger.Debug("Not adding target because not running")
}
return nil
}
func (pm *ProxyManager) RemoveTarget(protocol, listen string, port int) error {
pm.Lock()
defer pm.Unlock()
func (pm *ProxyManager) RemoveTarget(proto, listenIP string, port int) error {
pm.mutex.Lock()
defer pm.mutex.Unlock()
protocol = strings.ToLower(protocol)
if protocol != "tcp" && protocol != "udp" {
return fmt.Errorf("unsupported protocol: %s", protocol)
}
for i, target := range pm.targets {
if target.Listen == listen &&
target.Port == port &&
strings.ToLower(target.Protocol) == protocol {
// Signal the serving goroutine to stop
select {
case <-target.cancel:
// Channel is already closed, no need to close it again
default:
close(target.cancel)
}
// Close the appropriate listener/connection based on protocol
target.Lock()
switch protocol {
case "tcp":
if target.listener != nil {
select {
case <-target.cancel:
// Listener was already closed by Stop()
default:
target.listener.Close()
}
}
case "udp":
if target.udpConn != nil {
select {
case <-target.cancel:
// Connection was already closed by Stop()
default:
target.udpConn.Close()
}
switch proto {
case "tcp":
if targets, ok := pm.tcpTargets[listenIP]; ok {
delete(targets, port)
// Remove and close the corresponding TCP listener
for i, listener := range pm.listeners {
if addr, ok := listener.Addr().(*net.TCPAddr); ok && addr.Port == port {
listener.Close()
time.Sleep(50 * time.Millisecond)
// Remove from slice
pm.listeners = append(pm.listeners[:i], pm.listeners[i+1:]...)
break
}
}
target.Unlock()
// Wait for the target to fully stop
<-target.done
// Remove the target from the slice
pm.targets = append(pm.targets[:i], pm.targets[i+1:]...)
return nil
}
}
return fmt.Errorf("target not found for %s %s:%d", protocol, listen, port)
}
func (pm *ProxyManager) Start() error {
pm.RLock()
defer pm.RUnlock()
for i := range pm.targets {
target := &pm.targets[i]
target.Lock()
// If target is already running, skip it
if target.listener != nil || target.udpConn != nil {
target.Unlock()
continue
}
// Mark the target as starting by creating a nil listener/connection
// This prevents other goroutines from trying to start it
if strings.ToLower(target.Protocol) == "tcp" {
target.listener = nil
} else {
target.udpConn = nil
return fmt.Errorf("target not found: %s:%d", listenIP, port)
}
target.Unlock()
case "udp":
if targets, ok := pm.udpTargets[listenIP]; ok {
delete(targets, port)
// Remove and close the corresponding UDP connection
for i, conn := range pm.udpConns {
if addr, ok := conn.LocalAddr().(*net.UDPAddr); ok && addr.Port == port {
conn.Close()
time.Sleep(50 * time.Millisecond)
// Remove from slice
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...)
break
}
}
} else {
return fmt.Errorf("target not found: %s:%d", listenIP, port)
}
default:
return fmt.Errorf("unsupported protocol: %s", proto)
}
return nil
}
switch strings.ToLower(target.Protocol) {
case "tcp":
go pm.serveTCP(target)
case "udp":
go pm.serveUDP(target)
default:
return fmt.Errorf("unsupported protocol: %s", target.Protocol)
// Start begins listening for all configured proxy targets
func (pm *ProxyManager) Start() error {
pm.mutex.Lock()
defer pm.mutex.Unlock()
if pm.running {
return nil
}
// Start TCP targets
for listenIP, targets := range pm.tcpTargets {
for port, targetAddr := range targets {
if err := pm.startTarget("tcp", listenIP, port, targetAddr); err != nil {
return fmt.Errorf("failed to start TCP target: %v", err)
}
}
}
// Start UDP targets
for listenIP, targets := range pm.udpTargets {
for port, targetAddr := range targets {
if err := pm.startTarget("udp", listenIP, port, targetAddr); err != nil {
return fmt.Errorf("failed to start UDP target: %v", err)
}
}
}
pm.running = true
return nil
}
func (pm *ProxyManager) Stop() error {
pm.Lock()
defer pm.Unlock()
pm.mutex.Lock()
defer pm.mutex.Unlock()
var wg sync.WaitGroup
for i := range pm.targets {
target := &pm.targets[i]
wg.Add(1)
go func(t *ProxyTarget) {
defer wg.Done()
close(t.cancel)
t.Lock()
if t.listener != nil {
t.listener.Close()
}
if t.udpConn != nil {
t.udpConn.Close()
}
t.Unlock()
// Wait for the target to fully stop
<-t.done
}(target)
if !pm.running {
return nil
}
wg.Wait()
// Set running to false first to signal handlers to stop
pm.running = false
// Close TCP listeners
for i := len(pm.listeners) - 1; i >= 0; i-- {
listener := pm.listeners[i]
if err := listener.Close(); err != nil {
logger.Error("Error closing TCP listener: %v", err)
}
// Remove from slice
pm.listeners = append(pm.listeners[:i], pm.listeners[i+1:]...)
}
// Close UDP connections
for i := len(pm.udpConns) - 1; i >= 0; i-- {
conn := pm.udpConns[i]
if err := conn.Close(); err != nil {
logger.Error("Error closing UDP connection: %v", err)
}
// Remove from slice
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...)
}
// Clear the target maps
for k := range pm.tcpTargets {
delete(pm.tcpTargets, k)
}
for k := range pm.udpTargets {
delete(pm.udpTargets, k)
}
// Give active connections a chance to close gracefully
time.Sleep(100 * time.Millisecond)
return nil
}
func (pm *ProxyManager) serveTCP(target *ProxyTarget) {
defer close(target.done) // Signal that this target is fully stopped
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)
}
listener, err := pm.tnet.ListenTCP(&net.TCPAddr{
IP: net.ParseIP(target.Listen),
Port: target.Port,
})
if err != nil {
logger.Info("Failed to start TCP listener for %s:%d: %v", target.Listen, target.Port, err)
return
pm.listeners = append(pm.listeners, listener)
go pm.handleTCPProxy(listener, 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)
}
pm.udpConns = append(pm.udpConns, conn)
go pm.handleUDPProxy(conn, targetAddr)
default:
return fmt.Errorf("unsupported protocol: %s", proto)
}
target.Lock()
target.listener = listener
target.Unlock()
logger.Info("Started %s proxy to %s", proto, targetAddr)
logger.Debug("Started %s proxy from %s:%d to %s", proto, listenIP, port, targetAddr)
defer listener.Close()
logger.Info("TCP proxy listening on %s", listener.Addr())
var activeConns sync.WaitGroup
acceptDone := make(chan struct{})
// Goroutine to handle shutdown signal
go func() {
<-target.cancel
close(acceptDone)
listener.Close()
}()
return nil
}
func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string) {
for {
conn, err := listener.Accept()
if err != nil {
select {
case <-target.cancel:
// Wait for active connections to finish
activeConns.Wait()
// Check if we're shutting down or the listener was closed
if !pm.running {
return
default:
logger.Info("Failed to accept TCP connection: %v", err)
// Don't return here, try to accept new connections
time.Sleep(time.Second)
continue
}
// Check for specific network errors that indicate the listener is closed
if ne, ok := err.(net.Error); ok && !ne.Temporary() {
logger.Info("TCP listener closed, stopping proxy handler for %v", listener.Addr())
return
}
logger.Error("Error accepting TCP connection: %v", err)
// Don't hammer the CPU if we hit a temporary error
time.Sleep(100 * time.Millisecond)
continue
}
activeConns.Add(1)
go func() {
defer activeConns.Done()
pm.handleTCPConnection(conn, target.Target, acceptDone)
target, err := net.Dial("tcp", targetAddr)
if err != nil {
logger.Error("Error connecting to target: %v", err)
conn.Close()
return
}
// Create a WaitGroup to ensure both copy operations complete
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
io.Copy(target, conn)
target.Close()
}()
go func() {
defer wg.Done()
io.Copy(conn, target)
conn.Close()
}()
// Wait for both copies to complete
wg.Wait()
}()
}
}
func (pm *ProxyManager) handleTCPConnection(clientConn net.Conn, target string, done chan struct{}) {
defer clientConn.Close()
serverConn, err := net.Dial("tcp", target)
if err != nil {
logger.Info("Failed to connect to target %s: %v", target, err)
return
}
defer serverConn.Close()
var wg sync.WaitGroup
wg.Add(2)
// Client -> Server
go func() {
defer wg.Done()
select {
case <-done:
return
default:
io.Copy(serverConn, clientConn)
}
}()
// Server -> Client
go func() {
defer wg.Done()
select {
case <-done:
return
default:
io.Copy(clientConn, serverConn)
}
}()
wg.Wait()
}
func (pm *ProxyManager) serveUDP(target *ProxyTarget) {
defer close(target.done) // Signal that this target is fully stopped
addr := &net.UDPAddr{
IP: net.ParseIP(target.Listen),
Port: target.Port,
}
conn, err := pm.tnet.ListenUDP(addr)
if err != nil {
logger.Info("Failed to start UDP listener for %s:%d: %v", target.Listen, target.Port, err)
return
}
target.Lock()
target.udpConn = conn
target.Unlock()
defer conn.Close()
logger.Info("UDP proxy listening on %s", conn.LocalAddr())
buffer := make([]byte, 65535)
var activeConns sync.WaitGroup
func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
buffer := make([]byte, 65507) // Max UDP packet size
clientConns := make(map[string]*net.UDPConn)
var clientsMutex sync.RWMutex
for {
select {
case <-target.cancel:
activeConns.Wait() // Wait for all active UDP handlers to complete
return
default:
n, remoteAddr, err := conn.ReadFrom(buffer)
if err != nil {
select {
case <-target.cancel:
activeConns.Wait()
return
default:
logger.Info("Failed to read UDP packet: %v", err)
continue
}
n, remoteAddr, err := conn.ReadFrom(buffer)
if err != nil {
if !pm.running {
return
}
targetAddr, err := net.ResolveUDPAddr("udp", target.Target)
// Check for connection closed conditions
if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") {
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()
return
}
logger.Error("Error reading UDP packet: %v", err)
continue
}
clientKey := remoteAddr.String()
clientsMutex.RLock()
targetConn, exists := clientConns[clientKey]
clientsMutex.RUnlock()
if !exists {
targetUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr)
if err != nil {
logger.Info("Failed to resolve target address %s: %v", target.Target, err)
logger.Error("Error resolving target address: %v", err)
continue
}
activeConns.Add(1)
go func(data []byte, remote net.Addr) {
defer activeConns.Done()
targetConn, err := net.DialUDP("udp", nil, targetAddr)
if err != nil {
logger.Info("Failed to connect to target %s: %v", target.Target, err)
return
}
defer targetConn.Close()
targetConn, err = net.DialUDP("udp", nil, targetUDPAddr)
if err != nil {
logger.Error("Error connecting to target: %v", err)
continue
}
select {
case <-target.cancel:
return
default:
_, err = targetConn.Write(data)
clientsMutex.Lock()
clientConns[clientKey] = targetConn
clientsMutex.Unlock()
go func() {
buffer := make([]byte, 65507)
for {
n, _, err := targetConn.ReadFromUDP(buffer)
if err != nil {
logger.Info("Failed to write to target: %v", err)
logger.Error("Error reading from target: %v", err)
return
}
response := make([]byte, 65535)
n, err := targetConn.Read(response)
_, err = conn.WriteTo(buffer[:n], remoteAddr)
if err != nil {
logger.Info("Failed to read response from target: %v", err)
logger.Error("Error writing to client: %v", err)
return
}
_, err = conn.WriteTo(response[:n], remote)
if err != nil {
logger.Info("Failed to write response to client: %v", err)
}
}
}(buffer[:n], remoteAddr)
}()
}
_, err = targetConn.Write(buffer[:n])
if err != nil {
logger.Error("Error writing to target: %v", err)
targetConn.Close()
clientsMutex.Lock()
delete(clientConns, clientKey)
clientsMutex.Unlock()
}
}
}

View File

@@ -1,28 +0,0 @@
package proxy
import (
"log"
"net"
"sync"
"golang.zx2c4.com/wireguard/tun/netstack"
)
type ProxyTarget struct {
Protocol string
Listen string
Port int
Target string
cancel chan struct{} // Channel to signal shutdown
done chan struct{} // Channel to signal completion
listener net.Listener // For TCP
udpConn net.PacketConn // For UDP
sync.Mutex // Protect access to connection
}
type ProxyManager struct {
targets []ProxyTarget
tnet *netstack.Net
log *log.Logger
sync.RWMutex // Protect access to targets slice
}

125
self-signed-certs-for-mtls.sh Executable file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env bash
set -eu
echo -n "Enter username for certs (eg alice): "
read CERT_USERNAME
echo
echo -n "Enter domain of user (eg example.com): "
read DOMAIN
echo
# Prompt for password at the start
echo -n "Enter password for certificate: "
read -s PASSWORD
echo
echo -n "Confirm password: "
read -s PASSWORD2
echo
if [ "$PASSWORD" != "$PASSWORD2" ]; then
echo "Passwords don't match!"
exit 1
fi
CA_DIR="./certs/ca"
CLIENT_DIR="./certs/clients"
FILE_PREFIX=$(echo "$CERT_USERNAME-at-$DOMAIN" | sed 's/\./-/')
mkdir -p "$CA_DIR"
mkdir -p "$CLIENT_DIR"
if [ ! -f "$CA_DIR/ca.crt" ]; then
# Generate CA private key
openssl genrsa -out "$CA_DIR/ca.key" 4096
echo "CA key ✅"
# Generate CA root certificate
openssl req -x509 -new -nodes \
-key "$CA_DIR/ca.key" \
-sha256 \
-days 3650 \
-out "$CA_DIR/ca.crt" \
-subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=ca.$DOMAIN"
echo "CA cert ✅"
fi
# Generate client private key
openssl genrsa -aes256 -passout pass:"$PASSWORD" -out "$CLIENT_DIR/$FILE_PREFIX.key" 2048
echo "Client key ✅"
# Generate client Certificate Signing Request (CSR)
openssl req -new \
-key "$CLIENT_DIR/$FILE_PREFIX.key" \
-out "$CLIENT_DIR/$FILE_PREFIX.csr" \
-passin pass:"$PASSWORD" \
-subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=$CERT_USERNAME@$DOMAIN"
echo "Client cert ✅"
echo -n "Signing client cert..."
# Create client certificate configuration file
cat > "$CLIENT_DIR/$FILE_PREFIX.ext" << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = $DOMAIN
EOF
# Generate client certificate signed by CA
openssl x509 -req \
-in "$CLIENT_DIR/$FILE_PREFIX.csr" \
-CA "$CA_DIR/ca.crt" \
-CAkey "$CA_DIR/ca.key" \
-CAcreateserial \
-out "$CLIENT_DIR/$FILE_PREFIX.crt" \
-days 365 \
-sha256 \
-extfile "$CLIENT_DIR/$FILE_PREFIX.ext"
# Verify the client certificate
openssl verify -CAfile "$CA_DIR/ca.crt" "$CLIENT_DIR/$FILE_PREFIX.crt"
echo "Signed ✅"
# Create encrypted PEM bundle
openssl rsa -in "$CLIENT_DIR/$FILE_PREFIX.key" -passin pass:"$PASSWORD" \
| cat "$CLIENT_DIR/$FILE_PREFIX.crt" - > "$CLIENT_DIR/$FILE_PREFIX-bundle.enc.pem"
# Convert to PKCS12
echo "Converting to PKCS12 format..."
openssl pkcs12 -export \
-out "$CLIENT_DIR/$FILE_PREFIX.enc.p12" \
-inkey "$CLIENT_DIR/$FILE_PREFIX.key" \
-in "$CLIENT_DIR/$FILE_PREFIX.crt" \
-certfile "$CA_DIR/ca.crt" \
-name "$CERT_USERNAME@$DOMAIN" \
-passin pass:"$PASSWORD" \
-passout pass:"$PASSWORD"
echo "Converted to encrypted p12 for macOS ✅"
# Convert to PKCS12 format without encryption
echo "Converting to non-encrypted PKCS12 format..."
openssl pkcs12 -export \
-out "$CLIENT_DIR/$FILE_PREFIX.p12" \
-inkey "$CLIENT_DIR/$FILE_PREFIX.key" \
-in "$CLIENT_DIR/$FILE_PREFIX.crt" \
-certfile "$CA_DIR/ca.crt" \
-name "$CERT_USERNAME@$DOMAIN" \
-passin pass:"$PASSWORD" \
-passout pass:""
echo "Converted to non-encrypted p12 ✅"
# Clean up intermediate files
rm "$CLIENT_DIR/$FILE_PREFIX.csr" "$CLIENT_DIR/$FILE_PREFIX.ext" "$CA_DIR/ca.srl"
echo
echo
echo "CA certificate: $CA_DIR/ca.crt"
echo "CA private key: $CA_DIR/ca.key"
echo "Client certificate: $CLIENT_DIR/$FILE_PREFIX.crt"
echo "Client private key: $CLIENT_DIR/$FILE_PREFIX.key"
echo "Client cert bundle: $CLIENT_DIR/$FILE_PREFIX.p12"
echo "Client cert bundle (encrypted): $CLIENT_DIR/$FILE_PREFIX.enc.p12"

32
stub.go Normal file
View File

@@ -0,0 +1,32 @@
//go:build !linux
package main
import (
"github.com/fosrl/newt/proxy"
"github.com/fosrl/newt/websocket"
)
func setupClients(client *websocket.Client) {
return // This function is not implemented for non-Linux systems.
}
func closeClients() {
// This function is not implemented for non-Linux systems.
return
}
func clientsHandleNewtConnection(publicKey string) {
// This function is not implemented for non-Linux systems.
return
}
func clientsOnConnect() {
// This function is not implemented for non-Linux systems.
return
}
func clientsAddProxyTarget(pm *proxy.ProxyManager, tunnelIp string) {
// This function is not implemented for non-Linux systems.
return
}

173
updates/updates.go Normal file
View File

@@ -0,0 +1,173 @@
package updates
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"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"`
}
// Version represents a semantic version
type Version struct {
Major int
Minor int
Patch int
}
// parseVersion parses a semantic version string (e.g., "v1.2.3" or "1.2.3")
func parseVersion(versionStr string) (Version, error) {
// Remove 'v' prefix if present
versionStr = strings.TrimPrefix(versionStr, "v")
parts := strings.Split(versionStr, ".")
if len(parts) != 3 {
return Version{}, fmt.Errorf("invalid version format: %s", versionStr)
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return Version{}, fmt.Errorf("invalid major version: %s", parts[0])
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return Version{}, fmt.Errorf("invalid minor version: %s", parts[1])
}
patch, err := strconv.Atoi(parts[2])
if err != nil {
return Version{}, fmt.Errorf("invalid patch version: %s", parts[2])
}
return Version{Major: major, Minor: minor, Patch: patch}, nil
}
// isNewer returns true if v2 is newer than v1
func (v1 Version) isNewer(v2 Version) bool {
if v2.Major > v1.Major {
return true
}
if v2.Major < v1.Major {
return false
}
if v2.Minor > v1.Minor {
return true
}
if v2.Minor < v1.Minor {
return false
}
return v2.Patch > v1.Patch
}
// String returns the version as a string
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
func CheckForUpdate(owner, repo, currentVersion string) error {
if currentVersion == "version_replaceme" {
return nil
}
// GitHub API URL for latest release
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
// Create HTTP client with timeout
client := &http.Client{
Timeout: 10 * time.Second,
}
// Make the request
resp, err := client.Get(url)
if err != nil {
return fmt.Errorf("failed to fetch release info: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("GitHub 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 current and latest versions
currentVer, err := parseVersion(currentVersion)
if err != nil {
return fmt.Errorf("invalid current version: %w", err)
}
latestVer, err := parseVersion(release.TagName)
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(), release.HTMLURL)
}
return nil
}
// printUpdateBanner prints a colorful update notification banner
func printUpdateBanner(currentVersion, latestVersion, releaseURL string) {
const contentWidth = 70 // width between the border lines
borderTop := "╔" + strings.Repeat("═", contentWidth) + "╗"
borderMid := "╠" + strings.Repeat("═", contentWidth) + "╣"
borderBot := "╚" + strings.Repeat("═", contentWidth) + "╝"
emptyLine := "║" + strings.Repeat(" ", contentWidth) + "║"
lines := []string{
borderTop,
"║" + centerText("UPDATE AVAILABLE", contentWidth) + "║",
borderMid,
emptyLine,
"║ Current Version: " + padRight(currentVersion, contentWidth-19) + "║",
"║ Latest Version: " + padRight(latestVersion, contentWidth-19) + "║",
emptyLine,
"║ A newer version is available! Please update to get the" + padRight("", contentWidth-56) + "║",
"║ latest features, bug fixes, and security improvements." + padRight("", contentWidth-56) + "║",
emptyLine,
"║ Release URL: " + padRight(releaseURL, contentWidth-15) + "║",
emptyLine,
borderBot,
}
for _, line := range lines {
fmt.Println(line)
}
}
// padRight pads s with spaces on the right to the given width
func padRight(s string, width int) string {
if len(s) > width {
return s[:width]
}
return s + strings.Repeat(" ", width-len(s))
}
// centerText centers s in a field of width w
func centerText(s string, w int) string {
if len(s) >= w {
return s[:w]
}
padding := (w - len(s)) / 2
return strings.Repeat(" ", padding) + s + strings.Repeat(" ", w-len(s)-padding)
}

77
updown.py Normal file
View File

@@ -0,0 +1,77 @@
"""
Sample updown script for Newt proxy
Usage: update.py <action> <protocol> <target>
Parameters:
- action: 'add' or 'remove'
- protocol: 'tcp' or 'udp'
- target: the target address in format 'host:port'
If the action is 'add', the script can return a modified target that
will be used instead of the original.
"""
import sys
import logging
import json
from datetime import datetime
# Configure logging
LOG_FILE = "/tmp/newt-updown.log"
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def log_event(action, protocol, target):
"""Log each event to a file for auditing purposes"""
timestamp = datetime.now().isoformat()
event = {
"timestamp": timestamp,
"action": action,
"protocol": protocol,
"target": target
}
logging.info(json.dumps(event))
def handle_add(protocol, target):
"""Handle 'add' action"""
logging.info(f"Adding {protocol} target: {target}")
def handle_remove(protocol, target):
"""Handle 'remove' action"""
logging.info(f"Removing {protocol} target: {target}")
# For remove action, no return value is expected or used
def main():
# Check arguments
if len(sys.argv) != 4:
logging.error(f"Invalid arguments: {sys.argv}")
sys.exit(1)
action = sys.argv[1]
protocol = sys.argv[2]
target = sys.argv[3]
# Log the event
log_event(action, protocol, target)
# Handle the action
if action == "add":
new_target = handle_add(protocol, target)
# Print the new target to stdout (if empty, no change will be made)
if new_target and new_target != target:
print(new_target)
elif action == "remove":
handle_remove(protocol, target)
else:
logging.error(f"Unknown action: {action}")
sys.exit(1)
if __name__ == "__main__":
try:
main()
except Exception as e:
logging.error(f"Unhandled exception: {e}")
sys.exit(1)

560
util.go Normal file
View File

@@ -0,0 +1,560 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"os"
"os/exec"
"strings"
"time"
"math/rand"
"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/device"
"golang.zx2c4.com/wireguard/tun/netstack"
)
func fixKey(key string) string {
// Remove any whitespace
key = strings.TrimSpace(key)
// Decode from base64
decoded, err := base64.StdEncoding.DecodeString(key)
if err != nil {
logger.Fatal("Error decoding base64: %v", err)
}
// Convert to hex
return hex.EncodeToString(decoded)
}
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("Failed to write health file: %v", 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("Failed to write health file: %v", err)
}
}
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")
}
func startPingCheck(tnet *netstack.Net, serverIP string, client *websocket.Client) 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 consecutiveFailures >= failureThreshold && currentInterval < maxInterval {
if !connectionLost {
connectionLost = true
logger.Warn("Connection to server lost after %d failures. Continuous reconnection attempts will be made.", consecutiveFailures)
stopFunc = client.SendMessageInterval("newt/ping/request", map[string]interface{}{}, 3*time.Second)
// Send registration message to the server for backward compatibility
err := client.SendMessage("newt/wg/register", map[string]interface{}{
"publicKey": publicKey.String(),
"backwardsCompatible": true,
})
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)
}
}
}
currentInterval = time.Duration(float64(currentInterval) * 1.3) // Slower increase
if currentInterval > maxInterval {
currentInterval = maxInterval
}
ticker.Reset(currentInterval)
logger.Debug("Increased ping check interval to %v due to consecutive failures", currentInterval)
}
} else {
// Track recent latencies
recentLatencies = append(recentLatencies, latency)
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 parseLogLevel(level string) logger.LogLevel {
switch strings.ToUpper(level) {
case "DEBUG":
return logger.DEBUG
case "INFO":
return logger.INFO
case "WARN":
return logger.WARN
case "ERROR":
return logger.ERROR
case "FATAL":
return logger.FATAL
default:
return logger.INFO // default to INFO if invalid level provided
}
}
func mapToWireGuardLogLevel(level logger.LogLevel) int {
switch level {
case logger.DEBUG:
return device.LogLevelVerbose
// case logger.INFO:
// return device.LogLevel
case logger.WARN:
return device.LogLevelError
case logger.ERROR, logger.FATAL:
return device.LogLevelSilent
default:
return device.LogLevelSilent
}
}
func resolveDomain(domain string) (string, error) {
// Check if there's a port in the domain
host, port, err := net.SplitHostPort(domain)
if err != nil {
// No port found, use the domain as is
host = domain
port = ""
}
// Remove any protocol prefix if present
if strings.HasPrefix(host, "http://") {
host = strings.TrimPrefix(host, "http://")
} else if strings.HasPrefix(host, "https://") {
host = strings.TrimPrefix(host, "https://")
}
// if there are any trailing slashes, remove them
host = strings.TrimSuffix(host, "/")
// Lookup IP addresses
ips, err := net.LookupIP(host)
if err != nil {
return "", fmt.Errorf("DNS lookup failed: %v", err)
}
if len(ips) == 0 {
return "", fmt.Errorf("no IP addresses found for domain %s", host)
}
// Get the first IPv4 address if available
var ipAddr string
for _, ip := range ips {
if ipv4 := ip.To4(); ipv4 != nil {
ipAddr = ipv4.String()
break
}
}
// If no IPv4 found, use the first IP (might be IPv6)
if ipAddr == "" {
ipAddr = ips[0].String()
}
// Add port back if it existed
if port != "" {
ipAddr = net.JoinHostPort(ipAddr, port)
}
return ipAddr, nil
}
func 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
}
func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto string, targetData TargetData) error {
for _, t := range targetData.Targets {
// Split the first number off of the target with : separator and use as the port
parts := strings.Split(t, ":")
if len(parts) != 3 {
logger.Info("Invalid target format: %s", t)
continue
}
// Get the port as an int
port := 0
_, err := fmt.Sscanf(parts[0], "%d", &port)
if err != nil {
logger.Info("Invalid port: %s", parts[0])
continue
}
if action == "add" {
target := parts[1] + ":" + parts[2]
// 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)
} else if action == "remove" {
logger.Info("Removing target with port %d", port)
target := parts[1] + ":" + parts[2]
// Call updown script if provided
if updownScript != "" {
_, err := executeUpdownScript(action, proto, target)
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
}
}
}
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
}

View File

@@ -2,32 +2,38 @@ package websocket
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/fosrl/newt/logger"
"software.sslmate.com/src/go-pkcs12"
"github.com/fosrl/newt/logger"
"github.com/gorilla/websocket"
)
type Client struct {
conn *websocket.Conn
config *Config
baseURL string
handlers map[string]MessageHandler
done chan struct{}
handlersMux sync.RWMutex
conn *websocket.Conn
config *Config
baseURL string
handlers map[string]MessageHandler
done chan struct{}
handlersMux sync.RWMutex
reconnectInterval time.Duration
isConnected bool
reconnectMux sync.RWMutex
onConnect func() error
pingInterval time.Duration
pingTimeout time.Duration
onConnect func() error
onTokenUpdate func(token string)
writeMux sync.Mutex
}
type ClientOption func(*Client)
@@ -41,12 +47,22 @@ func WithBaseURL(url string) ClientOption {
}
}
func WithTLSConfig(tlsClientCertPath string) ClientOption {
return func(c *Client) {
c.config.TlsClientCert = tlsClientCertPath
}
}
func (c *Client) OnConnect(callback func() error) {
c.onConnect = callback
}
func (c *Client) OnTokenUpdate(callback func(token string)) {
c.onTokenUpdate = callback
}
// NewClient creates a new Newt client
func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*Client, error) {
func NewClient(newtID, secret string, endpoint string, pingInterval time.Duration, pingTimeout time.Duration, opts ...ClientOption) (*Client, error) {
config := &Config{
NewtID: newtID,
Secret: secret,
@@ -58,12 +74,17 @@ func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*C
baseURL: endpoint, // default value
handlers: make(map[string]MessageHandler),
done: make(chan struct{}),
reconnectInterval: 10 * time.Second,
reconnectInterval: 3 * time.Second,
isConnected: false,
pingInterval: pingInterval,
pingTimeout: pingTimeout,
}
// Apply options before loading config
for _, opt := range opts {
if opt == nil {
continue
}
opt(client)
}
@@ -81,16 +102,31 @@ func (c *Client) Connect() error {
return nil
}
// Close closes the WebSocket connection
// Close closes the WebSocket connection gracefully
func (c *Client) Close() error {
close(c.done)
if c.conn != nil {
return c.conn.Close()
// Signal shutdown to all goroutines first
select {
case <-c.done:
// Already closed
return nil
default:
close(c.done)
}
// stop the ping monitor
// Set connection status to false
c.setConnected(false)
// Close the WebSocket connection gracefully
if c.conn != nil {
// Send close message
c.writeMux.Lock()
c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
c.writeMux.Unlock()
// Close the connection
return c.conn.Close()
}
return nil
}
@@ -105,9 +141,39 @@ func (c *Client) SendMessage(messageType string, data interface{}) error {
Data: data,
}
logger.Debug("Sending message: %s, data: %+v", messageType, data)
c.writeMux.Lock()
defer c.writeMux.Unlock()
return c.conn.WriteJSON(msg)
}
func (c *Client) SendMessageInterval(messageType string, data interface{}, interval time.Duration) (stop func()) {
stopChan := make(chan struct{})
go func() {
err := c.SendMessage(messageType, data) // Send immediately
if err != nil {
logger.Error("Failed to send initial message: %v", err)
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
err = c.SendMessage(messageType, data)
if err != nil {
logger.Error("Failed to send message: %v", err)
}
case <-stopChan:
return
}
}
}()
return func() {
close(stopChan)
}
}
// RegisterHandler registers a handler for a specific message type
func (c *Client) RegisterHandler(messageType string, handler MessageHandler) {
c.handlersMux.Lock()
@@ -115,30 +181,6 @@ func (c *Client) RegisterHandler(messageType string, handler MessageHandler) {
c.handlers[messageType] = handler
}
// readPump pumps messages from the WebSocket connection
func (c *Client) readPump() {
defer c.conn.Close()
for {
select {
case <-c.done:
return
default:
var msg WSMessage
err := c.conn.ReadJSON(&msg)
if err != nil {
return
}
c.handlersMux.RLock()
if handler, ok := c.handlers[msg.Type]; ok {
handler(msg)
}
c.handlersMux.RUnlock()
}
}
}
func (c *Client) getToken() (string, error) {
// Parse the base URL to ensure we have the correct hostname
baseURL, err := url.Parse(c.baseURL)
@@ -149,48 +191,11 @@ func (c *Client) getToken() (string, error) {
// Ensure we have the base URL without trailing slashes
baseEndpoint := strings.TrimRight(baseURL.String(), "/")
// If we already have a token, try to use it
if c.config.Token != "" {
tokenCheckData := map[string]interface{}{
"newtId": c.config.NewtID,
"secret": c.config.Secret,
"token": c.config.Token,
}
jsonData, err := json.Marshal(tokenCheckData)
var tlsConfig *tls.Config = nil
if c.config.TlsClientCert != "" {
tlsConfig, err = loadClientCertificate(c.config.TlsClientCert)
if err != nil {
return "", fmt.Errorf("failed to marshal token check data: %w", err)
}
// Create a new request
req, err := http.NewRequest(
"POST",
baseEndpoint+"/api/v1/auth/newt/get-token",
bytes.NewBuffer(jsonData),
)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
// Make the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to check token validity: %w", err)
}
defer resp.Body.Close()
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return "", fmt.Errorf("failed to decode token check response: %w", err)
}
// If token is still valid, return it
if tokenResp.Success && tokenResp.Message == "Token session already valid" {
return c.config.Token, nil
return "", fmt.Errorf("failed to load certificate %s: %w", c.config.TlsClientCert, err)
}
}
@@ -220,14 +225,25 @@ func (c *Client) getToken() (string, error) {
// Make the request
client := &http.Client{}
if tlsConfig != nil {
client.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to request new token: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Error("Failed to get token with status code: %d", resp.StatusCode)
return "", fmt.Errorf("failed to get token with status code: %d", resp.StatusCode)
}
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
logger.Error("Failed to decode token response.")
return "", fmt.Errorf("failed to decode token response: %w", err)
}
@@ -239,6 +255,8 @@ func (c *Client) getToken() (string, error) {
return "", fmt.Errorf("received empty token from server")
}
logger.Debug("Received token: %s", tokenResp.Data.Token)
return tokenResp.Data.Token, nil
}
@@ -266,6 +284,10 @@ func (c *Client) establishConnection() error {
return fmt.Errorf("failed to get token: %w", err)
}
if c.onTokenUpdate != nil {
c.onTokenUpdate(token)
}
// Parse the base URL to determine protocol and hostname
baseURL, err := url.Parse(c.baseURL)
if err != nil {
@@ -288,10 +310,20 @@ func (c *Client) establishConnection() error {
// Add token to query parameters
q := u.Query()
q.Set("token", token)
q.Set("clientType", "newt")
u.RawQuery = q.Encode()
// Connect to WebSocket
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
dialer := websocket.DefaultDialer
if c.config.TlsClientCert != "" {
logger.Info("Adding tls to req")
tlsConfig, err := loadClientCertificate(c.config.TlsClientCert)
if err != nil {
return fmt.Errorf("failed to load certificate %s: %w", c.config.TlsClientCert, err)
}
dialer.TLSClientConfig = tlsConfig
}
conn, _, err := dialer.Dial(u.String(), nil)
if err != nil {
return fmt.Errorf("failed to connect to WebSocket: %w", err)
}
@@ -301,10 +333,14 @@ func (c *Client) establishConnection() error {
// Start the ping monitor
go c.pingMonitor()
// Start the read pump
go c.readPump()
// Start the read pump with disconnect detection
go c.readPumpWithDisconnectDetection()
if c.onConnect != nil {
err := c.saveConfig()
if err != nil {
logger.Error("Failed to save config: %v", err)
}
if err := c.onConnect(); err != nil {
logger.Error("OnConnect callback failed: %v", err)
}
@@ -313,8 +349,9 @@ func (c *Client) establishConnection() error {
return nil
}
// pingMonitor sends pings at a short interval and triggers reconnect on failure
func (c *Client) pingMonitor() {
ticker := time.NewTicker(30 * time.Second)
ticker := time.NewTicker(c.pingInterval)
defer ticker.Stop()
for {
@@ -322,11 +359,74 @@ func (c *Client) pingMonitor() {
case <-c.done:
return
case <-ticker.C:
if err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
logger.Error("Ping failed: %v", err)
c.reconnect()
if c.conn == nil {
return
}
c.writeMux.Lock()
err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(c.pingTimeout))
c.writeMux.Unlock()
if err != nil {
// Check if we're shutting down before logging error and reconnecting
select {
case <-c.done:
// Expected during shutdown
return
default:
logger.Error("Ping failed: %v", err)
c.reconnect()
return
}
}
}
}
}
// readPumpWithDisconnectDetection reads messages and triggers reconnect on error
func (c *Client) readPumpWithDisconnectDetection() {
defer func() {
if c.conn != nil {
c.conn.Close()
}
// Only attempt reconnect if we're not shutting down
select {
case <-c.done:
// Shutting down, don't reconnect
return
default:
c.reconnect()
}
}()
for {
select {
case <-c.done:
return
default:
var msg WSMessage
err := c.conn.ReadJSON(&msg)
if err != nil {
// Check if we're shutting down before logging error
select {
case <-c.done:
// Expected during shutdown, don't log as error
logger.Debug("WebSocket connection closed during shutdown")
return
default:
// Unexpected error during normal operation
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNormalClosure) {
logger.Error("WebSocket read error: %v", err)
} else {
logger.Debug("WebSocket connection closed: %v", err)
}
return // triggers reconnect via defer
}
}
c.handlersMux.RLock()
if handler, ok := c.handlers[msg.Type]; ok {
handler(msg)
}
c.handlersMux.RUnlock()
}
}
}
@@ -335,9 +435,16 @@ func (c *Client) reconnect() {
c.setConnected(false)
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
go c.connectWithRetry()
// Only reconnect if we're not shutting down
select {
case <-c.done:
return
default:
go c.connectWithRetry()
}
}
func (c *Client) setConnected(status bool) {
@@ -345,3 +452,42 @@ func (c *Client) setConnected(status bool) {
defer c.reconnectMux.Unlock()
c.isConnected = status
}
// LoadClientCertificate Helper method to load client certificates
func loadClientCertificate(p12Path string) (*tls.Config, error) {
logger.Info("Loading tls-client-cert %s", p12Path)
// Read the PKCS12 file
p12Data, err := os.ReadFile(p12Path)
if err != nil {
return nil, fmt.Errorf("failed to read PKCS12 file: %w", err)
}
// Parse PKCS12 with empty password for non-encrypted files
privateKey, certificate, caCerts, err := pkcs12.DecodeChain(p12Data, "")
if err != nil {
return nil, fmt.Errorf("failed to decode PKCS12: %w", err)
}
// Create certificate
cert := tls.Certificate{
Certificate: [][]byte{certificate.Raw},
PrivateKey: privateKey,
}
// Optional: Add CA certificates if present
rootCAs, err := x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("failed to load system cert pool: %w", err)
}
if len(caCerts) > 0 {
for _, caCert := range caCerts {
rootCAs.AddCert(caCert)
}
}
// Create TLS configuration
return &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: rootCAs,
}, nil
}

View File

@@ -48,12 +48,12 @@ func (c *Client) loadConfig() error {
if c.config.NewtID == "" {
c.config.NewtID = config.NewtID
}
if c.config.Token == "" {
c.config.Token = config.Token
}
if c.config.Secret == "" {
c.config.Secret = config.Secret
}
if c.config.TlsClientCert == "" {
c.config.TlsClientCert = config.TlsClientCert
}
if c.config.Endpoint == "" {
c.config.Endpoint = config.Endpoint
c.baseURL = config.Endpoint

View File

@@ -1,10 +1,10 @@
package websocket
type Config struct {
NewtID string `json:"newtId"`
Secret string `json:"secret"`
Token string `json:"token"`
Endpoint string `json:"endpoint"`
NewtID string `json:"newtId"`
Secret string `json:"secret"`
Endpoint string `json:"endpoint"`
TlsClientCert string `json:"tlsClientCert"`
}
type TokenResponse struct {

981
wg/wg.go Normal file
View File

@@ -0,0 +1,981 @@
//go:build linux
package wg
import (
"encoding/json"
"fmt"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/network"
"github.com/fosrl/newt/websocket"
"github.com/vishvananda/netlink"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/exp/rand"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type WgConfig struct {
IpAddress string `json:"ipAddress"`
Peers []Peer `json:"peers"`
}
type Peer struct {
PublicKey string `json:"publicKey"`
AllowedIPs []string `json:"allowedIps"`
Endpoint string `json:"endpoint"`
}
type PeerBandwidth struct {
PublicKey string `json:"publicKey"`
BytesIn float64 `json:"bytesIn"`
BytesOut float64 `json:"bytesOut"`
}
type PeerReading struct {
BytesReceived int64
BytesTransmitted int64
LastChecked time.Time
}
type WireGuardService struct {
interfaceName string
mtu int
client *websocket.Client
wgClient *wgctrl.Client
config WgConfig
key wgtypes.Key
newtId string
lastReadings map[string]PeerReading
mu sync.Mutex
Port uint16
stopHolepunch chan struct{}
host string
serverPubKey string
token string
stopGetConfig chan struct{}
}
// Add this type definition
type fixedPortBind struct {
port uint16
conn.Bind
}
func (b *fixedPortBind) Open(port uint16) ([]conn.ReceiveFunc, uint16, error) {
// Ignore the requested port and use our fixed port
return b.Bind.Open(b.port)
}
func NewFixedPortBind(port uint16) conn.Bind {
return &fixedPortBind{
port: port,
Bind: conn.NewDefaultBind(),
}
}
// find an available UDP port in the range [minPort, maxPort] and also the next port for the wgtester
func FindAvailableUDPPort(minPort, maxPort uint16) (uint16, error) {
if maxPort < minPort {
return 0, fmt.Errorf("invalid port range: min=%d, max=%d", minPort, maxPort)
}
// We need to check port+1 as well, so adjust the max port to avoid going out of range
adjustedMaxPort := maxPort - 1
if adjustedMaxPort < minPort {
return 0, fmt.Errorf("insufficient port range to find consecutive ports: min=%d, max=%d", minPort, maxPort)
}
// Create a slice of all ports in the range (excluding the last one)
portRange := make([]uint16, adjustedMaxPort-minPort+1)
for i := range portRange {
portRange[i] = minPort + uint16(i)
}
// Fisher-Yates shuffle to randomize the port order
rand.Seed(uint64(time.Now().UnixNano()))
for i := len(portRange) - 1; i > 0; i-- {
j := rand.Intn(i + 1)
portRange[i], portRange[j] = portRange[j], portRange[i]
}
// Try each port in the randomized order
for _, port := range portRange {
// Check if port is available
addr1 := &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: int(port),
}
conn1, err1 := net.ListenUDP("udp", addr1)
if err1 != nil {
continue // Port is in use or there was an error, try next port
}
// Check if port+1 is also available
addr2 := &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: int(port + 1),
}
conn2, err2 := net.ListenUDP("udp", addr2)
if err2 != nil {
// The next port is not available, so close the first connection and try again
conn1.Close()
continue
}
// Both ports are available, close connections and return the first port
conn1.Close()
conn2.Close()
return port, nil
}
return 0, fmt.Errorf("no available consecutive UDP ports found in range %d-%d", minPort, maxPort)
}
func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo string, host string, newtId string, wsClient *websocket.Client) (*WireGuardService, error) {
wgClient, err := wgctrl.New()
if err != nil {
return nil, fmt.Errorf("failed to create WireGuard client: %v", err)
}
var key wgtypes.Key
// if generateAndSaveKeyTo is provided, generate a private key and save it to the file. if the file already exists, load the key from the file
if _, err := os.Stat(generateAndSaveKeyTo); os.IsNotExist(err) {
// generate a new private key
key, err = wgtypes.GeneratePrivateKey()
if err != nil {
logger.Fatal("Failed to generate private key: %v", err)
}
// save the key to the file
err = os.WriteFile(generateAndSaveKeyTo, []byte(key.String()), 0644)
if err != nil {
logger.Fatal("Failed to save private key: %v", err)
}
} else {
keyData, err := os.ReadFile(generateAndSaveKeyTo)
if err != nil {
logger.Fatal("Failed to read private key: %v", err)
}
key, err = wgtypes.ParseKey(string(keyData))
if err != nil {
logger.Fatal("Failed to parse private key: %v", err)
}
}
service := &WireGuardService{
interfaceName: interfaceName,
mtu: mtu,
client: wsClient,
wgClient: wgClient,
key: key,
newtId: newtId,
host: host,
lastReadings: make(map[string]PeerReading),
stopHolepunch: make(chan struct{}),
stopGetConfig: make(chan struct{}),
}
// Get the existing wireguard port (keep this part)
device, err := service.wgClient.Device(service.interfaceName)
if err == nil {
service.Port = uint16(device.ListenPort)
logger.Info("WireGuard interface %s already exists with port %d\n", service.interfaceName, service.Port)
} else {
service.Port, err = FindAvailableUDPPort(49152, 65535)
if err != nil {
fmt.Printf("Error finding available port: %v\n", err)
return nil, err
}
}
// Register websocket handlers
wsClient.RegisterHandler("newt/wg/receive-config", service.handleConfig)
wsClient.RegisterHandler("newt/wg/peer/add", service.handleAddPeer)
wsClient.RegisterHandler("newt/wg/peer/remove", service.handleRemovePeer)
wsClient.RegisterHandler("newt/wg/peer/update", service.handleUpdatePeer)
if err := service.sendUDPHolePunch(service.host + ":21820"); err != nil {
logger.Error("Failed to send UDP hole punch: %v", err)
}
// start the UDP holepunch
go service.keepSendingUDPHolePunch(service.host)
return service, nil
}
func (s *WireGuardService) Close(rm bool) {
select {
case <-s.stopGetConfig:
// Already closed, do nothing
default:
close(s.stopGetConfig)
}
s.wgClient.Close()
// Remove the WireGuard interface
if rm {
if err := s.removeInterface(); err != nil {
logger.Error("Failed to remove WireGuard interface: %v", err)
}
// Remove the private key file
if err := os.Remove(s.key.String()); err != nil {
logger.Error("Failed to remove private key file: %v", err)
}
}
}
func (s *WireGuardService) SetServerPubKey(serverPubKey string) {
s.serverPubKey = serverPubKey
}
func (s *WireGuardService) SetToken(token string) {
s.token = token
}
func (s *WireGuardService) LoadRemoteConfig() error {
// Send the initial message
err := s.sendGetConfigMessage()
if err != nil {
logger.Error("Failed to send initial get-config message: %v", err)
return err
}
// Start goroutine to periodically send the message until config is received
go s.keepSendingGetConfig()
go s.periodicBandwidthCheck()
return nil
}
func (s *WireGuardService) handleConfig(msg websocket.WSMessage) {
var config WgConfig
logger.Info("Received message: %v", msg)
jsonData, err := json.Marshal(msg.Data)
if err != nil {
logger.Info("Error marshaling data: %v", err)
return
}
if err := json.Unmarshal(jsonData, &config); err != nil {
logger.Info("Error unmarshaling target data: %v", err)
return
}
s.config = config
close(s.stopGetConfig)
// Ensure the WireGuard interface and peers are configured
if err := s.ensureWireguardInterface(config); err != nil {
logger.Error("Failed to ensure WireGuard interface: %v", err)
}
if err := s.ensureWireguardPeers(config.Peers); err != nil {
logger.Error("Failed to ensure WireGuard peers: %v", err)
}
}
func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error {
// Check if the WireGuard interface exists
_, err := netlink.LinkByName(s.interfaceName)
if err != nil {
if _, ok := err.(netlink.LinkNotFoundError); ok {
// Interface doesn't exist, so create it
err = s.createWireGuardInterface()
if err != nil {
logger.Fatal("Failed to create WireGuard interface: %v", err)
}
logger.Info("Created WireGuard interface %s\n", s.interfaceName)
} else {
logger.Fatal("Error checking for WireGuard interface: %v", err)
}
} else {
logger.Info("WireGuard interface %s already exists\n", s.interfaceName)
// get the exising wireguard port
device, err := s.wgClient.Device(s.interfaceName)
if err != nil {
return fmt.Errorf("failed to get device: %v", err)
}
// get the existing port
s.Port = uint16(device.ListenPort)
logger.Info("WireGuard interface %s already exists with port %d\n", s.interfaceName, s.Port)
return nil
}
logger.Info("Assigning IP address %s to interface %s\n", wgconfig.IpAddress, s.interfaceName)
// Assign IP address to the interface
err = s.assignIPAddress(wgconfig.IpAddress)
if err != nil {
logger.Fatal("Failed to assign IP address: %v", err)
}
// Check if the interface already exists
_, err = s.wgClient.Device(s.interfaceName)
if err != nil {
return fmt.Errorf("interface %s does not exist", s.interfaceName)
}
// Parse the private key
key, err := wgtypes.ParseKey(s.key.String())
if err != nil {
return fmt.Errorf("failed to parse private key: %v", err)
}
config := wgtypes.Config{
PrivateKey: &key,
ListenPort: new(int),
}
// Use the service's fixed port instead of the config port
*config.ListenPort = int(s.Port)
// Create and configure the WireGuard interface
err = s.wgClient.ConfigureDevice(s.interfaceName, config)
if err != nil {
return fmt.Errorf("failed to configure WireGuard device: %v", err)
}
// bring up the interface
link, err := netlink.LinkByName(s.interfaceName)
if err != nil {
return fmt.Errorf("failed to get interface: %v", err)
}
if err := netlink.LinkSetMTU(link, s.mtu); err != nil {
return fmt.Errorf("failed to set MTU: %v", err)
}
if err := netlink.LinkSetUp(link); err != nil {
return fmt.Errorf("failed to bring up interface: %v", err)
}
// if err := s.ensureMSSClamping(); err != nil {
// logger.Warn("Failed to ensure MSS clamping: %v", err)
// }
logger.Info("WireGuard interface %s created and configured", s.interfaceName)
return nil
}
func (s *WireGuardService) createWireGuardInterface() error {
wgLink := &netlink.GenericLink{
LinkAttrs: netlink.LinkAttrs{Name: s.interfaceName},
LinkType: "wireguard",
}
return netlink.LinkAdd(wgLink)
}
func (s *WireGuardService) assignIPAddress(ipAddress string) error {
link, err := netlink.LinkByName(s.interfaceName)
if err != nil {
return fmt.Errorf("failed to get interface: %v", err)
}
addr, err := netlink.ParseAddr(ipAddress)
if err != nil {
return fmt.Errorf("failed to parse IP address: %v", err)
}
return netlink.AddrAdd(link, addr)
}
func (s *WireGuardService) ensureWireguardPeers(peers []Peer) error {
// get the current peers
device, err := s.wgClient.Device(s.interfaceName)
if err != nil {
return fmt.Errorf("failed to get device: %v", err)
}
// get the peer public keys
var currentPeers []string
for _, peer := range device.Peers {
currentPeers = append(currentPeers, peer.PublicKey.String())
}
// remove any peers that are not in the config
for _, peer := range currentPeers {
found := false
for _, configPeer := range peers {
if peer == configPeer.PublicKey {
found = true
break
}
}
if !found {
err := s.removePeer(peer)
if err != nil {
return fmt.Errorf("failed to remove peer: %v", err)
}
}
}
// add any peers that are in the config but not in the current peers
for _, configPeer := range peers {
found := false
for _, peer := range currentPeers {
if configPeer.PublicKey == peer {
found = true
break
}
}
if !found {
err := s.addPeer(configPeer)
if err != nil {
return fmt.Errorf("failed to add peer: %v", err)
}
}
}
return nil
}
func (s *WireGuardService) handleAddPeer(msg websocket.WSMessage) {
logger.Info("Received message: %v", msg.Data)
var peer Peer
jsonData, err := json.Marshal(msg.Data)
if err != nil {
logger.Info("Error marshaling data: %v", err)
}
if err := json.Unmarshal(jsonData, &peer); err != nil {
logger.Info("Error unmarshaling target data: %v", err)
}
err = s.addPeer(peer)
if err != nil {
logger.Info("Error adding peer: %v", err)
return
}
}
func (s *WireGuardService) addPeer(peer Peer) error {
pubKey, err := wgtypes.ParseKey(peer.PublicKey)
if err != nil {
return fmt.Errorf("failed to parse public key: %v", err)
}
// parse allowed IPs into array of net.IPNet
var allowedIPs []net.IPNet
for _, ipStr := range peer.AllowedIPs {
_, ipNet, err := net.ParseCIDR(ipStr)
if err != nil {
return fmt.Errorf("failed to parse allowed IP: %v", err)
}
allowedIPs = append(allowedIPs, *ipNet)
}
// add keep alive using *time.Duration of 1 second
keepalive := time.Second
var peerConfig wgtypes.PeerConfig
if peer.Endpoint != "" {
endpoint, err := net.ResolveUDPAddr("udp", peer.Endpoint)
if err != nil {
return fmt.Errorf("failed to resolve endpoint address: %w", err)
}
peerConfig = wgtypes.PeerConfig{
PublicKey: pubKey,
AllowedIPs: allowedIPs,
PersistentKeepaliveInterval: &keepalive,
Endpoint: endpoint,
}
} else {
peerConfig = wgtypes.PeerConfig{
PublicKey: pubKey,
AllowedIPs: allowedIPs,
PersistentKeepaliveInterval: &keepalive,
}
logger.Info("Added peer with no endpoint!")
}
config := wgtypes.Config{
Peers: []wgtypes.PeerConfig{peerConfig},
}
if err := s.wgClient.ConfigureDevice(s.interfaceName, config); err != nil {
return fmt.Errorf("failed to add peer: %v", err)
}
logger.Info("Peer %s added successfully", peer.PublicKey)
return nil
}
func (s *WireGuardService) handleRemovePeer(msg websocket.WSMessage) {
logger.Info("Received message: %v", msg.Data)
// parse the publicKey from the message which is json { "publicKey": "asdfasdfl;akjsdf" }
type RemoveRequest struct {
PublicKey string `json:"publicKey"`
}
jsonData, err := json.Marshal(msg.Data)
if err != nil {
logger.Info("Error marshaling data: %v", err)
}
var request RemoveRequest
if err := json.Unmarshal(jsonData, &request); err != nil {
logger.Info("Error unmarshaling data: %v", err)
return
}
if err := s.removePeer(request.PublicKey); err != nil {
logger.Info("Error removing peer: %v", err)
return
}
}
func (s *WireGuardService) removePeer(publicKey string) error {
pubKey, err := wgtypes.ParseKey(publicKey)
if err != nil {
return fmt.Errorf("failed to parse public key: %v", err)
}
peerConfig := wgtypes.PeerConfig{
PublicKey: pubKey,
Remove: true,
}
config := wgtypes.Config{
Peers: []wgtypes.PeerConfig{peerConfig},
}
if err := s.wgClient.ConfigureDevice(s.interfaceName, config); err != nil {
return fmt.Errorf("failed to remove peer: %v", err)
}
logger.Info("Peer %s removed successfully", publicKey)
return nil
}
func (s *WireGuardService) handleUpdatePeer(msg websocket.WSMessage) {
logger.Info("Received message: %v", msg.Data)
// Define a struct to match the incoming message structure with optional fields
type UpdatePeerRequest struct {
PublicKey string `json:"publicKey"`
AllowedIPs []string `json:"allowedIps,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
}
jsonData, err := json.Marshal(msg.Data)
if err != nil {
logger.Info("Error marshaling data: %v", err)
return
}
var request UpdatePeerRequest
if err := json.Unmarshal(jsonData, &request); err != nil {
logger.Info("Error unmarshaling peer data: %v", err)
return
}
// First, get the current peer configuration to preserve any unmodified fields
device, err := s.wgClient.Device(s.interfaceName)
if err != nil {
logger.Info("Error getting WireGuard device: %v", err)
return
}
pubKey, err := wgtypes.ParseKey(request.PublicKey)
if err != nil {
logger.Info("Error parsing public key: %v", err)
return
}
// Find the existing peer configuration
var currentPeer *wgtypes.Peer
for _, p := range device.Peers {
if p.PublicKey == pubKey {
currentPeer = &p
break
}
}
if currentPeer == nil {
logger.Info("Peer %s not found, cannot update", request.PublicKey)
return
}
// Create the update peer config
peerConfig := wgtypes.PeerConfig{
PublicKey: pubKey,
UpdateOnly: true,
}
// Keep the default persistent keepalive of 1 second
keepalive := time.Second
peerConfig.PersistentKeepaliveInterval = &keepalive
// Handle Endpoint field special case
// If Endpoint is included in the request but empty, we want to remove the endpoint
// If Endpoint is not included, we don't modify it
endpointSpecified := false
for key := range msg.Data.(map[string]interface{}) {
if key == "endpoint" {
endpointSpecified = true
break
}
}
// Only update AllowedIPs if provided in the request
if request.AllowedIPs != nil && len(request.AllowedIPs) > 0 {
var allowedIPs []net.IPNet
for _, ipStr := range request.AllowedIPs {
_, ipNet, err := net.ParseCIDR(ipStr)
if err != nil {
logger.Info("Error parsing allowed IP %s: %v", ipStr, err)
return
}
allowedIPs = append(allowedIPs, *ipNet)
}
peerConfig.AllowedIPs = allowedIPs
peerConfig.ReplaceAllowedIPs = true
logger.Info("Updating AllowedIPs for peer %s", request.PublicKey)
} else if endpointSpecified && request.Endpoint == "" {
peerConfig.ReplaceAllowedIPs = false
}
if endpointSpecified {
if request.Endpoint != "" {
// Update to new endpoint
endpoint, err := net.ResolveUDPAddr("udp", request.Endpoint)
if err != nil {
logger.Info("Error resolving endpoint address %s: %v", request.Endpoint, err)
return
}
peerConfig.Endpoint = endpoint
logger.Info("Updating Endpoint for peer %s to %s", request.PublicKey, request.Endpoint)
} else {
// specify any address to listen for any incoming packets
peerConfig.Endpoint = &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
}
logger.Info("Removing Endpoint for peer %s", request.PublicKey)
}
}
// Apply the configuration update
config := wgtypes.Config{
Peers: []wgtypes.PeerConfig{peerConfig},
}
if err := s.wgClient.ConfigureDevice(s.interfaceName, config); err != nil {
logger.Info("Error updating peer configuration: %v", err)
return
}
logger.Info("Peer %s updated successfully", request.PublicKey)
}
func (s *WireGuardService) periodicBandwidthCheck() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
if err := s.reportPeerBandwidth(); err != nil {
logger.Info("Failed to report peer bandwidth: %v", err)
}
}
}
func (s *WireGuardService) calculatePeerBandwidth() ([]PeerBandwidth, error) {
device, err := s.wgClient.Device(s.interfaceName)
if err != nil {
return nil, fmt.Errorf("failed to get device: %v", err)
}
peerBandwidths := []PeerBandwidth{}
now := time.Now()
s.mu.Lock()
defer s.mu.Unlock()
for _, peer := range device.Peers {
publicKey := peer.PublicKey.String()
currentReading := PeerReading{
BytesReceived: peer.ReceiveBytes,
BytesTransmitted: peer.TransmitBytes,
LastChecked: now,
}
var bytesInDiff, bytesOutDiff float64
lastReading, exists := s.lastReadings[publicKey]
if exists {
timeDiff := currentReading.LastChecked.Sub(lastReading.LastChecked).Seconds()
if timeDiff > 0 {
// Calculate bytes transferred since last reading
bytesInDiff = float64(currentReading.BytesReceived - lastReading.BytesReceived)
bytesOutDiff = float64(currentReading.BytesTransmitted - lastReading.BytesTransmitted)
// Handle counter wraparound (if the counter resets or overflows)
if bytesInDiff < 0 {
bytesInDiff = float64(currentReading.BytesReceived)
}
if bytesOutDiff < 0 {
bytesOutDiff = float64(currentReading.BytesTransmitted)
}
// Convert to MB
bytesInMB := bytesInDiff / (1024 * 1024)
bytesOutMB := bytesOutDiff / (1024 * 1024)
peerBandwidths = append(peerBandwidths, PeerBandwidth{
PublicKey: publicKey,
BytesIn: bytesInMB,
BytesOut: bytesOutMB,
})
} else {
// If readings are too close together or time hasn't passed, report 0
peerBandwidths = append(peerBandwidths, PeerBandwidth{
PublicKey: publicKey,
BytesIn: 0,
BytesOut: 0,
})
}
} else {
// For first reading of a peer, report 0 to establish baseline
peerBandwidths = append(peerBandwidths, PeerBandwidth{
PublicKey: publicKey,
BytesIn: 0,
BytesOut: 0,
})
}
// Update the last reading
s.lastReadings[publicKey] = currentReading
}
// Clean up old peers
for publicKey := range s.lastReadings {
found := false
for _, peer := range device.Peers {
if peer.PublicKey.String() == publicKey {
found = true
break
}
}
if !found {
delete(s.lastReadings, publicKey)
}
}
return peerBandwidths, nil
}
func (s *WireGuardService) reportPeerBandwidth() error {
bandwidths, err := s.calculatePeerBandwidth()
if err != nil {
return fmt.Errorf("failed to calculate peer bandwidth: %v", err)
}
err = s.client.SendMessage("newt/receive-bandwidth", map[string]interface{}{
"bandwidthData": bandwidths,
})
if err != nil {
return fmt.Errorf("failed to send bandwidth data: %v", err)
}
return nil
}
func (s *WireGuardService) sendUDPHolePunch(serverAddr string) error {
if s.serverPubKey == "" || s.token == "" {
logger.Debug("Server public key or token not set, skipping UDP hole punch")
return nil
}
// Parse server address
serverSplit := strings.Split(serverAddr, ":")
if len(serverSplit) < 2 {
return fmt.Errorf("invalid server address format, expected hostname:port")
}
serverHostname := serverSplit[0]
serverPort, err := strconv.ParseUint(serverSplit[1], 10, 16)
if err != nil {
return fmt.Errorf("failed to parse server port: %v", err)
}
// Resolve server hostname to IP
serverIPAddr := network.HostToAddr(serverHostname)
if serverIPAddr == nil {
return fmt.Errorf("failed to resolve server hostname")
}
// Get client IP based on route to server
clientIP := network.GetClientIP(serverIPAddr.IP)
// Create server and client configs
server := &network.Server{
Hostname: serverHostname,
Addr: serverIPAddr,
Port: uint16(serverPort),
}
client := &network.PeerNet{
IP: clientIP,
Port: s.Port,
NewtID: s.newtId,
}
// Setup raw connection with BPF filtering
rawConn := network.SetupRawConn(server, client)
defer rawConn.Close()
// Create JSON payload
payload := struct {
NewtID string `json:"newtId"`
Token string `json:"token"`
}{
NewtID: s.newtId,
Token: s.token,
}
// Convert payload to JSON
payloadBytes, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %v", err)
}
// Encrypt the payload using the server's WireGuard public key
encryptedPayload, err := s.encryptPayload(payloadBytes)
if err != nil {
return fmt.Errorf("failed to encrypt payload: %v", err)
}
// Send the encrypted packet using the raw connection
err = network.SendDataPacket(encryptedPayload, rawConn, server, client)
if err != nil {
return fmt.Errorf("failed to send UDP packet: %v", err)
}
return nil
}
func (s *WireGuardService) encryptPayload(payload []byte) (interface{}, error) {
// Generate an ephemeral keypair for this message
ephemeralPrivateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, fmt.Errorf("failed to generate ephemeral private key: %v", err)
}
ephemeralPublicKey := ephemeralPrivateKey.PublicKey()
// Parse the server's public key
serverPubKey, err := wgtypes.ParseKey(s.serverPubKey)
if err != nil {
return nil, fmt.Errorf("failed to parse server public key: %v", err)
}
// Use X25519 for key exchange (replacing deprecated ScalarMult)
var ephPrivKeyFixed [32]byte
copy(ephPrivKeyFixed[:], ephemeralPrivateKey[:])
// Perform X25519 key exchange
sharedSecret, err := curve25519.X25519(ephPrivKeyFixed[:], serverPubKey[:])
if err != nil {
return nil, fmt.Errorf("failed to perform X25519 key exchange: %v", err)
}
// Create an AEAD cipher using the shared secret
aead, err := chacha20poly1305.New(sharedSecret)
if err != nil {
return nil, fmt.Errorf("failed to create AEAD cipher: %v", err)
}
// Generate a random nonce
nonce := make([]byte, aead.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, fmt.Errorf("failed to generate nonce: %v", err)
}
// Encrypt the payload
ciphertext := aead.Seal(nil, nonce, payload, nil)
// Prepare the final encrypted message
encryptedMsg := struct {
EphemeralPublicKey string `json:"ephemeralPublicKey"`
Nonce []byte `json:"nonce"`
Ciphertext []byte `json:"ciphertext"`
}{
EphemeralPublicKey: ephemeralPublicKey.String(),
Nonce: nonce,
Ciphertext: ciphertext,
}
return encryptedMsg, nil
}
func (s *WireGuardService) keepSendingUDPHolePunch(host string) {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
select {
case <-s.stopHolepunch:
logger.Info("Stopping UDP holepunch")
return
case <-ticker.C:
if err := s.sendUDPHolePunch(host + ":21820"); err != nil {
logger.Error("Failed to send UDP hole punch: %v", err)
}
}
}
}
func (s *WireGuardService) removeInterface() error {
// Remove the WireGuard interface
link, err := netlink.LinkByName(s.interfaceName)
if err != nil {
return fmt.Errorf("failed to get interface: %v", err)
}
err = netlink.LinkDel(link)
if err != nil {
return fmt.Errorf("failed to delete interface: %v", err)
}
logger.Info("WireGuard interface %s removed successfully", s.interfaceName)
return nil
}
func (s *WireGuardService) sendGetConfigMessage() error {
err := s.client.SendMessage("newt/wg/get-config", map[string]interface{}{
"publicKey": fmt.Sprintf("%s", s.key.PublicKey().String()),
"port": s.Port,
})
if err != nil {
logger.Error("Failed to send get-config message: %v", err)
return err
}
logger.Info("Requesting WireGuard configuration from remote server")
return nil
}
func (s *WireGuardService) keepSendingGetConfig() {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
select {
case <-s.stopGetConfig:
logger.Info("Stopping get-config messages")
return
case <-ticker.C:
if err := s.sendGetConfigMessage(); err != nil {
logger.Error("Failed to send periodic get-config: %v", err)
}
}
}
}

164
wgtester/wgtester.go Normal file
View File

@@ -0,0 +1,164 @@
package wgtester
import (
"encoding/binary"
"fmt"
"net"
"sync"
"time"
"github.com/fosrl/newt/logger"
)
const (
// Magic bytes to identify our packets
magicHeader uint32 = 0xDEADBEEF
// Request packet type
packetTypeRequest uint8 = 1
// Response packet type
packetTypeResponse uint8 = 2
// Packet format:
// - 4 bytes: magic header (0xDEADBEEF)
// - 1 byte: packet type (1 = request, 2 = response)
// - 8 bytes: timestamp (for round-trip timing)
packetSize = 13
)
// Server handles listening for connection check requests using UDP
type Server struct {
conn *net.UDPConn
serverAddr string
serverPort uint16
shutdownCh chan struct{}
isRunning bool
runningLock sync.Mutex
newtID string
outputPrefix string
}
// NewServer creates a new connection test server using UDP
func NewServer(serverAddr string, serverPort uint16, newtID string) *Server {
return &Server{
serverAddr: serverAddr,
serverPort: serverPort + 1, // use the next port for the server
shutdownCh: make(chan struct{}),
newtID: newtID,
outputPrefix: "[WGTester] ",
}
}
// Start begins listening for connection test packets using UDP
func (s *Server) Start() error {
s.runningLock.Lock()
defer s.runningLock.Unlock()
if s.isRunning {
return nil
}
//create the address to listen on
addr := net.JoinHostPort(s.serverAddr, fmt.Sprintf("%d", s.serverPort))
// Create UDP address to listen on
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return err
}
// Create UDP connection
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
return err
}
s.conn = conn
s.isRunning = true
go s.handleConnections()
logger.Info("%sServer started on %s:%d", s.outputPrefix, s.serverAddr, s.serverPort)
return nil
}
// Stop shuts down the server
func (s *Server) Stop() {
s.runningLock.Lock()
defer s.runningLock.Unlock()
if !s.isRunning {
return
}
close(s.shutdownCh)
if s.conn != nil {
s.conn.Close()
}
s.isRunning = false
logger.Info(s.outputPrefix + "Server stopped")
}
// handleConnections processes incoming packets
func (s *Server) handleConnections() {
buffer := make([]byte, 2000) // Buffer large enough for any UDP packet
for {
select {
case <-s.shutdownCh:
return
default:
// Set read deadline to avoid blocking forever
err := s.conn.SetReadDeadline(time.Now().Add(1 * time.Second))
if err != nil {
logger.Error(s.outputPrefix+"Error setting read deadline: %v", err)
continue
}
// Read from UDP connection
n, addr, err := s.conn.ReadFromUDP(buffer)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
// Just a timeout, keep going
continue
}
logger.Error(s.outputPrefix+"Error reading from UDP: %v", err)
continue
}
// Process packet only if it meets minimum size requirements
if n < packetSize {
continue // Too small to be our packet
}
// Check magic header
magic := binary.BigEndian.Uint32(buffer[0:4])
if magic != magicHeader {
continue // Not our packet
}
// Check packet type
packetType := buffer[4]
if packetType != packetTypeRequest {
continue // Not a request packet
}
// Create response packet
responsePacket := make([]byte, packetSize)
// Copy the same magic header
binary.BigEndian.PutUint32(responsePacket[0:4], magicHeader)
// Change the packet type to response
responsePacket[4] = packetTypeResponse
// Copy the timestamp (for RTT calculation)
copy(responsePacket[5:13], buffer[5:13])
// Log response being sent for debugging
logger.Debug(s.outputPrefix+"Sending response to %s", addr.String())
// Send the response packet directly to the source address
_, err = s.conn.WriteToUDP(responsePacket, addr)
if err != nil {
logger.Error(s.outputPrefix+"Error sending response: %v", err)
} else {
logger.Debug(s.outputPrefix + "Response sent successfully")
}
}
}
}