Compare commits

...

34 Commits

Author SHA1 Message Date
Owen Schwartz
c423f6692a Merge pull request #53 from fosrl/dev
Docker socket integration & Go Updates
2025-06-05 11:50:29 -04:00
Owen
a9b96637b9 Merge branch 'main' into dev 2025-06-05 11:49:53 -04:00
Owen
f566f599d6 Remove link 2025-06-05 11:44:35 -04:00
Owen
918a9bdb84 Cap 2025-06-05 11:42:30 -04:00
Owen
315b6f3721 Update readme about docker socket 2025-06-05 11:41:44 -04:00
Owen
37940444c1 Package updates 2025-06-04 17:28:06 -04:00
Owen Schwartz
4e9aa30686 Merge pull request #49 from fosrl/dependabot/go_modules/golang.org/x/net-0.38.0
Bump golang.org/x/net from 0.30.0 to 0.38.0
2025-06-02 22:08:59 -04:00
Owen Schwartz
c2d3f00a6e Merge pull request #46 from improbableone/hub-dev
🐳 Add Docker Socket Integration
2025-06-02 20:34:37 -04:00
Owen
9f006b1cbd Update packages 2025-06-02 20:04:39 -04:00
dependabot[bot]
1ef61d7470 Bump golang.org/x/net from 0.30.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.30.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.30.0...v0.38.0)

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

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


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

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

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


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

Updates `alpine` from 3.19 to 3.22

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-01 08:51:11 +00:00
Marvin
78f31a56b0 Update dependabot.yml 2025-06-01 10:50:23 +02:00
Marvin
03988655b6 Create dependabot.yml 2025-06-01 10:47:06 +02:00
Rajesh V
4cf83f4cfc docker socket 2025-05-29 20:41:28 +05:30
Owen
67abd239b6 Merge branch 'main' into dev 2025-04-07 21:38:12 -04:00
Owen Schwartz
3b166c465d Merge pull request #29 from firecat53/flake
Flake update for newt 1.1.3
2025-04-07 21:37:53 -04:00
Owen
e0d2349efa Add tzdata package
Resolves #23 again
2025-04-07 09:47:58 -04:00
Scott Hansen
7b7d7228a6 Flake update for newt 1.1.3 2025-04-06 16:40:32 -07:00
Owen Schwartz
a1a439c75c Merge pull request #28 from fosrl/dev
MTLS, Connection Monitoring, time zone logger
2025-04-06 14:09:59 -04:00
Owen Schwartz
e7c8dbc1c8 Merge pull request #26 from progressive-kiwi/feat-mtls-support
Feat: mTLS support
2025-04-02 21:23:17 -04:00
progressive-kiwi
d28e3ca5e8 feat/mtls-support-cert: doc update, removing config.Endpoint loading duplicates, handling null-pointer case and some logging 2025-04-02 21:00:09 +02:00
progressive-kiwi
b41570eb2c feat/mtls-support-cert: config support 2025-04-01 20:43:42 +02:00
Owen
72e0adc1bf Monitor connection with pings and keep pining
Resolves #24
2025-03-30 19:31:55 -04:00
progressive-kiwi
435b638701 feat/mtls-support-cert-script 2025-03-31 00:52:48 +02:00
progressive-kiwi
9b3c82648b feat/mtls-support 2025-03-31 00:06:40 +02:00
Owen Schwartz
f713c294b2 Merge pull request #25 from firecat53/flake
Add flake for build and devshell.
2025-03-30 10:55:00 -04:00
Owen
b3e8bf7d12 Add LOGGER_TIMEZONE env to control the time zone
Closes #23

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

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

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

the directory or uncompressed zip file named by the ZONEINFO environment
variable
on a Unix system, the system standard installation location
$GOROOT/lib/time/zoneinfo.zip
the time/tzdata package, if it was imported
2025-03-30 10:52:07 -04:00
Scott Hansen
7852f11e8d Add flake for build and devshell.
Package named newt-pangolin to avoid conflicts with existing package name
2025-03-25 15:46:12 -07:00
Owen
2ff8df9a8d Merge branch 'dev' 2025-03-22 12:54:31 -04:00
Owen
9d80161ab7 Increases ping attempts to 15
Might help #7
2025-03-21 17:24:04 -04:00
15 changed files with 913 additions and 99 deletions

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

@@ -0,0 +1,46 @@
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"
dev-major-updates:
dependency-type: "development"
update-types:
- "major"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
prod-major-updates:
dependency-type: "production"
update-types:
- "major"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
groups:
patch-updates:
update-types:
- "patch"
minor-updates:
update-types:
- "minor"
major-updates:
update-types:
- "major"

5
.gitignore vendored
View File

@@ -1,3 +1,6 @@
newt
.DS_Store
bin/
bin/
.idea
*.iml
certs/

View File

@@ -1,4 +1,4 @@
FROM golang:1.23.1-alpine AS builder
FROM golang:1.24.3-alpine AS builder
# Set the working directory inside the container
WORKDIR /app
@@ -15,9 +15,9 @@ COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -o /newt
FROM alpine:3.19 AS runner
FROM alpine:3.22 AS runner
RUN apk --no-cache add ca-certificates
RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /newt /usr/local/bin/
COPY entrypoint.sh /

View File

@@ -6,7 +6,6 @@ Newt is a fully user space [WireGuard](https://www.wireguard.com/) tunnel client
Newt is used with Pangolin and Gerbil as part of the larger system. See documentation below:
- [Installation Instructions](https://docs.fossorial.io)
- [Full Documentation](https://docs.fossorial.io)
## Preview
@@ -37,8 +36,10 @@ When Newt receives WireGuard control messages, it will use the information encod
- `dns`: DNS server to use to resolve the endpoint
- `log-level` (optional): The log level to use. Default: INFO
- `updown` (optional): A script to be called when targets are added or removed.
Example:
- `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls)
- `docker-socket` (optional): Override the Docker socket integration
- Example:
```bash
./newt \
@@ -75,23 +76,16 @@ services:
- --endpoint https://example.com
```
Finally a basic systemd service:
### Docker Socket Integration
```
[Unit]
Description=Newt VPN Client
After=network.target
Newt can integrate with the Docker socket to provide remote inspection of Docker containers. This allows Pangolin to query and retrieve detailed information about containers running on the Newt client, including metadata, network configuration, port mappings, and more.
[Service]
ExecStart=/usr/local/bin/newt --id 31frd0uzbjvp721 --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 --endpoint https://example.com
Restart=always
User=root
**Configuration:**
[Install]
WantedBy=multi-user.target
```
- By default, Newt will look for the Docker socket at `/var/run/docker.sock`.
- You can specify a custom socket path using the `--docker-socket` CLI argument or by setting the `DOCKER_SOCKET` environment variable.
Make sure to `mv ./newt /usr/local/bin/newt`!
If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation.
### Updown
@@ -107,6 +101,38 @@ Returning a string from the script in the format of a target (`ip:dst` so `10.0.
You can look at updown.py as a reference script to get started!
### mTLS
Newt supports mutual TLS (mTLS) authentication, if the server has been configured to request a client certificate.
* Only PKCS12 (.p12 or .pfx) file format is accepted
* The PKCS12 file must contain:
* Private key
* Public certificate
* CA certificate
* Encrypted PKCS12 files are currently not supported
Examples:
```bash
./newt \
--id 31frd0uzbjvp721 \
--secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
--endpoint https://example.com \
--tls-client-cert ./client.p12
```
```yaml
services:
newt:
image: fosrl/newt
container_name: newt
restart: unless-stopped
environment:
- PANGOLIN_ENDPOINT=https://example.com
- NEWT_ID=2ix2t8xk22ubpfy
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
- TLS_CLIENT_CERT=./client.p12
```
## Build
### Container
@@ -125,6 +151,16 @@ Make sure to have Go 1.23.1 installed.
make local
```
### Nix Flake
```bash
nix build
```
Binary will be at `./result/bin/newt`
Development shell available with `nix develop`
## Licensing
Newt is dual licensed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.

166
docker/client.go Normal file
View File

@@ -0,0 +1,166 @@
package docker
import (
"context"
"fmt"
"net"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/fosrl/newt/logger"
)
// Container represents a Docker container
type Container struct {
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
State string `json:"state"`
Status string `json:"status"`
Ports []Port `json:"ports"`
Labels map[string]string `json:"labels"`
Created int64 `json:"created"`
Networks map[string]Network `json:"networks"`
}
// Port represents a port mapping for a Docker container
type Port struct {
PrivatePort int `json:"privatePort"`
PublicPort int `json:"publicPort,omitempty"`
Type string `json:"type"`
IP string `json:"ip,omitempty"`
}
// Network represents network information for a Docker container
type Network struct {
NetworkID string `json:"networkId"`
EndpointID string `json:"endpointId"`
Gateway string `json:"gateway,omitempty"`
IPAddress string `json:"ipAddress,omitempty"`
IPPrefixLen int `json:"ipPrefixLen,omitempty"`
IPv6Gateway string `json:"ipv6Gateway,omitempty"`
GlobalIPv6Address string `json:"globalIPv6Address,omitempty"`
GlobalIPv6PrefixLen int `json:"globalIPv6PrefixLen,omitempty"`
MacAddress string `json:"macAddress,omitempty"`
Aliases []string `json:"aliases,omitempty"`
DNSNames []string `json:"dnsNames,omitempty"`
}
// CheckSocket checks if Docker socket is available
func CheckSocket(socketPath string) bool {
// Use the provided socket path or default to standard location
if socketPath == "" {
socketPath = "/var/run/docker.sock"
}
// Try to create a connection to the Docker socket
conn, err := net.Dial("unix", socketPath)
if err != nil {
logger.Debug("Docker socket not available at %s: %v", socketPath, err)
return false
}
defer conn.Close()
logger.Debug("Docker socket is available at %s", socketPath)
return true
}
// ListContainers lists all Docker containers with their network information
func ListContainers(socketPath string) ([]Container, error) {
// Use the provided socket path or default to standard location
if socketPath == "" {
socketPath = "/var/run/docker.sock"
}
// Create a new Docker client
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Create client with custom socket path
cli, err := client.NewClientWithOpts(
client.WithHost("unix://"+socketPath),
client.WithAPIVersionNegotiation(),
)
if err != nil {
return nil, fmt.Errorf("failed to create Docker client: %v", err)
}
defer cli.Close()
// List containers
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return nil, fmt.Errorf("failed to list containers: %v", err)
}
var dockerContainers []Container
for _, c := range containers {
// Convert ports
var ports []Port
for _, port := range c.Ports {
dockerPort := Port{
PrivatePort: int(port.PrivatePort),
Type: port.Type,
}
if port.PublicPort != 0 {
dockerPort.PublicPort = int(port.PublicPort)
}
if port.IP != "" {
dockerPort.IP = port.IP
}
ports = append(ports, dockerPort)
}
// Get container name (remove leading slash)
name := ""
if len(c.Names) > 0 {
name = strings.TrimPrefix(c.Names[0], "/")
}
// Get network information by inspecting the container
networks := make(map[string]Network)
// Inspect container to get detailed network information
containerInfo, err := cli.ContainerInspect(ctx, c.ID)
if err != nil {
logger.Debug("Failed to inspect container %s for network info: %v", c.ID[:12], err)
// Continue without network info if inspection fails
} else {
// Extract network information from inspection
if containerInfo.NetworkSettings != nil && containerInfo.NetworkSettings.Networks != nil {
for networkName, endpoint := range containerInfo.NetworkSettings.Networks {
dockerNetwork := Network{
NetworkID: endpoint.NetworkID,
EndpointID: endpoint.EndpointID,
Gateway: endpoint.Gateway,
IPAddress: endpoint.IPAddress,
IPPrefixLen: endpoint.IPPrefixLen,
IPv6Gateway: endpoint.IPv6Gateway,
GlobalIPv6Address: endpoint.GlobalIPv6Address,
GlobalIPv6PrefixLen: endpoint.GlobalIPv6PrefixLen,
MacAddress: endpoint.MacAddress,
Aliases: endpoint.Aliases,
DNSNames: endpoint.DNSNames,
}
networks[networkName] = dockerNetwork
}
}
}
dockerContainer := Container{
ID: c.ID[:12], // Show short ID like docker ps
Name: name,
Image: c.Image,
State: c.State,
Status: c.Status,
Ports: ports,
Labels: c.Labels,
Created: c.Created,
Networks: networks,
}
dockerContainers = append(dockerContainers, dockerContainer)
}
return dockerContainers, nil
}

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1743827369,
"narHash": "sha256-rpqepOZ8Eo1zg+KJeWoq1HAOgoMCDloqv5r2EAa9TSA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "42a1c966be226125b48c384171c44c651c236c22",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

65
flake.nix Normal file
View File

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

31
go.mod
View File

@@ -5,18 +5,41 @@ go 1.23.1
toolchain go1.23.2
require (
github.com/docker/docker v28.2.2+incompatible
github.com/gorilla/websocket v1.5.3
golang.org/x/net v0.30.0
golang.org/x/net v0.40.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259
software.sslmate.com/src/go-pkcs12 v0.5.0
)
require (
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/sys v0.26.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.13.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
)

91
go.sum
View File

@@ -1,17 +1,90 @@
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
@@ -20,3 +93,5 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

View File

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

251
main.go
View File

@@ -18,6 +18,7 @@ import (
"syscall"
"time"
"github.com/fosrl/newt/docker"
"github.com/fosrl/newt/logger"
"github.com/fosrl/newt/proxy"
"github.com/fosrl/newt/websocket"
@@ -55,7 +56,7 @@ func fixKey(key string) string {
// Decode from base64
decoded, err := base64.StdEncoding.DecodeString(key)
if err != nil {
logger.Fatal("Error decoding base64:", err)
logger.Fatal("Error decoding base64: %v", err)
}
// Convert to hex
@@ -115,7 +116,12 @@ func ping(tnet *netstack.Net, dst string) error {
}
func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) {
ticker := time.NewTicker(10 * time.Second)
initialInterval := 10 * time.Second
maxInterval := 60 * time.Second
currentInterval := initialInterval
consecutiveFailures := 0
ticker := time.NewTicker(currentInterval)
defer ticker.Stop()
go func() {
@@ -124,8 +130,34 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{})
case <-ticker.C:
err := ping(tnet, serverIP)
if err != nil {
logger.Warn("Periodic ping failed: %v", err)
consecutiveFailures++
logger.Warn("Periodic ping failed (%d consecutive failures): %v",
consecutiveFailures, err)
logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?")
// Increase interval if we have consistent failures, with a maximum cap
if consecutiveFailures >= 3 && currentInterval < maxInterval {
// Increase by 50% each time, up to the maximum
currentInterval = time.Duration(float64(currentInterval) * 1.5)
if currentInterval > maxInterval {
currentInterval = maxInterval
}
ticker.Reset(currentInterval)
logger.Info("Increased ping check interval to %v due to consecutive failures",
currentInterval)
}
} else {
// On success, if we've backed off, gradually return to normal interval
if currentInterval > initialInterval {
currentInterval = time.Duration(float64(currentInterval) * 0.8)
if currentInterval < initialInterval {
currentInterval = initialInterval
}
ticker.Reset(currentInterval)
logger.Info("Decreased ping check interval to %v after successful ping",
currentInterval)
}
consecutiveFailures = 0
}
case <-stopChan:
logger.Info("Stopping ping check")
@@ -135,34 +167,97 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{})
}()
}
// Function to track connection status and trigger reconnection as needed
func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websocket.Client) {
const checkInterval = 30 * time.Second
connectionLost := false
ticker := time.NewTicker(checkInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Try a ping to see if connection is alive
err := ping(tnet, serverIP)
if err != nil && !connectionLost {
// We just lost connection
connectionLost = true
logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.")
// Notify the user they might need to check their network
logger.Warn("Please check your internet connection and ensure the Pangolin server is online.")
logger.Warn("Newt will continue reconnection attempts automatically when connectivity is restored.")
} else if err == nil && connectionLost {
// Connection has been restored
connectionLost = false
logger.Info("Connection to server restored!")
// Tell the server we're back
err := client.SendMessage("newt/wg/register", map[string]interface{}{
"publicKey": privateKey.PublicKey().String(),
})
if err != nil {
logger.Error("Failed to send registration message after reconnection: %v", err)
} else {
logger.Info("Successfully re-registered with server after reconnection")
}
}
}
}
}
func pingWithRetry(tnet *netstack.Net, dst string) error {
const (
maxAttempts = 5
retryDelay = 2 * time.Second
initialMaxAttempts = 15
initialRetryDelay = 2 * time.Second
maxRetryDelay = 60 * time.Second // Cap the maximum delay
)
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
logger.Info("Ping attempt %d of %d", attempt, maxAttempts)
if err := ping(tnet, dst); err != nil {
lastErr = err
logger.Warn("Ping attempt %d failed: %v", attempt, err)
if attempt < maxAttempts {
time.Sleep(retryDelay)
continue
}
return fmt.Errorf("all ping attempts failed after %d tries, last error: %w",
maxAttempts, lastErr)
}
attempt := 1
retryDelay := initialRetryDelay
// First try with the initial parameters
logger.Info("Ping attempt %d", attempt)
if err := ping(tnet, dst); err == nil {
// Successful ping
return nil
} else {
logger.Warn("Ping attempt %d failed: %v", attempt, err)
}
// This shouldn't be reached due to the return in the loop, but added for completeness
return fmt.Errorf("unexpected error: all ping attempts failed")
// Start a goroutine that will attempt pings indefinitely with increasing delays
go func() {
attempt = 2 // Continue from attempt 2
for {
logger.Info("Ping attempt %d", attempt)
if err := ping(tnet, dst); err != nil {
logger.Warn("Ping attempt %d failed: %v", attempt, err)
// Increase delay after certain thresholds but cap it
if attempt%5 == 0 && retryDelay < maxRetryDelay {
retryDelay = time.Duration(float64(retryDelay) * 1.5)
if retryDelay > maxRetryDelay {
retryDelay = maxRetryDelay
}
logger.Info("Increasing ping retry delay to %v", retryDelay)
}
time.Sleep(retryDelay)
attempt++
} else {
// Successful ping
logger.Info("Ping succeeded after %d attempts", attempt)
return
}
}
}()
// Return an error for the first batch of attempts (to maintain compatibility with existing code)
return fmt.Errorf("initial ping attempts failed, continuing in background")
}
func parseLogLevel(level string) logger.LogLevel {
@@ -246,16 +341,18 @@ func resolveDomain(domain string) (string, error) {
}
var (
endpoint string
id string
secret string
mtu string
mtuInt int
dns string
privateKey wgtypes.Key
err error
logLevel string
updownScript string
endpoint string
id string
secret string
mtu string
mtuInt int
dns string
privateKey wgtypes.Key
err error
logLevel string
updownScript string
tlsPrivateKey string
dockerSocket string
)
func main() {
@@ -267,6 +364,8 @@ func main() {
dns = os.Getenv("DNS")
logLevel = os.Getenv("LOG_LEVEL")
updownScript = os.Getenv("UPDOWN_SCRIPT")
tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT")
dockerSocket = os.Getenv("DOCKER_SOCKET")
if endpoint == "" {
flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server")
@@ -289,6 +388,12 @@ func main() {
if updownScript == "" {
flag.StringVar(&updownScript, "updown", "", "Path to updown script to be called when targets are added or removed")
}
if tlsPrivateKey == "" {
flag.StringVar(&tlsPrivateKey, "tls-client-cert", "", "Path to client certificate used for mTLS")
}
if dockerSocket == "" {
flag.StringVar(&dockerSocket, "docker-socket", "/var/run/docker.sock", "Path to Docker socket")
}
// do a --version check
version := flag.Bool("version", false, "Print the version")
@@ -314,12 +419,16 @@ func main() {
if err != nil {
logger.Fatal("Failed to generate private key: %v", err)
}
var opt websocket.ClientOption
if tlsPrivateKey != "" {
opt = websocket.WithTLSConfig(tlsPrivateKey)
}
// Create a new client
client, err := websocket.NewClient(
id, // CLI arg takes precedence
secret, // CLI arg takes precedence
endpoint,
opt,
)
if err != nil {
logger.Fatal("Failed to create client: %v", err)
@@ -353,13 +462,8 @@ func main() {
if connected {
logger.Info("Already connected! But I will send a ping anyway...")
// ping(tnet, wgData.ServerIP)
err = pingWithRetry(tnet, wgData.ServerIP)
if err != nil {
// Handle complete failure after all retries
logger.Warn("Failed to ping %s: %v", wgData.ServerIP, err)
logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?")
}
// Even if pingWithRetry returns an error, it will continue trying in the background
_ = pingWithRetry(tnet, wgData.ServerIP) // Ignoring initial error as pings will continue
return
}
@@ -400,7 +504,7 @@ func main() {
public_key=%s
allowed_ip=%s/32
endpoint=%s
persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(wgData.PublicKey), wgData.ServerIP, endpoint)
persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.PublicKey), wgData.ServerIP, endpoint)
err = dev.IpcSet(config)
if err != nil {
@@ -414,17 +518,18 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
}
logger.Info("WireGuard device created. Lets ping the server now...")
// Ping to bring the tunnel up on the server side quickly
// ping(tnet, wgData.ServerIP)
err = pingWithRetry(tnet, wgData.ServerIP)
if err != nil {
// Handle complete failure after all retries
logger.Error("Failed to ping %s: %v", wgData.ServerIP, err)
}
// Even if pingWithRetry returns an error, it will continue trying in the background
_ = pingWithRetry(tnet, wgData.ServerIP)
// Always mark as connected and start the proxy manager regardless of initial ping result
// as the pings will continue in the background
if !connected {
logger.Info("Starting ping check")
startPingCheck(tnet, wgData.ServerIP, pingStopChan)
// Start connection monitoring in a separate goroutine
go monitorConnectionStatus(tnet, wgData.ServerIP, client)
}
// Create proxy manager
@@ -527,12 +632,53 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
}
})
// Register handler for Docker socket check
client.RegisterHandler("newt/socket/check", func(msg websocket.WSMessage) {
logger.Info("Received Docker socket check request")
// Check if Docker socket is available
isAvailable := docker.CheckSocket(dockerSocket)
// Send response back to server
err := client.SendMessage("newt/socket/status", map[string]interface{}{
"available": isAvailable,
"socketPath": dockerSocket,
})
if err != nil {
logger.Error("Failed to send Docker socket check response: %v", err)
} else {
logger.Info("Docker socket check response sent: available=%t", isAvailable)
}
})
// Register handler for Docker container listing
client.RegisterHandler("newt/socket/fetch", func(msg websocket.WSMessage) {
logger.Info("Received Docker container fetch request")
// List Docker containers
containers, err := docker.ListContainers(dockerSocket)
if err != nil {
logger.Error("Failed to list Docker containers: %v", err)
return
}
// Send container list back to server
err = client.SendMessage("newt/socket/containers", map[string]interface{}{
"containers": containers,
})
if err != nil {
logger.Error("Failed to send Docker container list: %v", err)
} else {
logger.Info("Docker container list sent, count: %d", len(containers))
}
})
client.OnConnect(func() error {
publicKey := privateKey.PublicKey()
logger.Debug("Public key: %s", publicKey)
err := client.SendMessage("newt/wg/register", map[string]interface{}{
"publicKey": fmt.Sprintf("%s", publicKey),
"publicKey": publicKey.String(),
})
if err != nil {
logger.Error("Failed to send registration message: %v", err)
@@ -552,10 +698,13 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
// Wait for interrupt signal
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
sigReceived := <-sigCh
// Cleanup
dev.Close()
logger.Info("Received %s signal, stopping", sigReceived.String())
if dev != nil {
dev.Close()
}
}
func parseTargetData(data interface{}) (TargetData, error) {

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

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

View File

@@ -2,27 +2,29 @@ package websocket
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"software.sslmate.com/src/go-pkcs12"
"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
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
@@ -41,6 +43,12 @@ func WithBaseURL(url string) ClientOption {
}
}
func WithTLSConfig(tlsClientCertPath string) ClientOption {
return func(c *Client) {
c.config.TlsClientCert = tlsClientCertPath
}
}
func (c *Client) OnConnect(callback func() error) {
c.onConnect = callback
}
@@ -63,8 +71,13 @@ func NewClient(newtID, secret string, endpoint string, opts ...ClientOption) (*C
}
// Apply options before loading config
for _, opt := range opts {
opt(client)
if opts != nil {
for _, opt := range opts {
if opt == nil {
continue
}
opt(client)
}
}
// Load existing config if available
@@ -149,6 +162,14 @@ func (c *Client) getToken() (string, error) {
// Ensure we have the base URL without trailing slashes
baseEndpoint := strings.TrimRight(baseURL.String(), "/")
var tlsConfig *tls.Config = nil
if c.config.TlsClientCert != "" {
tlsConfig, err = loadClientCertificate(c.config.TlsClientCert)
if err != nil {
return "", fmt.Errorf("failed to load certificate %s: %w", c.config.TlsClientCert, err)
}
}
// If we already have a token, try to use it
if c.config.Token != "" {
tokenCheckData := map[string]interface{}{
@@ -177,6 +198,11 @@ func (c *Client) getToken() (string, error) {
// Make the request
client := &http.Client{}
if tlsConfig != nil {
client.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to check token validity: %w", err)
@@ -220,6 +246,11 @@ func (c *Client) getToken() (string, error) {
// Make the request
client := &http.Client{}
if tlsConfig != nil {
client.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to request new token: %w", err)
@@ -295,7 +326,16 @@ func (c *Client) establishConnection() error {
u.RawQuery = q.Encode()
// Connect to WebSocket
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
dialer := websocket.DefaultDialer
if c.config.TlsClientCert != "" {
logger.Info("Adding tls to req")
tlsConfig, err := loadClientCertificate(c.config.TlsClientCert)
if err != nil {
return fmt.Errorf("failed to load certificate %s: %w", c.config.TlsClientCert, err)
}
dialer.TLSClientConfig = tlsConfig
}
conn, _, err := dialer.Dial(u.String(), nil)
if err != nil {
return fmt.Errorf("failed to connect to WebSocket: %w", err)
}
@@ -353,3 +393,42 @@ func (c *Client) setConnected(status bool) {
defer c.reconnectMux.Unlock()
c.isConnected = status
}
// LoadClientCertificate Helper method to load client certificates
func loadClientCertificate(p12Path string) (*tls.Config, error) {
logger.Info("Loading tls-client-cert %s", p12Path)
// Read the PKCS12 file
p12Data, err := os.ReadFile(p12Path)
if err != nil {
return nil, fmt.Errorf("failed to read PKCS12 file: %w", err)
}
// Parse PKCS12 with empty password for non-encrypted files
privateKey, certificate, caCerts, err := pkcs12.DecodeChain(p12Data, "")
if err != nil {
return nil, fmt.Errorf("failed to decode PKCS12: %w", err)
}
// Create certificate
cert := tls.Certificate{
Certificate: [][]byte{certificate.Raw},
PrivateKey: privateKey,
}
// Optional: Add CA certificates if present
rootCAs, err := x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("failed to load system cert pool: %w", err)
}
if len(caCerts) > 0 {
for _, caCert := range caCerts {
rootCAs.AddCert(caCert)
}
}
// Create TLS configuration
return &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: rootCAs,
}, nil
}

View File

@@ -54,6 +54,9 @@ func (c *Client) loadConfig() error {
if c.config.Secret == "" {
c.config.Secret = config.Secret
}
if c.config.TlsClientCert == "" {
c.config.TlsClientCert = config.TlsClientCert
}
if c.config.Endpoint == "" {
c.config.Endpoint = config.Endpoint
c.baseURL = config.Endpoint

View File

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