diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 2364f89..f51541d 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -1,5 +1,8 @@ name: CI/CD Pipeline +permissions: + contents: read + on: push: tags: @@ -31,7 +34,7 @@ jobs: run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: 1.25 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79143df..643628b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,8 @@ name: Run Tests +permissions: + contents: read + on: pull_request: branches: @@ -14,7 +17,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: 1.25 diff --git a/README.md b/README.md index 6eb32ee..82ff42a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # Newt +[![PkgGoDev](https://pkg.go.dev/badge/github.com/fosrl/newt)](https://pkg.go.dev/github.com/fosrl/newt) +[![GitHub License](https://img.shields.io/github/license/fosrl/newt)](https://github.com/fosrl/newt/blob/main/LICENSE) +[![Go Report Card](https://goreportcard.com/badge/github.com/fosrl/newt)](https://goreportcard.com/report/github.com/fosrl/newt) 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. @@ -53,6 +56,7 @@ When Newt receives WireGuard control messages, it will use the information encod - `native` (optional): Use native WireGuard interface when accepting clients (requires WireGuard kernel module and Linux, must run as root). Default: false (uses userspace netstack) - `interface` (optional): Name of the WireGuard interface. Default: newt - `keep-interface` (optional): Keep the WireGuard interface. Default: false +- `blueprint-file` (optional): Path to blueprint file to define Pangolin resources and configurations. ## Environment Variables @@ -81,6 +85,7 @@ All CLI arguments can be set using environment variables as an alternative to co - `INTERFACE`: Name of the WireGuard interface. Default: newt (equivalent to `--interface`) - `KEEP_INTERFACE`: Keep the WireGuard interface after shutdown. Default: false (equivalent to `--keep-interface`) - `CONFIG_FILE`: Load the config json from this file instead of in the home folder. +- `BLUEPRINT_FILE`: Path to blueprint file to define Pangolin resources and configurations. (equivalent to `--blueprint-file`) ## Loading secrets from files diff --git a/go.mod b/go.mod index 0ee2cbb..a18c2d4 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,9 @@ require ( github.com/google/gopacket v1.1.19 github.com/gorilla/websocket v1.5.3 github.com/vishvananda/netlink v1.3.1 - golang.org/x/crypto v0.41.0 + golang.org/x/crypto v0.42.0 golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 - golang.org/x/net v0.43.0 + golang.org/x/net v0.44.0 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c @@ -48,8 +48,8 @@ require ( go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect + golang.org/x/sys v0.36.0 // indirect golang.org/x/time v0.12.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) +) \ No newline at end of file diff --git a/go.sum b/go.sum index c10012f..76b7685 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,8 @@ go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAj golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -117,8 +117,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 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= @@ -129,12 +129,12 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -161,7 +161,6 @@ google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= @@ -169,4 +168,4 @@ gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU= -software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= \ No newline at end of file diff --git a/key b/key new file mode 100644 index 0000000..62c22b9 --- /dev/null +++ b/key @@ -0,0 +1 @@ +oBvcoMJZXGzTZ4X+aNSCCQIjroREFBeRCs+a328xWGA= \ No newline at end of file diff --git a/main.go b/main.go index c080d22..fb31cfe 100644 --- a/main.go +++ b/main.go @@ -175,7 +175,7 @@ func main() { // Legacy PKCS12 support (deprecated) tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT_PKCS12") // Keep backward compatibility with old environment variable name - if tlsPrivateKey == "" { + if tlsPrivateKey == "" && tlsClientKey == "" && len(tlsClientCAs) == 0 { tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") } blueprintFile = os.Getenv("BLUEPRINT_FILE") diff --git a/websocket/config.go b/websocket/config.go index 6803e81..1d821b6 100644 --- a/websocket/config.go +++ b/websocket/config.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" "runtime" + + "github.com/fosrl/newt/logger" ) func getConfigPath(clientType string) string { @@ -34,6 +36,7 @@ func getConfigPath(clientType string) string { func (c *Client) loadConfig() error { if c.config.ID != "" && c.config.Secret != "" && c.config.Endpoint != "" { + logger.Debug("Config already provided, skipping loading from file") return nil } @@ -65,6 +68,9 @@ func (c *Client) loadConfig() error { c.baseURL = config.Endpoint } + logger.Debug("Loaded config from %s", configPath) + logger.Debug("Config: %+v", c.config) + return nil } diff --git a/wg/wg.go b/wg/wg.go index 3cee1a9..5a512d6 100644 --- a/wg/wg.go +++ b/wg/wg.go @@ -152,6 +152,7 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str } var key wgtypes.Key + var port uint16 // if generateAndSaveKeyTo is provided, generate a private key and save it to the file. if the file already exists, load the key from the file key, err = wgtypes.GeneratePrivateKey() if err != nil { @@ -177,40 +178,43 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str } } - service := &WireGuardService{ - interfaceName: interfaceName, - mtu: mtu, - client: wsClient, - wgClient: wgClient, - key: key, - keyFilePath: generateAndSaveKeyTo, - newtId: newtId, - host: host, - lastReadings: make(map[string]PeerReading), - stopHolepunch: make(chan struct{}), - } - - // Get the existing wireguard port (keep this part) - device, err := service.wgClient.Device(service.interfaceName) + // Get the existing wireguard port + device, err := wgClient.Device(interfaceName) if err == nil { - service.Port = uint16(device.ListenPort) - if service.Port != 0 { - logger.Info("WireGuard interface %s already exists with port %d\n", service.interfaceName, service.Port) + port = uint16(device.ListenPort) + // also set the private key to the existing key + key = device.PrivateKey + if port != 0 { + logger.Info("WireGuard interface %s already exists with port %d\n", interfaceName, port) } else { - service.Port, err = FindAvailableUDPPort(49152, 65535) + port, err = FindAvailableUDPPort(49152, 65535) if err != nil { fmt.Printf("Error finding available port: %v\n", err) return nil, err } } } else { - service.Port, err = FindAvailableUDPPort(49152, 65535) + port, err = FindAvailableUDPPort(49152, 65535) if err != nil { fmt.Printf("Error finding available port: %v\n", err) return nil, err } } + service := &WireGuardService{ + interfaceName: interfaceName, + mtu: mtu, + client: wsClient, + wgClient: wgClient, + key: key, + Port: port, + keyFilePath: generateAndSaveKeyTo, + newtId: newtId, + host: host, + lastReadings: make(map[string]PeerReading), + stopHolepunch: make(chan struct{}), + } + // Register websocket handlers wsClient.RegisterHandler("newt/wg/receive-config", service.handleConfig) wsClient.RegisterHandler("newt/wg/peer/add", service.handleAddPeer) diff --git a/wgnetstack/wgnetstack.go b/wgnetstack/wgnetstack.go index 6684c40..08d740e 100644 --- a/wgnetstack/wgnetstack.go +++ b/wgnetstack/wgnetstack.go @@ -187,6 +187,13 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str // Load or generate private key if generateAndSaveKeyTo != "" { if _, err := os.Stat(generateAndSaveKeyTo); os.IsNotExist(err) { + // File doesn't exist, save the generated key + err = os.WriteFile(generateAndSaveKeyTo, []byte(key.String()), 0600) + if err != nil { + return nil, fmt.Errorf("failed to save private key: %v", err) + } + } else { + // File exists, read the existing key keyData, err := os.ReadFile(generateAndSaveKeyTo) if err != nil { return nil, fmt.Errorf("failed to read private key: %v", err) @@ -195,11 +202,6 @@ func NewWireGuardService(interfaceName string, mtu int, generateAndSaveKeyTo str if err != nil { return nil, fmt.Errorf("failed to parse private key: %v", err) } - } else { - err = os.WriteFile(generateAndSaveKeyTo, []byte(key.String()), 0600) - if err != nil { - return nil, fmt.Errorf("failed to save private key: %v", err) - } } }