mirror of
https://github.com/fosrl/newt.git
synced 2026-03-27 13:06:38 +00:00
Compare commits
66 Commits
1.0.0-beta
...
1.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1a439c75c | ||
|
|
e7c8dbc1c8 | ||
|
|
d28e3ca5e8 | ||
|
|
b41570eb2c | ||
|
|
72e0adc1bf | ||
|
|
435b638701 | ||
|
|
9b3c82648b | ||
|
|
f713c294b2 | ||
|
|
b3e8bf7d12 | ||
|
|
7852f11e8d | ||
|
|
2ff8df9a8d | ||
|
|
9d80161ab7 | ||
|
|
f4e17a4dd7 | ||
|
|
ab544fc9ed | ||
|
|
623be5ea0d | ||
|
|
72d264d427 | ||
|
|
a19fc8c588 | ||
|
|
dbc2a92456 | ||
|
|
437d8b67a4 | ||
|
|
6f1d4752f0 | ||
|
|
683312c78e | ||
|
|
29543aece3 | ||
|
|
e68a38e929 | ||
|
|
bc72c96b5e | ||
|
|
3d15ecb732 | ||
|
|
a69618310b | ||
|
|
ed8a2ccd23 | ||
|
|
e8141a177b | ||
|
|
b23eda9c06 | ||
|
|
92bc883b5b | ||
|
|
76503f3f2c | ||
|
|
9c3112f9bd | ||
|
|
462af30d16 | ||
|
|
fa6038eb38 | ||
|
|
f346b6cc5d | ||
|
|
f20b9ebb14 | ||
|
|
39bfe5b230 | ||
|
|
a1a3dd9ba2 | ||
|
|
7b1492f327 | ||
|
|
4e50819785 | ||
|
|
f8dccbec80 | ||
|
|
0c5c59cf00 | ||
|
|
868bb55f87 | ||
|
|
5b4245402a | ||
|
|
f7a705e6f8 | ||
|
|
3a63657822 | ||
|
|
759780508a | ||
|
|
533886f2e4 | ||
|
|
79f8745909 | ||
|
|
7b663027ac | ||
|
|
e90e55d982 | ||
|
|
a46fb23cdd | ||
|
|
10982b47a5 | ||
|
|
ab12098c9c | ||
|
|
446eb4d6f1 | ||
|
|
313afdb4c5 | ||
|
|
235a3b9426 | ||
|
|
c298ff52f3 | ||
|
|
75518b2e04 | ||
|
|
739f708ff7 | ||
|
|
2897b92f72 | ||
|
|
2c612d4018 | ||
|
|
41f0973308 | ||
|
|
4a791bdb6e | ||
|
|
9497f9c96f | ||
|
|
e17276b0c4 |
61
.github/workflows/cicd.yml
vendored
Normal file
61
.github/workflows/cicd.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: CI/CD Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build and Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract tag name
|
||||||
|
id: get-tag
|
||||||
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 1.23.1
|
||||||
|
|
||||||
|
- name: Update version in main.go
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [ -f main.go ]; then
|
||||||
|
sed -i 's/Newt version replaceme/Newt version '"$TAG"'/' main.go
|
||||||
|
echo "Updated main.go with version $TAG"
|
||||||
|
else
|
||||||
|
echo "main.go not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build and push Docker images
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
make docker-build-release tag=$TAG
|
||||||
|
|
||||||
|
- name: Build binaries
|
||||||
|
run: |
|
||||||
|
make go-build-release
|
||||||
|
|
||||||
|
- name: Upload artifacts from /bin
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binaries
|
||||||
|
path: bin/
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1 +1,6 @@
|
|||||||
newt
|
newt
|
||||||
|
.DS_Store
|
||||||
|
bin/
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
certs/
|
||||||
1
.go-version
Normal file
1
.go-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1.23.2
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please see the following page in our documentation with future plans and feature ideas if you are looking for a place to start.
|
Contributions are welcome!
|
||||||
|
|
||||||
|
Please see the contribution and local development guide on the docs page before getting started:
|
||||||
|
|
||||||
|
https://docs.fossorial.io/development
|
||||||
|
|
||||||
|
For ideas about what features to work on and our future plans, please see the roadmap:
|
||||||
|
|
||||||
https://docs.fossorial.io/roadmap
|
https://docs.fossorial.io/roadmap
|
||||||
|
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -15,19 +15,13 @@ COPY . .
|
|||||||
# Build the application
|
# Build the application
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /newt
|
RUN CGO_ENABLED=0 GOOS=linux go build -o /newt
|
||||||
|
|
||||||
# Start a new stage from scratch
|
FROM alpine:3.19 AS runner
|
||||||
FROM ubuntu:22.04 AS runner
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install ca-certificates -y && rm -rf /var/lib/apt/lists/*
|
RUN apk --no-cache add ca-certificates
|
||||||
|
|
||||||
# Copy the pre-built binary file from the previous stage and the entrypoint script
|
|
||||||
COPY --from=builder /newt /usr/local/bin/
|
COPY --from=builder /newt /usr/local/bin/
|
||||||
COPY entrypoint.sh /
|
COPY entrypoint.sh /
|
||||||
|
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
# Copy the entrypoint script
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|
||||||
# Command to run the executable
|
|
||||||
CMD ["newt"]
|
CMD ["newt"]
|
||||||
20
Makefile
20
Makefile
@@ -1,6 +1,14 @@
|
|||||||
|
|
||||||
all: build push
|
all: build push
|
||||||
|
|
||||||
|
docker-build-release:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-all tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t fosrl/newt:latest -f Dockerfile --push .
|
||||||
|
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t fosrl/newt:$(tag) -f Dockerfile --push .
|
||||||
|
|
||||||
build:
|
build:
|
||||||
docker build -t fosrl/newt:latest .
|
docker build -t fosrl/newt:latest .
|
||||||
|
|
||||||
@@ -13,5 +21,17 @@ test:
|
|||||||
local:
|
local:
|
||||||
CGO_ENABLED=0 go build -o newt
|
CGO_ENABLED=0 go build -o newt
|
||||||
|
|
||||||
|
go-build-release:
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/newt_linux_arm64
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o bin/newt_linux_arm32
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o bin/newt_linux_arm32v6
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/newt_linux_amd64
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -o bin/newt_linux_riscv64
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/newt_darwin_arm64
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/newt_darwin_amd64
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/newt_windows_amd64.exe
|
||||||
|
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/newt_freebsd_amd64
|
||||||
|
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/newt_freebsd_arm64
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm newt
|
rm newt
|
||||||
|
|||||||
102
README.md
102
README.md
@@ -19,7 +19,7 @@ _Sample output of a Newt container connected to Pangolin and hosting various res
|
|||||||
|
|
||||||
### Registers with Pangolin
|
### Registers with Pangolin
|
||||||
|
|
||||||
Using the Newt ID and a secret the client will make HTTP requests to Pangolin to receive a session token. Using that token it will connect to a websocket and maintain that connection. Control messages will be sent over the websocket.
|
Using the Newt ID and a secret, the client will make HTTP requests to Pangolin to receive a session token. Using that token, it will connect to a websocket and maintain that connection. Control messages will be sent over the websocket.
|
||||||
|
|
||||||
### Receives WireGuard Control Messages
|
### Receives WireGuard Control Messages
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ When Newt receives WireGuard control messages, it will use the information encod
|
|||||||
|
|
||||||
### Receives Proxy Control Messages
|
### Receives Proxy Control Messages
|
||||||
|
|
||||||
When Newt receives WireGuard control messages, it will use the information encoded to crate local low level TCP and UDP proxies attached to the virtual tunnel in order to relay traffic to programmed targets.
|
When Newt receives WireGuard control messages, it will use the information encoded to create a local low level TCP and UDP proxies attached to the virtual tunnel in order to relay traffic to programmed targets.
|
||||||
|
|
||||||
## CLI Args
|
## CLI Args
|
||||||
|
|
||||||
@@ -36,8 +36,10 @@ When Newt receives WireGuard control messages, it will use the information encod
|
|||||||
- `secret`: A unique secret (not shared and kept private) used to authenticate the client ID with the websocket in order to receive commands.
|
- `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
|
- `dns`: DNS server to use to resolve the endpoint
|
||||||
- `log-level` (optional): The log level to use. Default: INFO
|
- `log-level` (optional): The log level to use. Default: INFO
|
||||||
|
- `updown` (optional): A script to be called when targets are added or removed.
|
||||||
|
- `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls)
|
||||||
|
|
||||||
Example:
|
- Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./newt \
|
./newt \
|
||||||
@@ -46,6 +48,22 @@ Example:
|
|||||||
--endpoint https://example.com
|
--endpoint https://example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also run it with Docker compose. For example, a service in your `docker-compose.yml` might look like this using environment vars (recommended):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
newt:
|
||||||
|
image: fosrl/newt
|
||||||
|
container_name: newt
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PANGOLIN_ENDPOINT=https://example.com
|
||||||
|
- NEWT_ID=2ix2t8xk22ubpfy
|
||||||
|
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pass the CLI args to the container:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
newt:
|
newt:
|
||||||
@@ -53,11 +71,75 @@ services:
|
|||||||
container_name: newt
|
container_name: newt
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
- --id 31frd0uzbjvp721 \
|
- --id 31frd0uzbjvp721
|
||||||
- --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
|
- --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6
|
||||||
- --endpoint https://example.com
|
- --endpoint https://example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Finally a basic systemd service:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=Newt VPN Client
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/newt --id 31frd0uzbjvp721 --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 --endpoint https://example.com
|
||||||
|
Restart=always
|
||||||
|
User=root
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure to `mv ./newt /usr/local/bin/newt`!
|
||||||
|
|
||||||
|
### Updown
|
||||||
|
|
||||||
|
You can pass in a updown script for Newt to call when it is adding or removing a target:
|
||||||
|
|
||||||
|
`--updown "python3 test.py"`
|
||||||
|
|
||||||
|
It will get called with args when a target is added:
|
||||||
|
`python3 test.py add tcp localhost:8556`
|
||||||
|
`python3 test.py remove tcp localhost:8556`
|
||||||
|
|
||||||
|
Returning a string from the script in the format of a target (`ip:dst` so `10.0.0.1:8080`) it will override the target and use this value instead to proxy.
|
||||||
|
|
||||||
|
You can look at updown.py as a reference script to get started!
|
||||||
|
|
||||||
|
### mTLS
|
||||||
|
Newt supports mutual TLS (mTLS) authentication, if the server has been configured to request a client certificate.
|
||||||
|
* Only PKCS12 (.p12 or .pfx) file format is accepted
|
||||||
|
* The PKCS12 file must contain:
|
||||||
|
* Private key
|
||||||
|
* Public certificate
|
||||||
|
* CA certificate
|
||||||
|
* Encrypted PKCS12 files are currently not supported
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./newt \
|
||||||
|
--id 31frd0uzbjvp721 \
|
||||||
|
--secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
|
||||||
|
--endpoint https://example.com \
|
||||||
|
--tls-client-cert ./client.p12
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
newt:
|
||||||
|
image: fosrl/newt
|
||||||
|
container_name: newt
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PANGOLIN_ENDPOINT=https://example.com
|
||||||
|
- NEWT_ID=2ix2t8xk22ubpfy
|
||||||
|
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
|
||||||
|
- TLS_CLIENT_CERT=./client.p12
|
||||||
|
```
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
### Container
|
### Container
|
||||||
@@ -76,6 +158,16 @@ Make sure to have Go 1.23.1 installed.
|
|||||||
make local
|
make local
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Nix Flake
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix build
|
||||||
|
```
|
||||||
|
|
||||||
|
Binary will be at `./result/bin/newt`
|
||||||
|
|
||||||
|
Development shell available with `nix develop`
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
Newt is dual licensed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.
|
Newt is dual licensed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.
|
||||||
|
|||||||
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
|
||||||
|
|
||||||
|
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
|
||||||
|
2. Send a detailed report to [security@fossorial.io](mailto:security@fossorial.io) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||||
|
|
||||||
|
- Description and location of the vulnerability.
|
||||||
|
- Potential impact of the vulnerability.
|
||||||
|
- Steps to reproduce the vulnerability.
|
||||||
|
- Potential solutions to fix the vulnerability.
|
||||||
|
- Your name/handle and a link for recognition (optional).
|
||||||
|
|
||||||
|
We aim to address the issue as soon as possible.
|
||||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
newt:
|
||||||
|
image: fosrl/newt:latest
|
||||||
|
container_name: newt
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PANGOLIN_ENDPOINT=https://example.com
|
||||||
|
- NEWT_ID=2ix2t8xk22ubpfy
|
||||||
|
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
|
||||||
|
- LOG_LEVEL=DEBUG
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Sample from https://github.com/traefik/traefik-library-image/blob/5070edb25b03cca6802d75d5037576c840f73fdd/v3.1/alpine/entrypoint.sh
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# first arg is `-f` or `--some-option`
|
# first arg is `-f` or `--some-option`
|
||||||
@@ -9,13 +7,4 @@ if [ "${1#-}" != "$1" ]; then
|
|||||||
set -- newt "$@"
|
set -- newt "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# if our command is a valid newt subcommand, let's invoke it through newt instead
|
|
||||||
# (this allows for "docker run newt version", etc)
|
|
||||||
if newt "$1" --help >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
set -- newt "$@"
|
|
||||||
else
|
|
||||||
echo "= '$1' is not a newt command: assuming shell execution." 1>&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1742669843,
|
||||||
|
"narHash": "sha256-G5n+FOXLXcRx+3hCJ6Rt6ZQyF1zqQ0DL0sWAMn2Nk0w=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "1e5b653dff12029333a6546c11e108ede13052eb",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
65
flake.nix
Normal file
65
flake.nix
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
description = "newt - A tunneling client for Pangolin";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ self, nixpkgs }:
|
||||||
|
let
|
||||||
|
supportedSystems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||||
|
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = self.packages.${system}.pangolin-newt;
|
||||||
|
pangolin-newt = pkgs.buildGoModule {
|
||||||
|
pname = "pangolin-newt";
|
||||||
|
version = "1.1.2";
|
||||||
|
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
vendorHash = "sha256-sTtiBBkZ9cuhWnrn2VG20kv4nzNFfdzP5p+ewESCjyM=";
|
||||||
|
|
||||||
|
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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
14
go.mod
14
go.mod
@@ -4,16 +4,20 @@ go 1.23.1
|
|||||||
|
|
||||||
toolchain go1.23.2
|
toolchain go1.23.2
|
||||||
|
|
||||||
require golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
require (
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
golang.org/x/net v0.30.0
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.5.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
golang.org/x/crypto v0.28.0 // indirect
|
golang.org/x/crypto v0.28.0 // indirect
|
||||||
golang.org/x/net v0.30.0 // indirect
|
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
golang.org/x/time v0.7.0 // indirect
|
golang.org/x/time v0.7.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // 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
|
|
||||||
)
|
)
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,5 +1,7 @@
|
|||||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/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 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||||
@@ -18,3 +20,5 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY
|
|||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
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 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||||
|
|||||||
@@ -53,7 +53,23 @@ func (l *Logger) log(level LogLevel, format string, args ...interface{}) {
|
|||||||
if level < l.level {
|
if level < l.level {
|
||||||
return
|
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...)
|
message := fmt.Sprintf(format, args...)
|
||||||
l.logger.Printf("%s: %s %s", level.String(), timestamp, message)
|
l.logger.Printf("%s: %s %s", level.String(), timestamp, message)
|
||||||
}
|
}
|
||||||
|
|||||||
349
main.go
349
main.go
@@ -11,7 +11,9 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -112,34 +114,149 @@ func ping(tnet *netstack.Net, dst string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) {
|
||||||
|
initialInterval := 10 * time.Second
|
||||||
|
maxInterval := 60 * time.Second
|
||||||
|
currentInterval := initialInterval
|
||||||
|
consecutiveFailures := 0
|
||||||
|
|
||||||
|
ticker := time.NewTicker(currentInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
err := ping(tnet, serverIP)
|
||||||
|
if err != nil {
|
||||||
|
consecutiveFailures++
|
||||||
|
logger.Warn("Periodic ping failed (%d consecutive failures): %v",
|
||||||
|
consecutiveFailures, err)
|
||||||
|
logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?")
|
||||||
|
|
||||||
|
// Increase interval if we have consistent failures, with a maximum cap
|
||||||
|
if consecutiveFailures >= 3 && currentInterval < maxInterval {
|
||||||
|
// Increase by 50% each time, up to the maximum
|
||||||
|
currentInterval = time.Duration(float64(currentInterval) * 1.5)
|
||||||
|
if currentInterval > maxInterval {
|
||||||
|
currentInterval = maxInterval
|
||||||
|
}
|
||||||
|
ticker.Reset(currentInterval)
|
||||||
|
logger.Info("Increased ping check interval to %v due to consecutive failures",
|
||||||
|
currentInterval)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// On success, if we've backed off, gradually return to normal interval
|
||||||
|
if currentInterval > initialInterval {
|
||||||
|
currentInterval = time.Duration(float64(currentInterval) * 0.8)
|
||||||
|
if currentInterval < initialInterval {
|
||||||
|
currentInterval = initialInterval
|
||||||
|
}
|
||||||
|
ticker.Reset(currentInterval)
|
||||||
|
logger.Info("Decreased ping check interval to %v after successful ping",
|
||||||
|
currentInterval)
|
||||||
|
}
|
||||||
|
consecutiveFailures = 0
|
||||||
|
}
|
||||||
|
case <-stopChan:
|
||||||
|
logger.Info("Stopping ping check")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to track connection status and trigger reconnection as needed
|
||||||
|
func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websocket.Client) {
|
||||||
|
const checkInterval = 30 * time.Second
|
||||||
|
connectionLost := false
|
||||||
|
ticker := time.NewTicker(checkInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
// Try a ping to see if connection is alive
|
||||||
|
err := ping(tnet, serverIP)
|
||||||
|
|
||||||
|
if err != nil && !connectionLost {
|
||||||
|
// We just lost connection
|
||||||
|
connectionLost = true
|
||||||
|
logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.")
|
||||||
|
|
||||||
|
// Notify the user they might need to check their network
|
||||||
|
logger.Warn("Please check your internet connection and ensure the Pangolin server is online.")
|
||||||
|
logger.Warn("Newt will continue reconnection attempts automatically when connectivity is restored.")
|
||||||
|
} else if err == nil && connectionLost {
|
||||||
|
// Connection has been restored
|
||||||
|
connectionLost = false
|
||||||
|
logger.Info("Connection to server restored!")
|
||||||
|
|
||||||
|
// Tell the server we're back
|
||||||
|
err := client.SendMessage("newt/wg/register", map[string]interface{}{
|
||||||
|
"publicKey": fmt.Sprintf("%s", privateKey.PublicKey()),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to send registration message after reconnection: %v", err)
|
||||||
|
} else {
|
||||||
|
logger.Info("Successfully re-registered with server after reconnection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func pingWithRetry(tnet *netstack.Net, dst string) error {
|
func pingWithRetry(tnet *netstack.Net, dst string) error {
|
||||||
const (
|
const (
|
||||||
maxAttempts = 5
|
initialMaxAttempts = 15
|
||||||
retryDelay = 2 * time.Second
|
initialRetryDelay = 2 * time.Second
|
||||||
|
maxRetryDelay = 60 * time.Second // Cap the maximum delay
|
||||||
)
|
)
|
||||||
|
|
||||||
var lastErr error
|
attempt := 1
|
||||||
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
retryDelay := initialRetryDelay
|
||||||
logger.Info("Ping attempt %d of %d", attempt, maxAttempts)
|
|
||||||
|
|
||||||
if err := ping(tnet, dst); err != nil {
|
|
||||||
lastErr = err
|
|
||||||
logger.Warn("Ping attempt %d failed: %v", attempt, err)
|
|
||||||
|
|
||||||
if attempt < maxAttempts {
|
|
||||||
time.Sleep(retryDelay)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return fmt.Errorf("all ping attempts failed after %d tries, last error: %w",
|
|
||||||
maxAttempts, lastErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// First try with the initial parameters
|
||||||
|
logger.Info("Ping attempt %d", attempt)
|
||||||
|
if err := ping(tnet, dst); err == nil {
|
||||||
// Successful ping
|
// Successful ping
|
||||||
return nil
|
return nil
|
||||||
|
} else {
|
||||||
|
logger.Warn("Ping attempt %d failed: %v", attempt, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This shouldn't be reached due to the return in the loop, but added for completeness
|
// Start a goroutine that will attempt pings indefinitely with increasing delays
|
||||||
return fmt.Errorf("unexpected error: all ping attempts failed")
|
go func() {
|
||||||
|
attempt = 2 // Continue from attempt 2
|
||||||
|
|
||||||
|
for {
|
||||||
|
logger.Info("Ping attempt %d", attempt)
|
||||||
|
|
||||||
|
if err := ping(tnet, dst); 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.Info("Ping succeeded after %d attempts", attempt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Return an error for the first batch of attempts (to maintain compatibility with existing code)
|
||||||
|
return fmt.Errorf("initial ping attempts failed, continuing in background")
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLogLevel(level string) logger.LogLevel {
|
func parseLogLevel(level string) logger.LogLevel {
|
||||||
@@ -222,51 +339,90 @@ func resolveDomain(domain string) (string, error) {
|
|||||||
return ipAddr, nil
|
return ipAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnvWithDefault(key, defaultValue string) string {
|
var (
|
||||||
if value := os.Getenv(key); value != "" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var (
|
|
||||||
endpoint string
|
endpoint string
|
||||||
id string
|
id string
|
||||||
secret string
|
secret string
|
||||||
|
mtu string
|
||||||
|
mtuInt int
|
||||||
dns string
|
dns string
|
||||||
privateKey wgtypes.Key
|
privateKey wgtypes.Key
|
||||||
err error
|
err error
|
||||||
logLevel string
|
logLevel string
|
||||||
)
|
updownScript string
|
||||||
|
tlsPrivateKey string
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// if PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET are set as environment variables, they will be used as default values
|
||||||
|
endpoint = os.Getenv("PANGOLIN_ENDPOINT")
|
||||||
|
id = os.Getenv("NEWT_ID")
|
||||||
|
secret = os.Getenv("NEWT_SECRET")
|
||||||
|
mtu = os.Getenv("MTU")
|
||||||
|
dns = os.Getenv("DNS")
|
||||||
|
logLevel = os.Getenv("LOG_LEVEL")
|
||||||
|
updownScript = os.Getenv("UPDOWN_SCRIPT")
|
||||||
|
tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT")
|
||||||
|
|
||||||
|
if endpoint == "" {
|
||||||
|
flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server")
|
||||||
|
}
|
||||||
|
if id == "" {
|
||||||
|
flag.StringVar(&id, "id", "", "Newt ID")
|
||||||
|
}
|
||||||
|
if secret == "" {
|
||||||
|
flag.StringVar(&secret, "secret", "", "Newt secret")
|
||||||
|
}
|
||||||
|
if mtu == "" {
|
||||||
|
flag.StringVar(&mtu, "mtu", "1280", "MTU to use")
|
||||||
|
}
|
||||||
|
if dns == "" {
|
||||||
|
flag.StringVar(&dns, "dns", "8.8.8.8", "DNS server to use")
|
||||||
|
}
|
||||||
|
if logLevel == "" {
|
||||||
|
flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
|
||||||
|
}
|
||||||
|
if updownScript == "" {
|
||||||
|
flag.StringVar(&updownScript, "updown", "", "Path to updown script to be called when targets are added or removed")
|
||||||
|
}
|
||||||
|
if tlsPrivateKey == "" {
|
||||||
|
flag.StringVar(&tlsPrivateKey, "tls-client-cert", "", "Path to client certificate used for mTLS")
|
||||||
|
}
|
||||||
|
|
||||||
|
// do a --version check
|
||||||
|
version := flag.Bool("version", false, "Print the version")
|
||||||
|
|
||||||
// Define CLI flags with default values from environment variables
|
|
||||||
flag.StringVar(&endpoint, "endpoint", os.Getenv("PANGOLIN_ENDPOINT"), "Endpoint of your pangolin server")
|
|
||||||
flag.StringVar(&id, "id", os.Getenv("NEWT_ID"), "Newt ID")
|
|
||||||
flag.StringVar(&secret, "secret", os.Getenv("NEWT_SECRET"), "Newt secret")
|
|
||||||
flag.StringVar(&dns, "dns", getEnvWithDefault("DEFAULT_DNS", "8.8.8.8"), "DNS server to use")
|
|
||||||
flag.StringVar(&logLevel, "log-level", getEnvWithDefault("LOG_LEVEL", "INFO"), "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if *version {
|
||||||
|
fmt.Println("Newt version replaceme")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
logger.Init()
|
logger.Init()
|
||||||
loggerLevel := parseLogLevel(logLevel)
|
loggerLevel := parseLogLevel(logLevel)
|
||||||
logger.GetLogger().SetLevel(parseLogLevel(logLevel))
|
logger.GetLogger().SetLevel(parseLogLevel(logLevel))
|
||||||
|
|
||||||
// Validate required fields
|
// parse the mtu string into an int
|
||||||
if endpoint == "" || id == "" || secret == "" {
|
mtuInt, err = strconv.Atoi(mtu)
|
||||||
logger.Fatal("endpoint, id, and secret are required either via CLI flags or environment variables")
|
if err != nil {
|
||||||
|
logger.Fatal("Failed to parse MTU: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey, err = wgtypes.GeneratePrivateKey()
|
privateKey, err = wgtypes.GeneratePrivateKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Failed to generate private key: %v", err)
|
logger.Fatal("Failed to generate private key: %v", err)
|
||||||
}
|
}
|
||||||
|
var opt websocket.ClientOption
|
||||||
|
if tlsPrivateKey != "" {
|
||||||
|
opt = websocket.WithTLSConfig(tlsPrivateKey)
|
||||||
|
}
|
||||||
// Create a new client
|
// Create a new client
|
||||||
client, err := websocket.NewClient(
|
client, err := websocket.NewClient(
|
||||||
id, // CLI arg takes precedence
|
id, // CLI arg takes precedence
|
||||||
secret, // CLI arg takes precedence
|
secret, // CLI arg takes precedence
|
||||||
endpoint,
|
endpoint,
|
||||||
|
opt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Failed to create client: %v", err)
|
logger.Fatal("Failed to create client: %v", err)
|
||||||
@@ -291,18 +447,17 @@ func main() {
|
|||||||
client.Close()
|
client.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
pingStopChan := make(chan struct{})
|
||||||
|
defer close(pingStopChan)
|
||||||
|
|
||||||
// Register handlers for different message types
|
// Register handlers for different message types
|
||||||
client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) {
|
client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) {
|
||||||
logger.Info("Received registration message")
|
logger.Info("Received registration message")
|
||||||
|
|
||||||
if connected {
|
if connected {
|
||||||
logger.Info("Already connected! Put I will send a ping anyway...")
|
logger.Info("Already connected! But I will send a ping anyway...")
|
||||||
// ping(tnet, wgData.ServerIP)
|
// Even if pingWithRetry returns an error, it will continue trying in the background
|
||||||
err = pingWithRetry(tnet, wgData.ServerIP)
|
_ = pingWithRetry(tnet, wgData.ServerIP) // Ignoring initial error as pings will continue
|
||||||
if err != nil {
|
|
||||||
// Handle complete failure after all retries
|
|
||||||
logger.Error("Failed to ping %s: %v", wgData.ServerIP, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +476,7 @@ func main() {
|
|||||||
tun, tnet, err = netstack.CreateNetTUN(
|
tun, tnet, err = netstack.CreateNetTUN(
|
||||||
[]netip.Addr{netip.MustParseAddr(wgData.TunnelIP)},
|
[]netip.Addr{netip.MustParseAddr(wgData.TunnelIP)},
|
||||||
[]netip.Addr{netip.MustParseAddr(dns)},
|
[]netip.Addr{netip.MustParseAddr(dns)},
|
||||||
1420)
|
mtuInt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to create TUN device: %v", err)
|
logger.Error("Failed to create TUN device: %v", err)
|
||||||
}
|
}
|
||||||
@@ -357,12 +512,18 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("WireGuard device created. Lets ping the server now...")
|
logger.Info("WireGuard device created. Lets ping the server now...")
|
||||||
// Ping to bring the tunnel up on the server side quickly
|
|
||||||
// ping(tnet, wgData.ServerIP)
|
// Even if pingWithRetry returns an error, it will continue trying in the background
|
||||||
err = pingWithRetry(tnet, wgData.ServerIP)
|
_ = pingWithRetry(tnet, wgData.ServerIP)
|
||||||
if err != nil {
|
|
||||||
// Handle complete failure after all retries
|
// Always mark as connected and start the proxy manager regardless of initial ping result
|
||||||
logger.Error("Failed to ping %s: %v", wgData.ServerIP, err)
|
// as the pings will continue in the background
|
||||||
|
if !connected {
|
||||||
|
logger.Info("Starting ping check")
|
||||||
|
startPingCheck(tnet, wgData.ServerIP, pingStopChan)
|
||||||
|
|
||||||
|
// Start connection monitoring in a separate goroutine
|
||||||
|
go monitorConnectionStatus(tnet, wgData.ServerIP, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create proxy manager
|
// Create proxy manager
|
||||||
@@ -403,11 +564,6 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
|
|||||||
if len(targetData.Targets) > 0 {
|
if len(targetData.Targets) > 0 {
|
||||||
updateTargets(pm, "add", wgData.TunnelIP, "tcp", targetData)
|
updateTargets(pm, "add", wgData.TunnelIP, "tcp", targetData)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pm.Start()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Failed to start proxy manager: %v", err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
client.RegisterHandler("newt/udp/add", func(msg websocket.WSMessage) {
|
client.RegisterHandler("newt/udp/add", func(msg websocket.WSMessage) {
|
||||||
@@ -428,11 +584,6 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
|
|||||||
if len(targetData.Targets) > 0 {
|
if len(targetData.Targets) > 0 {
|
||||||
updateTargets(pm, "add", wgData.TunnelIP, "udp", targetData)
|
updateTargets(pm, "add", wgData.TunnelIP, "udp", targetData)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = pm.Start()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Failed to start proxy manager: %v", err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
client.RegisterHandler("newt/udp/remove", func(msg websocket.WSMessage) {
|
client.RegisterHandler("newt/udp/remove", func(msg websocket.WSMessage) {
|
||||||
@@ -500,10 +651,13 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
|
|||||||
// Wait for interrupt signal
|
// Wait for interrupt signal
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-sigCh
|
sigReceived := <-sigCh
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
|
logger.Info("Received %s signal, stopping", sigReceived.String())
|
||||||
|
if dev != nil {
|
||||||
dev.Close()
|
dev.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseTargetData(data interface{}) (TargetData, error) {
|
func parseTargetData(data interface{}) (TargetData, error) {
|
||||||
@@ -540,6 +694,18 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto
|
|||||||
|
|
||||||
if action == "add" {
|
if action == "add" {
|
||||||
target := parts[1] + ":" + parts[2]
|
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
|
// Only remove the specific target if it exists
|
||||||
err := pm.RemoveTarget(proto, tunnelIP, port)
|
err := pm.RemoveTarget(proto, tunnelIP, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -550,10 +716,21 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the new target
|
// Add the new target
|
||||||
pm.AddTarget(proto, tunnelIP, port, target)
|
pm.AddTarget(proto, tunnelIP, port, processedTarget)
|
||||||
|
|
||||||
} else if action == "remove" {
|
} else if action == "remove" {
|
||||||
logger.Info("Removing target with port %d", port)
|
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)
|
err := pm.RemoveTarget(proto, tunnelIP, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to remove target: %v", err)
|
logger.Error("Failed to remove target: %v", err)
|
||||||
@@ -564,3 +741,45 @@ func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
484
proxy/manager.go
484
proxy/manager.go
@@ -9,326 +9,344 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fosrl/newt/logger"
|
"github.com/fosrl/newt/logger"
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Target represents a proxy target with its address and port
|
||||||
|
type Target struct {
|
||||||
|
Address string
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyManager handles the creation and management of proxy connections
|
||||||
|
type ProxyManager struct {
|
||||||
|
tnet *netstack.Net
|
||||||
|
tcpTargets map[string]map[int]string // map[listenIP]map[port]targetAddress
|
||||||
|
udpTargets map[string]map[int]string
|
||||||
|
listeners []*gonet.TCPListener
|
||||||
|
udpConns []*gonet.UDPConn
|
||||||
|
running bool
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProxyManager creates a new proxy manager instance
|
||||||
func NewProxyManager(tnet *netstack.Net) *ProxyManager {
|
func NewProxyManager(tnet *netstack.Net) *ProxyManager {
|
||||||
return &ProxyManager{
|
return &ProxyManager{
|
||||||
tnet: tnet,
|
tnet: tnet,
|
||||||
|
tcpTargets: make(map[string]map[int]string),
|
||||||
|
udpTargets: make(map[string]map[int]string),
|
||||||
|
listeners: make([]*gonet.TCPListener, 0),
|
||||||
|
udpConns: make([]*gonet.UDPConn, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProxyManager) AddTarget(protocol, listen string, port int, target string) {
|
// AddTarget adds as new target for proxying
|
||||||
pm.Lock()
|
func (pm *ProxyManager) AddTarget(proto, listenIP string, port int, targetAddr string) error {
|
||||||
defer pm.Unlock()
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
logger.Info("Adding target: %s://%s:%d -> %s", protocol, listen, port, target)
|
switch proto {
|
||||||
|
|
||||||
newTarget := ProxyTarget{
|
|
||||||
Protocol: protocol,
|
|
||||||
Listen: listen,
|
|
||||||
Port: port,
|
|
||||||
Target: target,
|
|
||||||
cancel: make(chan struct{}),
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
pm.targets = append(pm.targets, newTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProxyManager) RemoveTarget(protocol, listen string, port int) error {
|
|
||||||
pm.Lock()
|
|
||||||
defer pm.Unlock()
|
|
||||||
|
|
||||||
protocol = strings.ToLower(protocol)
|
|
||||||
if protocol != "tcp" && protocol != "udp" {
|
|
||||||
return fmt.Errorf("unsupported protocol: %s", protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, target := range pm.targets {
|
|
||||||
if target.Listen == listen &&
|
|
||||||
target.Port == port &&
|
|
||||||
strings.ToLower(target.Protocol) == protocol {
|
|
||||||
|
|
||||||
// Signal the serving goroutine to stop
|
|
||||||
select {
|
|
||||||
case <-target.cancel:
|
|
||||||
// Channel is already closed, no need to close it again
|
|
||||||
default:
|
|
||||||
close(target.cancel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the appropriate listener/connection based on protocol
|
|
||||||
target.Lock()
|
|
||||||
switch protocol {
|
|
||||||
case "tcp":
|
case "tcp":
|
||||||
if target.listener != nil {
|
if pm.tcpTargets[listenIP] == nil {
|
||||||
select {
|
pm.tcpTargets[listenIP] = make(map[int]string)
|
||||||
case <-target.cancel:
|
|
||||||
// Listener was already closed by Stop()
|
|
||||||
default:
|
|
||||||
target.listener.Close()
|
|
||||||
}
|
}
|
||||||
|
pm.tcpTargets[listenIP][port] = targetAddr
|
||||||
|
case "udp":
|
||||||
|
if pm.udpTargets[listenIP] == nil {
|
||||||
|
pm.udpTargets[listenIP] = make(map[int]string)
|
||||||
|
}
|
||||||
|
pm.udpTargets[listenIP][port] = targetAddr
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported protocol: %s", proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pm.running {
|
||||||
|
return pm.startTarget(proto, listenIP, port, targetAddr)
|
||||||
|
} else {
|
||||||
|
logger.Debug("Not adding target because not running")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProxyManager) RemoveTarget(proto, listenIP string, port int) error {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
|
switch proto {
|
||||||
|
case "tcp":
|
||||||
|
if targets, ok := pm.tcpTargets[listenIP]; ok {
|
||||||
|
delete(targets, port)
|
||||||
|
// Remove and close the corresponding TCP listener
|
||||||
|
for i, listener := range pm.listeners {
|
||||||
|
if addr, ok := listener.Addr().(*net.TCPAddr); ok && addr.Port == port {
|
||||||
|
listener.Close()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
// Remove from slice
|
||||||
|
pm.listeners = append(pm.listeners[:i], pm.listeners[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("target not found: %s:%d", listenIP, port)
|
||||||
}
|
}
|
||||||
case "udp":
|
case "udp":
|
||||||
if target.udpConn != nil {
|
if targets, ok := pm.udpTargets[listenIP]; ok {
|
||||||
select {
|
delete(targets, port)
|
||||||
case <-target.cancel:
|
// Remove and close the corresponding UDP connection
|
||||||
// Connection was already closed by Stop()
|
for i, conn := range pm.udpConns {
|
||||||
|
if addr, ok := conn.LocalAddr().(*net.UDPAddr); ok && addr.Port == port {
|
||||||
|
conn.Close()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
// Remove from slice
|
||||||
|
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("target not found: %s:%d", listenIP, port)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
target.udpConn.Close()
|
return fmt.Errorf("unsupported protocol: %s", proto)
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
}
|
}
|
||||||
target.Unlock()
|
|
||||||
|
|
||||||
// Wait for the target to fully stop
|
// Start begins listening for all configured proxy targets
|
||||||
<-target.done
|
func (pm *ProxyManager) Start() error {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
// Remove the target from the slice
|
if pm.running {
|
||||||
pm.targets = append(pm.targets[:i], pm.targets[i+1:]...)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("target not found for %s %s:%d", protocol, listen, port)
|
// Start TCP targets
|
||||||
}
|
for listenIP, targets := range pm.tcpTargets {
|
||||||
|
for port, targetAddr := range targets {
|
||||||
func (pm *ProxyManager) Start() error {
|
if err := pm.startTarget("tcp", listenIP, port, targetAddr); err != nil {
|
||||||
pm.RLock()
|
return fmt.Errorf("failed to start TCP target: %v", err)
|
||||||
defer pm.RUnlock()
|
|
||||||
|
|
||||||
for i := range pm.targets {
|
|
||||||
target := &pm.targets[i]
|
|
||||||
|
|
||||||
target.Lock()
|
|
||||||
// If target is already running, skip it
|
|
||||||
if target.listener != nil || target.udpConn != nil {
|
|
||||||
target.Unlock()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the target as starting by creating a nil listener/connection
|
|
||||||
// This prevents other goroutines from trying to start it
|
|
||||||
if strings.ToLower(target.Protocol) == "tcp" {
|
|
||||||
target.listener = nil
|
|
||||||
} else {
|
|
||||||
target.udpConn = nil
|
|
||||||
}
|
|
||||||
target.Unlock()
|
|
||||||
|
|
||||||
switch strings.ToLower(target.Protocol) {
|
|
||||||
case "tcp":
|
|
||||||
go pm.serveTCP(target)
|
|
||||||
case "udp":
|
|
||||||
go pm.serveUDP(target)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported protocol: %s", target.Protocol)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start UDP targets
|
||||||
|
for listenIP, targets := range pm.udpTargets {
|
||||||
|
for port, targetAddr := range targets {
|
||||||
|
if err := pm.startTarget("udp", listenIP, port, targetAddr); err != nil {
|
||||||
|
return fmt.Errorf("failed to start UDP target: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.running = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProxyManager) Stop() error {
|
func (pm *ProxyManager) Stop() error {
|
||||||
pm.Lock()
|
pm.mutex.Lock()
|
||||||
defer pm.Unlock()
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
if !pm.running {
|
||||||
for i := range pm.targets {
|
return nil
|
||||||
target := &pm.targets[i]
|
|
||||||
wg.Add(1)
|
|
||||||
go func(t *ProxyTarget) {
|
|
||||||
defer wg.Done()
|
|
||||||
close(t.cancel)
|
|
||||||
t.Lock()
|
|
||||||
if t.listener != nil {
|
|
||||||
t.listener.Close()
|
|
||||||
}
|
}
|
||||||
if t.udpConn != nil {
|
|
||||||
t.udpConn.Close()
|
// Set running to false first to signal handlers to stop
|
||||||
|
pm.running = false
|
||||||
|
|
||||||
|
// Close TCP listeners
|
||||||
|
for i := len(pm.listeners) - 1; i >= 0; i-- {
|
||||||
|
listener := pm.listeners[i]
|
||||||
|
if err := listener.Close(); err != nil {
|
||||||
|
logger.Error("Error closing TCP listener: %v", err)
|
||||||
}
|
}
|
||||||
t.Unlock()
|
// Remove from slice
|
||||||
// Wait for the target to fully stop
|
pm.listeners = append(pm.listeners[:i], pm.listeners[i+1:]...)
|
||||||
<-t.done
|
|
||||||
}(target)
|
|
||||||
}
|
}
|
||||||
wg.Wait()
|
|
||||||
|
// Close UDP connections
|
||||||
|
for i := len(pm.udpConns) - 1; i >= 0; i-- {
|
||||||
|
conn := pm.udpConns[i]
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
logger.Error("Error closing UDP connection: %v", err)
|
||||||
|
}
|
||||||
|
// Remove from slice
|
||||||
|
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the target maps
|
||||||
|
for k := range pm.tcpTargets {
|
||||||
|
delete(pm.tcpTargets, k)
|
||||||
|
}
|
||||||
|
for k := range pm.udpTargets {
|
||||||
|
delete(pm.udpTargets, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give active connections a chance to close gracefully
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProxyManager) serveTCP(target *ProxyTarget) {
|
func (pm *ProxyManager) startTarget(proto, listenIP string, port int, targetAddr string) error {
|
||||||
defer close(target.done) // Signal that this target is fully stopped
|
switch proto {
|
||||||
|
case "tcp":
|
||||||
listener, err := pm.tnet.ListenTCP(&net.TCPAddr{
|
listener, err := pm.tnet.ListenTCP(&net.TCPAddr{Port: port})
|
||||||
IP: net.ParseIP(target.Listen),
|
|
||||||
Port: target.Port,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Info("Failed to start TCP listener for %s:%d: %v", target.Listen, target.Port, err)
|
return fmt.Errorf("failed to create TCP listener: %v", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
target.Lock()
|
pm.listeners = append(pm.listeners, listener)
|
||||||
target.listener = listener
|
go pm.handleTCPProxy(listener, targetAddr)
|
||||||
target.Unlock()
|
|
||||||
|
|
||||||
defer listener.Close()
|
case "udp":
|
||||||
logger.Info("TCP proxy listening on %s", listener.Addr())
|
addr := &net.UDPAddr{Port: port}
|
||||||
|
conn, err := pm.tnet.ListenUDP(addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create UDP listener: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
var activeConns sync.WaitGroup
|
pm.udpConns = append(pm.udpConns, conn)
|
||||||
acceptDone := make(chan struct{})
|
go pm.handleUDPProxy(conn, targetAddr)
|
||||||
|
|
||||||
// Goroutine to handle shutdown signal
|
default:
|
||||||
go func() {
|
return fmt.Errorf("unsupported protocol: %s", proto)
|
||||||
<-target.cancel
|
}
|
||||||
close(acceptDone)
|
|
||||||
listener.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
|
logger.Info("Started %s proxy from %s:%d to %s", proto, listenIP, port, targetAddr)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProxyManager) handleTCPProxy(listener net.Listener, targetAddr string) {
|
||||||
for {
|
for {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
// Check if we're shutting down or the listener was closed
|
||||||
case <-target.cancel:
|
if !pm.running {
|
||||||
// Wait for active connections to finish
|
|
||||||
activeConns.Wait()
|
|
||||||
return
|
return
|
||||||
default:
|
}
|
||||||
logger.Info("Failed to accept TCP connection: %v", err)
|
|
||||||
// Don't return here, try to accept new connections
|
// Check for specific network errors that indicate the listener is closed
|
||||||
time.Sleep(time.Second)
|
if ne, ok := err.(net.Error); ok && !ne.Temporary() {
|
||||||
|
logger.Info("TCP listener closed, stopping proxy handler for %v", listener.Addr())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error("Error accepting TCP connection: %v", err)
|
||||||
|
// Don't hammer the CPU if we hit a temporary error
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
activeConns.Add(1)
|
|
||||||
go func() {
|
go func() {
|
||||||
defer activeConns.Done()
|
target, err := net.Dial("tcp", targetAddr)
|
||||||
pm.handleTCPConnection(conn, target.Target, acceptDone)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProxyManager) handleTCPConnection(clientConn net.Conn, target string, done chan struct{}) {
|
|
||||||
defer clientConn.Close()
|
|
||||||
|
|
||||||
serverConn, err := net.Dial("tcp", target)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Info("Failed to connect to target %s: %v", target, err)
|
logger.Error("Error connecting to target: %v", err)
|
||||||
|
conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer serverConn.Close()
|
|
||||||
|
|
||||||
|
// Create a WaitGroup to ensure both copy operations complete
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
|
|
||||||
// Client -> Server
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
select {
|
io.Copy(target, conn)
|
||||||
case <-done:
|
target.Close()
|
||||||
return
|
|
||||||
default:
|
|
||||||
io.Copy(serverConn, clientConn)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Server -> Client
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
select {
|
io.Copy(conn, target)
|
||||||
case <-done:
|
conn.Close()
|
||||||
return
|
|
||||||
default:
|
|
||||||
io.Copy(clientConn, serverConn)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Wait for both copies to complete
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProxyManager) serveUDP(target *ProxyTarget) {
|
func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
|
||||||
defer close(target.done) // Signal that this target is fully stopped
|
buffer := make([]byte, 65507) // Max UDP packet size
|
||||||
|
clientConns := make(map[string]*net.UDPConn)
|
||||||
addr := &net.UDPAddr{
|
var clientsMutex sync.RWMutex
|
||||||
IP: net.ParseIP(target.Listen),
|
|
||||||
Port: target.Port,
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := pm.tnet.ListenUDP(addr)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to start UDP listener for %s:%d: %v", target.Listen, target.Port, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target.Lock()
|
|
||||||
target.udpConn = conn
|
|
||||||
target.Unlock()
|
|
||||||
|
|
||||||
defer conn.Close()
|
|
||||||
logger.Info("UDP proxy listening on %s", conn.LocalAddr())
|
|
||||||
|
|
||||||
buffer := make([]byte, 65535)
|
|
||||||
var activeConns sync.WaitGroup
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
|
||||||
case <-target.cancel:
|
|
||||||
activeConns.Wait() // Wait for all active UDP handlers to complete
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
n, remoteAddr, err := conn.ReadFrom(buffer)
|
n, remoteAddr, err := conn.ReadFrom(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
if !pm.running {
|
||||||
case <-target.cancel:
|
|
||||||
activeConns.Wait()
|
|
||||||
return
|
return
|
||||||
default:
|
|
||||||
logger.Info("Failed to read UDP packet: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAddr, err := net.ResolveUDPAddr("udp", target.Target)
|
// Check for connection closed conditions
|
||||||
if err != nil {
|
if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") {
|
||||||
logger.Info("Failed to resolve target address %s: %v", target.Target, err)
|
logger.Info("UDP connection closed, stopping proxy handler")
|
||||||
|
|
||||||
|
// Clean up existing client connections
|
||||||
|
clientsMutex.Lock()
|
||||||
|
for _, targetConn := range clientConns {
|
||||||
|
targetConn.Close()
|
||||||
|
}
|
||||||
|
clientConns = nil
|
||||||
|
clientsMutex.Unlock()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error("Error reading UDP packet: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
activeConns.Add(1)
|
clientKey := remoteAddr.String()
|
||||||
go func(data []byte, remote net.Addr) {
|
clientsMutex.RLock()
|
||||||
defer activeConns.Done()
|
targetConn, exists := clientConns[clientKey]
|
||||||
targetConn, err := net.DialUDP("udp", nil, targetAddr)
|
clientsMutex.RUnlock()
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to connect to target %s: %v", target.Target, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer targetConn.Close()
|
|
||||||
|
|
||||||
select {
|
if !exists {
|
||||||
case <-target.cancel:
|
targetUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr)
|
||||||
return
|
|
||||||
default:
|
|
||||||
_, err = targetConn.Write(data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Info("Failed to write to target: %v", err)
|
logger.Error("Error resolving target address: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
targetConn, err = net.DialUDP("udp", nil, targetUDPAddr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error connecting to target: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
clientsMutex.Lock()
|
||||||
|
clientConns[clientKey] = targetConn
|
||||||
|
clientsMutex.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
buffer := make([]byte, 65507)
|
||||||
|
for {
|
||||||
|
n, _, err := targetConn.ReadFromUDP(buffer)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error reading from target: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := make([]byte, 65535)
|
_, err = conn.WriteTo(buffer[:n], remoteAddr)
|
||||||
n, err := targetConn.Read(response)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Info("Failed to read response from target: %v", err)
|
logger.Error("Error writing to client: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
_, err = conn.WriteTo(response[:n], remote)
|
_, err = targetConn.Write(buffer[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Info("Failed to write response to client: %v", err)
|
logger.Error("Error writing to target: %v", err)
|
||||||
}
|
targetConn.Close()
|
||||||
}
|
clientsMutex.Lock()
|
||||||
}(buffer[:n], remoteAddr)
|
delete(clientConns, clientKey)
|
||||||
|
clientsMutex.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProxyTarget struct {
|
|
||||||
Protocol string
|
|
||||||
Listen string
|
|
||||||
Port int
|
|
||||||
Target string
|
|
||||||
cancel chan struct{} // Channel to signal shutdown
|
|
||||||
done chan struct{} // Channel to signal completion
|
|
||||||
listener net.Listener // For TCP
|
|
||||||
udpConn net.PacketConn // For UDP
|
|
||||||
sync.Mutex // Protect access to connection
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProxyManager struct {
|
|
||||||
targets []ProxyTarget
|
|
||||||
tnet *netstack.Net
|
|
||||||
log *log.Logger
|
|
||||||
sync.RWMutex // Protect access to targets slice
|
|
||||||
}
|
|
||||||
125
self-signed-certs-for-mtls.sh
Executable file
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"
|
||||||
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)
|
||||||
@@ -2,16 +2,19 @@ package websocket
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"software.sslmate.com/src/go-pkcs12"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fosrl/newt/logger"
|
"github.com/fosrl/newt/logger"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,7 +25,6 @@ type Client struct {
|
|||||||
handlers map[string]MessageHandler
|
handlers map[string]MessageHandler
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
handlersMux sync.RWMutex
|
handlersMux sync.RWMutex
|
||||||
|
|
||||||
reconnectInterval time.Duration
|
reconnectInterval time.Duration
|
||||||
isConnected bool
|
isConnected bool
|
||||||
reconnectMux sync.RWMutex
|
reconnectMux sync.RWMutex
|
||||||
@@ -41,6 +43,12 @@ func WithBaseURL(url string) ClientOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithTLSConfig(tlsClientCertPath string) ClientOption {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.config.TlsClientCert = tlsClientCertPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) OnConnect(callback func() error) {
|
func (c *Client) OnConnect(callback func() error) {
|
||||||
c.onConnect = callback
|
c.onConnect = callback
|
||||||
}
|
}
|
||||||
@@ -63,9 +71,14 @@ func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply options before loading config
|
// Apply options before loading config
|
||||||
|
if opts != nil {
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
if opt == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
opt(client)
|
opt(client)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load existing config if available
|
// Load existing config if available
|
||||||
if err := client.loadConfig(); err != nil {
|
if err := client.loadConfig(); err != nil {
|
||||||
@@ -149,6 +162,14 @@ func (c *Client) getToken() (string, error) {
|
|||||||
// Ensure we have the base URL without trailing slashes
|
// Ensure we have the base URL without trailing slashes
|
||||||
baseEndpoint := strings.TrimRight(baseURL.String(), "/")
|
baseEndpoint := strings.TrimRight(baseURL.String(), "/")
|
||||||
|
|
||||||
|
var tlsConfig *tls.Config = nil
|
||||||
|
if c.config.TlsClientCert != "" {
|
||||||
|
tlsConfig, err = loadClientCertificate(c.config.TlsClientCert)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to load certificate %s: %w", c.config.TlsClientCert, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we already have a token, try to use it
|
// If we already have a token, try to use it
|
||||||
if c.config.Token != "" {
|
if c.config.Token != "" {
|
||||||
tokenCheckData := map[string]interface{}{
|
tokenCheckData := map[string]interface{}{
|
||||||
@@ -177,6 +198,11 @@ func (c *Client) getToken() (string, error) {
|
|||||||
|
|
||||||
// Make the request
|
// Make the request
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
if tlsConfig != nil {
|
||||||
|
client.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to check token validity: %w", err)
|
return "", fmt.Errorf("failed to check token validity: %w", err)
|
||||||
@@ -220,6 +246,11 @@ func (c *Client) getToken() (string, error) {
|
|||||||
|
|
||||||
// Make the request
|
// Make the request
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
if tlsConfig != nil {
|
||||||
|
client.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to request new token: %w", err)
|
return "", fmt.Errorf("failed to request new token: %w", err)
|
||||||
@@ -228,6 +259,10 @@ func (c *Client) getToken() (string, error) {
|
|||||||
|
|
||||||
var tokenResp TokenResponse
|
var tokenResp TokenResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
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())
|
||||||
return "", fmt.Errorf("failed to decode token response: %w", err)
|
return "", fmt.Errorf("failed to decode token response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +326,16 @@ func (c *Client) establishConnection() error {
|
|||||||
u.RawQuery = q.Encode()
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
// Connect to WebSocket
|
// Connect to WebSocket
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
dialer := websocket.DefaultDialer
|
||||||
|
if c.config.TlsClientCert != "" {
|
||||||
|
logger.Info("Adding tls to req")
|
||||||
|
tlsConfig, err := loadClientCertificate(c.config.TlsClientCert)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load certificate %s: %w", c.config.TlsClientCert, err)
|
||||||
|
}
|
||||||
|
dialer.TLSClientConfig = tlsConfig
|
||||||
|
}
|
||||||
|
conn, _, err := dialer.Dial(u.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to WebSocket: %w", err)
|
return fmt.Errorf("failed to connect to WebSocket: %w", err)
|
||||||
}
|
}
|
||||||
@@ -305,6 +349,10 @@ func (c *Client) establishConnection() error {
|
|||||||
go c.readPump()
|
go c.readPump()
|
||||||
|
|
||||||
if c.onConnect != nil {
|
if c.onConnect != nil {
|
||||||
|
err := c.saveConfig()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to save config: %v", err)
|
||||||
|
}
|
||||||
if err := c.onConnect(); err != nil {
|
if err := c.onConnect(); err != nil {
|
||||||
logger.Error("OnConnect callback failed: %v", err)
|
logger.Error("OnConnect callback failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -345,3 +393,42 @@ func (c *Client) setConnected(status bool) {
|
|||||||
defer c.reconnectMux.Unlock()
|
defer c.reconnectMux.Unlock()
|
||||||
c.isConnected = status
|
c.isConnected = status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadClientCertificate Helper method to load client certificates
|
||||||
|
func loadClientCertificate(p12Path string) (*tls.Config, error) {
|
||||||
|
logger.Info("Loading tls-client-cert %s", p12Path)
|
||||||
|
// Read the PKCS12 file
|
||||||
|
p12Data, err := os.ReadFile(p12Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read PKCS12 file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse PKCS12 with empty password for non-encrypted files
|
||||||
|
privateKey, certificate, caCerts, err := pkcs12.DecodeChain(p12Data, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode PKCS12: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create certificate
|
||||||
|
cert := tls.Certificate{
|
||||||
|
Certificate: [][]byte{certificate.Raw},
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Add CA certificates if present
|
||||||
|
rootCAs, err := x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load system cert pool: %w", err)
|
||||||
|
}
|
||||||
|
if len(caCerts) > 0 {
|
||||||
|
for _, caCert := range caCerts {
|
||||||
|
rootCAs.AddCert(caCert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create TLS configuration
|
||||||
|
return &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
RootCAs: rootCAs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ func (c *Client) loadConfig() error {
|
|||||||
if c.config.Secret == "" {
|
if c.config.Secret == "" {
|
||||||
c.config.Secret = config.Secret
|
c.config.Secret = config.Secret
|
||||||
}
|
}
|
||||||
|
if c.config.TlsClientCert == "" {
|
||||||
|
c.config.TlsClientCert = config.TlsClientCert
|
||||||
|
}
|
||||||
if c.config.Endpoint == "" {
|
if c.config.Endpoint == "" {
|
||||||
c.config.Endpoint = config.Endpoint
|
c.config.Endpoint = config.Endpoint
|
||||||
c.baseURL = config.Endpoint
|
c.baseURL = config.Endpoint
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ type Config struct {
|
|||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Endpoint string `json:"endpoint"`
|
Endpoint string `json:"endpoint"`
|
||||||
|
TlsClientCert string `json:"tlsClientCert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenResponse struct {
|
type TokenResponse struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user