mirror of
https://github.com/fosrl/newt.git
synced 2026-03-27 04:56:41 +00:00
Compare commits
48 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 |
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/
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,2 +1,6 @@
|
|||||||
newt
|
newt
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
bin/
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
certs/
|
||||||
1
.go-version
Normal file
1
.go-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1.23.2
|
||||||
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"]
|
||||||
27
Makefile
27
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,14 +21,17 @@ test:
|
|||||||
local:
|
local:
|
||||||
CGO_ENABLED=0 go build -o newt
|
CGO_ENABLED=0 go build -o newt
|
||||||
|
|
||||||
all_arches:
|
go-build-release:
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o newt_linux_arm64
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/newt_linux_arm64
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o newt_linux_amd64
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o bin/newt_linux_arm32
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o newt_darwin_arm64
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o bin/newt_linux_arm32v6
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o newt_darwin_amd64
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/newt_linux_amd64
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o newt_windows_amd64.exe
|
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -o bin/newt_linux_riscv64
|
||||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o newt_freebsd_amd64
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/newt_darwin_arm64
|
||||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o newt_freebsd_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
|
||||||
|
|||||||
78
README.md
78
README.md
@@ -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 \
|
||||||
@@ -74,6 +76,70 @@ services:
|
|||||||
- --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
|
||||||
@@ -92,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.
|
||||||
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
301
main.go
301
main.go
@@ -11,6 +11,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -114,7 +115,12 @@ func ping(tnet *netstack.Net, dst string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) {
|
func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) {
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
initialInterval := 10 * time.Second
|
||||||
|
maxInterval := 60 * time.Second
|
||||||
|
currentInterval := initialInterval
|
||||||
|
consecutiveFailures := 0
|
||||||
|
|
||||||
|
ticker := time.NewTicker(currentInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -123,8 +129,34 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{})
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
err := ping(tnet, serverIP)
|
err := ping(tnet, serverIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Periodic ping failed: %v", err)
|
consecutiveFailures++
|
||||||
logger.Warn("HINT: Do you have UDP port 51280 (or the port in config.yml) open on your Pangolin server?")
|
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:
|
case <-stopChan:
|
||||||
logger.Info("Stopping ping check")
|
logger.Info("Stopping ping check")
|
||||||
@@ -134,34 +166,97 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{})
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
@@ -244,19 +339,21 @@ func resolveDomain(domain string) (string, error) {
|
|||||||
return ipAddr, nil
|
return ipAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
var (
|
||||||
var (
|
endpoint string
|
||||||
endpoint string
|
id string
|
||||||
id string
|
secret string
|
||||||
secret string
|
mtu string
|
||||||
mtu string
|
mtuInt int
|
||||||
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
|
// 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")
|
endpoint = os.Getenv("PANGOLIN_ENDPOINT")
|
||||||
id = os.Getenv("NEWT_ID")
|
id = os.Getenv("NEWT_ID")
|
||||||
@@ -264,6 +361,8 @@ func main() {
|
|||||||
mtu = os.Getenv("MTU")
|
mtu = os.Getenv("MTU")
|
||||||
dns = os.Getenv("DNS")
|
dns = os.Getenv("DNS")
|
||||||
logLevel = os.Getenv("LOG_LEVEL")
|
logLevel = os.Getenv("LOG_LEVEL")
|
||||||
|
updownScript = os.Getenv("UPDOWN_SCRIPT")
|
||||||
|
tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT")
|
||||||
|
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server")
|
flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server")
|
||||||
@@ -283,17 +382,27 @@ func main() {
|
|||||||
if logLevel == "" {
|
if logLevel == "" {
|
||||||
flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
|
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")
|
||||||
|
|
||||||
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
|
|
||||||
if endpoint == "" || id == "" || secret == "" {
|
|
||||||
logger.Fatal("endpoint, id, and secret are required either via CLI flags or environment variables")
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse the mtu string into an int
|
// parse the mtu string into an int
|
||||||
mtuInt, err = strconv.Atoi(mtu)
|
mtuInt, err = strconv.Atoi(mtu)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -304,12 +413,16 @@ func main() {
|
|||||||
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)
|
||||||
@@ -343,13 +456,8 @@ func main() {
|
|||||||
|
|
||||||
if connected {
|
if connected {
|
||||||
logger.Info("Already connected! But 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.Warn("Failed to ping %s: %v", wgData.ServerIP, err)
|
|
||||||
logger.Warn("HINT: Do you have UDP port 51280 (or the port in config.yml) open on your Pangolin server?")
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,17 +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)
|
|
||||||
err = pingWithRetry(tnet, wgData.ServerIP)
|
|
||||||
if err != nil {
|
|
||||||
// Handle complete failure after all retries
|
|
||||||
logger.Error("Failed to ping %s: %v", wgData.ServerIP, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Even if pingWithRetry returns an error, it will continue trying in the background
|
||||||
|
_ = pingWithRetry(tnet, wgData.ServerIP)
|
||||||
|
|
||||||
|
// Always mark as connected and start the proxy manager regardless of initial ping result
|
||||||
|
// as the pings will continue in the background
|
||||||
if !connected {
|
if !connected {
|
||||||
logger.Info("Starting ping check")
|
logger.Info("Starting ping check")
|
||||||
startPingCheck(tnet, wgData.ServerIP, pingStopChan)
|
startPingCheck(tnet, wgData.ServerIP, pingStopChan)
|
||||||
|
|
||||||
|
// Start connection monitoring in a separate goroutine
|
||||||
|
go monitorConnectionStatus(tnet, wgData.ServerIP, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create proxy manager
|
// Create proxy manager
|
||||||
@@ -455,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) {
|
||||||
@@ -480,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) {
|
||||||
@@ -552,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
|
||||||
dev.Close()
|
logger.Info("Received %s signal, stopping", sigReceived.String())
|
||||||
|
if dev != nil {
|
||||||
|
dev.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseTargetData(data interface{}) (TargetData, error) {
|
func parseTargetData(data interface{}) (TargetData, error) {
|
||||||
@@ -592,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 {
|
||||||
@@ -602,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)
|
||||||
@@ -616,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
|
||||||
|
}
|
||||||
|
|||||||
530
proxy/manager.go
530
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 {
|
||||||
|
case "tcp":
|
||||||
newTarget := ProxyTarget{
|
if pm.tcpTargets[listenIP] == nil {
|
||||||
Protocol: protocol,
|
pm.tcpTargets[listenIP] = make(map[int]string)
|
||||||
Listen: listen,
|
}
|
||||||
Port: port,
|
pm.tcpTargets[listenIP][port] = targetAddr
|
||||||
Target: target,
|
case "udp":
|
||||||
cancel: make(chan struct{}),
|
if pm.udpTargets[listenIP] == nil {
|
||||||
done: make(chan struct{}),
|
pm.udpTargets[listenIP] = make(map[int]string)
|
||||||
|
}
|
||||||
|
pm.udpTargets[listenIP][port] = targetAddr
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported protocol: %s", proto)
|
||||||
}
|
}
|
||||||
|
|
||||||
pm.targets = append(pm.targets, newTarget)
|
if pm.running {
|
||||||
|
return pm.startTarget(proto, listenIP, port, targetAddr)
|
||||||
|
} else {
|
||||||
|
logger.Debug("Not adding target because not running")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProxyManager) RemoveTarget(protocol, listen string, port int) error {
|
func (pm *ProxyManager) RemoveTarget(proto, listenIP string, port int) error {
|
||||||
pm.Lock()
|
pm.mutex.Lock()
|
||||||
defer pm.Unlock()
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
protocol = strings.ToLower(protocol)
|
switch proto {
|
||||||
if protocol != "tcp" && protocol != "udp" {
|
case "tcp":
|
||||||
return fmt.Errorf("unsupported protocol: %s", protocol)
|
if targets, ok := pm.tcpTargets[listenIP]; ok {
|
||||||
}
|
delete(targets, port)
|
||||||
|
// Remove and close the corresponding TCP listener
|
||||||
for i, target := range pm.targets {
|
for i, listener := range pm.listeners {
|
||||||
if target.Listen == listen &&
|
if addr, ok := listener.Addr().(*net.TCPAddr); ok && addr.Port == port {
|
||||||
target.Port == port &&
|
listener.Close()
|
||||||
strings.ToLower(target.Protocol) == protocol {
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
// Remove from slice
|
||||||
// Signal the serving goroutine to stop
|
pm.listeners = append(pm.listeners[:i], pm.listeners[i+1:]...)
|
||||||
select {
|
break
|
||||||
case <-target.cancel:
|
|
||||||
// Channel is already closed, no need to close it again
|
|
||||||
default:
|
|
||||||
close(target.cancel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the appropriate listener/connection based on protocol
|
|
||||||
target.Lock()
|
|
||||||
switch protocol {
|
|
||||||
case "tcp":
|
|
||||||
if target.listener != nil {
|
|
||||||
select {
|
|
||||||
case <-target.cancel:
|
|
||||||
// Listener was already closed by Stop()
|
|
||||||
default:
|
|
||||||
target.listener.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "udp":
|
|
||||||
if target.udpConn != nil {
|
|
||||||
select {
|
|
||||||
case <-target.cancel:
|
|
||||||
// Connection was already closed by Stop()
|
|
||||||
default:
|
|
||||||
target.udpConn.Close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
target.Unlock()
|
|
||||||
|
|
||||||
// Wait for the target to fully stop
|
|
||||||
<-target.done
|
|
||||||
|
|
||||||
// Remove the target from the slice
|
|
||||||
pm.targets = append(pm.targets[:i], pm.targets[i+1:]...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("target not found for %s %s:%d", protocol, listen, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProxyManager) Start() error {
|
|
||||||
pm.RLock()
|
|
||||||
defer pm.RUnlock()
|
|
||||||
|
|
||||||
for i := range pm.targets {
|
|
||||||
target := &pm.targets[i]
|
|
||||||
|
|
||||||
target.Lock()
|
|
||||||
// If target is already running, skip it
|
|
||||||
if target.listener != nil || target.udpConn != nil {
|
|
||||||
target.Unlock()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the target as starting by creating a nil listener/connection
|
|
||||||
// This prevents other goroutines from trying to start it
|
|
||||||
if strings.ToLower(target.Protocol) == "tcp" {
|
|
||||||
target.listener = nil
|
|
||||||
} else {
|
} else {
|
||||||
target.udpConn = nil
|
return fmt.Errorf("target not found: %s:%d", listenIP, port)
|
||||||
}
|
}
|
||||||
target.Unlock()
|
case "udp":
|
||||||
|
if targets, ok := pm.udpTargets[listenIP]; ok {
|
||||||
|
delete(targets, port)
|
||||||
|
// Remove and close the corresponding UDP connection
|
||||||
|
for i, conn := range pm.udpConns {
|
||||||
|
if addr, ok := conn.LocalAddr().(*net.UDPAddr); ok && addr.Port == port {
|
||||||
|
conn.Close()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
// Remove from slice
|
||||||
|
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("target not found: %s:%d", listenIP, port)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported protocol: %s", proto)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
switch strings.ToLower(target.Protocol) {
|
// Start begins listening for all configured proxy targets
|
||||||
case "tcp":
|
func (pm *ProxyManager) Start() error {
|
||||||
go pm.serveTCP(target)
|
pm.mutex.Lock()
|
||||||
case "udp":
|
defer pm.mutex.Unlock()
|
||||||
go pm.serveUDP(target)
|
|
||||||
default:
|
if pm.running {
|
||||||
return fmt.Errorf("unsupported protocol: %s", target.Protocol)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start TCP targets
|
||||||
|
for listenIP, targets := range pm.tcpTargets {
|
||||||
|
for port, targetAddr := range targets {
|
||||||
|
if err := pm.startTarget("tcp", listenIP, port, targetAddr); err != nil {
|
||||||
|
return fmt.Errorf("failed to start TCP target: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start UDP targets
|
||||||
|
for listenIP, targets := range pm.udpTargets {
|
||||||
|
for port, targetAddr := range targets {
|
||||||
|
if err := pm.startTarget("udp", listenIP, port, targetAddr); err != nil {
|
||||||
|
return fmt.Errorf("failed to start UDP target: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.running = true
|
||||||
return nil
|
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()
|
|
||||||
}
|
|
||||||
t.Unlock()
|
|
||||||
// Wait for the target to fully stop
|
|
||||||
<-t.done
|
|
||||||
}(target)
|
|
||||||
}
|
}
|
||||||
wg.Wait()
|
|
||||||
|
// Set running to false first to signal handlers to stop
|
||||||
|
pm.running = false
|
||||||
|
|
||||||
|
// Close TCP listeners
|
||||||
|
for i := len(pm.listeners) - 1; i >= 0; i-- {
|
||||||
|
listener := pm.listeners[i]
|
||||||
|
if err := listener.Close(); err != nil {
|
||||||
|
logger.Error("Error closing TCP listener: %v", err)
|
||||||
|
}
|
||||||
|
// Remove from slice
|
||||||
|
pm.listeners = append(pm.listeners[:i], pm.listeners[i+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close UDP connections
|
||||||
|
for i := len(pm.udpConns) - 1; i >= 0; i-- {
|
||||||
|
conn := pm.udpConns[i]
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
logger.Error("Error closing UDP connection: %v", err)
|
||||||
|
}
|
||||||
|
// Remove from slice
|
||||||
|
pm.udpConns = append(pm.udpConns[:i], pm.udpConns[i+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the target maps
|
||||||
|
for k := range pm.tcpTargets {
|
||||||
|
delete(pm.tcpTargets, k)
|
||||||
|
}
|
||||||
|
for k := range pm.udpTargets {
|
||||||
|
delete(pm.udpTargets, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give active connections a chance to close gracefully
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
return nil
|
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{Port: port})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create TCP listener: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
listener, err := pm.tnet.ListenTCP(&net.TCPAddr{
|
pm.listeners = append(pm.listeners, listener)
|
||||||
IP: net.ParseIP(target.Listen),
|
go pm.handleTCPProxy(listener, targetAddr)
|
||||||
Port: target.Port,
|
|
||||||
})
|
case "udp":
|
||||||
if err != nil {
|
addr := &net.UDPAddr{Port: port}
|
||||||
logger.Info("Failed to start TCP listener for %s:%d: %v", target.Listen, target.Port, err)
|
conn, err := pm.tnet.ListenUDP(addr)
|
||||||
return
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create UDP listener: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.udpConns = append(pm.udpConns, conn)
|
||||||
|
go pm.handleUDPProxy(conn, targetAddr)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported protocol: %s", proto)
|
||||||
}
|
}
|
||||||
|
|
||||||
target.Lock()
|
logger.Info("Started %s proxy from %s:%d to %s", proto, listenIP, port, targetAddr)
|
||||||
target.listener = listener
|
|
||||||
target.Unlock()
|
|
||||||
|
|
||||||
defer listener.Close()
|
return nil
|
||||||
logger.Info("TCP proxy listening on %s", listener.Addr())
|
}
|
||||||
|
|
||||||
var activeConns sync.WaitGroup
|
|
||||||
acceptDone := make(chan struct{})
|
|
||||||
|
|
||||||
// Goroutine to handle shutdown signal
|
|
||||||
go func() {
|
|
||||||
<-target.cancel
|
|
||||||
close(acceptDone)
|
|
||||||
listener.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
|
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
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for specific network errors that indicate the listener is closed
|
||||||
|
if ne, ok := err.(net.Error); ok && !ne.Temporary() {
|
||||||
|
logger.Info("TCP listener closed, stopping proxy handler for %v", listener.Addr())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error("Error accepting TCP connection: %v", err)
|
||||||
|
// Don't hammer the CPU if we hit a temporary error
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
activeConns.Add(1)
|
|
||||||
go func() {
|
go func() {
|
||||||
defer activeConns.Done()
|
target, err := net.Dial("tcp", targetAddr)
|
||||||
pm.handleTCPConnection(conn, target.Target, acceptDone)
|
if err != nil {
|
||||||
|
logger.Error("Error connecting to target: %v", err)
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a WaitGroup to ensure both copy operations complete
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
io.Copy(target, conn)
|
||||||
|
target.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
io.Copy(conn, target)
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for both copies to complete
|
||||||
|
wg.Wait()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProxyManager) handleTCPConnection(clientConn net.Conn, target string, done chan struct{}) {
|
func (pm *ProxyManager) handleUDPProxy(conn *gonet.UDPConn, targetAddr string) {
|
||||||
defer clientConn.Close()
|
buffer := make([]byte, 65507) // Max UDP packet size
|
||||||
|
clientConns := make(map[string]*net.UDPConn)
|
||||||
serverConn, err := net.Dial("tcp", target)
|
var clientsMutex sync.RWMutex
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to connect to target %s: %v", target, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer serverConn.Close()
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(2)
|
|
||||||
|
|
||||||
// Client -> Server
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
io.Copy(serverConn, clientConn)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Server -> Client
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
io.Copy(clientConn, serverConn)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProxyManager) serveUDP(target *ProxyTarget) {
|
|
||||||
defer close(target.done) // Signal that this target is fully stopped
|
|
||||||
|
|
||||||
addr := &net.UDPAddr{
|
|
||||||
IP: net.ParseIP(target.Listen),
|
|
||||||
Port: target.Port,
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := pm.tnet.ListenUDP(addr)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to start UDP listener for %s:%d: %v", target.Listen, target.Port, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target.Lock()
|
|
||||||
target.udpConn = conn
|
|
||||||
target.Unlock()
|
|
||||||
|
|
||||||
defer conn.Close()
|
|
||||||
logger.Info("UDP proxy listening on %s", conn.LocalAddr())
|
|
||||||
|
|
||||||
buffer := make([]byte, 65535)
|
|
||||||
var activeConns sync.WaitGroup
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
n, remoteAddr, err := conn.ReadFrom(buffer)
|
||||||
case <-target.cancel:
|
if err != nil {
|
||||||
activeConns.Wait() // Wait for all active UDP handlers to complete
|
if !pm.running {
|
||||||
return
|
return
|
||||||
default:
|
|
||||||
n, remoteAddr, err := conn.ReadFrom(buffer)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-target.cancel:
|
|
||||||
activeConns.Wait()
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
logger.Info("Failed to read UDP packet: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAddr, err := net.ResolveUDPAddr("udp", target.Target)
|
// Check for connection closed conditions
|
||||||
|
if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") {
|
||||||
|
logger.Info("UDP connection closed, stopping proxy handler")
|
||||||
|
|
||||||
|
// Clean up existing client connections
|
||||||
|
clientsMutex.Lock()
|
||||||
|
for _, targetConn := range clientConns {
|
||||||
|
targetConn.Close()
|
||||||
|
}
|
||||||
|
clientConns = nil
|
||||||
|
clientsMutex.Unlock()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error("Error reading UDP packet: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
clientKey := remoteAddr.String()
|
||||||
|
clientsMutex.RLock()
|
||||||
|
targetConn, exists := clientConns[clientKey]
|
||||||
|
clientsMutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
targetUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Info("Failed to resolve target address %s: %v", target.Target, err)
|
logger.Error("Error resolving target address: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
activeConns.Add(1)
|
targetConn, err = net.DialUDP("udp", nil, targetUDPAddr)
|
||||||
go func(data []byte, remote net.Addr) {
|
if err != nil {
|
||||||
defer activeConns.Done()
|
logger.Error("Error connecting to target: %v", err)
|
||||||
targetConn, err := net.DialUDP("udp", nil, targetAddr)
|
continue
|
||||||
if err != nil {
|
}
|
||||||
logger.Info("Failed to connect to target %s: %v", target.Target, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer targetConn.Close()
|
|
||||||
|
|
||||||
select {
|
clientsMutex.Lock()
|
||||||
case <-target.cancel:
|
clientConns[clientKey] = targetConn
|
||||||
return
|
clientsMutex.Unlock()
|
||||||
default:
|
|
||||||
_, err = targetConn.Write(data)
|
go func() {
|
||||||
|
buffer := make([]byte, 65507)
|
||||||
|
for {
|
||||||
|
n, _, err := targetConn.ReadFromUDP(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Info("Failed to write to target: %v", err)
|
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)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to write response to client: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}(buffer[:n], remoteAddr)
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = targetConn.Write(buffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error writing to target: %v", err)
|
||||||
|
targetConn.Close()
|
||||||
|
clientsMutex.Lock()
|
||||||
|
delete(clientConns, clientKey)
|
||||||
|
clientsMutex.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,27 +2,29 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
config *Config
|
config *Config
|
||||||
baseURL string
|
baseURL string
|
||||||
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,8 +71,13 @@ func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply options before loading config
|
// Apply options before loading config
|
||||||
for _, opt := range opts {
|
if opts != nil {
|
||||||
opt(client)
|
for _, opt := range opts {
|
||||||
|
if opt == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opt(client)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load existing config if available
|
// Load existing config if available
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package websocket
|
package websocket
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
NewtID string `json:"newtId"`
|
NewtID string `json:"newtId"`
|
||||||
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