mirror of
https://github.com/fosrl/newt.git
synced 2026-03-12 18:04:28 -05:00
Compare commits
318 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92cedd00b3 | ||
|
|
8b0cc36554 | ||
|
|
8b4a88937c | ||
|
|
58412a7a61 | ||
|
|
2675b812aa | ||
|
|
217a9346c6 | ||
|
|
eda8073bce | ||
|
|
2969f9d2d6 | ||
|
|
07b7025a24 | ||
|
|
502ebfc362 | ||
|
|
288413fd15 | ||
|
|
0ba44206b1 | ||
|
|
3f8dcd8f22 | ||
|
|
c5c0143013 | ||
|
|
87ac5c97e3 | ||
|
|
e2238c3cc8 | ||
|
|
58a67328d3 | ||
|
|
002fdc4d3f | ||
|
|
9a1fa2c19f | ||
|
|
a6797172ef | ||
|
|
d373de7fa1 | ||
|
|
f876bad632 | ||
|
|
54b096e6a7 | ||
|
|
10720afd31 | ||
|
|
0b37f20d5d | ||
|
|
aa6e54f383 | ||
|
|
30f8eb9785 | ||
|
|
e765d9c774 | ||
|
|
3ae4ac23ef | ||
|
|
6a98b90b01 | ||
|
|
e0ce9d4e48 | ||
|
|
5914c9ed33 | ||
|
|
109bda961f | ||
|
|
c2a93134b1 | ||
|
|
100d8e6afe | ||
|
|
04f2048a0a | ||
|
|
04de5ef8ba | ||
|
|
e77601cccc | ||
|
|
e9752f868e | ||
|
|
866afaf749 | ||
|
|
a12ae17a66 | ||
|
|
e0cba2e5c6 | ||
|
|
79f3db6fb6 | ||
|
|
009b4cf425 | ||
|
|
9c28d75155 | ||
|
|
bad244d0ea | ||
|
|
d013dc0543 | ||
|
|
0047b54e94 | ||
|
|
f0c8d2c7c7 | ||
|
|
28b6865f73 | ||
|
|
d52f89f629 | ||
|
|
289cce3a22 | ||
|
|
e8612c7e6b | ||
|
|
6820f8d23e | ||
|
|
151d0e38e6 | ||
|
|
a9d8ec0b1e | ||
|
|
e9dbfb239b | ||
|
|
a79dccc0e4 | ||
|
|
42dfb6b3d8 | ||
|
|
3ccd755d55 | ||
|
|
a0f0b674e8 | ||
|
|
9e675121d3 | ||
|
|
45d17da570 | ||
|
|
dfba35f8bb | ||
|
|
9e73aab21d | ||
|
|
e1ddad006a | ||
|
|
29567d6e0b | ||
|
|
47321ea9ad | ||
|
|
abfc9d8efc | ||
|
|
c6929621e7 | ||
|
|
46993203a3 | ||
|
|
8306084354 | ||
|
|
02c1e2b7d0 | ||
|
|
ae7e2a1055 | ||
|
|
88f1335cff | ||
|
|
8bf9c9795b | ||
|
|
5d343cd420 | ||
|
|
d1473b7e22 | ||
|
|
2efbd7dd6a | ||
|
|
82a3a39a1f | ||
|
|
df09193834 | ||
|
|
b2fe4e3b03 | ||
|
|
e14d53087f | ||
|
|
3583270f73 | ||
|
|
f5be05c55a | ||
|
|
d09e3fbd60 | ||
|
|
493831b5f0 | ||
|
|
9fc692c090 | ||
|
|
ccb7008579 | ||
|
|
f17dbe1fef | ||
|
|
27561f52ca | ||
|
|
499ebcd928 | ||
|
|
40dfab31a5 | ||
|
|
56377ec87e | ||
|
|
008be54c55 | ||
|
|
64c22a94a4 | ||
|
|
468c93c581 | ||
|
|
c53b859cda | ||
|
|
6cd824baf2 | ||
|
|
d8c5182acd | ||
|
|
c8c4666d63 | ||
|
|
f1fcc13e66 | ||
|
|
52bbc2fe31 | ||
|
|
b5ee12f84a | ||
|
|
510e78437c | ||
|
|
e14cffce1c | ||
|
|
629a92ee81 | ||
|
|
56df75544d | ||
|
|
5b2e743470 | ||
|
|
b5025c142f | ||
|
|
cd86e6b6de | ||
|
|
230c34e4e0 | ||
|
|
a038ce1458 | ||
|
|
cd83efd365 | ||
|
|
702f39e870 | ||
|
|
02b7ea51af | ||
|
|
e8421364fc | ||
|
|
7264bb7001 | ||
|
|
86e262ac1e | ||
|
|
dcacc03e96 | ||
|
|
6f4469a5a4 | ||
|
|
663e28329b | ||
|
|
f513f97fc3 | ||
|
|
ce4f3e4cdf | ||
|
|
58a74fce6f | ||
|
|
fc965abbc4 | ||
|
|
b881808cae | ||
|
|
6160e4c8a6 | ||
|
|
4d343e3541 | ||
|
|
71d1bbaaf2 | ||
|
|
9eb8e5122a | ||
|
|
c593e2aa97 | ||
|
|
c3483ded8f | ||
|
|
5aa00a65c2 | ||
|
|
675797c23f | ||
|
|
b1cfd3ba02 | ||
|
|
2d9b761de9 | ||
|
|
99506a10f6 | ||
|
|
9410b92169 | ||
|
|
53397663ef | ||
|
|
221d5862fb | ||
|
|
e4bdbbec7c | ||
|
|
07bd283604 | ||
|
|
b7d4ea0c84 | ||
|
|
d10c5e0366 | ||
|
|
61a9097baf | ||
|
|
54416bbc92 | ||
|
|
a88d25f369 | ||
|
|
c2a326c70a | ||
|
|
be56550da4 | ||
|
|
2ecf3297cd | ||
|
|
a896291831 | ||
|
|
9348842e2c | ||
|
|
ec8fc20438 | ||
|
|
a39d056725 | ||
|
|
450fc6c20f | ||
|
|
b7aa5d1396 | ||
|
|
837f749cdb | ||
|
|
56b68ae3b0 | ||
|
|
295cadc0b7 | ||
|
|
0add2ec668 | ||
|
|
071a51afbc | ||
|
|
9db3b78373 | ||
|
|
700287163e | ||
|
|
e357e7befb | ||
|
|
a76e6c9637 | ||
|
|
678d82fa68 | ||
|
|
6dd9b44fd2 | ||
|
|
63aea704bd | ||
|
|
c1a2a3208c | ||
|
|
ac67df63fa | ||
|
|
e642983b88 | ||
|
|
e2dd965654 | ||
|
|
a6849059b9 | ||
|
|
5f14ff3a07 | ||
|
|
040f5375a6 | ||
|
|
227631665e | ||
|
|
ca3ffa00ed | ||
|
|
6d3938e14e | ||
|
|
37191924ee | ||
|
|
a14f70dbaa | ||
|
|
bb1318278a | ||
|
|
1c75eb3bee | ||
|
|
4b64b04603 | ||
|
|
b397016da8 | ||
|
|
850c230c4a | ||
|
|
c82b84194f | ||
|
|
50df49e556 | ||
|
|
5f9c041c6b | ||
|
|
7f9b700ec3 | ||
|
|
95d4cb2758 | ||
|
|
bbea9a91da | ||
|
|
7c971d278c | ||
|
|
f6bfa70c10 | ||
|
|
7f8e3d9ef9 | ||
|
|
6d7e7e7a93 | ||
|
|
48cb0bf5a7 | ||
|
|
58f7835072 | ||
|
|
6d9160ab5e | ||
|
|
8d4d8b91b9 | ||
|
|
d0e220511a | ||
|
|
5cb86f3e47 | ||
|
|
e26552a5d7 | ||
|
|
22f44c860a | ||
|
|
64aef4ff34 | ||
|
|
ef2cc34f02 | ||
|
|
5476a69963 | ||
|
|
2e6ab2ba41 | ||
|
|
126ced6d57 | ||
|
|
cbbd5b0c76 | ||
|
|
e335bb8a1f | ||
|
|
e053eff879 | ||
|
|
a2c22eff35 | ||
|
|
52a8aabdb8 | ||
|
|
38b7f17e58 | ||
|
|
8941604f71 | ||
|
|
1820c8c019 | ||
|
|
1ecffab79a | ||
|
|
4ce2a656b8 | ||
|
|
d4b88c3985 | ||
|
|
a5f4d5fdf6 | ||
|
|
ce6d340a8d | ||
|
|
eaf812a2a7 | ||
|
|
a52260b49d | ||
|
|
a4d4976103 | ||
|
|
acab633da6 | ||
|
|
036e255b47 | ||
|
|
faffaad926 | ||
|
|
157bb98fd3 | ||
|
|
50b621f17c | ||
|
|
35d82ea15c | ||
|
|
21f5aa906d | ||
|
|
4a70af44bb | ||
|
|
5280c7ccda | ||
|
|
ef2f25ef98 | ||
|
|
eb8a12f290 | ||
|
|
a937027838 | ||
|
|
f8653e245e | ||
|
|
c423f6692a | ||
|
|
a9b96637b9 | ||
|
|
f566f599d6 | ||
|
|
918a9bdb84 | ||
|
|
315b6f3721 | ||
|
|
37940444c1 | ||
|
|
4e9aa30686 | ||
|
|
c2d3f00a6e | ||
|
|
9f006b1cbd | ||
|
|
1ef61d7470 | ||
|
|
6935c3b8db | ||
|
|
994d11b40c | ||
|
|
f060306654 | ||
|
|
a3cfda9fc5 | ||
|
|
607d197b02 | ||
|
|
78f31a56b0 | ||
|
|
03988655b6 | ||
|
|
4cf83f4cfc | ||
|
|
494e30704b | ||
|
|
175718a48e | ||
|
|
6a146ed371 | ||
|
|
027d9a059f | ||
|
|
0ced66e157 | ||
|
|
641c7f27a2 | ||
|
|
67abd239b6 | ||
|
|
3b166c465d | ||
|
|
e0d2349efa | ||
|
|
7b7d7228a6 | ||
|
|
a1a439c75c | ||
|
|
6b0ca9cab5 | ||
|
|
e7c8dbc1c8 | ||
|
|
d28e3ca5e8 | ||
|
|
b41570eb2c | ||
|
|
e47ddaa916 | ||
|
|
65dc81ca8b | ||
|
|
09d6829f8b | ||
|
|
f677376fae | ||
|
|
72e0adc1bf | ||
|
|
435b638701 | ||
|
|
9b3c82648b | ||
|
|
f713c294b2 | ||
|
|
b3e8bf7d12 | ||
|
|
c5978d9c4e | ||
|
|
7f9a31ac3e | ||
|
|
f08378b67e | ||
|
|
7852f11e8d | ||
|
|
2ff8df9a8d | ||
|
|
9d80161ab7 | ||
|
|
1501de691a | ||
|
|
2a19856556 | ||
|
|
f4e17a4dd7 | ||
|
|
ab544fc9ed | ||
|
|
f9e52c4d91 | ||
|
|
14eff8e16c | ||
|
|
067e079293 | ||
|
|
623be5ea0d | ||
|
|
72d264d427 | ||
|
|
a19fc8c588 | ||
|
|
dbc2a92456 | ||
|
|
437d8b67a4 | ||
|
|
6f1d4752f0 | ||
|
|
683312c78e | ||
|
|
5e673c829b | ||
|
|
cd3ec0b259 | ||
|
|
b68502de9e | ||
|
|
f6429b6eee | ||
|
|
8795c57b2e | ||
|
|
4aa718d55f | ||
|
|
afa93d8a3f | ||
|
|
270ee9cd19 | ||
|
|
0affef401c | ||
|
|
18d99de924 | ||
|
|
bff6707577 | ||
|
|
95eab504fa | ||
|
|
56e75902e3 | ||
|
|
45a1ab91d7 | ||
|
|
fb199cc94b | ||
|
|
66edae4288 | ||
|
|
f69a7f647d | ||
|
|
e8bd55bed9 |
40
.github/dependabot.yml
vendored
Normal file
40
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
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"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
15
.github/workflows/cicd.yml
vendored
15
.github/workflows/cicd.yml
vendored
@@ -12,13 +12,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
@@ -28,15 +31,15 @@ jobs:
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23.1
|
||||
go-version: 1.25
|
||||
|
||||
- name: Update version in main.go
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
if [ -f main.go ]; then
|
||||
sed -i 's/Newt version replaceme/Newt version '"$TAG"'/' main.go
|
||||
sed -i 's/version_replaceme/'"$TAG"'/' main.go
|
||||
echo "Updated main.go with version $TAG"
|
||||
else
|
||||
echo "main.go not found"
|
||||
|
||||
28
.github/workflows/test.yml
vendored
Normal file
28
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25
|
||||
|
||||
- name: Build go
|
||||
run: go build
|
||||
|
||||
- name: Build Docker image
|
||||
run: make build
|
||||
|
||||
- name: Build binaries
|
||||
run: make go-build-release
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,3 +1,8 @@
|
||||
newt
|
||||
.DS_Store
|
||||
bin/
|
||||
bin/
|
||||
nohup.out
|
||||
.idea
|
||||
*.iml
|
||||
certs/
|
||||
newt_arm64
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.23.2
|
||||
1.25
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23.1-alpine AS builder
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /app
|
||||
@@ -15,9 +15,9 @@ COPY . .
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /newt
|
||||
|
||||
FROM alpine:3.19 AS runner
|
||||
FROM alpine:3.22 AS runner
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
COPY --from=builder /newt /usr/local/bin/
|
||||
COPY entrypoint.sh /
|
||||
|
||||
2
Makefile
2
Makefile
@@ -3,7 +3,7 @@ all: build push
|
||||
|
||||
docker-build-release:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-all tag=<tag>"; \
|
||||
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 .
|
||||
|
||||
365
README.md
365
README.md
@@ -6,14 +6,13 @@ 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
|
||||
|
||||
<img src="public/screenshots/preview.png" alt="Preview"/>
|
||||
|
||||
_Sample output of a Newt container connected to Pangolin and hosting various resource target proxies._
|
||||
_Sample output of a Newt connected to Pangolin and hosting various resource target proxies._
|
||||
|
||||
## Key Functions
|
||||
|
||||
@@ -23,7 +22,7 @@ Using the Newt ID and a secret, the client will make HTTP requests to Pangolin t
|
||||
|
||||
### Receives WireGuard Control Messages
|
||||
|
||||
When Newt receives WireGuard control messages, it will use the information encoded (endpoint, public key) to bring up a WireGuard tunnel using [netstack](https://github.com/WireGuard/wireguard-go/blob/master/tun/netstack/examples/http_server.go) fully in user space. It will ping over the tunnel to ensure the peer on the Gerbil side is brought up.
|
||||
When Newt receives WireGuard control messages, it will use the information encoded (endpoint, public key) to bring up a WireGuard tunnel using [netstack](https://github.com/WireGuard/wireguard-go/blob/master/tun/netstack/examples/http_server.go) fully in user space. It will ping over the tunnel to ensure the peer on the Gerbil side is brought up.
|
||||
|
||||
### Receives Proxy Control Messages
|
||||
|
||||
@@ -31,16 +30,88 @@ When Newt receives WireGuard control messages, it will use the information encod
|
||||
|
||||
## CLI Args
|
||||
|
||||
- `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.
|
||||
- `dns`: DNS server to use to resolve the endpoint
|
||||
- `log-level` (optional): The log level to use. Default: INFO
|
||||
- `id`: Newt ID generated by Pangolin to identify the client.
|
||||
- `secret`: A unique secret (not shared and kept private) used to authenticate the client ID with the websocket in order to receive commands.
|
||||
- `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket.
|
||||
|
||||
Example:
|
||||
- `mtu` (optional): MTU for the internal WG interface. Default: 1280
|
||||
- `dns` (optional): DNS server to use to resolve the endpoint. Default: 9.9.9.9
|
||||
- `log-level` (optional): The log level to use (DEBUG, INFO, WARN, ERROR, FATAL). Default: INFO
|
||||
- `enforce-hc-cert` (optional): Enforce certificate validation for health checks. Default: false (accepts any cert)
|
||||
- `docker-socket` (optional): Set the Docker socket to use the container discovery integration
|
||||
- `ping-interval` (optional): Interval for pinging the server. Default: 3s
|
||||
- `ping-timeout` (optional): Timeout for each ping. Default: 5s
|
||||
- `updown` (optional): A script to be called when targets are added or removed.
|
||||
- `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls)
|
||||
- `tls-client-cert` (optional): Path to client certificate (PEM format, optional if using PKCS12). See [mTLS](#mtls)
|
||||
- `tls-client-key` (optional): Path to private key for mTLS (PEM format, optional if using PKCS12)
|
||||
- `tls-ca-cert` (optional): Path to CA certificate to verify server (PEM format, optional if using PKCS12)
|
||||
- `docker-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process. Default: false
|
||||
- `health-file` (optional): Check if connection to WG server (pangolin) is ok. creates a file if ok, removes it if not ok. Can be used with docker healtcheck to restart newt
|
||||
- `accept-clients` (optional): Enable WireGuard server mode to accept incoming newt client connections. Default: false
|
||||
- `generateAndSaveKeyTo` (optional): Path to save generated private key
|
||||
- `native` (optional): Use native WireGuard interface when accepting clients (requires WireGuard kernel module and Linux, must run as root). Default: false (uses userspace netstack)
|
||||
- `interface` (optional): Name of the WireGuard interface. Default: newt
|
||||
- `keep-interface` (optional): Keep the WireGuard interface. Default: false
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All CLI arguments can be set using environment variables as an alternative to command line flags. Environment variables are particularly useful when running Newt in containerized environments.
|
||||
|
||||
- `PANGOLIN_ENDPOINT`: Endpoint of your pangolin server (equivalent to `--endpoint`)
|
||||
- `NEWT_ID`: Newt ID generated by Pangolin (equivalent to `--id`)
|
||||
- `NEWT_SECRET`: Newt secret for authentication (equivalent to `--secret`)
|
||||
- `MTU`: MTU for the internal WG interface. Default: 1280 (equivalent to `--mtu`)
|
||||
- `DNS`: DNS server to use to resolve the endpoint. Default: 9.9.9.9 (equivalent to `--dns`)
|
||||
- `LOG_LEVEL`: Log level (DEBUG, INFO, WARN, ERROR, FATAL). Default: INFO (equivalent to `--log-level`)
|
||||
- `DOCKER_SOCKET`: Path to Docker socket for container discovery (equivalent to `--docker-socket`)
|
||||
- `PING_INTERVAL`: Interval for pinging the server. Default: 3s (equivalent to `--ping-interval`)
|
||||
- `PING_TIMEOUT`: Timeout for each ping. Default: 5s (equivalent to `--ping-timeout`)
|
||||
- `UPDOWN_SCRIPT`: Path to updown script for target add/remove events (equivalent to `--updown`)
|
||||
- `TLS_CLIENT_CERT`: Path to client certificate for mTLS (equivalent to `--tls-client-cert`)
|
||||
- `TLS_CLIENT_CERT`: Path to client certificate for mTLS (equivalent to `--tls-client-cert`)
|
||||
- `TLS_CLIENT_KEY`: Path to private key for mTLS (equivalent to `--tls-client-key`)
|
||||
- `TLS_CA_CERT`: Path to CA certificate to verify server (equivalent to `--tls-ca-cert`)
|
||||
- `DOCKER_ENFORCE_NETWORK_VALIDATION`: Validate container targets are on same network. Default: false (equivalent to `--docker-enforce-network-validation`)
|
||||
- `ENFORCE_HC_CERT`: Enforce certificate validation for health checks. Default: false (equivalent to `--enforce-hc-cert`)
|
||||
- `HEALTH_FILE`: Path to health file for connection monitoring (equivalent to `--health-file`)
|
||||
- `ACCEPT_CLIENTS`: Enable WireGuard server mode. Default: false (equivalent to `--accept-clients`)
|
||||
- `GENERATE_AND_SAVE_KEY_TO`: Path to save generated private key (equivalent to `--generateAndSaveKeyTo`)
|
||||
- `USE_NATIVE_INTERFACE`: Use native WireGuard interface (Linux only). Default: false (equivalent to `--native`)
|
||||
- `INTERFACE`: Name of the WireGuard interface. Default: newt (equivalent to `--interface`)
|
||||
- `KEEP_INTERFACE`: Keep the WireGuard interface after shutdown. Default: false (equivalent to `--keep-interface`)
|
||||
- `CONFIG_FILE`: Load the config json from this file instead of in the home folder.
|
||||
|
||||
## Loading secrets from files
|
||||
|
||||
You can use `CONFIG_FILE` to define a location of a config file to store the credentials between runs.
|
||||
|
||||
```
|
||||
$ cat ~/.config/newt-client/config.json
|
||||
{
|
||||
"id": "spmzu8rbpzj1qq6",
|
||||
"secret": "f6v61mjutwme2kkydbw3fjo227zl60a2tsf5psw9r25hgae3",
|
||||
"endpoint": "https://pangolin.fossorial.io",
|
||||
"tlsClientCert": ""
|
||||
}
|
||||
```
|
||||
|
||||
This file is also written to when newt first starts up. So you do not need to run every time with --id and secret if you have run it once!
|
||||
|
||||
Default locations:
|
||||
|
||||
- **macOS**: `~/Library/Application Support/newt-client/config.json`
|
||||
- **Windows**: `%PROGRAMDATA%\newt\newt-client\config.json`
|
||||
- **Linux/Others**: `~/.config/newt-client/config.json`
|
||||
|
||||
## Examples
|
||||
|
||||
**Note**: When both environment variables and CLI arguments are provided, CLI arguments take precedence.
|
||||
|
||||
- Example:
|
||||
|
||||
```bash
|
||||
./newt \
|
||||
newt \
|
||||
--id 31frd0uzbjvp721 \
|
||||
--secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
|
||||
--endpoint https://example.com
|
||||
@@ -50,51 +121,257 @@ You can also run it with Docker compose. For example, a service in your `docker-
|
||||
|
||||
```yaml
|
||||
services:
|
||||
newt:
|
||||
image: fosrl/newt
|
||||
container_name: newt
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PANGOLIN_ENDPOINT=https://example.com
|
||||
- NEWT_ID=2ix2t8xk22ubpfy
|
||||
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
|
||||
newt:
|
||||
image: fosrl/newt
|
||||
container_name: newt
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PANGOLIN_ENDPOINT=https://example.com
|
||||
- NEWT_ID=2ix2t8xk22ubpfy
|
||||
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
|
||||
- HEALTH_FILE=/tmp/healthy
|
||||
```
|
||||
|
||||
You can also pass the CLI args to the container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
newt:
|
||||
image: fosrl/newt
|
||||
container_name: newt
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --id 31frd0uzbjvp721
|
||||
- --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6
|
||||
- --endpoint https://example.com
|
||||
newt:
|
||||
image: fosrl/newt
|
||||
container_name: newt
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --id 31frd0uzbjvp721
|
||||
- --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6
|
||||
- --endpoint https://example.com
|
||||
- --health-file /tmp/healthy
|
||||
```
|
||||
|
||||
Finally a basic systemd service:
|
||||
## Accept Client Connections
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Newt VPN Client
|
||||
After=network.target
|
||||
When the `--accept-clients` flag is enabled (or `ACCEPT_CLIENTS=true` environment variable is set), Newt operates as a WireGuard server that can accept incoming client connections from other devices. This enables peer-to-peer connectivity through the Newt instance.
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/newt --id 31frd0uzbjvp721 --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 --endpoint https://example.com
|
||||
Restart=always
|
||||
User=root
|
||||
### How It Works
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
In client acceptance mode, Newt:
|
||||
|
||||
- **Creates a WireGuard service** that can accept incoming connections from other WireGuard clients
|
||||
- **Starts a connection testing server** (WGTester) that responds to connectivity checks from remote clients
|
||||
- **Manages peer configurations** dynamically based on Pangolin's instructions
|
||||
- **Enables bidirectional communication** between the Newt instance and connected clients
|
||||
|
||||
### Use Cases
|
||||
|
||||
- **Site-to-site connectivity**: Connect multiple locations through a central Newt instance
|
||||
- **Client access to private networks**: Allow remote clients to access resources behind the Newt instance
|
||||
- **Development environments**: Provide developers secure access to internal services
|
||||
|
||||
### Client Tunneling Modes
|
||||
|
||||
Newt supports two WireGuard tunneling modes:
|
||||
|
||||
#### Userspace Mode (Default)
|
||||
|
||||
By default, Newt uses a fully userspace WireGuard implementation using [netstack](https://github.com/WireGuard/wireguard-go/blob/master/tun/netstack/examples/http_server.go). This mode:
|
||||
|
||||
- **Does not require root privileges**
|
||||
- **Works on all supported platforms** (Linux, Windows, macOS)
|
||||
- **Does not require WireGuard kernel module** to be installed
|
||||
- **Runs entirely in userspace** - no system network interface is created
|
||||
- **Is containerization-friendly** - works seamlessly in Docker containers
|
||||
|
||||
This is the recommended mode for most deployments, especially containerized environments.
|
||||
|
||||
In this mode, TCP and UDP is proxied out of newt from the remote client using TCP/UDP resources in Pangolin.
|
||||
|
||||
#### Native Mode (Linux only)
|
||||
|
||||
When using the `--native` flag or setting `USE_NATIVE_INTERFACE=true`, Newt uses the native WireGuard kernel module. This mode:
|
||||
|
||||
- **Requires root privileges** to create and manage network interfaces
|
||||
- **Only works on Linux** with the WireGuard kernel module installed
|
||||
- **Creates a real network interface** (e.g., `newt0`) on the system
|
||||
- **May offer better performance** for high-throughput scenarios
|
||||
- **Requires proper network permissions** and may conflict with existing network configurations
|
||||
|
||||
In this mode it functions like a traditional VPN interface - all data arrives on the interface and you must get it to the destination (or access things locally).
|
||||
|
||||
#### Native Mode Requirements
|
||||
|
||||
To use native mode:
|
||||
|
||||
1. Run on a Linux system
|
||||
2. Install the WireGuard kernel module
|
||||
3. Run Newt as root (`sudo`)
|
||||
4. Ensure the system allows creation of network interfaces
|
||||
|
||||
Docker Compose example:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
newt:
|
||||
image: fosrl/newt
|
||||
container_name: newt
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PANGOLIN_ENDPOINT=https://example.com
|
||||
- NEWT_ID=2ix2t8xk22ubpfy
|
||||
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
|
||||
- ACCEPT_CLIENTS=true
|
||||
```
|
||||
|
||||
Make sure to `mv ./newt /usr/local/bin/newt`!
|
||||
### Technical Details
|
||||
|
||||
When client acceptance is enabled:
|
||||
|
||||
- **WGTester Server**: Runs on `port + 1` (e.g., if WireGuard uses port 51820, WGTester uses 51821)
|
||||
- **Connection Testing**: Responds to UDP packets with magic header `0xDEADBEEF` for connectivity verification
|
||||
- **Dynamic Configuration**: Peer configurations are managed remotely through Pangolin
|
||||
- **Proxy Integration**: Can work with both userspace (netstack) and native WireGuard modes
|
||||
|
||||
**Note**: Client acceptance mode requires coordination with Pangolin for peer management and configuration distribution.
|
||||
|
||||
### 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. If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation.
|
||||
|
||||
Supported values include:
|
||||
|
||||
- Local UNIX socket (default):
|
||||
>You must mount the socket file into the container using a volume, so Newt can access it.
|
||||
|
||||
`unix:///var/run/docker.sock`
|
||||
|
||||
- TCP socket (e.g., via Docker Socket Proxy):
|
||||
|
||||
`tcp://localhost:2375`
|
||||
|
||||
- HTTP/HTTPS endpoints (e.g., remote Docker APIs):
|
||||
|
||||
`http://your-host:2375`
|
||||
|
||||
- SSH connections (experimental, requires SSH setup):
|
||||
|
||||
`ssh://user@host`
|
||||
|
||||
|
||||
```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=unix:///var/run/docker.sock
|
||||
```
|
||||
>If you previously used just a path like `/var/run/docker.sock`, it still works — Newt assumes it is a UNIX socket by default.
|
||||
|
||||
#### 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 is configured to request a client certificate. You can use either a PKCS12 (.p12/.pfx) file or split PEM files for the client cert, private key, and CA.
|
||||
|
||||
#### Option 1: PKCS12 (Legacy)
|
||||
|
||||
> This is the original method and still supported.
|
||||
|
||||
* File must contain:
|
||||
|
||||
* Client private key
|
||||
* Public certificate
|
||||
* CA certificate
|
||||
* Encrypted `.p12` files are **not supported**
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
newt \
|
||||
--id 31frd0uzbjvp721 \
|
||||
--secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
|
||||
--endpoint https://example.com \
|
||||
--tls-client-cert ./client.p12
|
||||
```
|
||||
|
||||
#### Option 2: Split PEM Files (Preferred)
|
||||
|
||||
You can now provide separate files for:
|
||||
|
||||
* `--tls-client-cert`: client certificate (`.crt` or `.pem`)
|
||||
* `--tls-client-key`: client private key (`.key` or `.pem`)
|
||||
* `--tls-ca-cert`: CA cert to verify the server
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
newt \
|
||||
--id 31frd0uzbjvp721 \
|
||||
--secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
|
||||
--endpoint https://example.com \
|
||||
--tls-client-cert ./client.crt \
|
||||
--tls-client-key ./client.key \
|
||||
--tls-ca-cert ./ca.crt
|
||||
```
|
||||
|
||||
|
||||
```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
|
||||
|
||||
### Container
|
||||
### Container
|
||||
|
||||
Ensure Docker is installed.
|
||||
|
||||
@@ -110,6 +387,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.
|
||||
|
||||
37
blueprint.yaml
Normal file
37
blueprint.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
resources:
|
||||
resource-nice-id:
|
||||
name: this is my resource
|
||||
protocol: http
|
||||
full-domain: level1.test3.example.com
|
||||
host-header: example.com
|
||||
tls-server-name: example.com
|
||||
auth:
|
||||
pincode: 123456
|
||||
password: sadfasdfadsf
|
||||
sso-enabled: true
|
||||
sso-roles:
|
||||
- Member
|
||||
sso-users:
|
||||
- owen@fossorial.io
|
||||
whitelist-users:
|
||||
- owen@fossorial.io
|
||||
targets:
|
||||
# - site: glossy-plains-viscacha-rat
|
||||
- hostname: localhost
|
||||
method: http
|
||||
port: 8000
|
||||
healthcheck:
|
||||
port: 8000
|
||||
hostname: localhost
|
||||
# - site: glossy-plains-viscacha-rat
|
||||
- hostname: localhost
|
||||
method: http
|
||||
port: 8001
|
||||
resource-nice-id2:
|
||||
name: this is other resource
|
||||
protocol: tcp
|
||||
proxy-port: 3000
|
||||
targets:
|
||||
# - site: glossy-plains-viscacha-rat
|
||||
- hostname: localhost
|
||||
port: 3000
|
||||
132
clients.go
Normal file
132
clients.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fosrl/newt/logger"
|
||||
"github.com/fosrl/newt/proxy"
|
||||
"github.com/fosrl/newt/websocket"
|
||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||
|
||||
"github.com/fosrl/newt/wgnetstack"
|
||||
"github.com/fosrl/newt/wgtester"
|
||||
)
|
||||
|
||||
var wgService *wgnetstack.WireGuardService
|
||||
var wgTesterServer *wgtester.Server
|
||||
var ready bool
|
||||
|
||||
func setupClients(client *websocket.Client) {
|
||||
var host = endpoint
|
||||
if strings.HasPrefix(host, "http://") {
|
||||
host = strings.TrimPrefix(host, "http://")
|
||||
} else if strings.HasPrefix(host, "https://") {
|
||||
host = strings.TrimPrefix(host, "https://")
|
||||
}
|
||||
|
||||
host = strings.TrimSuffix(host, "/")
|
||||
|
||||
if useNativeInterface {
|
||||
setupClientsNative(client, host)
|
||||
} else {
|
||||
setupClientsNetstack(client, host)
|
||||
}
|
||||
|
||||
ready = true
|
||||
}
|
||||
|
||||
func setupClientsNetstack(client *websocket.Client, host string) {
|
||||
logger.Info("Setting up clients with netstack...")
|
||||
// Create WireGuard service
|
||||
wgService, err = wgnetstack.NewWireGuardService(interfaceName, mtuInt, generateAndSaveKeyTo, host, id, client, "9.9.9.9")
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to create WireGuard service: %v", err)
|
||||
}
|
||||
|
||||
// // Set up callback to restart wgtester with netstack when WireGuard is ready
|
||||
wgService.SetOnNetstackReady(func(tnet *netstack.Net) {
|
||||
|
||||
wgTesterServer = wgtester.NewServerWithNetstack("0.0.0.0", wgService.Port, id, tnet) // 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)
|
||||
}
|
||||
})
|
||||
|
||||
wgService.SetOnNetstackClose(func() {
|
||||
if wgTesterServer != nil {
|
||||
wgTesterServer.Stop()
|
||||
wgTesterServer = nil
|
||||
}
|
||||
})
|
||||
|
||||
client.OnTokenUpdate(func(token string) {
|
||||
wgService.SetToken(token)
|
||||
})
|
||||
}
|
||||
|
||||
func setDownstreamTNetstack(tnet *netstack.Net) {
|
||||
if wgService != nil {
|
||||
wgService.SetOthertnet(tnet)
|
||||
}
|
||||
}
|
||||
|
||||
func closeClients() {
|
||||
logger.Info("Closing clients...")
|
||||
if wgService != nil {
|
||||
wgService.Close(!keepInterface)
|
||||
wgService = nil
|
||||
}
|
||||
|
||||
closeWgServiceNative()
|
||||
|
||||
if wgTesterServer != nil {
|
||||
wgTesterServer.Stop()
|
||||
wgTesterServer = nil
|
||||
}
|
||||
}
|
||||
|
||||
func clientsHandleNewtConnection(publicKey string, endpoint string) {
|
||||
if !ready {
|
||||
return
|
||||
}
|
||||
|
||||
// split off the port from the endpoint
|
||||
parts := strings.Split(endpoint, ":")
|
||||
if len(parts) < 2 {
|
||||
logger.Error("Invalid endpoint format: %s", endpoint)
|
||||
return
|
||||
}
|
||||
endpoint = strings.Join(parts[:len(parts)-1], ":")
|
||||
|
||||
if wgService != nil {
|
||||
wgService.StartHolepunch(publicKey, endpoint)
|
||||
}
|
||||
|
||||
clientsHandleNewtConnectionNative(publicKey, endpoint)
|
||||
}
|
||||
|
||||
func clientsOnConnect() {
|
||||
if !ready {
|
||||
return
|
||||
}
|
||||
if wgService != nil {
|
||||
wgService.LoadRemoteConfig()
|
||||
}
|
||||
|
||||
clientsOnConnectNative()
|
||||
}
|
||||
|
||||
func clientsAddProxyTarget(pm *proxy.ProxyManager, tunnelIp string) {
|
||||
if !ready {
|
||||
return
|
||||
}
|
||||
// add a udp proxy for localost and the wgService port
|
||||
// TODO: make sure this port is not used in a target
|
||||
if wgService != nil {
|
||||
pm.AddTarget("udp", tunnelIp, int(wgService.Port), fmt.Sprintf("127.0.0.1:%d", wgService.Port))
|
||||
}
|
||||
|
||||
clientsAddProxyTargetNative(pm, tunnelIp)
|
||||
}
|
||||
323
docker/client.go
Normal file
323
docker/client.go
Normal file
@@ -0,0 +1,323 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
// Strcuture parts of docker api endpoint
|
||||
type dockerHost struct {
|
||||
protocol string // e.g. unix, http, tcp, ssh
|
||||
address string // e.g. "/var/run/docker.sock" or "host:port"
|
||||
}
|
||||
|
||||
// Parse the docker api endpoint into its parts
|
||||
func parseDockerHost(raw string) (dockerHost, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(raw, "unix://"):
|
||||
return dockerHost{"unix", strings.TrimPrefix(raw, "unix://")}, nil
|
||||
case strings.HasPrefix(raw, "ssh://"):
|
||||
// SSH is treated as TCP-like transport by the docker client
|
||||
return dockerHost{"ssh", strings.TrimPrefix(raw, "ssh://")}, nil
|
||||
case strings.HasPrefix(raw, "tcp://"), strings.HasPrefix(raw, "http://"), strings.HasPrefix(raw, "https://"):
|
||||
s := raw
|
||||
s = strings.TrimPrefix(s, "tcp://")
|
||||
s = strings.TrimPrefix(s, "http://")
|
||||
s = strings.TrimPrefix(s, "https://")
|
||||
return dockerHost{"tcp", s}, nil
|
||||
case strings.HasPrefix(raw, "/"):
|
||||
// Absolute path without scheme - treat as unix socket
|
||||
return dockerHost{"unix", raw}, nil
|
||||
default:
|
||||
// For relative paths or other formats, also default to unix
|
||||
return dockerHost{"unix", raw}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = "unix:///var/run/docker.sock"
|
||||
}
|
||||
|
||||
// Ensure the socket path is properly formatted
|
||||
if !strings.Contains(socketPath, "://") {
|
||||
// If no scheme provided, assume unix socket
|
||||
socketPath = "unix://" + socketPath
|
||||
}
|
||||
|
||||
host, err := parseDockerHost(socketPath)
|
||||
if err != nil {
|
||||
logger.Debug("Invalid Docker socket path '%s': %v", socketPath, err)
|
||||
return false
|
||||
}
|
||||
protocol := host.protocol
|
||||
addr := host.address
|
||||
|
||||
// ssh might need different verification, but tcp works for basic reachability
|
||||
conn, err := net.DialTimeout(protocol, addr, 2*time.Second)
|
||||
if err != nil {
|
||||
logger.Debug("Docker not reachable via %s at %s: %v", protocol, addr, err)
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
logger.Debug("Docker reachable via %s at %s", protocol, addr)
|
||||
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 = "unix:///var/run/docker.sock"
|
||||
}
|
||||
|
||||
// Ensure the socket path is properly formatted for the Docker client
|
||||
if !strings.Contains(socketPath, "://") {
|
||||
// If no scheme provided, assume unix socket
|
||||
socketPath = "unix://" + socketPath
|
||||
}
|
||||
|
||||
// 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(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
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1756217674,
|
||||
"narHash": "sha256-TH1SfSP523QI7kcPiNtMAEuwZR3Jdz0MCDXPs7TS8uo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4e7667a90c167f7a81d906e5a75cba4ad8bee620",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
73
flake.nix
Normal file
73
flake.nix
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
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;
|
||||
|
||||
# Update version when releasing
|
||||
version = "1.4.2";
|
||||
|
||||
# Update the version in a new source tree
|
||||
srcWithReplacedVersion = pkgs.runCommand "newt-src-with-version" { } ''
|
||||
cp -r ${./.} $out
|
||||
chmod -R +w $out
|
||||
rm -rf $out/.git $out/result $out/.envrc $out/.direnv
|
||||
sed -i "s/version_replaceme/${version}/g" $out/main.go
|
||||
'';
|
||||
in
|
||||
{
|
||||
default = self.packages.${system}.pangolin-newt;
|
||||
pangolin-newt = pkgs.buildGoModule {
|
||||
pname = "pangolin-newt";
|
||||
version = version;
|
||||
src = srcWithReplacedVersion;
|
||||
vendorHash = "sha256-PENsCO2yFxLVZNPgx2OP+gWVNfjJAfXkwWS7tzlm490=";
|
||||
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
235
get-newt.sh
Normal 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 "$@"
|
||||
65
go.mod
65
go.mod
@@ -1,20 +1,55 @@
|
||||
module github.com/fosrl/newt
|
||||
|
||||
go 1.23.1
|
||||
|
||||
toolchain go1.23.2
|
||||
|
||||
require golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
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/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/time v0.7.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
|
||||
github.com/docker/docker v28.3.3+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.41.0
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792
|
||||
golang.org/x/net v0.43.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.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // 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.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/btree v1.1.3 // 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.62.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
186
go.sum
186
go.sum
@@ -1,22 +1,172 @@
|
||||
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/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.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
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.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+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.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/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.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
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/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
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=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
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.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
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.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||
golang.org/x/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/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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-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/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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
|
||||
software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||
|
||||
517
healthcheck/healthcheck.go
Normal file
517
healthcheck/healthcheck.go
Normal file
@@ -0,0 +1,517 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fosrl/newt/logger"
|
||||
)
|
||||
|
||||
// Health represents the health status of a target
|
||||
type Health int
|
||||
|
||||
const (
|
||||
StatusUnknown Health = iota
|
||||
StatusHealthy
|
||||
StatusUnhealthy
|
||||
)
|
||||
|
||||
func (s Health) String() string {
|
||||
switch s {
|
||||
case StatusHealthy:
|
||||
return "healthy"
|
||||
case StatusUnhealthy:
|
||||
return "unhealthy"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Config holds the health check configuration for a target
|
||||
type Config struct {
|
||||
ID int `json:"id"`
|
||||
Enabled bool `json:"hcEnabled"`
|
||||
Path string `json:"hcPath"`
|
||||
Scheme string `json:"hcScheme"`
|
||||
Mode string `json:"hcMode"`
|
||||
Hostname string `json:"hcHostname"`
|
||||
Port int `json:"hcPort"`
|
||||
Interval int `json:"hcInterval"` // in seconds
|
||||
UnhealthyInterval int `json:"hcUnhealthyInterval"` // in seconds
|
||||
Timeout int `json:"hcTimeout"` // in seconds
|
||||
Headers map[string]string `json:"hcHeaders"`
|
||||
Method string `json:"hcMethod"`
|
||||
Status int `json:"hcStatus"` // HTTP status code
|
||||
}
|
||||
|
||||
// Target represents a health check target with its current status
|
||||
type Target struct {
|
||||
Config Config `json:"config"`
|
||||
Status Health `json:"status"`
|
||||
LastCheck time.Time `json:"lastCheck"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
CheckCount int `json:"checkCount"`
|
||||
ticker *time.Ticker
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// StatusChangeCallback is called when any target's status changes
|
||||
type StatusChangeCallback func(targets map[int]*Target)
|
||||
|
||||
// Monitor manages health check targets and their monitoring
|
||||
type Monitor struct {
|
||||
targets map[int]*Target
|
||||
mutex sync.RWMutex
|
||||
callback StatusChangeCallback
|
||||
client *http.Client
|
||||
enforceCert bool
|
||||
}
|
||||
|
||||
// NewMonitor creates a new health check monitor
|
||||
func NewMonitor(callback StatusChangeCallback, enforceCert bool) *Monitor {
|
||||
logger.Debug("Creating new health check monitor with certificate enforcement: %t", enforceCert)
|
||||
|
||||
// Configure TLS settings based on certificate enforcement
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: !enforceCert,
|
||||
},
|
||||
}
|
||||
|
||||
return &Monitor{
|
||||
targets: make(map[int]*Target),
|
||||
callback: callback,
|
||||
enforceCert: enforceCert,
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: transport,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// parseHeaders parses the headers string into a map
|
||||
func parseHeaders(headersStr string) map[string]string {
|
||||
headers := make(map[string]string)
|
||||
if headersStr == "" {
|
||||
return headers
|
||||
}
|
||||
|
||||
// Try to parse as JSON first
|
||||
if err := json.Unmarshal([]byte(headersStr), &headers); err == nil {
|
||||
return headers
|
||||
}
|
||||
|
||||
// Fallback to simple key:value parsing
|
||||
pairs := strings.Split(headersStr, ",")
|
||||
for _, pair := range pairs {
|
||||
kv := strings.SplitN(strings.TrimSpace(pair), ":", 2)
|
||||
if len(kv) == 2 {
|
||||
headers[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
// AddTarget adds a new health check target
|
||||
func (m *Monitor) AddTarget(config Config) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
logger.Info("Adding health check target: ID=%d, hostname=%s, port=%d, enabled=%t",
|
||||
config.ID, config.Hostname, config.Port, config.Enabled)
|
||||
|
||||
return m.addTargetUnsafe(config)
|
||||
}
|
||||
|
||||
// AddTargets adds multiple health check targets in bulk
|
||||
func (m *Monitor) AddTargets(configs []Config) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
logger.Debug("Adding %d health check targets in bulk", len(configs))
|
||||
|
||||
for _, config := range configs {
|
||||
if err := m.addTargetUnsafe(config); err != nil {
|
||||
logger.Error("Failed to add target %d: %v", config.ID, err)
|
||||
return fmt.Errorf("failed to add target %d: %v", config.ID, err)
|
||||
}
|
||||
logger.Debug("Successfully added target: ID=%d, hostname=%s", config.ID, config.Hostname)
|
||||
}
|
||||
|
||||
// Don't notify callback immediately - let the initial health checks complete first
|
||||
// The callback will be triggered when the first health check results are available
|
||||
|
||||
logger.Debug("Successfully added all %d health check targets", len(configs))
|
||||
return nil
|
||||
}
|
||||
|
||||
// addTargetUnsafe adds a target without acquiring the mutex (internal method)
|
||||
func (m *Monitor) addTargetUnsafe(config Config) error {
|
||||
// Set defaults
|
||||
if config.Scheme == "" {
|
||||
config.Scheme = "http"
|
||||
}
|
||||
if config.Mode == "" {
|
||||
config.Mode = "http"
|
||||
}
|
||||
if config.Method == "" {
|
||||
config.Method = "GET"
|
||||
}
|
||||
if config.Interval == 0 {
|
||||
config.Interval = 30
|
||||
}
|
||||
if config.UnhealthyInterval == 0 {
|
||||
config.UnhealthyInterval = 30
|
||||
}
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 5
|
||||
}
|
||||
|
||||
logger.Debug("Target %d configuration: scheme=%s, method=%s, interval=%ds, timeout=%ds",
|
||||
config.ID, config.Scheme, config.Method, config.Interval, config.Timeout)
|
||||
|
||||
// Parse headers if provided as string
|
||||
if len(config.Headers) == 0 && config.Path != "" {
|
||||
// This is a simplified header parsing - in real use you might want more robust parsing
|
||||
config.Headers = make(map[string]string)
|
||||
}
|
||||
|
||||
// Remove existing target if it exists
|
||||
if existing, exists := m.targets[config.ID]; exists {
|
||||
logger.Info("Replacing existing target with ID %d", config.ID)
|
||||
existing.cancel()
|
||||
}
|
||||
|
||||
// Create new target
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
target := &Target{
|
||||
Config: config,
|
||||
Status: StatusUnknown,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
m.targets[config.ID] = target
|
||||
|
||||
// Start monitoring if enabled
|
||||
if config.Enabled {
|
||||
logger.Info("Starting monitoring for target %d (%s:%d)", config.ID, config.Hostname, config.Port)
|
||||
go m.monitorTarget(target)
|
||||
} else {
|
||||
logger.Debug("Target %d added but monitoring is disabled", config.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTarget removes a health check target
|
||||
func (m *Monitor) RemoveTarget(id int) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
target, exists := m.targets[id]
|
||||
if !exists {
|
||||
logger.Warn("Attempted to remove non-existent target with ID %d", id)
|
||||
return fmt.Errorf("target with id %d not found", id)
|
||||
}
|
||||
|
||||
logger.Info("Removing health check target: ID=%d", id)
|
||||
target.cancel()
|
||||
delete(m.targets, id)
|
||||
|
||||
// Notify callback of status change
|
||||
if m.callback != nil {
|
||||
go m.callback(m.GetTargets())
|
||||
}
|
||||
|
||||
logger.Info("Successfully removed target %d", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTargets removes multiple health check targets
|
||||
func (m *Monitor) RemoveTargets(ids []int) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
logger.Info("Removing %d health check targets", len(ids))
|
||||
var notFound []int
|
||||
|
||||
for _, id := range ids {
|
||||
target, exists := m.targets[id]
|
||||
if !exists {
|
||||
notFound = append(notFound, id)
|
||||
logger.Warn("Target with ID %d not found during bulk removal", id)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debug("Removing target %d", id)
|
||||
target.cancel()
|
||||
delete(m.targets, id)
|
||||
}
|
||||
|
||||
removedCount := len(ids) - len(notFound)
|
||||
logger.Info("Successfully removed %d targets", removedCount)
|
||||
|
||||
// Notify callback of status change if any targets were removed
|
||||
if len(notFound) != len(ids) && m.callback != nil {
|
||||
go m.callback(m.GetTargets())
|
||||
}
|
||||
|
||||
if len(notFound) > 0 {
|
||||
logger.Error("Some targets not found during removal: %v", notFound)
|
||||
return fmt.Errorf("targets not found: %v", notFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTargetsByID is a convenience method that accepts either a single ID or multiple IDs
|
||||
func (m *Monitor) RemoveTargetsByID(ids ...int) error {
|
||||
return m.RemoveTargets(ids)
|
||||
}
|
||||
|
||||
// GetTargets returns a copy of all targets
|
||||
func (m *Monitor) GetTargets() map[int]*Target {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.getAllTargetsUnsafe()
|
||||
}
|
||||
|
||||
// getAllTargetsUnsafe returns a copy of all targets without acquiring the mutex (internal method)
|
||||
func (m *Monitor) getAllTargetsUnsafe() map[int]*Target {
|
||||
targets := make(map[int]*Target)
|
||||
for id, target := range m.targets {
|
||||
// Create a copy to avoid race conditions
|
||||
targetCopy := *target
|
||||
targets[id] = &targetCopy
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
// getAllTargets returns a copy of all targets (deprecated, use GetTargets)
|
||||
func (m *Monitor) getAllTargets() map[int]*Target {
|
||||
return m.GetTargets()
|
||||
}
|
||||
|
||||
// monitorTarget monitors a single target
|
||||
func (m *Monitor) monitorTarget(target *Target) {
|
||||
logger.Info("Starting health check monitoring for target %d (%s:%d)",
|
||||
target.Config.ID, target.Config.Hostname, target.Config.Port)
|
||||
|
||||
// Initial check
|
||||
oldStatus := target.Status
|
||||
m.performHealthCheck(target)
|
||||
|
||||
// Notify callback after initial check if status changed or if it's the first check
|
||||
if (oldStatus != target.Status || oldStatus == StatusUnknown) && m.callback != nil {
|
||||
logger.Info("Target %d initial status: %s", target.Config.ID, target.Status.String())
|
||||
go m.callback(m.GetTargets())
|
||||
}
|
||||
|
||||
// Set up ticker based on current status
|
||||
interval := time.Duration(target.Config.Interval) * time.Second
|
||||
if target.Status == StatusUnhealthy {
|
||||
interval = time.Duration(target.Config.UnhealthyInterval) * time.Second
|
||||
}
|
||||
|
||||
logger.Debug("Target %d: initial check interval set to %v", target.Config.ID, interval)
|
||||
target.ticker = time.NewTicker(interval)
|
||||
defer target.ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-target.ctx.Done():
|
||||
logger.Info("Stopping health check monitoring for target %d", target.Config.ID)
|
||||
return
|
||||
case <-target.ticker.C:
|
||||
oldStatus := target.Status
|
||||
m.performHealthCheck(target)
|
||||
|
||||
// Update ticker interval if status changed
|
||||
newInterval := time.Duration(target.Config.Interval) * time.Second
|
||||
if target.Status == StatusUnhealthy {
|
||||
newInterval = time.Duration(target.Config.UnhealthyInterval) * time.Second
|
||||
}
|
||||
|
||||
if newInterval != interval {
|
||||
logger.Debug("Target %d: updating check interval from %v to %v due to status change",
|
||||
target.Config.ID, interval, newInterval)
|
||||
target.ticker.Stop()
|
||||
target.ticker = time.NewTicker(newInterval)
|
||||
interval = newInterval
|
||||
}
|
||||
|
||||
// Notify callback if status changed
|
||||
if oldStatus != target.Status && m.callback != nil {
|
||||
logger.Info("Target %d status changed: %s -> %s",
|
||||
target.Config.ID, oldStatus.String(), target.Status.String())
|
||||
go m.callback(m.GetTargets())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// performHealthCheck performs a health check on a target
|
||||
func (m *Monitor) performHealthCheck(target *Target) {
|
||||
target.CheckCount++
|
||||
target.LastCheck = time.Now()
|
||||
target.LastError = ""
|
||||
|
||||
// Build URL
|
||||
url := fmt.Sprintf("%s://%s", target.Config.Scheme, target.Config.Hostname)
|
||||
if target.Config.Port > 0 {
|
||||
url = fmt.Sprintf("%s:%d", url, target.Config.Port)
|
||||
}
|
||||
if target.Config.Path != "" {
|
||||
if !strings.HasPrefix(target.Config.Path, "/") {
|
||||
url += "/"
|
||||
}
|
||||
url += target.Config.Path
|
||||
}
|
||||
|
||||
logger.Debug("Target %d: performing health check %d to %s",
|
||||
target.Config.ID, target.CheckCount, url)
|
||||
|
||||
if target.Config.Scheme == "https" {
|
||||
logger.Debug("Target %d: HTTPS health check with certificate enforcement: %t",
|
||||
target.Config.ID, m.enforceCert)
|
||||
}
|
||||
|
||||
// Create request
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(target.Config.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, target.Config.Method, url, nil)
|
||||
if err != nil {
|
||||
target.Status = StatusUnhealthy
|
||||
target.LastError = fmt.Sprintf("failed to create request: %v", err)
|
||||
logger.Warn("Target %d: failed to create request: %v", target.Config.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add headers
|
||||
for key, value := range target.Config.Headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
// Perform request
|
||||
resp, err := m.client.Do(req)
|
||||
if err != nil {
|
||||
target.Status = StatusUnhealthy
|
||||
target.LastError = fmt.Sprintf("request failed: %v", err)
|
||||
logger.Warn("Target %d: health check failed: %v", target.Config.ID, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response status
|
||||
var expectedStatus int
|
||||
if target.Config.Status > 0 {
|
||||
expectedStatus = target.Config.Status
|
||||
} else {
|
||||
expectedStatus = 0 // Use range check for 200-299
|
||||
}
|
||||
|
||||
if expectedStatus > 0 {
|
||||
logger.Debug("Target %d: checking health status against expected code %d", target.Config.ID, expectedStatus)
|
||||
// Check for specific status code
|
||||
if resp.StatusCode == expectedStatus {
|
||||
target.Status = StatusHealthy
|
||||
logger.Debug("Target %d: health check passed (status: %d, expected: %d)", target.Config.ID, resp.StatusCode, expectedStatus)
|
||||
} else {
|
||||
target.Status = StatusUnhealthy
|
||||
target.LastError = fmt.Sprintf("unexpected status code: %d (expected: %d)", resp.StatusCode, expectedStatus)
|
||||
logger.Warn("Target %d: health check failed with status code %d (expected: %d)", target.Config.ID, resp.StatusCode, expectedStatus)
|
||||
}
|
||||
} else {
|
||||
// Check for 2xx range
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
target.Status = StatusHealthy
|
||||
logger.Debug("Target %d: health check passed (status: %d)", target.Config.ID, resp.StatusCode)
|
||||
} else {
|
||||
target.Status = StatusUnhealthy
|
||||
target.LastError = fmt.Sprintf("unhealthy status code: %d", resp.StatusCode)
|
||||
logger.Warn("Target %d: health check failed with status code %d", target.Config.ID, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops monitoring all targets
|
||||
func (m *Monitor) Stop() {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
targetCount := len(m.targets)
|
||||
logger.Info("Stopping health check monitor with %d targets", targetCount)
|
||||
|
||||
for id, target := range m.targets {
|
||||
logger.Debug("Stopping monitoring for target %d", id)
|
||||
target.cancel()
|
||||
}
|
||||
m.targets = make(map[int]*Target)
|
||||
|
||||
logger.Info("Health check monitor stopped")
|
||||
}
|
||||
|
||||
// EnableTarget enables monitoring for a specific target
|
||||
func (m *Monitor) EnableTarget(id int) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
target, exists := m.targets[id]
|
||||
if !exists {
|
||||
logger.Warn("Attempted to enable non-existent target with ID %d", id)
|
||||
return fmt.Errorf("target with id %d not found", id)
|
||||
}
|
||||
|
||||
if !target.Config.Enabled {
|
||||
logger.Info("Enabling health check monitoring for target %d", id)
|
||||
target.Config.Enabled = true
|
||||
target.cancel() // Stop existing monitoring
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
target.ctx = ctx
|
||||
target.cancel = cancel
|
||||
|
||||
go m.monitorTarget(target)
|
||||
} else {
|
||||
logger.Debug("Target %d is already enabled", id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableTarget disables monitoring for a specific target
|
||||
func (m *Monitor) DisableTarget(id int) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
target, exists := m.targets[id]
|
||||
if !exists {
|
||||
logger.Warn("Attempted to disable non-existent target with ID %d", id)
|
||||
return fmt.Errorf("target with id %d not found", id)
|
||||
}
|
||||
|
||||
if target.Config.Enabled {
|
||||
logger.Info("Disabling health check monitoring for target %d", id)
|
||||
target.Config.Enabled = false
|
||||
target.cancel()
|
||||
target.Status = StatusUnknown
|
||||
|
||||
// Notify callback of status change
|
||||
if m.callback != nil {
|
||||
go m.callback(m.GetTargets())
|
||||
}
|
||||
} else {
|
||||
logger.Debug("Target %d is already disabled", id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
74
linux.go
Normal file
74
linux.go
Normal file
@@ -0,0 +1,74 @@
|
||||
//go:build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"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 wgServiceNative *wg.WireGuardService
|
||||
|
||||
func setupClientsNative(client *websocket.Client, host string) {
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
logger.Fatal("Tunnel management is only supported on Linux right now!")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// make sure we are sudo
|
||||
if os.Geteuid() != 0 {
|
||||
logger.Fatal("You must run this program as root to manage tunnels on Linux.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create WireGuard service
|
||||
wgServiceNative, err = wg.NewWireGuardService(interfaceName, mtuInt, generateAndSaveKeyTo, host, id, client)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to create WireGuard service: %v", err)
|
||||
}
|
||||
|
||||
wgTesterServer = wgtester.NewServer("0.0.0.0", wgServiceNative.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)
|
||||
}
|
||||
|
||||
client.OnTokenUpdate(func(token string) {
|
||||
wgServiceNative.SetToken(token)
|
||||
})
|
||||
}
|
||||
|
||||
func closeWgServiceNative() {
|
||||
if wgServiceNative != nil {
|
||||
wgServiceNative.Close(!keepInterface)
|
||||
wgServiceNative = nil
|
||||
}
|
||||
}
|
||||
|
||||
func clientsOnConnectNative() {
|
||||
if wgServiceNative != nil {
|
||||
wgServiceNative.LoadRemoteConfig()
|
||||
}
|
||||
}
|
||||
|
||||
func clientsHandleNewtConnectionNative(publicKey, endpoint string) {
|
||||
if wgServiceNative != nil {
|
||||
wgServiceNative.StartHolepunch(publicKey, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func clientsAddProxyTargetNative(pm *proxy.ProxyManager, tunnelIp string) {
|
||||
// add a udp proxy for localost and the wgService port
|
||||
// TODO: make sure this port is not used in a target
|
||||
if wgServiceNative != nil {
|
||||
pm.AddTarget("udp", tunnelIp, int(wgServiceNative.Port), fmt.Sprintf("127.0.0.1:%d", wgServiceNative.Port))
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
@@ -48,12 +49,33 @@ func (l *Logger) SetLevel(level LogLevel) {
|
||||
l.level = level
|
||||
}
|
||||
|
||||
// SetOutput sets the output destination for the logger
|
||||
func (l *Logger) SetOutput(w io.Writer) {
|
||||
l.logger.SetOutput(w)
|
||||
}
|
||||
|
||||
// log handles the actual logging
|
||||
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)
|
||||
}
|
||||
@@ -104,3 +126,8 @@ func Error(format string, args ...interface{}) {
|
||||
func Fatal(format string, args ...interface{}) {
|
||||
GetLogger().Fatal(format, args...)
|
||||
}
|
||||
|
||||
// SetOutput sets the output destination for the default logger
|
||||
func SetOutput(w io.Writer) {
|
||||
GetLogger().SetOutput(w)
|
||||
}
|
||||
|
||||
195
network/network.go
Normal file
195
network/network.go
Normal 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
|
||||
}
|
||||
@@ -41,6 +41,23 @@ func NewProxyManager(tnet *netstack.Net) *ProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// init function without tnet
|
||||
func NewProxyManagerWithoutTNet() *ProxyManager {
|
||||
return &ProxyManager{
|
||||
tcpTargets: make(map[string]map[int]string),
|
||||
udpTargets: make(map[string]map[int]string),
|
||||
listeners: make([]*gonet.TCPListener, 0),
|
||||
udpConns: make([]*gonet.UDPConn, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Function to add tnet to existing ProxyManager
|
||||
func (pm *ProxyManager) SetTNet(tnet *netstack.Net) {
|
||||
pm.mutex.Lock()
|
||||
defer pm.mutex.Unlock()
|
||||
pm.tnet = tnet
|
||||
}
|
||||
|
||||
// AddTarget adds as new target for proxying
|
||||
func (pm *ProxyManager) AddTarget(proto, listenIP string, port int, targetAddr string) error {
|
||||
pm.mutex.Lock()
|
||||
@@ -174,13 +191,13 @@ func (pm *ProxyManager) Stop() error {
|
||||
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...)
|
||||
}
|
||||
|
||||
// Clear the target maps
|
||||
for k := range pm.tcpTargets {
|
||||
delete(pm.tcpTargets, k)
|
||||
}
|
||||
for k := range pm.udpTargets {
|
||||
delete(pm.udpTargets, k)
|
||||
}
|
||||
// // 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)
|
||||
@@ -213,7 +230,8 @@ func (pm *ProxyManager) startTarget(proto, listenIP string, port int, targetAddr
|
||||
return fmt.Errorf("unsupported protocol: %s", proto)
|
||||
}
|
||||
|
||||
logger.Info("Started %s proxy from %s:%d to %s", proto, listenIP, port, targetAddr)
|
||||
logger.Info("Started %s proxy to %s", proto, targetAddr)
|
||||
logger.Debug("Started %s proxy from %s:%d to %s", proto, listenIP, port, targetAddr)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -278,6 +296,13 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
|
||||
n, remoteAddr, err := conn.ReadFrom(buffer)
|
||||
if err != nil {
|
||||
if !pm.running {
|
||||
// Clean up all connections when stopping
|
||||
clientsMutex.Lock()
|
||||
for _, targetConn := range clientConns {
|
||||
targetConn.Close()
|
||||
}
|
||||
clientConns = nil
|
||||
clientsMutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -322,22 +347,32 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
|
||||
clientConns[clientKey] = targetConn
|
||||
clientsMutex.Unlock()
|
||||
|
||||
go func() {
|
||||
go func(clientKey string, targetConn *net.UDPConn, remoteAddr net.Addr) {
|
||||
defer func() {
|
||||
// Always clean up when this goroutine exits
|
||||
clientsMutex.Lock()
|
||||
if storedConn, exists := clientConns[clientKey]; exists && storedConn == targetConn {
|
||||
delete(clientConns, clientKey)
|
||||
targetConn.Close()
|
||||
}
|
||||
clientsMutex.Unlock()
|
||||
}()
|
||||
|
||||
buffer := make([]byte, 65507)
|
||||
for {
|
||||
n, _, err := targetConn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
logger.Error("Error reading from target: %v", err)
|
||||
return
|
||||
return // defer will handle cleanup
|
||||
}
|
||||
|
||||
_, err = conn.WriteTo(buffer[:n], remoteAddr)
|
||||
if err != nil {
|
||||
logger.Error("Error writing to client: %v", err)
|
||||
return
|
||||
return // defer will handle cleanup
|
||||
}
|
||||
}
|
||||
}()
|
||||
}(clientKey, targetConn, remoteAddr)
|
||||
}
|
||||
|
||||
_, err = targetConn.Write(buffer[:n])
|
||||
@@ -350,3 +385,23 @@ func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write a function to print out the current targets in the ProxyManager
|
||||
func (pm *ProxyManager) PrintTargets() {
|
||||
pm.mutex.RLock()
|
||||
defer pm.mutex.RUnlock()
|
||||
|
||||
logger.Info("Current TCP Targets:")
|
||||
for listenIP, targets := range pm.tcpTargets {
|
||||
for port, targetAddr := range targets {
|
||||
logger.Info("TCP %s:%d -> %s", listenIP, port, targetAddr)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Current UDP Targets:")
|
||||
for listenIP, targets := range pm.udpTargets {
|
||||
for port, targetAddr := range targets {
|
||||
logger.Info("UDP %s:%d -> %s", listenIP, port, targetAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 774 KiB After Width: | Height: | Size: 93 KiB |
125
self-signed-certs-for-mtls.sh
Executable file
125
self-signed-certs-for-mtls.sh
Executable 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
32
stub.go
Normal file
@@ -0,0 +1,32 @@
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/fosrl/newt/proxy"
|
||||
"github.com/fosrl/newt/websocket"
|
||||
)
|
||||
|
||||
func setupClientsNative(client *websocket.Client, host string) {
|
||||
return // This function is not implemented for non-Linux systems.
|
||||
}
|
||||
|
||||
func closeWgServiceNative() {
|
||||
// No-op for non-Linux systems
|
||||
return
|
||||
}
|
||||
|
||||
func clientsOnConnectNative() {
|
||||
// No-op for non-Linux systems
|
||||
return
|
||||
}
|
||||
|
||||
func clientsHandleNewtConnectionNative(publicKey, endpoint string) {
|
||||
// No-op for non-Linux systems
|
||||
return
|
||||
}
|
||||
|
||||
func clientsAddProxyTargetNative(pm *proxy.ProxyManager, tunnelIp string) {
|
||||
// No-op for non-Linux systems
|
||||
return
|
||||
}
|
||||
173
updates/updates.go
Normal file
173
updates/updates.go
Normal 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
77
updown.py
Normal 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)
|
||||
605
util.go
Normal file
605
util.go
Normal file
@@ -0,0 +1,605 @@
|
||||
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"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func sendBlueprint(client *websocket.Client) error {
|
||||
if blueprintFile == "" {
|
||||
return nil
|
||||
}
|
||||
// try to read the blueprint file
|
||||
blueprintData, err := os.ReadFile(blueprintFile)
|
||||
if err != nil {
|
||||
logger.Error("Failed to read blueprint file: %v", err)
|
||||
} else {
|
||||
// first we should convert the yaml to json and error if the yaml is bad
|
||||
var yamlObj interface{}
|
||||
var blueprintJsonData string
|
||||
|
||||
err = yaml.Unmarshal(blueprintData, &yamlObj)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse blueprint YAML: %v", err)
|
||||
} else {
|
||||
// convert to json
|
||||
jsonBytes, err := json.Marshal(yamlObj)
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert blueprint to JSON: %v", err)
|
||||
} else {
|
||||
blueprintJsonData = string(jsonBytes)
|
||||
logger.Debug("Converted blueprint to JSON: %s", blueprintJsonData)
|
||||
}
|
||||
}
|
||||
|
||||
// if we have valid json data, we can send it to the server
|
||||
if blueprintJsonData == "" {
|
||||
logger.Error("No valid blueprint JSON data to send to server")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info("Sending blueprint to server for application")
|
||||
|
||||
// send the blueprint data to the server
|
||||
err = client.SendMessage("newt/blueprint/apply", map[string]interface{}{
|
||||
"blueprint": blueprintJsonData,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,38 +2,58 @@ package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"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
|
||||
clientType string // Type of client (e.g., "newt", "olm")
|
||||
tlsConfig TLSConfig
|
||||
}
|
||||
|
||||
type ClientOption func(*Client)
|
||||
|
||||
type MessageHandler func(message WSMessage)
|
||||
|
||||
// TLSConfig holds TLS configuration options
|
||||
type TLSConfig struct {
|
||||
// New separate certificate support
|
||||
ClientCertFile string
|
||||
ClientKeyFile string
|
||||
CAFiles []string
|
||||
|
||||
// Existing PKCS12 support (deprecated)
|
||||
PKCS12File string
|
||||
}
|
||||
|
||||
// WithBaseURL sets the base URL for the client
|
||||
func WithBaseURL(url string) ClientOption {
|
||||
return func(c *Client) {
|
||||
@@ -41,14 +61,29 @@ func WithBaseURL(url string) ClientOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithTLSConfig sets the TLS configuration for the client
|
||||
func WithTLSConfig(config TLSConfig) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.tlsConfig = config
|
||||
// For backward compatibility, also set the legacy field
|
||||
if config.PKCS12File != "" {
|
||||
c.config.TlsClientCert = config.PKCS12File
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) OnConnect(callback func() error) {
|
||||
c.onConnect = callback
|
||||
}
|
||||
|
||||
// NewClient creates a new Newt client
|
||||
func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*Client, error) {
|
||||
func (c *Client) OnTokenUpdate(callback func(token string)) {
|
||||
c.onTokenUpdate = callback
|
||||
}
|
||||
|
||||
// NewClient creates a new websocket client
|
||||
func NewClient(clientType string, ID, secret string, endpoint string, pingInterval time.Duration, pingTimeout time.Duration, opts ...ClientOption) (*Client, error) {
|
||||
config := &Config{
|
||||
NewtID: newtID,
|
||||
ID: ID,
|
||||
Secret: secret,
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
@@ -58,12 +93,18 @@ 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,
|
||||
clientType: clientType,
|
||||
}
|
||||
|
||||
// Apply options before loading config
|
||||
for _, opt := range opts {
|
||||
if opt == nil {
|
||||
continue
|
||||
}
|
||||
opt(client)
|
||||
}
|
||||
|
||||
@@ -75,22 +116,41 @@ func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*C
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetConfig() *Config {
|
||||
return c.config
|
||||
}
|
||||
|
||||
// Connect establishes the WebSocket connection
|
||||
func (c *Client) Connect() error {
|
||||
go c.connectWithRetry()
|
||||
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 +165,49 @@ 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() {
|
||||
count := 0
|
||||
maxAttempts := 10
|
||||
|
||||
err := c.SendMessage(messageType, data) // Send immediately
|
||||
if err != nil {
|
||||
logger.Error("Failed to send initial message: %v", err)
|
||||
}
|
||||
count++
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if count >= maxAttempts {
|
||||
logger.Info("SendMessageInterval timed out after %d attempts for message type: %s", maxAttempts, messageType)
|
||||
return
|
||||
}
|
||||
err = c.SendMessage(messageType, data)
|
||||
if err != nil {
|
||||
logger.Error("Failed to send message: %v", err)
|
||||
}
|
||||
count++
|
||||
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 +215,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,57 +225,41 @@ 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
|
||||
|
||||
// Use new TLS configuration method
|
||||
if c.tlsConfig.ClientCertFile != "" || c.tlsConfig.ClientKeyFile != "" || len(c.tlsConfig.CAFiles) > 0 || c.tlsConfig.PKCS12File != "" {
|
||||
tlsConfig, err = c.setupTLS()
|
||||
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 setup TLS configuration: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for environment variable to skip TLS verification
|
||||
if os.Getenv("SKIP_TLS_VERIFY") == "true" {
|
||||
if tlsConfig == nil {
|
||||
tlsConfig = &tls.Config{}
|
||||
}
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
logger.Debug("TLS certificate verification disabled via SKIP_TLS_VERIFY environment variable")
|
||||
}
|
||||
|
||||
var tokenData map[string]interface{}
|
||||
|
||||
// Get a new token
|
||||
tokenData := map[string]interface{}{
|
||||
"newtId": c.config.NewtID,
|
||||
"secret": c.config.Secret,
|
||||
if c.clientType == "newt" {
|
||||
tokenData = map[string]interface{}{
|
||||
"newtId": c.config.ID,
|
||||
"secret": c.config.Secret,
|
||||
}
|
||||
} else if c.clientType == "olm" {
|
||||
tokenData = map[string]interface{}{
|
||||
"olmId": c.config.ID,
|
||||
"secret": c.config.Secret,
|
||||
}
|
||||
}
|
||||
jsonData, err := json.Marshal(tokenData)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal token request data: %w", err)
|
||||
}
|
||||
@@ -207,7 +267,7 @@ func (c *Client) getToken() (string, error) {
|
||||
// Create a new request
|
||||
req, err := http.NewRequest(
|
||||
"POST",
|
||||
baseEndpoint+"/api/v1/auth/newt/get-token",
|
||||
baseEndpoint+"/api/v1/auth/"+c.clientType+"/get-token",
|
||||
bytes.NewBuffer(jsonData),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -220,18 +280,26 @@ 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 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
logger.Error("Failed to get token with status code: %d, body: %s", resp.StatusCode, string(body))
|
||||
return "", fmt.Errorf("failed to get token with status code: %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var tokenResp TokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||
// print out the token response for debugging
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(resp.Body)
|
||||
logger.Info("Token response: %s", buf.String())
|
||||
logger.Error("Failed to decode token response.")
|
||||
return "", fmt.Errorf("failed to decode token response: %w", err)
|
||||
}
|
||||
|
||||
@@ -243,6 +311,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
|
||||
}
|
||||
|
||||
@@ -270,6 +340,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 {
|
||||
@@ -292,10 +366,32 @@ func (c *Client) establishConnection() error {
|
||||
// Add token to query parameters
|
||||
q := u.Query()
|
||||
q.Set("token", token)
|
||||
q.Set("clientType", c.clientType)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
// Connect to WebSocket
|
||||
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
||||
dialer := websocket.DefaultDialer
|
||||
|
||||
// Use new TLS configuration method
|
||||
if c.tlsConfig.ClientCertFile != "" || c.tlsConfig.ClientKeyFile != "" || len(c.tlsConfig.CAFiles) > 0 || c.tlsConfig.PKCS12File != "" {
|
||||
logger.Info("Setting up TLS configuration for WebSocket connection")
|
||||
tlsConfig, err := c.setupTLS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup TLS configuration: %w", err)
|
||||
}
|
||||
dialer.TLSClientConfig = tlsConfig
|
||||
}
|
||||
|
||||
// Check for environment variable to skip TLS verification for WebSocket connection
|
||||
if os.Getenv("SKIP_TLS_VERIFY") == "true" {
|
||||
if dialer.TLSClientConfig == nil {
|
||||
dialer.TLSClientConfig = &tls.Config{}
|
||||
}
|
||||
dialer.TLSClientConfig.InsecureSkipVerify = true
|
||||
logger.Debug("WebSocket TLS certificate verification disabled via SKIP_TLS_VERIFY environment variable")
|
||||
}
|
||||
|
||||
conn, _, err := dialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to WebSocket: %w", err)
|
||||
}
|
||||
@@ -305,8 +401,8 @@ 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()
|
||||
@@ -321,8 +417,72 @@ func (c *Client) establishConnection() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupTLS configures TLS based on the TLS configuration
|
||||
func (c *Client) setupTLS() (*tls.Config, error) {
|
||||
tlsConfig := &tls.Config{}
|
||||
|
||||
// Handle new separate certificate configuration
|
||||
if c.tlsConfig.ClientCertFile != "" && c.tlsConfig.ClientKeyFile != "" {
|
||||
logger.Info("Loading separate certificate files for mTLS")
|
||||
logger.Debug("Client cert: %s", c.tlsConfig.ClientCertFile)
|
||||
logger.Debug("Client key: %s", c.tlsConfig.ClientKeyFile)
|
||||
|
||||
// Load client certificate and key
|
||||
cert, err := tls.LoadX509KeyPair(c.tlsConfig.ClientCertFile, c.tlsConfig.ClientKeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load client certificate pair: %w", err)
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
|
||||
// Load CA certificates for remote validation if specified
|
||||
if len(c.tlsConfig.CAFiles) > 0 {
|
||||
logger.Debug("Loading CA certificates: %v", c.tlsConfig.CAFiles)
|
||||
caCertPool := x509.NewCertPool()
|
||||
for _, caFile := range c.tlsConfig.CAFiles {
|
||||
caCert, err := os.ReadFile(caFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read CA file %s: %w", caFile, err)
|
||||
}
|
||||
|
||||
// Try to parse as PEM first, then DER
|
||||
if !caCertPool.AppendCertsFromPEM(caCert) {
|
||||
// If PEM parsing failed, try DER
|
||||
cert, err := x509.ParseCertificate(caCert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse CA certificate from %s: %w", caFile, err)
|
||||
}
|
||||
caCertPool.AddCert(cert)
|
||||
}
|
||||
}
|
||||
tlsConfig.RootCAs = caCertPool
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// Fallback to existing PKCS12 implementation for backward compatibility
|
||||
if c.tlsConfig.PKCS12File != "" {
|
||||
logger.Info("Loading PKCS12 certificate for mTLS (deprecated)")
|
||||
return c.setupPKCS12TLS()
|
||||
}
|
||||
|
||||
// Legacy fallback using config.TlsClientCert
|
||||
if c.config.TlsClientCert != "" {
|
||||
logger.Info("Loading legacy PKCS12 certificate for mTLS (deprecated)")
|
||||
return loadClientCertificate(c.config.TlsClientCert)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// setupPKCS12TLS loads TLS configuration from PKCS12 file
|
||||
func (c *Client) setupPKCS12TLS() (*tls.Config, error) {
|
||||
return loadClientCertificate(c.tlsConfig.PKCS12File)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -330,11 +490,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,9 +566,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) {
|
||||
@@ -353,3 +583,42 @@ func (c *Client) setConnected(status bool) {
|
||||
defer c.reconnectMux.Unlock()
|
||||
c.isConnected = status
|
||||
}
|
||||
|
||||
// LoadClientCertificate Helper method to load client certificates (PKCS12 format)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -8,30 +8,36 @@ import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func getConfigPath() string {
|
||||
var configDir string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
configDir = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "newt-client")
|
||||
case "windows":
|
||||
configDir = filepath.Join(os.Getenv("APPDATA"), "newt-client")
|
||||
default: // linux and others
|
||||
configDir = filepath.Join(os.Getenv("HOME"), ".config", "newt-client")
|
||||
func getConfigPath(clientType string) string {
|
||||
configFile := os.Getenv("CONFIG_FILE")
|
||||
if configFile == "" {
|
||||
var configDir string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
configDir = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", clientType+"-client")
|
||||
case "windows":
|
||||
logDir := filepath.Join(os.Getenv("PROGRAMDATA"), "olm")
|
||||
configDir = filepath.Join(logDir, clientType+"-client")
|
||||
default: // linux and others
|
||||
configDir = filepath.Join(os.Getenv("HOME"), ".config", clientType+"-client")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
log.Printf("Failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
return filepath.Join(configDir, "config.json")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
log.Printf("Failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
return filepath.Join(configDir, "config.json")
|
||||
return configFile
|
||||
}
|
||||
|
||||
func (c *Client) loadConfig() error {
|
||||
if c.config.NewtID != "" && c.config.Secret != "" && c.config.Endpoint != "" {
|
||||
if c.config.ID != "" && c.config.Secret != "" && c.config.Endpoint != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
configPath := getConfigPath()
|
||||
configPath := getConfigPath(c.clientType)
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -45,15 +51,15 @@ func (c *Client) loadConfig() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.config.NewtID == "" {
|
||||
c.config.NewtID = config.NewtID
|
||||
}
|
||||
if c.config.Token == "" {
|
||||
c.config.Token = config.Token
|
||||
if c.config.ID == "" {
|
||||
c.config.ID = config.ID
|
||||
}
|
||||
if c.config.Secret == "" {
|
||||
c.config.Secret = config.Secret
|
||||
}
|
||||
if c.config.TlsClientCert == "" {
|
||||
c.config.TlsClientCert = config.TlsClientCert
|
||||
}
|
||||
if c.config.Endpoint == "" {
|
||||
c.config.Endpoint = config.Endpoint
|
||||
c.baseURL = config.Endpoint
|
||||
@@ -63,7 +69,7 @@ func (c *Client) loadConfig() error {
|
||||
}
|
||||
|
||||
func (c *Client) saveConfig() error {
|
||||
configPath := getConfigPath()
|
||||
configPath := getConfigPath(c.clientType)
|
||||
data, err := json.MarshalIndent(c.config, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -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"`
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
TlsClientCert string `json:"tlsClientCert"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
|
||||
987
wg/wg.go
Normal file
987
wg/wg.go
Normal file
@@ -0,0 +1,987 @@
|
||||
//go:build linux
|
||||
|
||||
package wg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"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
|
||||
keyFilePath string
|
||||
newtId string
|
||||
lastReadings map[string]PeerReading
|
||||
mu sync.Mutex
|
||||
Port uint16
|
||||
stopHolepunch chan struct{}
|
||||
host string
|
||||
serverPubKey string
|
||||
holePunchEndpoint string
|
||||
token string
|
||||
stopGetConfig func()
|
||||
interfaceCreated bool
|
||||
}
|
||||
|
||||
// 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
|
||||
key, err = wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate private key: %v", err)
|
||||
}
|
||||
|
||||
// Load or generate private key
|
||||
if generateAndSaveKeyTo != "" {
|
||||
if _, err := os.Stat(generateAndSaveKeyTo); os.IsNotExist(err) {
|
||||
keyData, err := os.ReadFile(generateAndSaveKeyTo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read private key: %v", err)
|
||||
}
|
||||
key, err = wgtypes.ParseKey(strings.TrimSpace(string(keyData)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse private key: %v", err)
|
||||
}
|
||||
} else {
|
||||
err = os.WriteFile(generateAndSaveKeyTo, []byte(key.String()), 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to save private key: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service := &WireGuardService{
|
||||
interfaceName: interfaceName,
|
||||
mtu: mtu,
|
||||
client: wsClient,
|
||||
wgClient: wgClient,
|
||||
key: key,
|
||||
keyFilePath: generateAndSaveKeyTo,
|
||||
newtId: newtId,
|
||||
host: host,
|
||||
lastReadings: make(map[string]PeerReading),
|
||||
stopHolepunch: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Get the existing wireguard port (keep this part)
|
||||
device, err := service.wgClient.Device(service.interfaceName)
|
||||
if err == nil {
|
||||
service.Port = uint16(device.ListenPort)
|
||||
if service.Port != 0 {
|
||||
logger.Info("WireGuard interface %s already exists with port %d\n", service.interfaceName, service.Port)
|
||||
} else {
|
||||
service.Port, err = FindAvailableUDPPort(49152, 65535)
|
||||
if err != nil {
|
||||
fmt.Printf("Error finding available port: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
service.Port, err = FindAvailableUDPPort(49152, 65535)
|
||||
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)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (s *WireGuardService) Close(rm bool) {
|
||||
if s.stopGetConfig != nil {
|
||||
s.stopGetConfig()
|
||||
s.stopGetConfig = nil
|
||||
}
|
||||
|
||||
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 s.keyFilePath != "" {
|
||||
// if err := os.Remove(s.keyFilePath); err != nil {
|
||||
// logger.Error("Failed to remove private key file: %v", err)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WireGuardService) StartHolepunch(serverPubKey string, endpoint string) {
|
||||
// if the device is already created dont start a new holepunch
|
||||
if s.interfaceCreated {
|
||||
return
|
||||
}
|
||||
|
||||
s.serverPubKey = serverPubKey
|
||||
s.holePunchEndpoint = endpoint
|
||||
|
||||
logger.Debug("Starting UDP hole punch to %s", s.holePunchEndpoint)
|
||||
|
||||
s.stopHolepunch = make(chan struct{})
|
||||
|
||||
// start the UDP holepunch
|
||||
go s.keepSendingUDPHolePunch(s.holePunchEndpoint)
|
||||
}
|
||||
|
||||
func (s *WireGuardService) SetToken(token string) {
|
||||
s.token = token
|
||||
}
|
||||
|
||||
func (s *WireGuardService) LoadRemoteConfig() error {
|
||||
s.stopGetConfig = s.client.SendMessageInterval("newt/wg/get-config", map[string]interface{}{
|
||||
"publicKey": s.key.PublicKey().String(),
|
||||
"port": s.Port,
|
||||
}, 2*time.Second)
|
||||
|
||||
logger.Info("Requesting WireGuard configuration from remote server")
|
||||
go s.periodicBandwidthCheck()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WireGuardService) handleConfig(msg websocket.WSMessage) {
|
||||
var config WgConfig
|
||||
|
||||
logger.Debug("Received message: %v", msg)
|
||||
logger.Info("Received WireGuard clients configuration from remote server")
|
||||
|
||||
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
|
||||
|
||||
if s.stopGetConfig != nil {
|
||||
s.stopGetConfig()
|
||||
s.stopGetConfig = nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
s.interfaceCreated = true
|
||||
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)
|
||||
|
||||
s.interfaceCreated = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// stop the holepunch its a channel
|
||||
if s.stopHolepunch != nil {
|
||||
close(s.stopHolepunch)
|
||||
s.stopHolepunch = 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 {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("interface %s does not exist", s.interfaceName)
|
||||
}
|
||||
return fmt.Errorf("failed to get device: %v", err)
|
||||
}
|
||||
|
||||
// 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.Debug("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.Debug("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.Debug("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 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) {
|
||||
// send initial hole punch
|
||||
if err := s.sendUDPHolePunch(host + ":21820"); err != nil {
|
||||
logger.Error("Failed to send initial UDP hole punch: %v", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
1288
wgnetstack/wgnetstack.go
Normal file
1288
wgnetstack/wgnetstack.go
Normal file
File diff suppressed because it is too large
Load Diff
242
wgtester/wgtester.go
Normal file
242
wgtester/wgtester.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package wgtester
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fosrl/newt/logger"
|
||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||
)
|
||||
|
||||
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.Conn // Generic net.Conn interface (could be *net.UDPConn or *gonet.UDPConn)
|
||||
udpConn *net.UDPConn // Regular UDP connection (when not using netstack)
|
||||
netstackConn interface{} // Netstack UDP connection (when using netstack)
|
||||
serverAddr string
|
||||
serverPort uint16
|
||||
shutdownCh chan struct{}
|
||||
isRunning bool
|
||||
runningLock sync.Mutex
|
||||
newtID string
|
||||
outputPrefix string
|
||||
useNetstack bool
|
||||
tnet interface{} // Will be *netstack.Net when using netstack
|
||||
}
|
||||
|
||||
// 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] ",
|
||||
useNetstack: false,
|
||||
tnet: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// NewServerWithNetstack creates a new connection test server using WireGuard netstack
|
||||
func NewServerWithNetstack(serverAddr string, serverPort uint16, newtID string, tnet *netstack.Net) *Server {
|
||||
return &Server{
|
||||
serverAddr: serverAddr,
|
||||
serverPort: serverPort + 1, // use the next port for the server
|
||||
shutdownCh: make(chan struct{}),
|
||||
newtID: newtID,
|
||||
outputPrefix: "[WGTester] ",
|
||||
useNetstack: true,
|
||||
tnet: tnet,
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
|
||||
if s.useNetstack && s.tnet != nil {
|
||||
// Use WireGuard netstack
|
||||
tnet := s.tnet.(*netstack.Net)
|
||||
udpAddr := &net.UDPAddr{Port: int(s.serverPort)}
|
||||
netstackConn, err := tnet.ListenUDP(udpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.netstackConn = netstackConn
|
||||
s.conn = netstackConn
|
||||
} else {
|
||||
// Use regular UDP socket
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.udpConn = udpConn
|
||||
s.conn = udpConn
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// RestartWithNetstack stops the current server and restarts it with netstack
|
||||
func (s *Server) RestartWithNetstack(tnet *netstack.Net) error {
|
||||
s.Stop()
|
||||
|
||||
// Update configuration to use netstack
|
||||
s.useNetstack = true
|
||||
s.tnet = tnet
|
||||
|
||||
// Clear previous connections
|
||||
s.conn = nil
|
||||
s.udpConn = nil
|
||||
s.netstackConn = nil
|
||||
|
||||
// Create new shutdown channel
|
||||
s.shutdownCh = make(chan struct{})
|
||||
|
||||
// Restart the server
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
// 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 - handle both regular UDP and netstack UDP
|
||||
var n int
|
||||
var addr net.Addr
|
||||
if s.useNetstack {
|
||||
// Use netstack UDP connection
|
||||
netstackConn := s.netstackConn.(*gonet.UDPConn)
|
||||
n, addr, err = netstackConn.ReadFrom(buffer)
|
||||
} else {
|
||||
// Use regular UDP connection
|
||||
n, addr, err = s.udpConn.ReadFromUDP(buffer)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
// Just a timeout, keep going
|
||||
continue
|
||||
}
|
||||
// Check if we're shutting down and the connection was closed
|
||||
select {
|
||||
case <-s.shutdownCh:
|
||||
return // Don't log error if we're shutting down
|
||||
default:
|
||||
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 - handle both regular UDP and netstack UDP
|
||||
if s.useNetstack {
|
||||
// Use netstack UDP connection
|
||||
netstackConn := s.netstackConn.(*gonet.UDPConn)
|
||||
_, err = netstackConn.WriteTo(responsePacket, addr)
|
||||
} else {
|
||||
// Use regular UDP connection
|
||||
udpAddr := addr.(*net.UDPAddr)
|
||||
_, err = s.udpConn.WriteToUDP(responsePacket, udpAddr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error(s.outputPrefix+"Error sending response: %v", err)
|
||||
} else {
|
||||
logger.Debug(s.outputPrefix + "Response sent successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user