mirror of
https://github.com/fosrl/olm.git
synced 2026-02-13 08:26:43 +00:00
Compare commits
145 Commits
1.0.0-beta
...
1.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e8050949 | ||
|
|
8fa2da00b6 | ||
|
|
b851cd73c9 | ||
|
|
4c19d7ef6d | ||
|
|
cbecb9a0ce | ||
|
|
4fc8db08ba | ||
|
|
7ca46e0a75 | ||
|
|
a4ea5143af | ||
|
|
e9257b6423 | ||
|
|
3c9d3a1d2c | ||
|
|
b426f14190 | ||
|
|
d48acfba39 | ||
|
|
35b48cd8e5 | ||
|
|
15bca53309 | ||
|
|
898b599db5 | ||
|
|
c07bba18bb | ||
|
|
4c24d3b808 | ||
|
|
ad4ab3d04f | ||
|
|
e21153fae1 | ||
|
|
41c3360e23 | ||
|
|
1960d32443 | ||
|
|
74b83b3303 | ||
|
|
c2c3470868 | ||
|
|
2bda3dc3cc | ||
|
|
52573c8664 | ||
|
|
0d3c34e23f | ||
|
|
891df5c74b | ||
|
|
6f3f162d2b | ||
|
|
f6fa5fd02c | ||
|
|
8f4e0ba29e | ||
|
|
32b7dc7c43 | ||
|
|
78d2ebe1de | ||
|
|
014f8eb4e5 | ||
|
|
cd42803291 | ||
|
|
5c5b303994 | ||
|
|
968873da22 | ||
|
|
e2772f918b | ||
|
|
cdf6a31b67 | ||
|
|
2ce72065a7 | ||
|
|
b3e7aafb58 | ||
|
|
dd610ad850 | ||
|
|
b4b0a832e7 | ||
|
|
79963c1f66 | ||
|
|
5f95282161 | ||
|
|
1cca54f9d5 | ||
|
|
219df22919 | ||
|
|
337d9934fd | ||
|
|
bba4d72a78 | ||
|
|
fb5c793126 | ||
|
|
1821dbb672 | ||
|
|
4fda6fe031 | ||
|
|
f286f0faf6 | ||
|
|
cba3d607bf | ||
|
|
5ca12834a1 | ||
|
|
9d41154daa | ||
|
|
63933b57fc | ||
|
|
c25d77597d | ||
|
|
ad080046a1 | ||
|
|
c1f7cf93a5 | ||
|
|
f3f112fc42 | ||
|
|
612a9ddb15 | ||
|
|
ad1fa2e59a | ||
|
|
29235f6100 | ||
|
|
848ac6b0c4 | ||
|
|
6ab66e6c36 | ||
|
|
d7f29d4709 | ||
|
|
6fb2b68e21 | ||
|
|
8d72e77d57 | ||
|
|
25a9b83496 | ||
|
|
4d33016389 | ||
|
|
0f717aec01 | ||
|
|
4c58cd6eff | ||
|
|
3ad36f95e1 | ||
|
|
b58e7c9fad | ||
|
|
85a8a737e8 | ||
|
|
8e83a83294 | ||
|
|
c1ef56001f | ||
|
|
c04e727bd3 | ||
|
|
becc214078 | ||
|
|
13e7f55b30 | ||
|
|
0be3ee7eee | ||
|
|
0b1724a3f3 | ||
|
|
e606264deb | ||
|
|
5497eb8a4e | ||
|
|
2159371371 | ||
|
|
ad8a94fdc8 | ||
|
|
61b7feef80 | ||
|
|
4cb31df3c8 | ||
|
|
3b0eef6d60 | ||
|
|
5d305f1d03 | ||
|
|
31e5d4e3bd | ||
|
|
8fb9468d08 | ||
|
|
5bbd5016aa | ||
|
|
8c40b8c578 | ||
|
|
e35f7c2d36 | ||
|
|
aeb8f203a4 | ||
|
|
73bd036e58 | ||
|
|
eb6b310304 | ||
|
|
3d70ff190f | ||
|
|
7e2d7b93a1 | ||
|
|
f50ff67057 | ||
|
|
76d5e95fbf | ||
|
|
b6db70e285 | ||
|
|
3819823d95 | ||
|
|
b2830e8473 | ||
|
|
43a43b429d | ||
|
|
1593f22691 | ||
|
|
4883402393 | ||
|
|
02eab1ff52 | ||
|
|
e0ca38bb35 | ||
|
|
8d46ae3aa2 | ||
|
|
7424caca8a | ||
|
|
9d9f10a799 | ||
|
|
a42d2b75dd | ||
|
|
8b09545cf6 | ||
|
|
eb77be09e2 | ||
|
|
ad01296c41 | ||
|
|
b553209712 | ||
|
|
c5098f0cd0 | ||
|
|
5ec1aac0d1 | ||
|
|
313ef42883 | ||
|
|
085c98668d | ||
|
|
6107d20e26 | ||
|
|
66edae4288 | ||
|
|
f69a7f647d | ||
|
|
e8bd55bed9 | ||
|
|
b23eda9c06 | ||
|
|
76503f3f2c | ||
|
|
9c3112f9bd | ||
|
|
462af30d16 | ||
|
|
fa6038eb38 | ||
|
|
f346b6cc5d | ||
|
|
f20b9ebb14 | ||
|
|
39bfe5b230 | ||
|
|
a1a3dd9ba2 | ||
|
|
7b1492f327 | ||
|
|
4e50819785 | ||
|
|
f8dccbec80 | ||
|
|
0c5c59cf00 | ||
|
|
868bb55f87 | ||
|
|
5b4245402a | ||
|
|
f7a705e6f8 | ||
|
|
3a63657822 | ||
|
|
759780508a | ||
|
|
533886f2e4 |
@@ -1,6 +1,6 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
.dockerignore
|
.dockerignore
|
||||||
newt
|
olm
|
||||||
*.json
|
*.json
|
||||||
README.md
|
README.md
|
||||||
Makefile
|
Makefile
|
||||||
|
|||||||
40
.github/dependabot.yml
vendored
Normal file
40
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
groups:
|
||||||
|
dev-patch-updates:
|
||||||
|
dependency-type: "development"
|
||||||
|
update-types:
|
||||||
|
- "patch"
|
||||||
|
dev-minor-updates:
|
||||||
|
dependency-type: "development"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
prod-patch-updates:
|
||||||
|
dependency-type: "production"
|
||||||
|
update-types:
|
||||||
|
- "patch"
|
||||||
|
prod-minor-updates:
|
||||||
|
dependency-type: "production"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
groups:
|
||||||
|
patch-updates:
|
||||||
|
update-types:
|
||||||
|
- "patch"
|
||||||
|
minor-updates:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
60
.github/workflows/cicd.yml
vendored
Normal file
60
.github/workflows/cicd.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: CI/CD Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build and Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
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@v6
|
||||||
|
with:
|
||||||
|
go-version: 1.25
|
||||||
|
|
||||||
|
- name: Update version in main.go
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [ -f main.go ]; then
|
||||||
|
sed -i 's/version_replaceme/'"$TAG"'/' main.go
|
||||||
|
echo "Updated main.go with version $TAG"
|
||||||
|
else
|
||||||
|
echo "main.go not found"
|
||||||
|
fi
|
||||||
|
- name: Build and push Docker images
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
make docker-build-release tag=$TAG
|
||||||
|
|
||||||
|
- name: Build binaries
|
||||||
|
run: |
|
||||||
|
make go-build-release
|
||||||
|
|
||||||
|
- name: Upload artifacts from /bin
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binaries
|
||||||
|
path: bin/
|
||||||
28
.github/workflows/test.yml
vendored
Normal file
28
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 1.25
|
||||||
|
|
||||||
|
- name: Build go
|
||||||
|
run: go build
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: make build
|
||||||
|
|
||||||
|
- name: Build binaries
|
||||||
|
run: make go-build-release
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
newt
|
olm
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
bin/
|
||||||
1
.go-version
Normal file
1
.go-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1.25
|
||||||
12
Dockerfile
12
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23.1-alpine AS builder
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
# Set the working directory inside the container
|
# Set the working directory inside the container
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -13,15 +13,15 @@ RUN go mod download
|
|||||||
COPY . .
|
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 /olm
|
||||||
|
|
||||||
# Start a new stage from scratch
|
# Start a new stage from scratch
|
||||||
FROM ubuntu:22.04 AS runner
|
FROM alpine:3.22 AS runner
|
||||||
|
|
||||||
RUN apt-get update && apt-get install ca-certificates -y && rm -rf /var/lib/apt/lists/*
|
RUN apk --no-cache add ca-certificates
|
||||||
|
|
||||||
# Copy the pre-built binary file from the previous stage and the entrypoint script
|
# Copy the pre-built binary file from the previous stage and the entrypoint script
|
||||||
COPY --from=builder /newt /usr/local/bin/
|
COPY --from=builder /olm /usr/local/bin/
|
||||||
COPY entrypoint.sh /
|
COPY entrypoint.sh /
|
||||||
|
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
@@ -30,4 +30,4 @@ RUN chmod +x /entrypoint.sh
|
|||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|
||||||
# Command to run the executable
|
# Command to run the executable
|
||||||
CMD ["newt"]
|
CMD ["olm"]
|
||||||
38
Makefile
38
Makefile
@@ -1,26 +1,26 @@
|
|||||||
|
|
||||||
all: build push
|
all: go-build-release
|
||||||
|
|
||||||
build:
|
docker-build-release:
|
||||||
docker build -t fosrl/newt:latest .
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make docker-build-release tag=<tag>"; \
|
||||||
push:
|
exit 1; \
|
||||||
docker push fosrl/newt:latest
|
fi
|
||||||
|
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t fosrl/olm:latest -f Dockerfile --push .
|
||||||
test:
|
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t fosrl/olm:$(tag) -f Dockerfile --push .
|
||||||
docker run fosrl/newt:latest
|
|
||||||
|
|
||||||
local:
|
local:
|
||||||
CGO_ENABLED=0 go build -o newt
|
CGO_ENABLED=0 go build -o olm
|
||||||
|
|
||||||
all_arches:
|
build:
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o newt_linux_arm64
|
docker build -t fosrl/olm:latest .
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o newt_linux_amd64
|
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o newt_darwin_arm64
|
go-build-release:
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o newt_darwin_amd64
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/olm_linux_arm64
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o newt_windows_amd64.exe
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/olm_linux_amd64
|
||||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o newt_freebsd_amd64
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/olm_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/olm_darwin_amd64
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/olm_windows_amd64.exe
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm newt
|
rm olm
|
||||||
299
README.md
299
README.md
@@ -1,46 +1,59 @@
|
|||||||
# Newt
|
# Olm
|
||||||
|
|
||||||
Newt is a fully user space [WireGuard](https://www.wireguard.com/) tunnel client and TCP/UDP proxy, designed to securely expose private resources controlled by Pangolin. By using Newt, you don't need to manage complex WireGuard tunnels and NATing.
|
Olm is a [WireGuard](https://www.wireguard.com/) tunnel client designed to securely connect your computer to Newt sites running on remote networks.
|
||||||
|
|
||||||
### Installation and Documentation
|
### Installation and Documentation
|
||||||
|
|
||||||
Newt is used with Pangolin and Gerbil as part of the larger system. See documentation below:
|
Olm is used with Pangolin and Newt as part of the larger system. See documentation below:
|
||||||
|
|
||||||
- [Installation Instructions](https://docs.fossorial.io)
|
|
||||||
- [Full Documentation](https://docs.fossorial.io)
|
- [Full Documentation](https://docs.fossorial.io)
|
||||||
|
|
||||||
## Preview
|
|
||||||
|
|
||||||
<img src="public/screenshots/preview.png" alt="Preview"/>
|
|
||||||
|
|
||||||
_Sample output of a Newt container connected to Pangolin and hosting various resource target proxies._
|
|
||||||
|
|
||||||
## Key Functions
|
## Key Functions
|
||||||
|
|
||||||
### 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 Olm ID and a secret, the olm 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
|
||||||
|
|
||||||
When Newt receives WireGuard control messages, it will use the information encoded (endpoint, public key) to bring up a WireGuard tunnel using [netstack](https://github.com/WireGuard/wireguard-go/blob/master/tun/netstack/examples/http_server.go) fully in user space. It will ping over the tunnel to ensure the peer on the Gerbil side is brought up.
|
When Olm receives WireGuard control messages, it will use the information encoded (endpoint, public key) to bring up a WireGuard tunnel on your computer to a remote Newt. It will ping over the tunnel to ensure the peer is brought up.
|
||||||
|
|
||||||
### Receives Proxy Control Messages
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
- `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket.
|
- `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket.
|
||||||
- `id`: Newt ID generated by Pangolin to identify the client.
|
- `id`: Olm ID generated by Pangolin to identify the olm.
|
||||||
- `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 olm ID with the websocket in order to receive commands.
|
||||||
- `dns`: DNS server to use to resolve the endpoint
|
- `mtu` (optional): MTU for the internal WG interface. Default: 1280
|
||||||
- `log-level` (optional): The log level to use. Default: INFO
|
- `dns` (optional): DNS server to use to resolve the endpoint. Default: 8.8.8.8
|
||||||
|
- `log-level` (optional): The log level to use (DEBUG, INFO, WARN, ERROR, FATAL). Default: INFO
|
||||||
|
- `ping-interval` (optional): Interval for pinging the server. Default: 3s
|
||||||
|
- `ping-timeout` (optional): Timeout for each ping. Default: 5s
|
||||||
|
- `interface` (optional): Name of the WireGuard interface. Default: olm
|
||||||
|
- `enable-http` (optional): Enable HTTP server for receiving connection requests. Default: false
|
||||||
|
- `http-addr` (optional): HTTP server address (e.g., ':9452'). Default: :9452
|
||||||
|
- `holepunch` (optional): Enable hole punching. Default: false
|
||||||
|
|
||||||
Example:
|
## Environment Variables
|
||||||
|
|
||||||
|
All CLI arguments can also be set via environment variables:
|
||||||
|
|
||||||
|
- `PANGOLIN_ENDPOINT`: Equivalent to `--endpoint`
|
||||||
|
- `OLM_ID`: Equivalent to `--id`
|
||||||
|
- `OLM_SECRET`: Equivalent to `--secret`
|
||||||
|
- `MTU`: Equivalent to `--mtu`
|
||||||
|
- `DNS`: Equivalent to `--dns`
|
||||||
|
- `LOG_LEVEL`: Equivalent to `--log-level`
|
||||||
|
- `INTERFACE`: Equivalent to `--interface`
|
||||||
|
- `HTTP_ADDR`: Equivalent to `--http-addr`
|
||||||
|
- `PING_INTERVAL`: Equivalent to `--ping-interval`
|
||||||
|
- `PING_TIMEOUT`: Equivalent to `--ping-timeout`
|
||||||
|
- `HOLEPUNCH`: Set to "true" to enable hole punching (equivalent to `--holepunch`)
|
||||||
|
- `CONFIG_FILE`: Set to the location of a JSON file to load secret values
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./newt \
|
olm \
|
||||||
--id 31frd0uzbjvp721 \
|
--id 31frd0uzbjvp721 \
|
||||||
--secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
|
--secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
|
||||||
--endpoint https://example.com
|
--endpoint https://example.com
|
||||||
@@ -50,40 +63,230 @@ You can also run it with Docker compose. For example, a service in your `docker-
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
newt:
|
olm:
|
||||||
image: fosrl/newt
|
image: fosrl/olm
|
||||||
container_name: newt
|
container_name: olm
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
network_mode: host
|
||||||
- PANGOLIN_ENDPOINT=https://example.com
|
devices:
|
||||||
- NEWT_ID=2ix2t8xk22ubpfy
|
- /dev/net/tun:/dev/net/tun
|
||||||
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
|
environment:
|
||||||
|
- PANGOLIN_ENDPOINT=https://example.com
|
||||||
|
- OLM_ID=31frd0uzbjvp721
|
||||||
|
- OLM_SECRET=h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also pass the CLI args to the container:
|
You can also pass the CLI args to the container:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
newt:
|
olm:
|
||||||
image: fosrl/newt
|
image: fosrl/olm
|
||||||
container_name: newt
|
container_name: olm
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
network_mode: host
|
||||||
- --id 31frd0uzbjvp721
|
devices:
|
||||||
- --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6
|
- /dev/net/tun:/dev/net/tun
|
||||||
- --endpoint https://example.com
|
command:
|
||||||
|
- --id 31frd0uzbjvp721
|
||||||
|
- --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6
|
||||||
|
- --endpoint https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker Configuration Notes:**
|
||||||
|
|
||||||
|
- `network_mode: host` brings the olm network interface to the host system, allowing the WireGuard tunnel to function properly
|
||||||
|
- `devices: - /dev/net/tun:/dev/net/tun` is required to give the container access to the TUN device for creating WireGuard interfaces
|
||||||
|
|
||||||
|
## Loading secrets from files
|
||||||
|
|
||||||
|
You can use `CONFIG_FILE` to define a location of a config file to store the credentials between runs.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cat ~/.config/olm-client/config.json
|
||||||
|
{
|
||||||
|
"id": "spmzu8rbpzj1qq6",
|
||||||
|
"secret": "f6v61mjutwme2kkydbw3fjo227zl60a2tsf5psw9r25hgae3",
|
||||||
|
"endpoint": "https://pangolin.fossorial.io",
|
||||||
|
"tlsClientCert": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This file is also written to when newt first starts up. So you do not need to run every time with --id and secret if you have run it once!
|
||||||
|
|
||||||
|
Default locations:
|
||||||
|
|
||||||
|
- **macOS**: `~/Library/Application Support/olm-client/config.json`
|
||||||
|
- **Windows**: `%PROGRAMDATA%\olm\olm-client\config.json`
|
||||||
|
- **Linux/Others**: `~/.config/olm-client/config.json`
|
||||||
|
|
||||||
|
## Hole Punching
|
||||||
|
|
||||||
|
In the default mode, olm "relays" traffic through Gerbil in the cloud to get down to newt. This is a little more reliable. Support for NAT hole punching is also EXPERIMENTAL right now using the `--holepunch` flag. This will attempt to orchestrate a NAT hole punch between the two sites so that traffic flows directly. This will save data costs and speed. If it fails it should fall back to relaying.
|
||||||
|
|
||||||
|
Right now, basic NAT hole punching is supported. We plan to add:
|
||||||
|
|
||||||
|
- [ ] Birthday paradox
|
||||||
|
- [ ] UPnP
|
||||||
|
- [ ] LAN detection
|
||||||
|
|
||||||
|
## Windows Service
|
||||||
|
|
||||||
|
On Windows, olm has to be installed and run as a Windows service. When running it with the cli args live above it will attempt to install and run the service to function like a cli tool. You can also run the following:
|
||||||
|
|
||||||
|
### Service Management Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
# Install the service
|
||||||
|
olm.exe install
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
olm.exe start
|
||||||
|
|
||||||
|
# Stop the service
|
||||||
|
olm.exe stop
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
olm.exe status
|
||||||
|
|
||||||
|
# Remove the service
|
||||||
|
olm.exe remove
|
||||||
|
|
||||||
|
# Run in debug mode (console output) with our without id & secret
|
||||||
|
olm.exe debug
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
olm.exe help
|
||||||
|
```
|
||||||
|
|
||||||
|
Note running the service requires credentials in `%PROGRAMDATA%\olm\olm-client\config.json`.
|
||||||
|
|
||||||
|
### Service Configuration
|
||||||
|
|
||||||
|
When running as a service, Olm will read configuration from environment variables or you can modify the service to include command-line arguments:
|
||||||
|
|
||||||
|
1. Install the service: `olm.exe install`
|
||||||
|
2. Set the credentials in `%PROGRAMDATA%\olm\olm-client\config.json`. Hint: if you run olm once with --id and --secret this file will be populated!
|
||||||
|
3. Start the service: `olm.exe start`
|
||||||
|
|
||||||
|
### Service Logs
|
||||||
|
|
||||||
|
When running as a service, logs are written to:
|
||||||
|
|
||||||
|
- Windows Event Log (Application log, source: "OlmWireguardService")
|
||||||
|
- Log files in: `%PROGRAMDATA%\olm\logs\olm.log`
|
||||||
|
|
||||||
|
You can view the Windows Event Log using Event Viewer or PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-EventLog -LogName Application -Source "OlmWireguardService" -Newest 10
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Endpoints
|
||||||
|
|
||||||
|
Olm can be controlled with an embedded http server when using `--enable-http`. This allows you to start it as a daemon and trigger it with the following endpoints:
|
||||||
|
|
||||||
|
### POST /connect
|
||||||
|
Initiates a new connection request.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"secret": "string",
|
||||||
|
"endpoint": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required Fields:**
|
||||||
|
- `id`: Connection identifier
|
||||||
|
- `secret`: Authentication secret
|
||||||
|
- `endpoint`: Target endpoint URL
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
- **Status Code:** `202 Accepted`
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "connection request accepted"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- `405 Method Not Allowed` - Non-POST requests
|
||||||
|
- `400 Bad Request` - Invalid JSON or missing required fields
|
||||||
|
|
||||||
|
### GET /status
|
||||||
|
Returns the current connection status and peer information.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
- **Status Code:** `200 OK`
|
||||||
|
- **Content-Type:** `application/json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "connected",
|
||||||
|
"connected": true,
|
||||||
|
"tunnelIP": "100.89.128.3/20",
|
||||||
|
"version": "version_replaceme",
|
||||||
|
"peers": {
|
||||||
|
"10": {
|
||||||
|
"siteId": 10,
|
||||||
|
"connected": true,
|
||||||
|
"rtt": 145338339,
|
||||||
|
"lastSeen": "2025-08-13T14:39:17.208334428-07:00",
|
||||||
|
"endpoint": "p.fosrl.io:21820",
|
||||||
|
"isRelay": true
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"siteId": 8,
|
||||||
|
"connected": false,
|
||||||
|
"rtt": 0,
|
||||||
|
"lastSeen": "2025-08-13T14:39:19.663823645-07:00",
|
||||||
|
"endpoint": "p.fosrl.io:21820",
|
||||||
|
"isRelay": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
- `status`: Overall connection status ("connected" or "disconnected")
|
||||||
|
- `connected`: Boolean connection state
|
||||||
|
- `tunnelIP`: IP address and subnet of the tunnel (when connected)
|
||||||
|
- `version`: Olm version string
|
||||||
|
- `peers`: Map of peer statuses by site ID
|
||||||
|
- `siteId`: Peer site identifier
|
||||||
|
- `connected`: Boolean peer connection state
|
||||||
|
- `rtt`: Peer round-trip time (integer, nanoseconds)
|
||||||
|
- `lastSeen`: Last time peer was seen (RFC3339 timestamp)
|
||||||
|
- `endpoint`: Peer endpoint address
|
||||||
|
- `isRelay`: Whether the peer is relayed (true) or direct (false)
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- `405 Method Not Allowed` - Non-GET requests
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Connect to a peer
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/connect \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"id": "31frd0uzbjvp721",
|
||||||
|
"secret": "h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6",
|
||||||
|
"endpoint": "https://example.com"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check connection status
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/status
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
### Container
|
|
||||||
|
|
||||||
Ensure Docker is installed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
### Binary
|
### Binary
|
||||||
|
|
||||||
Make sure to have Go 1.23.1 installed.
|
Make sure to have Go 1.23.1 installed.
|
||||||
@@ -94,7 +297,7 @@ make local
|
|||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
Newt is dual licensed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.
|
Olm is dual licensed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
services:
|
services:
|
||||||
newt:
|
olm:
|
||||||
image: fosrl/newt:latest
|
image: fosrl/olm:latest
|
||||||
container_name: newt
|
container_name: olm
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- PANGOLIN_ENDPOINT=https://example.com
|
- PANGOLIN_ENDPOINT=https://example.com
|
||||||
- NEWT_ID=2ix2t8xk22ubpfy
|
- OLM_ID=vdqnz8rwgb95cnp
|
||||||
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
|
- OLM_SECRET=1sw05qv1tkfdb1k81zpw05nahnnjvmhxjvf746umwagddmdg
|
||||||
- LOG_LEVEL=DEBUG
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_MODULE
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
network_mode: host
|
||||||
@@ -4,7 +4,7 @@ set -e
|
|||||||
|
|
||||||
# first arg is `-f` or `--some-option`
|
# first arg is `-f` or `--some-option`
|
||||||
if [ "${1#-}" != "$1" ]; then
|
if [ "${1#-}" != "$1" ]; then
|
||||||
set -- newt "$@"
|
set -- olm "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
33
go.mod
33
go.mod
@@ -1,19 +1,22 @@
|
|||||||
module github.com/fosrl/newt
|
module github.com/fosrl/olm
|
||||||
|
|
||||||
go 1.23.1
|
go 1.25
|
||||||
|
|
||||||
toolchain go1.23.2
|
|
||||||
|
|
||||||
require golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/fosrl/newt v0.0.0-20250730062419-3ccd755d557a
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/vishvananda/netlink v1.3.1
|
||||||
golang.org/x/crypto v0.28.0 // indirect
|
golang.org/x/crypto v0.42.0
|
||||||
golang.org/x/net v0.30.0 // indirect
|
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
golang.org/x/sys v0.36.0
|
||||||
golang.org/x/time v0.7.0 // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect
|
)
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
|
golang.org/x/net v0.43.0 // indirect
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
|
gvisor.dev/gvisor v0.0.0-20250718192347-d7830d968c56 // indirect
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.6.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
46
go.sum
46
go.sum
@@ -1,20 +1,34 @@
|
|||||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
github.com/fosrl/newt v0.0.0-20250730062419-3ccd755d557a h1:bUGN4piHlcqgfdRLrwqiLZZxgcitzBzNDQS1+CHSmJI=
|
||||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
github.com/fosrl/newt v0.0.0-20250730062419-3ccd755d557a/go.mod h1:PbiPYp1hbL07awrmbqTSTz7lTenieTHN6cIkUVCGD3I=
|
||||||
|
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||||
|
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/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=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
|
||||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||||
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
gvisor.dev/gvisor v0.0.0-20250718192347-d7830d968c56 h1:H+qymc2ndLKNFR5TcaPmsHGiJnhJMqeofBYSRq4oG3c=
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
gvisor.dev/gvisor v0.0.0-20250718192347-d7830d968c56/go.mod h1:i8iCZyAdwRnLZYaIi2NUL1gfNtAveqxkKAe0JfAv9Bs=
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||||
|
|||||||
217
httpserver/httpserver.go
Normal file
217
httpserver/httpserver.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fosrl/newt/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnectionRequest defines the structure for an incoming connection request
|
||||||
|
type ConnectionRequest struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerStatus represents the status of a peer connection
|
||||||
|
type PeerStatus struct {
|
||||||
|
SiteID int `json:"siteId"`
|
||||||
|
Connected bool `json:"connected"`
|
||||||
|
RTT time.Duration `json:"rtt"`
|
||||||
|
LastSeen time.Time `json:"lastSeen"`
|
||||||
|
Endpoint string `json:"endpoint,omitempty"`
|
||||||
|
IsRelay bool `json:"isRelay"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusResponse is returned by the status endpoint
|
||||||
|
type StatusResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Connected bool `json:"connected"`
|
||||||
|
TunnelIP string `json:"tunnelIP,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
PeerStatuses map[int]*PeerStatus `json:"peers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPServer represents the HTTP server and its state
|
||||||
|
type HTTPServer struct {
|
||||||
|
addr string
|
||||||
|
server *http.Server
|
||||||
|
connectionChan chan ConnectionRequest
|
||||||
|
statusMu sync.RWMutex
|
||||||
|
peerStatuses map[int]*PeerStatus
|
||||||
|
connectedAt time.Time
|
||||||
|
isConnected bool
|
||||||
|
tunnelIP string
|
||||||
|
version string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPServer creates a new HTTP server
|
||||||
|
func NewHTTPServer(addr string) *HTTPServer {
|
||||||
|
s := &HTTPServer{
|
||||||
|
addr: addr,
|
||||||
|
connectionChan: make(chan ConnectionRequest, 1),
|
||||||
|
peerStatuses: make(map[int]*PeerStatus),
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the HTTP server
|
||||||
|
func (s *HTTPServer) Start() error {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/connect", s.handleConnect)
|
||||||
|
mux.HandleFunc("/status", s.handleStatus)
|
||||||
|
|
||||||
|
s.server = &http.Server{
|
||||||
|
Addr: s.addr,
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Starting HTTP server on %s", s.addr)
|
||||||
|
go func() {
|
||||||
|
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Error("HTTP server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the HTTP server
|
||||||
|
func (s *HTTPServer) Stop() error {
|
||||||
|
logger.Info("Stopping HTTP server")
|
||||||
|
return s.server.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConnectionChannel returns the channel for receiving connection requests
|
||||||
|
func (s *HTTPServer) GetConnectionChannel() <-chan ConnectionRequest {
|
||||||
|
return s.connectionChan
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePeerStatus updates the status of a peer including endpoint and relay info
|
||||||
|
func (s *HTTPServer) UpdatePeerStatus(siteID int, connected bool, rtt time.Duration, endpoint string, isRelay bool) {
|
||||||
|
s.statusMu.Lock()
|
||||||
|
defer s.statusMu.Unlock()
|
||||||
|
|
||||||
|
status, exists := s.peerStatuses[siteID]
|
||||||
|
if !exists {
|
||||||
|
status = &PeerStatus{
|
||||||
|
SiteID: siteID,
|
||||||
|
}
|
||||||
|
s.peerStatuses[siteID] = status
|
||||||
|
}
|
||||||
|
|
||||||
|
status.Connected = connected
|
||||||
|
status.RTT = rtt
|
||||||
|
status.LastSeen = time.Now()
|
||||||
|
status.Endpoint = endpoint
|
||||||
|
status.IsRelay = isRelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConnectionStatus sets the overall connection status
|
||||||
|
func (s *HTTPServer) SetConnectionStatus(isConnected bool) {
|
||||||
|
s.statusMu.Lock()
|
||||||
|
defer s.statusMu.Unlock()
|
||||||
|
|
||||||
|
s.isConnected = isConnected
|
||||||
|
|
||||||
|
if isConnected {
|
||||||
|
s.connectedAt = time.Now()
|
||||||
|
} else {
|
||||||
|
// Clear peer statuses when disconnected
|
||||||
|
s.peerStatuses = make(map[int]*PeerStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTunnelIP sets the tunnel IP address
|
||||||
|
func (s *HTTPServer) SetTunnelIP(tunnelIP string) {
|
||||||
|
s.statusMu.Lock()
|
||||||
|
defer s.statusMu.Unlock()
|
||||||
|
s.tunnelIP = tunnelIP
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetVersion sets the olm version
|
||||||
|
func (s *HTTPServer) SetVersion(version string) {
|
||||||
|
s.statusMu.Lock()
|
||||||
|
defer s.statusMu.Unlock()
|
||||||
|
s.version = version
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePeerRelayStatus updates only the relay status of a peer
|
||||||
|
func (s *HTTPServer) UpdatePeerRelayStatus(siteID int, endpoint string, isRelay bool) {
|
||||||
|
s.statusMu.Lock()
|
||||||
|
defer s.statusMu.Unlock()
|
||||||
|
|
||||||
|
status, exists := s.peerStatuses[siteID]
|
||||||
|
if !exists {
|
||||||
|
status = &PeerStatus{
|
||||||
|
SiteID: siteID,
|
||||||
|
}
|
||||||
|
s.peerStatuses[siteID] = status
|
||||||
|
}
|
||||||
|
|
||||||
|
status.Endpoint = endpoint
|
||||||
|
status.IsRelay = isRelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleConnect handles the /connect endpoint
|
||||||
|
func (s *HTTPServer) handleConnect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req ConnectionRequest
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
if err := decoder.Decode(&req); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if req.ID == "" || req.Secret == "" || req.Endpoint == "" {
|
||||||
|
http.Error(w, "Missing required fields: id, secret, and endpoint must be provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the request to the main goroutine
|
||||||
|
s.connectionChan <- req
|
||||||
|
|
||||||
|
// Return a success response
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "connection request accepted",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleStatus handles the /status endpoint
|
||||||
|
func (s *HTTPServer) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.statusMu.RLock()
|
||||||
|
defer s.statusMu.RUnlock()
|
||||||
|
|
||||||
|
resp := StatusResponse{
|
||||||
|
Connected: s.isConnected,
|
||||||
|
TunnelIP: s.tunnelIP,
|
||||||
|
Version: s.version,
|
||||||
|
PeerStatuses: s.peerStatuses,
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.isConnected {
|
||||||
|
resp.Status = "connected"
|
||||||
|
} else {
|
||||||
|
resp.Status = "disconnected"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
type LogLevel int
|
|
||||||
|
|
||||||
const (
|
|
||||||
DEBUG LogLevel = iota
|
|
||||||
INFO
|
|
||||||
WARN
|
|
||||||
ERROR
|
|
||||||
FATAL
|
|
||||||
)
|
|
||||||
|
|
||||||
var levelStrings = map[LogLevel]string{
|
|
||||||
DEBUG: "DEBUG",
|
|
||||||
INFO: "INFO",
|
|
||||||
WARN: "WARN",
|
|
||||||
ERROR: "ERROR",
|
|
||||||
FATAL: "FATAL",
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string representation of the log level
|
|
||||||
func (l LogLevel) String() string {
|
|
||||||
if s, ok := levelStrings[l]; ok {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return "UNKNOWN"
|
|
||||||
}
|
|
||||||
106
logger/logger.go
106
logger/logger.go
@@ -1,106 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Logger struct holds the logger instance
|
|
||||||
type Logger struct {
|
|
||||||
logger *log.Logger
|
|
||||||
level LogLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultLogger *Logger
|
|
||||||
once sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewLogger creates a new logger instance
|
|
||||||
func NewLogger() *Logger {
|
|
||||||
return &Logger{
|
|
||||||
logger: log.New(os.Stdout, "", 0),
|
|
||||||
level: DEBUG,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes the default logger
|
|
||||||
func Init() *Logger {
|
|
||||||
once.Do(func() {
|
|
||||||
defaultLogger = NewLogger()
|
|
||||||
})
|
|
||||||
return defaultLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLogger returns the default logger instance
|
|
||||||
func GetLogger() *Logger {
|
|
||||||
if defaultLogger == nil {
|
|
||||||
Init()
|
|
||||||
}
|
|
||||||
return defaultLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLevel sets the minimum logging level
|
|
||||||
func (l *Logger) SetLevel(level LogLevel) {
|
|
||||||
l.level = level
|
|
||||||
}
|
|
||||||
|
|
||||||
// log handles the actual logging
|
|
||||||
func (l *Logger) log(level LogLevel, format string, args ...interface{}) {
|
|
||||||
if level < l.level {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
|
||||||
message := fmt.Sprintf(format, args...)
|
|
||||||
l.logger.Printf("%s: %s %s", level.String(), timestamp, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug logs debug level messages
|
|
||||||
func (l *Logger) Debug(format string, args ...interface{}) {
|
|
||||||
l.log(DEBUG, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info logs info level messages
|
|
||||||
func (l *Logger) Info(format string, args ...interface{}) {
|
|
||||||
l.log(INFO, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn logs warning level messages
|
|
||||||
func (l *Logger) Warn(format string, args ...interface{}) {
|
|
||||||
l.log(WARN, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error logs error level messages
|
|
||||||
func (l *Logger) Error(format string, args ...interface{}) {
|
|
||||||
l.log(ERROR, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatal logs fatal level messages and exits
|
|
||||||
func (l *Logger) Fatal(format string, args ...interface{}) {
|
|
||||||
l.log(FATAL, format, args...)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global helper functions
|
|
||||||
func Debug(format string, args ...interface{}) {
|
|
||||||
GetLogger().Debug(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Info(format string, args ...interface{}) {
|
|
||||||
GetLogger().Info(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Warn(format string, args ...interface{}) {
|
|
||||||
GetLogger().Warn(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Error(format string, args ...interface{}) {
|
|
||||||
GetLogger().Error(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Fatal(format string, args ...interface{}) {
|
|
||||||
GetLogger().Fatal(format, args...)
|
|
||||||
}
|
|
||||||
331
peermonitor/peermonitor.go
Normal file
331
peermonitor/peermonitor.go
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
package peermonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fosrl/newt/logger"
|
||||||
|
"github.com/fosrl/newt/websocket"
|
||||||
|
"github.com/fosrl/olm/wgtester"
|
||||||
|
"golang.zx2c4.com/wireguard/device"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PeerMonitorCallback is the function type for connection status change callbacks
|
||||||
|
type PeerMonitorCallback func(siteID int, connected bool, rtt time.Duration)
|
||||||
|
|
||||||
|
// WireGuardConfig holds the WireGuard configuration for a peer
|
||||||
|
type WireGuardConfig struct {
|
||||||
|
SiteID int
|
||||||
|
PublicKey string
|
||||||
|
ServerIP string
|
||||||
|
Endpoint string
|
||||||
|
PrimaryRelay string // The primary relay endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerMonitor handles monitoring the connection status to multiple WireGuard peers
|
||||||
|
type PeerMonitor struct {
|
||||||
|
monitors map[int]*wgtester.Client
|
||||||
|
configs map[int]*WireGuardConfig
|
||||||
|
callback PeerMonitorCallback
|
||||||
|
mutex sync.Mutex
|
||||||
|
running bool
|
||||||
|
interval time.Duration
|
||||||
|
timeout time.Duration
|
||||||
|
maxAttempts int
|
||||||
|
privateKey string
|
||||||
|
wsClient *websocket.Client
|
||||||
|
device *device.Device
|
||||||
|
handleRelaySwitch bool // Whether to handle relay switching
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPeerMonitor creates a new peer monitor with the given callback
|
||||||
|
func NewPeerMonitor(callback PeerMonitorCallback, privateKey string, wsClient *websocket.Client, device *device.Device, handleRelaySwitch bool) *PeerMonitor {
|
||||||
|
return &PeerMonitor{
|
||||||
|
monitors: make(map[int]*wgtester.Client),
|
||||||
|
configs: make(map[int]*WireGuardConfig),
|
||||||
|
callback: callback,
|
||||||
|
interval: 1 * time.Second, // Default check interval
|
||||||
|
timeout: 2500 * time.Millisecond,
|
||||||
|
maxAttempts: 8,
|
||||||
|
privateKey: privateKey,
|
||||||
|
wsClient: wsClient,
|
||||||
|
device: device,
|
||||||
|
handleRelaySwitch: handleRelaySwitch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetInterval changes how frequently peers are checked
|
||||||
|
func (pm *PeerMonitor) SetInterval(interval time.Duration) {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
|
pm.interval = interval
|
||||||
|
|
||||||
|
// Update interval for all existing monitors
|
||||||
|
for _, client := range pm.monitors {
|
||||||
|
client.SetPacketInterval(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTimeout changes the timeout for waiting for responses
|
||||||
|
func (pm *PeerMonitor) SetTimeout(timeout time.Duration) {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
|
pm.timeout = timeout
|
||||||
|
|
||||||
|
// Update timeout for all existing monitors
|
||||||
|
for _, client := range pm.monitors {
|
||||||
|
client.SetTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxAttempts changes the maximum number of attempts for TestConnection
|
||||||
|
func (pm *PeerMonitor) SetMaxAttempts(attempts int) {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
|
pm.maxAttempts = attempts
|
||||||
|
|
||||||
|
// Update max attempts for all existing monitors
|
||||||
|
for _, client := range pm.monitors {
|
||||||
|
client.SetMaxAttempts(attempts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPeer adds a new peer to monitor
|
||||||
|
func (pm *PeerMonitor) AddPeer(siteID int, endpoint string, wgConfig *WireGuardConfig) error {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
|
// Check if we're already monitoring this peer
|
||||||
|
if _, exists := pm.monitors[siteID]; exists {
|
||||||
|
// Update the endpoint instead of creating a new monitor
|
||||||
|
pm.removePeerUnlocked(siteID)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := wgtester.NewClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the client with our settings
|
||||||
|
client.SetPacketInterval(pm.interval)
|
||||||
|
client.SetTimeout(pm.timeout)
|
||||||
|
client.SetMaxAttempts(pm.maxAttempts)
|
||||||
|
|
||||||
|
// Store the client and config
|
||||||
|
pm.monitors[siteID] = client
|
||||||
|
pm.configs[siteID] = wgConfig
|
||||||
|
|
||||||
|
// If monitor is already running, start monitoring this peer
|
||||||
|
if pm.running {
|
||||||
|
siteIDCopy := siteID // Create a copy for the closure
|
||||||
|
err = client.StartMonitor(func(status wgtester.ConnectionStatus) {
|
||||||
|
pm.handleConnectionStatusChange(siteIDCopy, status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// removePeerUnlocked stops monitoring a peer and removes it from the monitor
|
||||||
|
// This function assumes the mutex is already held by the caller
|
||||||
|
func (pm *PeerMonitor) removePeerUnlocked(siteID int) {
|
||||||
|
client, exists := pm.monitors[siteID]
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client.StopMonitor()
|
||||||
|
client.Close()
|
||||||
|
delete(pm.monitors, siteID)
|
||||||
|
delete(pm.configs, siteID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePeer stops monitoring a peer and removes it from the monitor
|
||||||
|
func (pm *PeerMonitor) RemovePeer(siteID int) {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
|
pm.removePeerUnlocked(siteID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins monitoring all peers
|
||||||
|
func (pm *PeerMonitor) Start() {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
|
if pm.running {
|
||||||
|
return // Already running
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.running = true
|
||||||
|
|
||||||
|
// Start monitoring all peers
|
||||||
|
for siteID, client := range pm.monitors {
|
||||||
|
siteIDCopy := siteID // Create a copy for the closure
|
||||||
|
err := client.StartMonitor(func(status wgtester.ConnectionStatus) {
|
||||||
|
pm.handleConnectionStatusChange(siteIDCopy, status)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to start monitoring peer %d: %v\n", siteID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Info("Started monitoring peer %d\n", siteID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleConnectionStatusChange is called when a peer's connection status changes
|
||||||
|
func (pm *PeerMonitor) handleConnectionStatusChange(siteID int, status wgtester.ConnectionStatus) {
|
||||||
|
// Call the user-provided callback first
|
||||||
|
if pm.callback != nil {
|
||||||
|
pm.callback(siteID, status.Connected, status.RTT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If disconnected, handle failover
|
||||||
|
if !status.Connected {
|
||||||
|
// Send relay message to the server
|
||||||
|
if pm.wsClient != nil {
|
||||||
|
pm.sendRelay(siteID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleFailover handles failover to the relay server when a peer is disconnected
|
||||||
|
func (pm *PeerMonitor) HandleFailover(siteID int, relayEndpoint string) {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
config, exists := pm.configs[siteID]
|
||||||
|
pm.mutex.Unlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for IPv6 and format the endpoint correctly
|
||||||
|
formattedEndpoint := relayEndpoint
|
||||||
|
if strings.Contains(relayEndpoint, ":") {
|
||||||
|
formattedEndpoint = fmt.Sprintf("[%s]", relayEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure WireGuard to use the relay
|
||||||
|
wgConfig := fmt.Sprintf(`private_key=%s
|
||||||
|
public_key=%s
|
||||||
|
allowed_ip=%s/32
|
||||||
|
endpoint=%s:21820
|
||||||
|
persistent_keepalive_interval=1`, pm.privateKey, config.PublicKey, config.ServerIP, formattedEndpoint)
|
||||||
|
|
||||||
|
err := pm.device.IpcSet(wgConfig)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to configure WireGuard device: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Adjusted peer %d to point to relay!\n", siteID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendRelay sends a relay message to the server
|
||||||
|
func (pm *PeerMonitor) sendRelay(siteID int) error {
|
||||||
|
if !pm.handleRelaySwitch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if pm.wsClient == nil {
|
||||||
|
return fmt.Errorf("websocket client is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pm.wsClient.SendMessage("olm/wg/relay", map[string]interface{}{
|
||||||
|
"siteId": siteID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to send registration message: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("Sent relay message")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops monitoring all peers
|
||||||
|
func (pm *PeerMonitor) Stop() {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
|
if !pm.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.running = false
|
||||||
|
|
||||||
|
// Stop all monitors
|
||||||
|
for _, client := range pm.monitors {
|
||||||
|
client.StopMonitor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops monitoring and cleans up resources
|
||||||
|
func (pm *PeerMonitor) Close() {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
defer pm.mutex.Unlock()
|
||||||
|
|
||||||
|
// Stop and close all clients
|
||||||
|
for siteID, client := range pm.monitors {
|
||||||
|
client.StopMonitor()
|
||||||
|
client.Close()
|
||||||
|
delete(pm.monitors, siteID)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.running = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPeer tests connectivity to a specific peer
|
||||||
|
func (pm *PeerMonitor) TestPeer(siteID int) (bool, time.Duration, error) {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
client, exists := pm.monitors[siteID]
|
||||||
|
pm.mutex.Unlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return false, 0, fmt.Errorf("peer with siteID %d not found", siteID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), pm.timeout*time.Duration(pm.maxAttempts))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
connected, rtt := client.TestConnection(ctx)
|
||||||
|
return connected, rtt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAllPeers tests connectivity to all peers
|
||||||
|
func (pm *PeerMonitor) TestAllPeers() map[int]struct {
|
||||||
|
Connected bool
|
||||||
|
RTT time.Duration
|
||||||
|
} {
|
||||||
|
pm.mutex.Lock()
|
||||||
|
peers := make(map[int]*wgtester.Client, len(pm.monitors))
|
||||||
|
for siteID, client := range pm.monitors {
|
||||||
|
peers[siteID] = client
|
||||||
|
}
|
||||||
|
pm.mutex.Unlock()
|
||||||
|
|
||||||
|
results := make(map[int]struct {
|
||||||
|
Connected bool
|
||||||
|
RTT time.Duration
|
||||||
|
})
|
||||||
|
for siteID, client := range peers {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), pm.timeout*time.Duration(pm.maxAttempts))
|
||||||
|
connected, rtt := client.TestConnection(ctx)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
results[siteID] = struct {
|
||||||
|
Connected bool
|
||||||
|
RTT time.Duration
|
||||||
|
}{
|
||||||
|
Connected: connected,
|
||||||
|
RTT: rtt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
334
proxy/manager.go
334
proxy/manager.go
@@ -1,334 +0,0 @@
|
|||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fosrl/newt/logger"
|
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewProxyManager(tnet *netstack.Net) *ProxyManager {
|
|
||||||
return &ProxyManager{
|
|
||||||
tnet: tnet,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProxyManager) AddTarget(protocol, listen string, port int, target string) {
|
|
||||||
pm.Lock()
|
|
||||||
defer pm.Unlock()
|
|
||||||
|
|
||||||
logger.Info("Adding target: %s://%s:%d -> %s", protocol, listen, port, target)
|
|
||||||
|
|
||||||
newTarget := ProxyTarget{
|
|
||||||
Protocol: protocol,
|
|
||||||
Listen: listen,
|
|
||||||
Port: port,
|
|
||||||
Target: target,
|
|
||||||
cancel: make(chan struct{}),
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
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":
|
|
||||||
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 {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProxyManager) Stop() error {
|
|
||||||
pm.Lock()
|
|
||||||
defer pm.Unlock()
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for i := range pm.targets {
|
|
||||||
target := &pm.targets[i]
|
|
||||||
wg.Add(1)
|
|
||||||
go func(t *ProxyTarget) {
|
|
||||||
defer wg.Done()
|
|
||||||
close(t.cancel)
|
|
||||||
t.Lock()
|
|
||||||
if t.listener != nil {
|
|
||||||
t.listener.Close()
|
|
||||||
}
|
|
||||||
if t.udpConn != nil {
|
|
||||||
t.udpConn.Close()
|
|
||||||
}
|
|
||||||
t.Unlock()
|
|
||||||
// Wait for the target to fully stop
|
|
||||||
<-t.done
|
|
||||||
}(target)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProxyManager) serveTCP(target *ProxyTarget) {
|
|
||||||
defer close(target.done) // Signal that this target is fully stopped
|
|
||||||
|
|
||||||
listener, err := pm.tnet.ListenTCP(&net.TCPAddr{
|
|
||||||
IP: net.ParseIP(target.Listen),
|
|
||||||
Port: target.Port,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to start TCP listener for %s:%d: %v", target.Listen, target.Port, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target.Lock()
|
|
||||||
target.listener = listener
|
|
||||||
target.Unlock()
|
|
||||||
|
|
||||||
defer listener.Close()
|
|
||||||
logger.Info("TCP proxy listening on %s", listener.Addr())
|
|
||||||
|
|
||||||
var activeConns sync.WaitGroup
|
|
||||||
acceptDone := make(chan struct{})
|
|
||||||
|
|
||||||
// Goroutine to handle shutdown signal
|
|
||||||
go func() {
|
|
||||||
<-target.cancel
|
|
||||||
close(acceptDone)
|
|
||||||
listener.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
conn, err := listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-target.cancel:
|
|
||||||
// Wait for active connections to finish
|
|
||||||
activeConns.Wait()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activeConns.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer activeConns.Done()
|
|
||||||
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 {
|
|
||||||
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 {
|
|
||||||
select {
|
|
||||||
case <-target.cancel:
|
|
||||||
activeConns.Wait() // Wait for all active UDP handlers to complete
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
n, remoteAddr, err := conn.ReadFrom(buffer)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-target.cancel:
|
|
||||||
activeConns.Wait()
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
logger.Info("Failed to read UDP packet: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
targetAddr, err := net.ResolveUDPAddr("udp", target.Target)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to resolve target address %s: %v", target.Target, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
activeConns.Add(1)
|
|
||||||
go func(data []byte, remote net.Addr) {
|
|
||||||
defer activeConns.Done()
|
|
||||||
targetConn, err := net.DialUDP("udp", nil, targetAddr)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to connect to target %s: %v", target.Target, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer targetConn.Close()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-target.cancel:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
_, err = targetConn.Write(data)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to write to target: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response := make([]byte, 65535)
|
|
||||||
n, err := targetConn.Read(response)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to read response from target: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = conn.WriteTo(response[:n], remote)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Failed to write response to client: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(buffer[:n], remoteAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 774 KiB |
50
service_unix.go
Normal file
50
service_unix.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service management functions are not available on non-Windows platforms
|
||||||
|
func installService() error {
|
||||||
|
return fmt.Errorf("service management is only available on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeService() error {
|
||||||
|
return fmt.Errorf("service management is only available on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
func startService(args []string) error {
|
||||||
|
_ = args // unused on Unix platforms
|
||||||
|
return fmt.Errorf("service management is only available on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopService() error {
|
||||||
|
return fmt.Errorf("service management is only available on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServiceStatus() (string, error) {
|
||||||
|
return "", fmt.Errorf("service management is only available on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugService(args []string) error {
|
||||||
|
_ = args // unused on Unix platforms
|
||||||
|
return fmt.Errorf("debug service is only available on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWindowsService() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func runService(name string, isDebug bool, args []string) {
|
||||||
|
// No-op on non-Windows platforms
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupWindowsEventLog() {
|
||||||
|
// No-op on non-Windows platforms
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchLogFile(end bool) error {
|
||||||
|
return fmt.Errorf("watching log file is only available on Windows")
|
||||||
|
}
|
||||||
537
service_windows.go
Normal file
537
service_windows.go
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fosrl/newt/logger"
|
||||||
|
"golang.org/x/sys/windows/svc"
|
||||||
|
"golang.org/x/sys/windows/svc/debug"
|
||||||
|
"golang.org/x/sys/windows/svc/eventlog"
|
||||||
|
"golang.org/x/sys/windows/svc/mgr"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
serviceName = "OlmWireguardService"
|
||||||
|
serviceDisplayName = "Olm WireGuard VPN Service"
|
||||||
|
serviceDescription = "Olm WireGuard VPN client service for secure network connectivity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global variable to store service arguments
|
||||||
|
var serviceArgs []string
|
||||||
|
|
||||||
|
// getServiceArgsPath returns the path where service arguments are stored
|
||||||
|
func getServiceArgsPath() string {
|
||||||
|
logDir := filepath.Join(os.Getenv("PROGRAMDATA"), "olm")
|
||||||
|
return filepath.Join(logDir, "service_args.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveServiceArgs saves the service arguments to a file
|
||||||
|
func saveServiceArgs(args []string) error {
|
||||||
|
logDir := filepath.Join(os.Getenv("PROGRAMDATA"), "olm")
|
||||||
|
err := os.MkdirAll(logDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
argsPath := getServiceArgsPath()
|
||||||
|
data, err := json.Marshal(args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal service args: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(argsPath, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write service args: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadServiceArgs loads the service arguments from a file
|
||||||
|
func loadServiceArgs() ([]string, error) {
|
||||||
|
argsPath := getServiceArgsPath()
|
||||||
|
data, err := os.ReadFile(argsPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return []string{}, nil // Return empty args if file doesn't exist
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read service args: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the file after reading
|
||||||
|
err = os.Remove(argsPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to delete service args file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
err = json.Unmarshal(data, &args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal service args: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type olmService struct {
|
||||||
|
elog debug.Log
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *olmService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
||||||
|
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||||
|
changes <- svc.Status{State: svc.StartPending}
|
||||||
|
|
||||||
|
s.elog.Info(1, "Service Execute called, starting main logic")
|
||||||
|
|
||||||
|
// Load saved service arguments
|
||||||
|
savedArgs, err := loadServiceArgs()
|
||||||
|
if err != nil {
|
||||||
|
s.elog.Error(1, fmt.Sprintf("Failed to load service args: %v", err))
|
||||||
|
// Continue with empty args if loading fails
|
||||||
|
savedArgs = []string{}
|
||||||
|
}
|
||||||
|
s.args = savedArgs
|
||||||
|
|
||||||
|
// Start the main olm functionality
|
||||||
|
olmDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
s.runOlm()
|
||||||
|
close(olmDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||||
|
s.elog.Info(1, "Service status set to Running")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case c := <-r:
|
||||||
|
switch c.Cmd {
|
||||||
|
case svc.Interrogate:
|
||||||
|
changes <- c.CurrentStatus
|
||||||
|
case svc.Stop, svc.Shutdown:
|
||||||
|
s.elog.Info(1, "Service stopping")
|
||||||
|
changes <- svc.Status{State: svc.StopPending}
|
||||||
|
if s.stop != nil {
|
||||||
|
s.stop()
|
||||||
|
}
|
||||||
|
// Wait for main logic to finish or timeout
|
||||||
|
select {
|
||||||
|
case <-olmDone:
|
||||||
|
s.elog.Info(1, "Main logic finished gracefully")
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
s.elog.Info(1, "Timeout waiting for main logic to finish")
|
||||||
|
}
|
||||||
|
return false, 0
|
||||||
|
default:
|
||||||
|
s.elog.Error(1, fmt.Sprintf("Unexpected control request #%d", c))
|
||||||
|
}
|
||||||
|
case <-olmDone:
|
||||||
|
s.elog.Info(1, "Main olm logic completed, stopping service")
|
||||||
|
changes <- svc.Status{State: svc.StopPending}
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *olmService) runOlm() {
|
||||||
|
// Create a context that can be cancelled when the service stops
|
||||||
|
s.ctx, s.stop = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// Setup logging for service mode
|
||||||
|
s.elog.Info(1, "Starting Olm main logic")
|
||||||
|
|
||||||
|
// Run the main olm logic and wait for it to complete
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
s.elog.Error(1, fmt.Sprintf("Olm panic: %v", r))
|
||||||
|
}
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Call the main olm function with stored arguments
|
||||||
|
runOlmMainWithArgs(s.ctx, s.args)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for either context cancellation or main logic completion
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
s.elog.Info(1, "Olm service context cancelled")
|
||||||
|
case <-done:
|
||||||
|
s.elog.Info(1, "Olm main logic completed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runService(name string, isDebug bool, args []string) {
|
||||||
|
var err error
|
||||||
|
var elog debug.Log
|
||||||
|
|
||||||
|
if isDebug {
|
||||||
|
elog = debug.New(name)
|
||||||
|
fmt.Printf("Starting %s service in debug mode\n", name)
|
||||||
|
} else {
|
||||||
|
elog, err = eventlog.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to open event log: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer elog.Close()
|
||||||
|
|
||||||
|
elog.Info(1, fmt.Sprintf("Starting %s service", name))
|
||||||
|
run := svc.Run
|
||||||
|
if isDebug {
|
||||||
|
run = debug.Run
|
||||||
|
}
|
||||||
|
|
||||||
|
service := &olmService{elog: elog, args: args}
|
||||||
|
err = run(name, service)
|
||||||
|
if err != nil {
|
||||||
|
elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err))
|
||||||
|
if isDebug {
|
||||||
|
fmt.Printf("Service failed: %v\n", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elog.Info(1, fmt.Sprintf("%s service stopped", name))
|
||||||
|
if isDebug {
|
||||||
|
fmt.Printf("%s service stopped\n", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func installService() error {
|
||||||
|
exepath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get executable path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to service manager: %v", err)
|
||||||
|
}
|
||||||
|
defer m.Disconnect()
|
||||||
|
|
||||||
|
s, err := m.OpenService(serviceName)
|
||||||
|
if err == nil {
|
||||||
|
s.Close()
|
||||||
|
return fmt.Errorf("service %s already exists", serviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := mgr.Config{
|
||||||
|
ServiceType: 0x10, // SERVICE_WIN32_OWN_PROCESS
|
||||||
|
StartType: mgr.StartManual,
|
||||||
|
ErrorControl: mgr.ErrorNormal,
|
||||||
|
DisplayName: serviceDisplayName,
|
||||||
|
Description: serviceDescription,
|
||||||
|
BinaryPathName: exepath,
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err = m.CreateService(serviceName, exepath, config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create service: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
err = eventlog.InstallAsEventCreate(serviceName, eventlog.Error|eventlog.Warning|eventlog.Info)
|
||||||
|
if err != nil {
|
||||||
|
s.Delete()
|
||||||
|
return fmt.Errorf("failed to install event log: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeService() error {
|
||||||
|
m, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to service manager: %v", err)
|
||||||
|
}
|
||||||
|
defer m.Disconnect()
|
||||||
|
|
||||||
|
s, err := m.OpenService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("service %s is not installed", serviceName)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Stop the service if it's running
|
||||||
|
status, err := s.Query()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query service status: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.State != svc.Stopped {
|
||||||
|
_, err = s.Control(svc.Stop)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stop service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for service to stop
|
||||||
|
timeout := time.Now().Add(30 * time.Second)
|
||||||
|
for status.State != svc.Stopped {
|
||||||
|
if timeout.Before(time.Now()) {
|
||||||
|
return fmt.Errorf("timeout waiting for service to stop")
|
||||||
|
}
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
status, err = s.Query()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query service status: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Delete()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = eventlog.Remove(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove event log: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startService(args []string) error {
|
||||||
|
// Save the service arguments before starting
|
||||||
|
if len(args) > 0 {
|
||||||
|
err := saveServiceArgs(args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save service args: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to service manager: %v", err)
|
||||||
|
}
|
||||||
|
defer m.Disconnect()
|
||||||
|
|
||||||
|
s, err := m.OpenService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("service %s is not installed", serviceName)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
err = s.Start()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopService() error {
|
||||||
|
m, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to service manager: %v", err)
|
||||||
|
}
|
||||||
|
defer m.Disconnect()
|
||||||
|
|
||||||
|
s, err := m.OpenService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("service %s is not installed", serviceName)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
status, err := s.Control(svc.Stop)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stop service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := time.Now().Add(30 * time.Second)
|
||||||
|
for status.State != svc.Stopped {
|
||||||
|
if timeout.Before(time.Now()) {
|
||||||
|
return fmt.Errorf("timeout waiting for service to stop")
|
||||||
|
}
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
status, err = s.Query()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query service status: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugService(args []string) error {
|
||||||
|
// Save the service arguments before starting
|
||||||
|
if len(args) > 0 {
|
||||||
|
err := saveServiceArgs(args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save service args: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("Starting service in debug mode...\n")
|
||||||
|
|
||||||
|
// Start the service
|
||||||
|
err := startService([]string{}) // Pass empty args since we already saved them
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("Service started. Watching logs (Press Ctrl+C to stop watching)...\n")
|
||||||
|
// fmt.Printf("================================================================================\n")
|
||||||
|
|
||||||
|
// Watch the log file
|
||||||
|
return watchLogFile(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchLogFile(end bool) error {
|
||||||
|
logDir := filepath.Join(os.Getenv("PROGRAMDATA"), "olm", "logs")
|
||||||
|
logPath := filepath.Join(logDir, "olm.log")
|
||||||
|
|
||||||
|
// Ensure the log directory exists
|
||||||
|
err := os.MkdirAll(logDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create log directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the log file to be created if it doesn't exist
|
||||||
|
var file *os.File
|
||||||
|
for i := 0; i < 30; i++ { // Wait up to 15 seconds
|
||||||
|
file, err = os.Open(logPath)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
fmt.Printf("Waiting for log file to be created...\n")
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open log file after waiting: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Seek to the end of the file to only show new logs
|
||||||
|
_, err = file.Seek(0, 2)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to seek to end of file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up signal handling for graceful exit
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
// Create a ticker to check for new content
|
||||||
|
ticker := time.NewTicker(500 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
buffer := make([]byte, 4096)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-sigCh:
|
||||||
|
fmt.Printf("\n\nStopping log watch...\n")
|
||||||
|
// stop the service if needed
|
||||||
|
if end {
|
||||||
|
if err := stopService(); err != nil {
|
||||||
|
fmt.Printf("Failed to stop service: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("Log watch stopped.\n")
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
// Read new content
|
||||||
|
n, err := file.Read(buffer)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
// Try to reopen the file in case it was recreated
|
||||||
|
file.Close()
|
||||||
|
file, err = os.Open(logPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reopening log file: %v", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
// Print the new content
|
||||||
|
fmt.Print(string(buffer[:n]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServiceStatus() (string, error) {
|
||||||
|
m, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to connect to service manager: %v", err)
|
||||||
|
}
|
||||||
|
defer m.Disconnect()
|
||||||
|
|
||||||
|
s, err := m.OpenService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return "Not Installed", nil
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
status, err := s.Query()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to query service status: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch status.State {
|
||||||
|
case svc.Stopped:
|
||||||
|
return "Stopped", nil
|
||||||
|
case svc.StartPending:
|
||||||
|
return "Starting", nil
|
||||||
|
case svc.StopPending:
|
||||||
|
return "Stopping", nil
|
||||||
|
case svc.Running:
|
||||||
|
return "Running", nil
|
||||||
|
case svc.ContinuePending:
|
||||||
|
return "Continue Pending", nil
|
||||||
|
case svc.PausePending:
|
||||||
|
return "Pause Pending", nil
|
||||||
|
case svc.Paused:
|
||||||
|
return "Paused", nil
|
||||||
|
default:
|
||||||
|
return "Unknown", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWindowsService() bool {
|
||||||
|
isWindowsService, err := svc.IsWindowsService()
|
||||||
|
return err == nil && isWindowsService
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupWindowsEventLog() {
|
||||||
|
// Create log directory if it doesn't exist
|
||||||
|
logDir := filepath.Join(os.Getenv("PROGRAMDATA"), "olm", "logs")
|
||||||
|
err := os.MkdirAll(logDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to create log directory: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logFile := filepath.Join(logDir, "olm.log")
|
||||||
|
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to open log file: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the custom logger output
|
||||||
|
logger.GetLogger().SetOutput(file)
|
||||||
|
|
||||||
|
log.Printf("Olm service logging initialized - log file: %s", logFile)
|
||||||
|
}
|
||||||
35
unix.go
Normal file
35
unix.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"golang.zx2c4.com/wireguard/ipc"
|
||||||
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTUNFromFD(tunFdStr string, mtuInt int) (tun.Device, error) {
|
||||||
|
fd, err := strconv.ParseUint(tunFdStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = unix.SetNonblock(int(fd), true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := os.NewFile(uintptr(fd), "")
|
||||||
|
return tun.CreateTUNFromFile(file, mtuInt)
|
||||||
|
}
|
||||||
|
func uapiOpen(interfaceName string) (*os.File, error) {
|
||||||
|
return ipc.UAPIOpen(interfaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func uapiListen(interfaceName string, fileUAPI *os.File) (net.Listener, error) {
|
||||||
|
return ipc.UAPIListen(interfaceName, fileUAPI)
|
||||||
|
}
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fosrl/newt/logger"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
conn *websocket.Conn
|
|
||||||
config *Config
|
|
||||||
baseURL string
|
|
||||||
handlers map[string]MessageHandler
|
|
||||||
done chan struct{}
|
|
||||||
handlersMux sync.RWMutex
|
|
||||||
|
|
||||||
reconnectInterval time.Duration
|
|
||||||
isConnected bool
|
|
||||||
reconnectMux sync.RWMutex
|
|
||||||
|
|
||||||
onConnect func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientOption func(*Client)
|
|
||||||
|
|
||||||
type MessageHandler func(message WSMessage)
|
|
||||||
|
|
||||||
// WithBaseURL sets the base URL for the client
|
|
||||||
func WithBaseURL(url string) ClientOption {
|
|
||||||
return func(c *Client) {
|
|
||||||
c.baseURL = url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) OnConnect(callback func() error) {
|
|
||||||
c.onConnect = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient creates a new Newt client
|
|
||||||
func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*Client, error) {
|
|
||||||
config := &Config{
|
|
||||||
NewtID: newtID,
|
|
||||||
Secret: secret,
|
|
||||||
Endpoint: endpoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &Client{
|
|
||||||
config: config,
|
|
||||||
baseURL: endpoint, // default value
|
|
||||||
handlers: make(map[string]MessageHandler),
|
|
||||||
done: make(chan struct{}),
|
|
||||||
reconnectInterval: 10 * time.Second,
|
|
||||||
isConnected: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply options before loading config
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load existing config if available
|
|
||||||
if err := client.loadConfig(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect establishes the WebSocket connection
|
|
||||||
func (c *Client) Connect() error {
|
|
||||||
go c.connectWithRetry()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the WebSocket connection
|
|
||||||
func (c *Client) Close() error {
|
|
||||||
close(c.done)
|
|
||||||
if c.conn != nil {
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop the ping monitor
|
|
||||||
c.setConnected(false)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessage sends a message through the WebSocket connection
|
|
||||||
func (c *Client) SendMessage(messageType string, data interface{}) error {
|
|
||||||
if c.conn == nil {
|
|
||||||
return fmt.Errorf("not connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := WSMessage{
|
|
||||||
Type: messageType,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.conn.WriteJSON(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterHandler registers a handler for a specific message type
|
|
||||||
func (c *Client) RegisterHandler(messageType string, handler MessageHandler) {
|
|
||||||
c.handlersMux.Lock()
|
|
||||||
defer c.handlersMux.Unlock()
|
|
||||||
c.handlers[messageType] = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// readPump pumps messages from the WebSocket connection
|
|
||||||
func (c *Client) readPump() {
|
|
||||||
defer c.conn.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
var msg WSMessage
|
|
||||||
err := c.conn.ReadJSON(&msg)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.handlersMux.RLock()
|
|
||||||
if handler, ok := c.handlers[msg.Type]; ok {
|
|
||||||
handler(msg)
|
|
||||||
}
|
|
||||||
c.handlersMux.RUnlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) getToken() (string, error) {
|
|
||||||
// Parse the base URL to ensure we have the correct hostname
|
|
||||||
baseURL, err := url.Parse(c.baseURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to parse base URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we have the base URL without trailing slashes
|
|
||||||
baseEndpoint := strings.TrimRight(baseURL.String(), "/")
|
|
||||||
|
|
||||||
// If we already have a token, try to use it
|
|
||||||
if c.config.Token != "" {
|
|
||||||
tokenCheckData := map[string]interface{}{
|
|
||||||
"newtId": c.config.NewtID,
|
|
||||||
"secret": c.config.Secret,
|
|
||||||
"token": c.config.Token,
|
|
||||||
}
|
|
||||||
jsonData, err := json.Marshal(tokenCheckData)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to marshal token check data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new request
|
|
||||||
req, err := http.NewRequest(
|
|
||||||
"POST",
|
|
||||||
baseEndpoint+"/api/v1/auth/newt/get-token",
|
|
||||||
bytes.NewBuffer(jsonData),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to create request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set headers
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
|
|
||||||
|
|
||||||
// Make the request
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to check token validity: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var tokenResp TokenResponse
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to decode token check response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If token is still valid, return it
|
|
||||||
if tokenResp.Success && tokenResp.Message == "Token session already valid" {
|
|
||||||
return c.config.Token, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a new token
|
|
||||||
tokenData := map[string]interface{}{
|
|
||||||
"newtId": c.config.NewtID,
|
|
||||||
"secret": c.config.Secret,
|
|
||||||
}
|
|
||||||
jsonData, err := json.Marshal(tokenData)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to marshal token request data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new request
|
|
||||||
req, err := http.NewRequest(
|
|
||||||
"POST",
|
|
||||||
baseEndpoint+"/api/v1/auth/newt/get-token",
|
|
||||||
bytes.NewBuffer(jsonData),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to create request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set headers
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
|
|
||||||
|
|
||||||
// Make the request
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to request new token: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var tokenResp TokenResponse
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to decode token response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tokenResp.Success {
|
|
||||||
return "", fmt.Errorf("failed to get token: %s", tokenResp.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokenResp.Data.Token == "" {
|
|
||||||
return "", fmt.Errorf("received empty token from server")
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenResp.Data.Token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) connectWithRetry() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
err := c.establishConnection()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Failed to connect: %v. Retrying in %v...", err, c.reconnectInterval)
|
|
||||||
time.Sleep(c.reconnectInterval)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) establishConnection() error {
|
|
||||||
// Get token for authentication
|
|
||||||
token, err := c.getToken()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the base URL to determine protocol and hostname
|
|
||||||
baseURL, err := url.Parse(c.baseURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse base URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine WebSocket protocol based on HTTP protocol
|
|
||||||
wsProtocol := "wss"
|
|
||||||
if baseURL.Scheme == "http" {
|
|
||||||
wsProtocol = "ws"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create WebSocket URL
|
|
||||||
wsURL := fmt.Sprintf("%s://%s/api/v1/ws", wsProtocol, baseURL.Host)
|
|
||||||
u, err := url.Parse(wsURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse WebSocket URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add token to query parameters
|
|
||||||
q := u.Query()
|
|
||||||
q.Set("token", token)
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
// Connect to WebSocket
|
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to connect to WebSocket: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.conn = conn
|
|
||||||
c.setConnected(true)
|
|
||||||
|
|
||||||
// Start the ping monitor
|
|
||||||
go c.pingMonitor()
|
|
||||||
// Start the read pump
|
|
||||||
go c.readPump()
|
|
||||||
|
|
||||||
if c.onConnect != nil {
|
|
||||||
if err := c.onConnect(); err != nil {
|
|
||||||
logger.Error("OnConnect callback failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) pingMonitor() {
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
if err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
|
|
||||||
logger.Error("Ping failed: %v", err)
|
|
||||||
c.reconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) reconnect() {
|
|
||||||
c.setConnected(false)
|
|
||||||
if c.conn != nil {
|
|
||||||
c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
go c.connectWithRetry()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) setConnected(status bool) {
|
|
||||||
c.reconnectMux.Lock()
|
|
||||||
defer c.reconnectMux.Unlock()
|
|
||||||
c.isConnected = status
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getConfigPath() string {
|
|
||||||
var configDir string
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "darwin":
|
|
||||||
configDir = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "newt-client")
|
|
||||||
case "windows":
|
|
||||||
configDir = filepath.Join(os.Getenv("APPDATA"), "newt-client")
|
|
||||||
default: // linux and others
|
|
||||||
configDir = filepath.Join(os.Getenv("HOME"), ".config", "newt-client")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
||||||
log.Printf("Failed to create config directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(configDir, "config.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) loadConfig() error {
|
|
||||||
if c.config.NewtID != "" && c.config.Secret != "" && c.config.Endpoint != "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
configPath := getConfigPath()
|
|
||||||
data, err := os.ReadFile(configPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var config Config
|
|
||||||
if err := json.Unmarshal(data, &config); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.config.NewtID == "" {
|
|
||||||
c.config.NewtID = config.NewtID
|
|
||||||
}
|
|
||||||
if c.config.Token == "" {
|
|
||||||
c.config.Token = config.Token
|
|
||||||
}
|
|
||||||
if c.config.Secret == "" {
|
|
||||||
c.config.Secret = config.Secret
|
|
||||||
}
|
|
||||||
if c.config.Endpoint == "" {
|
|
||||||
c.config.Endpoint = config.Endpoint
|
|
||||||
c.baseURL = config.Endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) saveConfig() error {
|
|
||||||
configPath := getConfigPath()
|
|
||||||
data, err := json.MarshalIndent(c.config, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(configPath, data, 0644)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package websocket
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
NewtID string `json:"newtId"`
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Endpoint string `json:"endpoint"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenResponse struct {
|
|
||||||
Data struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
} `json:"data"`
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WSMessage struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
260
wgtester/wgtester.go
Normal file
260
wgtester/wgtester.go
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
package wgtester
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fosrl/newt/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Magic bytes to identify our packets
|
||||||
|
magicHeader uint32 = 0xDEADBEEF
|
||||||
|
// Request packet type
|
||||||
|
packetTypeRequest uint8 = 1
|
||||||
|
// Response packet type
|
||||||
|
packetTypeResponse uint8 = 2
|
||||||
|
// Packet format:
|
||||||
|
// - 4 bytes: magic header (0xDEADBEEF)
|
||||||
|
// - 1 byte: packet type (1 = request, 2 = response)
|
||||||
|
// - 8 bytes: timestamp (for round-trip timing)
|
||||||
|
packetSize = 13
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client handles checking connectivity to a server
|
||||||
|
type Client struct {
|
||||||
|
conn *net.UDPConn
|
||||||
|
serverAddr string
|
||||||
|
monitorRunning bool
|
||||||
|
monitorLock sync.Mutex
|
||||||
|
connLock sync.Mutex // Protects connection operations
|
||||||
|
shutdownCh chan struct{}
|
||||||
|
packetInterval time.Duration
|
||||||
|
timeout time.Duration
|
||||||
|
maxAttempts int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionStatus represents the current connection state
|
||||||
|
type ConnectionStatus struct {
|
||||||
|
Connected bool
|
||||||
|
RTT time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new connection test client
|
||||||
|
func NewClient(serverAddr string) (*Client, error) {
|
||||||
|
return &Client{
|
||||||
|
serverAddr: serverAddr,
|
||||||
|
shutdownCh: make(chan struct{}),
|
||||||
|
packetInterval: 2 * time.Second,
|
||||||
|
timeout: 500 * time.Millisecond, // Timeout for individual packets
|
||||||
|
maxAttempts: 3, // Default max attempts
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPacketInterval changes how frequently packets are sent in monitor mode
|
||||||
|
func (c *Client) SetPacketInterval(interval time.Duration) {
|
||||||
|
c.packetInterval = interval
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTimeout changes the timeout for waiting for responses
|
||||||
|
func (c *Client) SetTimeout(timeout time.Duration) {
|
||||||
|
c.timeout = timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxAttempts changes the maximum number of attempts for TestConnection
|
||||||
|
func (c *Client) SetMaxAttempts(attempts int) {
|
||||||
|
c.maxAttempts = attempts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close cleans up client resources
|
||||||
|
func (c *Client) Close() {
|
||||||
|
c.StopMonitor()
|
||||||
|
|
||||||
|
c.connLock.Lock()
|
||||||
|
defer c.connLock.Unlock()
|
||||||
|
|
||||||
|
if c.conn != nil {
|
||||||
|
c.conn.Close()
|
||||||
|
c.conn = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureConnection makes sure we have an active UDP connection
|
||||||
|
func (c *Client) ensureConnection() error {
|
||||||
|
c.connLock.Lock()
|
||||||
|
defer c.connLock.Unlock()
|
||||||
|
|
||||||
|
if c.conn != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
serverAddr, err := net.ResolveUDPAddr("udp", c.serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn, err = net.DialUDP("udp", nil, serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConnection checks if the connection to the server is working
|
||||||
|
// Returns true if connected, false otherwise
|
||||||
|
func (c *Client) TestConnection(ctx context.Context) (bool, time.Duration) {
|
||||||
|
if err := c.ensureConnection(); err != nil {
|
||||||
|
logger.Warn("Failed to ensure connection: %v", err)
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare packet buffer
|
||||||
|
packet := make([]byte, packetSize)
|
||||||
|
binary.BigEndian.PutUint32(packet[0:4], magicHeader)
|
||||||
|
packet[4] = packetTypeRequest
|
||||||
|
|
||||||
|
// Send multiple attempts as specified
|
||||||
|
for attempt := 0; attempt < c.maxAttempts; attempt++ {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false, 0
|
||||||
|
default:
|
||||||
|
// Add current timestamp to packet
|
||||||
|
timestamp := time.Now().UnixNano()
|
||||||
|
binary.BigEndian.PutUint64(packet[5:13], uint64(timestamp))
|
||||||
|
|
||||||
|
// Lock the connection for the entire send/receive operation
|
||||||
|
c.connLock.Lock()
|
||||||
|
|
||||||
|
// Check if connection is still valid after acquiring lock
|
||||||
|
if c.conn == nil {
|
||||||
|
c.connLock.Unlock()
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("Attempting to send monitor packet to %s", c.serverAddr)
|
||||||
|
_, err := c.conn.Write(packet)
|
||||||
|
if err != nil {
|
||||||
|
c.connLock.Unlock()
|
||||||
|
logger.Info("Error sending packet: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Debug("Successfully sent monitor packet")
|
||||||
|
|
||||||
|
// Set read deadline
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(c.timeout))
|
||||||
|
|
||||||
|
// Wait for response
|
||||||
|
responseBuffer := make([]byte, packetSize)
|
||||||
|
n, err := c.conn.Read(responseBuffer)
|
||||||
|
c.connLock.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
|
// Timeout, try next attempt
|
||||||
|
time.Sleep(100 * time.Millisecond) // Brief pause between attempts
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Error("Error reading response: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != packetSize {
|
||||||
|
continue // Malformed packet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify response
|
||||||
|
magic := binary.BigEndian.Uint32(responseBuffer[0:4])
|
||||||
|
packetType := responseBuffer[4]
|
||||||
|
if magic != magicHeader || packetType != packetTypeResponse {
|
||||||
|
continue // Not our response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the original timestamp and calculate RTT
|
||||||
|
sentTimestamp := int64(binary.BigEndian.Uint64(responseBuffer[5:13]))
|
||||||
|
rtt := time.Duration(time.Now().UnixNano() - sentTimestamp)
|
||||||
|
|
||||||
|
return true, rtt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConnectionWithTimeout tries to test connection with a timeout
|
||||||
|
// Returns true if connected, false otherwise
|
||||||
|
func (c *Client) TestConnectionWithTimeout(timeout time.Duration) (bool, time.Duration) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
return c.TestConnection(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonitorCallback is the function type for connection status change callbacks
|
||||||
|
type MonitorCallback func(status ConnectionStatus)
|
||||||
|
|
||||||
|
// StartMonitor begins monitoring the connection and calls the callback
|
||||||
|
// when the connection status changes
|
||||||
|
func (c *Client) StartMonitor(callback MonitorCallback) error {
|
||||||
|
c.monitorLock.Lock()
|
||||||
|
defer c.monitorLock.Unlock()
|
||||||
|
|
||||||
|
if c.monitorRunning {
|
||||||
|
logger.Info("Monitor already running")
|
||||||
|
return nil // Already running
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ensureConnection(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.monitorRunning = true
|
||||||
|
c.shutdownCh = make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var lastConnected bool
|
||||||
|
firstRun := true
|
||||||
|
|
||||||
|
ticker := time.NewTicker(c.packetInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.shutdownCh:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
|
||||||
|
connected, rtt := c.TestConnection(ctx)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Callback if status changed or it's the first check
|
||||||
|
if connected != lastConnected || firstRun {
|
||||||
|
callback(ConnectionStatus{
|
||||||
|
Connected: connected,
|
||||||
|
RTT: rtt,
|
||||||
|
})
|
||||||
|
lastConnected = connected
|
||||||
|
firstRun = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopMonitor stops the connection monitoring
|
||||||
|
func (c *Client) StopMonitor() {
|
||||||
|
c.monitorLock.Lock()
|
||||||
|
defer c.monitorLock.Unlock()
|
||||||
|
|
||||||
|
if !c.monitorRunning {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
close(c.shutdownCh)
|
||||||
|
c.monitorRunning = false
|
||||||
|
}
|
||||||
25
windows.go
Normal file
25
windows.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.zx2c4.com/wireguard/ipc"
|
||||||
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTUNFromFD(tunFdStr string, mtuInt int) (tun.Device, error) {
|
||||||
|
return nil, errors.New("CreateTUNFromFile not supported on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
func uapiOpen(interfaceName string) (*os.File, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uapiListen(interfaceName string, fileUAPI *os.File) (net.Listener, error) {
|
||||||
|
// On Windows, UAPIListen only takes one parameter
|
||||||
|
return ipc.UAPIListen(interfaceName)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user