mirror of
https://github.com/fosrl/newt.git
synced 2026-09-04 19:08:36 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6535ec5c4b | ||
|
|
1121ac5357 | ||
|
|
d9405f4eae | ||
|
|
0e415f0a01 | ||
|
|
15224904a0 | ||
|
|
527edc8d80 |
+27
-27
@@ -68,7 +68,7 @@ jobs:
|
||||
echo "image_created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Configure AWS credentials (OIDC)
|
||||
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
|
||||
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -240,17 +240,17 @@ jobs:
|
||||
# uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
|
||||
#- name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
# uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
# Build ONLY amd64 and push arch-specific tag suffixes used later for manifest creation.
|
||||
- name: Build and push (amd64 -> *:amd64-TAG)
|
||||
@@ -309,7 +309,7 @@ jobs:
|
||||
IMAGE_CREATED: ${{ needs.pre-run.outputs.image_created }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -368,14 +368,14 @@ jobs:
|
||||
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -389,7 +389,7 @@ jobs:
|
||||
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
# Build ONLY arm64 and push arch-specific tag suffixes used later for manifest creation.
|
||||
- name: Build and push (arm64 -> *:arm64-TAG)
|
||||
@@ -434,7 +434,7 @@ jobs:
|
||||
IMAGE_CREATED: ${{ needs.pre-run.outputs.image_created }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -483,14 +483,14 @@ jobs:
|
||||
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -507,7 +507,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Build and push (arm/v7 -> *:armv7-TAG)
|
||||
id: build_armv7
|
||||
@@ -556,14 +556,14 @@ jobs:
|
||||
#PUBLISH_MINOR: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_minor || vars.PUBLISH_MINOR }}
|
||||
steps:
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -577,7 +577,7 @@ jobs:
|
||||
echo "DOCKERHUB_IMAGE=${DOCKERHUB_IMAGE,,}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Docker Buildx (needed for imagetools)
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Create & push multi-arch index (GHCR :TAG) via imagetools
|
||||
shell: bash
|
||||
@@ -642,7 +642,7 @@ jobs:
|
||||
IMAGE_CREATED: ${{ needs.pre-run.outputs.image_created }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -656,19 +656,19 @@ jobs:
|
||||
echo "Checked out $(git rev-parse --short HEAD) for tag ${TAG}"
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -692,7 +692,7 @@ jobs:
|
||||
sudo apt-get install -y jq
|
||||
|
||||
- name: Set up Docker Buildx (needed for imagetools)
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Resolve multi-arch digest refs (by TAG)
|
||||
shell: bash
|
||||
@@ -732,7 +732,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Attest build provenance (GHCR) (digest)
|
||||
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
||||
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_IMAGE }}
|
||||
subject-digest: ${{ env.GHCR_DIGEST }}
|
||||
@@ -742,7 +742,7 @@ jobs:
|
||||
- name: Attest build provenance (Docker Hub)
|
||||
continue-on-error: true
|
||||
if: ${{ env.DH_DIGEST != '' }}
|
||||
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
||||
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
||||
with:
|
||||
subject-name: index.docker.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
subject-digest: ${{ env.DH_DIGEST }}
|
||||
@@ -912,7 +912,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Create GitHub Release (draft)
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
with:
|
||||
tag_name: ${{ env.TAG }}
|
||||
generate_release_notes: true
|
||||
@@ -940,7 +940,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials (OIDC)
|
||||
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
|
||||
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||
with:
|
||||
days-before-stale: 14
|
||||
days-before-close: 14
|
||||
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
@@ -42,10 +42,10 @@ jobs:
|
||||
- go-build-release-windows-amd64
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
|
||||
+31
-38
@@ -98,21 +98,20 @@ type PeerReading struct {
|
||||
}
|
||||
|
||||
type WireGuardService struct {
|
||||
interfaceName string
|
||||
localEndpointInterfaces []string
|
||||
mtu int
|
||||
client *websocket.Client
|
||||
config WgConfig
|
||||
key wgtypes.Key
|
||||
newtId string
|
||||
lastReadings map[string]PeerReading
|
||||
mu sync.Mutex
|
||||
Port uint16
|
||||
host string
|
||||
serverPubKey string
|
||||
token string
|
||||
stopGetConfig func()
|
||||
pendingConfigChainId string
|
||||
interfaceName string
|
||||
mtu int
|
||||
client *websocket.Client
|
||||
config WgConfig
|
||||
key wgtypes.Key
|
||||
newtId string
|
||||
lastReadings map[string]PeerReading
|
||||
mu sync.Mutex
|
||||
Port uint16
|
||||
host string
|
||||
serverPubKey string
|
||||
token string
|
||||
stopGetConfig func()
|
||||
pendingConfigChainId string
|
||||
// Netstack fields
|
||||
tun tun.Device
|
||||
tnet *netstack2.Net
|
||||
@@ -155,7 +154,7 @@ func generateChainId() string {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func NewWireGuardService(interfaceName string, port uint16, mtu int, host string, newtId string, wsClient *websocket.Client, dns string, useNativeInterface bool, localEndpointInterfaces []string) (*WireGuardService, error) {
|
||||
func NewWireGuardService(interfaceName string, port uint16, mtu int, host string, newtId string, wsClient *websocket.Client, dns string, useNativeInterface bool) (*WireGuardService, error) {
|
||||
key, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate private key: %v", err)
|
||||
@@ -196,18 +195,17 @@ func NewWireGuardService(interfaceName string, port uint16, mtu int, host string
|
||||
dnsAddrs := []netip.Addr{netip.MustParseAddr(dns)}
|
||||
|
||||
service := &WireGuardService{
|
||||
interfaceName: interfaceName,
|
||||
localEndpointInterfaces: localEndpointInterfaces,
|
||||
mtu: mtu,
|
||||
client: wsClient,
|
||||
key: key,
|
||||
newtId: newtId,
|
||||
host: host,
|
||||
lastReadings: make(map[string]PeerReading),
|
||||
Port: port,
|
||||
dns: dnsAddrs,
|
||||
sharedBind: sharedBind,
|
||||
useNativeInterface: useNativeInterface,
|
||||
interfaceName: interfaceName,
|
||||
mtu: mtu,
|
||||
client: wsClient,
|
||||
key: key,
|
||||
newtId: newtId,
|
||||
host: host,
|
||||
lastReadings: make(map[string]PeerReading),
|
||||
Port: port,
|
||||
dns: dnsAddrs,
|
||||
sharedBind: sharedBind,
|
||||
useNativeInterface: useNativeInterface,
|
||||
}
|
||||
|
||||
// Create the holepunch manager
|
||||
@@ -534,7 +532,7 @@ func (s *WireGuardService) LoadRemoteConfig() error {
|
||||
"publicKey": s.key.PublicKey().String(),
|
||||
"port": s.Port,
|
||||
"chainId": chainId,
|
||||
"localEndpoints": network.GetLocalEndpoints(s.Port, s.interfaceName, s.localEndpointInterfaces),
|
||||
"localEndpoints": network.GetLocalEndpoints(s.Port, s.interfaceName),
|
||||
}, 2*time.Second)
|
||||
|
||||
logger.Debug("Requesting WireGuard configuration from remote server")
|
||||
@@ -834,16 +832,11 @@ func (s *WireGuardService) syncTargets(desiredTargets []Target) error {
|
||||
func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error {
|
||||
s.mu.Lock()
|
||||
|
||||
// split off the cidr from the IP address
|
||||
parts := strings.Split(wgconfig.IpAddress, "/")
|
||||
if len(parts) != 2 {
|
||||
interfaceAddress, tunnelIP, err := normalizeInterfaceAddress(wgconfig.IpAddress)
|
||||
if err != nil {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("invalid IP address format: %s", wgconfig.IpAddress)
|
||||
return err
|
||||
}
|
||||
// Parse the IP address and CIDR mask
|
||||
tunnelIP := netip.MustParseAddr(parts[0])
|
||||
|
||||
var err error
|
||||
|
||||
if s.useNativeInterface {
|
||||
// Create native TUN device
|
||||
@@ -919,7 +912,7 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error {
|
||||
}
|
||||
|
||||
// Configure the network interface with IP address
|
||||
if err := network.ConfigureInterface(interfaceName, wgconfig.IpAddress, s.mtu); err != nil {
|
||||
if err := network.ConfigureInterface(interfaceName, interfaceAddress, s.mtu); err != nil {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("failed to configure interface: %v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// normalizeInterfaceAddress accepts the address shape emitted by Pangolin's
|
||||
// site configuration. Older Pangolin versions can send a bare host address;
|
||||
// the client WireGuard interface needs an address/prefix pair, so treat a
|
||||
// bare IPv4 address as /32 and a bare IPv6 address as /128.
|
||||
func normalizeInterfaceAddress(raw string) (string, netip.Addr, error) {
|
||||
if prefix, err := netip.ParsePrefix(raw); err == nil {
|
||||
return prefix.String(), prefix.Addr(), nil
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddr(raw)
|
||||
if err != nil {
|
||||
return "", netip.Addr{}, fmt.Errorf("invalid IP address format: %s", raw)
|
||||
}
|
||||
|
||||
return netip.PrefixFrom(addr, addr.BitLen()).String(), addr, nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package clients
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeInterfaceAddress(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantCIDR string
|
||||
wantIP string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "bare IPv4", input: "100.90.128.4", wantCIDR: "100.90.128.4/32", wantIP: "100.90.128.4"},
|
||||
{name: "bare IPv6", input: "2001:db8::4", wantCIDR: "2001:db8::4/128", wantIP: "2001:db8::4"},
|
||||
{name: "IPv4 prefix", input: "100.90.128.4/24", wantCIDR: "100.90.128.4/24", wantIP: "100.90.128.4"},
|
||||
{name: "IPv6 prefix", input: "2001:db8::4/64", wantCIDR: "2001:db8::4/64", wantIP: "2001:db8::4"},
|
||||
{name: "invalid", input: "100.90.128.4/33", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotCIDR, gotIP, err := normalizeInterfaceAddress(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("normalizeInterfaceAddress(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||
}
|
||||
if tt.wantErr {
|
||||
return
|
||||
}
|
||||
if gotCIDR != tt.wantCIDR || gotIP.String() != tt.wantIP {
|
||||
t.Fatalf("normalizeInterfaceAddress(%q) = (%q, %q), want (%q, %q)", tt.input, gotCIDR, gotIP, tt.wantCIDR, tt.wantIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
// Package newtconfig resolves Newt's runtime configuration (CLI flags, env
|
||||
// vars, and config file) into a newt.Config. It is the same logic used by
|
||||
// the newt binary's entrypoint, factored out so other programs (such as the
|
||||
// Pangolin CLI, which embeds Newt as a library) can load configuration the
|
||||
// exact same way, from an explicit argument list rather than the process's
|
||||
// global os.Args/flag.CommandLine.
|
||||
package newtconfig
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -59,12 +53,11 @@ type fileSettings struct {
|
||||
MTU *int `json:"mtu"`
|
||||
Port *int `json:"port"`
|
||||
|
||||
UseNativeInterface *bool `json:"native"`
|
||||
UseNativeMainInterface *bool `json:"nativeMain"`
|
||||
NativeMainInterfaceName *string `json:"interfaceMain"`
|
||||
NoCloud *bool `json:"noCloud"`
|
||||
PreferEndpoint *string `json:"preferEndpoint"`
|
||||
LocalEndpointInterfaces []string `json:"localEndpointInterfaces"`
|
||||
UseNativeInterface *bool `json:"native"`
|
||||
UseNativeMainInterface *bool `json:"nativeMain"`
|
||||
NativeMainInterfaceName *string `json:"interfaceMain"`
|
||||
NoCloud *bool `json:"noCloud"`
|
||||
PreferEndpoint *string `json:"preferEndpoint"`
|
||||
|
||||
PingInterval *string `json:"pingInterval"`
|
||||
PingTimeout *string `json:"pingTimeout"`
|
||||
@@ -100,9 +93,8 @@ type fileSettings struct {
|
||||
|
||||
// resolveConfigFilePath determines the settings/credentials file path using
|
||||
// the same precedence as every other setting: CLI > env > OS default.
|
||||
// It has to run before the flag set is parsed (which needs the
|
||||
// file-resolved defaults), so it scans args directly instead of using the
|
||||
// flag package.
|
||||
// It has to run before flag.Parse (which needs the file-resolved defaults),
|
||||
// so it scans os.Args directly instead of using the flag package.
|
||||
func resolveConfigFilePath(args []string) string {
|
||||
for i, a := range args {
|
||||
if a == "--config-file" || a == "-config-file" {
|
||||
@@ -188,20 +180,6 @@ func applyEnvBool(dst *bool, envName, key string, sources map[string]string) {
|
||||
}
|
||||
}
|
||||
|
||||
// applyEnvStrAlias behaves like applyEnvStr, but checks a preferred env var
|
||||
// first and only falls back to an alias name when the preferred one is unset.
|
||||
func applyEnvStrAlias(dst *string, envName, aliasEnvName, key string, sources map[string]string) {
|
||||
if v := os.Getenv(envName); v != "" {
|
||||
*dst = v
|
||||
sources[key] = string(sourceEnv)
|
||||
return
|
||||
}
|
||||
if v := os.Getenv(aliasEnvName); v != "" {
|
||||
*dst = v
|
||||
sources[key] = string(sourceEnv)
|
||||
}
|
||||
}
|
||||
|
||||
// validateTLSConfig validates that TLS config fields are consistent and that
|
||||
// referenced files exist.
|
||||
func validateTLSConfig(cfg newtpkg.Config) error {
|
||||
@@ -256,28 +234,14 @@ func parseDurationEnvOrFlag(s string, defaultVal time.Duration, label string) ti
|
||||
return d
|
||||
}
|
||||
|
||||
// Options controls how Load resolves configuration.
|
||||
type Options struct {
|
||||
// Args are the newt command-line arguments, i.e. os.Args[1:] when newt
|
||||
// is run as its own binary, or whatever arguments were passed to a
|
||||
// subcommand that embeds newt as a library.
|
||||
Args []string
|
||||
// Version and Platform populate the resulting Config's build info and
|
||||
// are printed by --version.
|
||||
Version string
|
||||
Platform string
|
||||
}
|
||||
|
||||
// Load resolves configuration with priority cli > env > file > default,
|
||||
// validates it (e.g. TLS flag consistency and referenced file existence),
|
||||
// and returns a populated newtpkg.Config. This function parses Args with a
|
||||
// dedicated flag.FlagSet (safe to call more than once per process) and will
|
||||
// exit the process if --version or --show-config is passed, matching the
|
||||
// newt binary's own CLI behavior exactly.
|
||||
func Load(opts Options) (newtpkg.Config, error) {
|
||||
// loadNewtConfig resolves configuration with priority cli > env > file >
|
||||
// default, then returns a populated newtpkg.Config. This function calls
|
||||
// flag.Parse internally and will exit the process if --version or
|
||||
// --show-config is passed.
|
||||
func loadNewtConfig() newtpkg.Config {
|
||||
sources := make(map[string]string)
|
||||
|
||||
configPath := resolveConfigFilePath(opts.Args)
|
||||
configPath := resolveConfigFilePath(os.Args[1:])
|
||||
fileCfg, err := loadFileSettings(configPath)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to load config file: %v", err)
|
||||
@@ -285,8 +249,8 @@ func Load(opts Options) (newtpkg.Config, error) {
|
||||
|
||||
// ---- defaults ----
|
||||
cfg := newtpkg.Config{
|
||||
Version: opts.Version,
|
||||
Platform: opts.Platform,
|
||||
Version: newtVersion,
|
||||
Platform: newtPlatform,
|
||||
|
||||
DNS: "9.9.9.9",
|
||||
LogLevel: "INFO",
|
||||
@@ -330,10 +294,6 @@ func Load(opts Options) (newtpkg.Config, error) {
|
||||
applyStr(&cfg.NativeMainInterfaceName, fileCfg.NativeMainInterfaceName, "interface-main", sources, sourceFile)
|
||||
applyBool(&cfg.NoCloud, fileCfg.NoCloud, "no-cloud", sources, sourceFile)
|
||||
applyStr(&cfg.PreferEndpoint, fileCfg.PreferEndpoint, "prefer-endpoint", sources, sourceFile)
|
||||
if len(fileCfg.LocalEndpointInterfaces) > 0 {
|
||||
cfg.LocalEndpointInterfaces = fileCfg.LocalEndpointInterfaces
|
||||
sources["local-endpoint-interfaces"] = string(sourceFile)
|
||||
}
|
||||
|
||||
applyStr(&pingIntervalStr, fileCfg.PingInterval, "ping-interval", sources, sourceFile)
|
||||
applyStr(&pingTimeoutStr, fileCfg.PingTimeout, "ping-timeout", sources, sourceFile)
|
||||
@@ -375,12 +335,8 @@ func Load(opts Options) (newtpkg.Config, error) {
|
||||
|
||||
// ---- layer 2: environment variables ----
|
||||
applyEnvStr(&cfg.Endpoint, "PANGOLIN_ENDPOINT", "endpoint", sources)
|
||||
// SITE_ID/SITE_SECRET are accepted as aliases for NEWT_ID/NEWT_SECRET
|
||||
// (NEWT_ID/NEWT_SECRET win if both are set) so a site tunnel's
|
||||
// credentials can be named consistently with other Pangolin CLI
|
||||
// connection types (e.g. CLIENT_ID/CLIENT_SECRET for `up client`).
|
||||
applyEnvStrAlias(&cfg.ID, "NEWT_ID", "SITE_ID", "id", sources)
|
||||
applyEnvStrAlias(&cfg.Secret, "NEWT_SECRET", "SITE_SECRET", "secret", sources)
|
||||
applyEnvStr(&cfg.ID, "NEWT_ID", "id", sources)
|
||||
applyEnvStr(&cfg.Secret, "NEWT_SECRET", "secret", sources)
|
||||
applyEnvStr(&cfg.ProvisioningKey, "NEWT_PROVISIONING_KEY", "provisioning-key", sources)
|
||||
applyEnvStr(&cfg.NewtName, "NEWT_NAME", "name", sources)
|
||||
|
||||
@@ -395,16 +351,6 @@ func Load(opts Options) (newtpkg.Config, error) {
|
||||
applyEnvBool(&cfg.UseNativeMainInterface, "USE_NATIVE_MAIN_INTERFACE", "native-main", sources)
|
||||
applyEnvStr(&cfg.NativeMainInterfaceName, "INTERFACE_MAIN", "interface-main", sources)
|
||||
applyEnvBool(&cfg.NoCloud, "NO_CLOUD", "no-cloud", sources)
|
||||
if v := os.Getenv("LOCAL_ENDPOINT_INTERFACES"); v != "" {
|
||||
var names []string
|
||||
for _, n := range strings.Split(v, ",") {
|
||||
if t := strings.TrimSpace(n); t != "" {
|
||||
names = append(names, t)
|
||||
}
|
||||
}
|
||||
cfg.LocalEndpointInterfaces = names
|
||||
sources["local-endpoint-interfaces"] = string(sourceEnv)
|
||||
}
|
||||
|
||||
applyEnvStr(&pingIntervalStr, "PING_INTERVAL", "ping-interval", sources)
|
||||
applyEnvStr(&pingTimeoutStr, "PING_TIMEOUT", "ping-timeout", sources)
|
||||
@@ -470,69 +416,64 @@ func Load(opts Options) (newtpkg.Config, error) {
|
||||
origTLSCert, origTLSKey, origDockerEnforce := cfg.TLSClientCert, cfg.TLSClientKey, dockerEnforceStr
|
||||
origHealthFile, origBlueprintFile, origProvBlueprintFile := cfg.HealthFile, cfg.BlueprintFile, cfg.ProvisioningBlueprintFile
|
||||
origNoCloud, origTLSPrivateKey := cfg.NoCloud, cfg.TLSPrivateKey
|
||||
localEndpointInterfacesStr := strings.Join(cfg.LocalEndpointInterfaces, ",")
|
||||
origLocalEndpointInterfaces := localEndpointInterfacesStr
|
||||
origMetrics, origOTLP, origAdminAddr := cfg.MetricsEnabled, cfg.OTLPEnabled, cfg.AdminAddr
|
||||
origMetricsAsync, origPprof, origRegion := cfg.MetricsAsyncBytes, cfg.PprofEnabled, cfg.Region
|
||||
origADKey, origADPrincipals, origADCACert := cfg.AuthDaemonKey, cfg.AuthDaemonPrincipalsFile, cfg.AuthDaemonCACertPath
|
||||
origADRandomPass := cfg.AuthDaemonGenerateRandomPassword
|
||||
|
||||
fs := flag.NewFlagSet("newt", flag.ExitOnError)
|
||||
|
||||
fs.StringVar(&cfg.Endpoint, "endpoint", cfg.Endpoint, "Endpoint of your pangolin server")
|
||||
fs.StringVar(&cfg.ID, "id", cfg.ID, "Newt ID")
|
||||
fs.StringVar(&cfg.Secret, "secret", cfg.Secret, "Newt secret")
|
||||
fs.StringVar(&mtuStr, "mtu", mtuStr, "MTU to use")
|
||||
fs.StringVar(&cfg.DNS, "dns", cfg.DNS, "DNS server to use")
|
||||
fs.StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
|
||||
fs.StringVar(&cfg.UpdownScript, "updown", cfg.UpdownScript, "Path to updown script to be called when targets are added or removed")
|
||||
fs.StringVar(&cfg.InterfaceName, "interface", cfg.InterfaceName, "Name of the WireGuard interface")
|
||||
fs.StringVar(&portStr, "port", portStr, "Port for client WireGuard interface")
|
||||
fs.BoolVar(&cfg.UseNativeInterface, "native", cfg.UseNativeInterface, "Use native WireGuard interface for client tunnels")
|
||||
fs.BoolVar(&cfg.UseNativeMainInterface, "native-main", cfg.UseNativeMainInterface, "Use native WireGuard interface for the main tunnel (instead of netstack)")
|
||||
flag.StringVar(&cfg.Endpoint, "endpoint", cfg.Endpoint, "Endpoint of your pangolin server")
|
||||
flag.StringVar(&cfg.ID, "id", cfg.ID, "Newt ID")
|
||||
flag.StringVar(&cfg.Secret, "secret", cfg.Secret, "Newt secret")
|
||||
flag.StringVar(&mtuStr, "mtu", mtuStr, "MTU to use")
|
||||
flag.StringVar(&cfg.DNS, "dns", cfg.DNS, "DNS server to use")
|
||||
flag.StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
|
||||
flag.StringVar(&cfg.UpdownScript, "updown", cfg.UpdownScript, "Path to updown script to be called when targets are added or removed")
|
||||
flag.StringVar(&cfg.InterfaceName, "interface", cfg.InterfaceName, "Name of the WireGuard interface")
|
||||
flag.StringVar(&portStr, "port", portStr, "Port for client WireGuard interface")
|
||||
flag.BoolVar(&cfg.UseNativeInterface, "native", cfg.UseNativeInterface, "Use native WireGuard interface for client tunnels")
|
||||
flag.BoolVar(&cfg.UseNativeMainInterface, "native-main", cfg.UseNativeMainInterface, "Use native WireGuard interface for the main tunnel (instead of netstack)")
|
||||
// making this the same as above should prevent them from running together
|
||||
fs.StringVar(&cfg.NativeMainInterfaceName, "interface-main", cfg.NativeMainInterfaceName, "Name of the native main tunnel WireGuard interface (used with --native-main)")
|
||||
fs.BoolVar(&cfg.DisableClients, "disable-clients", cfg.DisableClients, "Disable clients on the WireGuard interface")
|
||||
fs.BoolVar(&cfg.DisableSSH, "disable-ssh", cfg.DisableSSH, "Disable SSH auth daemon and native SSH mode (remote auth daemon still works)")
|
||||
fs.BoolVar(&cfg.EnforceHealthcheckCert, "enforce-hc-cert", cfg.EnforceHealthcheckCert, "Enforce certificate validation for health checks (default: false, accepts any cert)")
|
||||
fs.StringVar(&cfg.DockerSocket, "docker-socket", cfg.DockerSocket, "Path or address to Docker socket (typically unix:///var/run/docker.sock)")
|
||||
fs.StringVar(&pingIntervalStr, "ping-interval", pingIntervalStr, "Interval for pinging the server (default 15s)")
|
||||
fs.StringVar(&pingTimeoutStr, "ping-timeout", pingTimeoutStr, "Timeout for each ping (default 7s)")
|
||||
fs.StringVar(&udpProxyIdleTimeoutStr, "udp-proxy-idle-timeout", udpProxyIdleTimeoutStr, "Idle timeout for UDP proxied client flows before cleanup")
|
||||
fs.StringVar(&cfg.PreferEndpoint, "prefer-endpoint", cfg.PreferEndpoint, "Prefer this endpoint for the connection (if set, will override the endpoint from the server)")
|
||||
fs.StringVar(&localEndpointInterfacesStr, "local-endpoint-interfaces", localEndpointInterfacesStr, "Comma-separated list of network interface names to restrict reported local endpoints to (default: report all interfaces)")
|
||||
fs.StringVar(&cfg.ProvisioningKey, "provisioning-key", cfg.ProvisioningKey, "One-time provisioning key used to obtain a newt ID and secret from the server")
|
||||
fs.StringVar(&cfg.NewtName, "name", cfg.NewtName, "Name for the site created during provisioning (supports {{env.VAR}} interpolation)")
|
||||
fs.StringVar(&cfg.ConfigFile, "config-file", configPath, "Path to config file (overrides CONFIG_FILE env var and default location)")
|
||||
fs.StringVar(&cfg.TLSClientCert, "tls-client-cert-file", cfg.TLSClientCert, "Path to client certificate file (PEM/DER format)")
|
||||
fs.StringVar(&cfg.TLSClientKey, "tls-client-key", cfg.TLSClientKey, "Path to client private key file (PEM/DER format)")
|
||||
flag.StringVar(&cfg.NativeMainInterfaceName, "interface-main", cfg.NativeMainInterfaceName, "Name of the native main tunnel WireGuard interface (used with --native-main)")
|
||||
flag.BoolVar(&cfg.DisableClients, "disable-clients", cfg.DisableClients, "Disable clients on the WireGuard interface")
|
||||
flag.BoolVar(&cfg.DisableSSH, "disable-ssh", cfg.DisableSSH, "Disable SSH auth daemon and native SSH mode (remote auth daemon still works)")
|
||||
flag.BoolVar(&cfg.EnforceHealthcheckCert, "enforce-hc-cert", cfg.EnforceHealthcheckCert, "Enforce certificate validation for health checks (default: false, accepts any cert)")
|
||||
flag.StringVar(&cfg.DockerSocket, "docker-socket", cfg.DockerSocket, "Path or address to Docker socket (typically unix:///var/run/docker.sock)")
|
||||
flag.StringVar(&pingIntervalStr, "ping-interval", pingIntervalStr, "Interval for pinging the server (default 15s)")
|
||||
flag.StringVar(&pingTimeoutStr, "ping-timeout", pingTimeoutStr, "Timeout for each ping (default 7s)")
|
||||
flag.StringVar(&udpProxyIdleTimeoutStr, "udp-proxy-idle-timeout", udpProxyIdleTimeoutStr, "Idle timeout for UDP proxied client flows before cleanup")
|
||||
flag.StringVar(&cfg.PreferEndpoint, "prefer-endpoint", cfg.PreferEndpoint, "Prefer this endpoint for the connection (if set, will override the endpoint from the server)")
|
||||
flag.StringVar(&cfg.ProvisioningKey, "provisioning-key", cfg.ProvisioningKey, "One-time provisioning key used to obtain a newt ID and secret from the server")
|
||||
flag.StringVar(&cfg.NewtName, "name", cfg.NewtName, "Name for the site created during provisioning (supports {{env.VAR}} interpolation)")
|
||||
flag.StringVar(&cfg.ConfigFile, "config-file", configPath, "Path to config file (overrides CONFIG_FILE env var and default location)")
|
||||
flag.StringVar(&cfg.TLSClientCert, "tls-client-cert-file", cfg.TLSClientCert, "Path to client certificate file (PEM/DER format)")
|
||||
flag.StringVar(&cfg.TLSClientKey, "tls-client-key", cfg.TLSClientKey, "Path to client private key file (PEM/DER format)")
|
||||
// Backward-compat dummy flag (auth daemon is always enabled now)
|
||||
fs.Bool("auth-daemon", false, "Enable auth daemon mode (deprecated, always enabled)")
|
||||
flag.Bool("auth-daemon", false, "Enable auth daemon mode (deprecated, always enabled)")
|
||||
|
||||
var tlsClientCAsFlag stringSlice
|
||||
fs.Var(&tlsClientCAsFlag, "tls-client-ca", "Path to CA certificate file for validating remote certificates (can be specified multiple times)")
|
||||
flag.Var(&tlsClientCAsFlag, "tls-client-ca", "Path to CA certificate file for validating remote certificates (can be specified multiple times)")
|
||||
|
||||
fs.StringVar(&cfg.TLSPrivateKey, "tls-client-cert", cfg.TLSPrivateKey, "Path to client certificate (PKCS12 format) - DEPRECATED: use --tls-client-cert-file and --tls-client-key instead")
|
||||
fs.StringVar(&dockerEnforceStr, "docker-enforce-network-validation", dockerEnforceStr, "Enforce validation of container on newt network (true or false)")
|
||||
fs.StringVar(&cfg.HealthFile, "health-file", cfg.HealthFile, "Path to health file (if unset, health file won't be written)")
|
||||
fs.StringVar(&cfg.BlueprintFile, "blueprint-file", cfg.BlueprintFile, "Path to blueprint file (if unset, no blueprint will be applied)")
|
||||
fs.StringVar(&cfg.ProvisioningBlueprintFile, "provisioning-blueprint-file", cfg.ProvisioningBlueprintFile, "Path to blueprint file applied once after a provisioning credential exchange (if unset, no provisioning blueprint will be applied)")
|
||||
fs.BoolVar(&cfg.NoCloud, "no-cloud", cfg.NoCloud, "Disable cloud failover")
|
||||
fs.BoolVar(&cfg.MetricsEnabled, "metrics", cfg.MetricsEnabled, "Enable Prometheus metrics exporter")
|
||||
fs.BoolVar(&cfg.OTLPEnabled, "otlp", cfg.OTLPEnabled, "Enable OTLP exporters (metrics/traces) to OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
fs.StringVar(&cfg.AdminAddr, "metrics-admin-addr", cfg.AdminAddr, "Admin/metrics bind address")
|
||||
fs.BoolVar(&cfg.MetricsAsyncBytes, "metrics-async-bytes", cfg.MetricsAsyncBytes, "Enable async bytes counting (background flush; lower hot path overhead)")
|
||||
fs.BoolVar(&cfg.PprofEnabled, "pprof", cfg.PprofEnabled, "Enable pprof debug endpoints on admin server")
|
||||
fs.StringVar(&cfg.Region, "region", cfg.Region, "Optional region resource attribute (also NEWT_REGION)")
|
||||
fs.StringVar(&cfg.AuthDaemonKey, "ad-pre-shared-key", cfg.AuthDaemonKey, "Pre-shared key for auth daemon authentication")
|
||||
fs.StringVar(&cfg.AuthDaemonPrincipalsFile, "ad-principals-file", cfg.AuthDaemonPrincipalsFile, "Path to the principals file for auth daemon")
|
||||
fs.StringVar(&cfg.AuthDaemonCACertPath, "ad-ca-cert-path", cfg.AuthDaemonCACertPath, "Path to the CA certificate file for auth daemon")
|
||||
fs.BoolVar(&cfg.AuthDaemonGenerateRandomPassword, "ad-generate-random-password", cfg.AuthDaemonGenerateRandomPassword, "Generate a random password for authenticated users")
|
||||
flag.StringVar(&cfg.TLSPrivateKey, "tls-client-cert", cfg.TLSPrivateKey, "Path to client certificate (PKCS12 format) - DEPRECATED: use --tls-client-cert-file and --tls-client-key instead")
|
||||
flag.StringVar(&dockerEnforceStr, "docker-enforce-network-validation", dockerEnforceStr, "Enforce validation of container on newt network (true or false)")
|
||||
flag.StringVar(&cfg.HealthFile, "health-file", cfg.HealthFile, "Path to health file (if unset, health file won't be written)")
|
||||
flag.StringVar(&cfg.BlueprintFile, "blueprint-file", cfg.BlueprintFile, "Path to blueprint file (if unset, no blueprint will be applied)")
|
||||
flag.StringVar(&cfg.ProvisioningBlueprintFile, "provisioning-blueprint-file", cfg.ProvisioningBlueprintFile, "Path to blueprint file applied once after a provisioning credential exchange (if unset, no provisioning blueprint will be applied)")
|
||||
flag.BoolVar(&cfg.NoCloud, "no-cloud", cfg.NoCloud, "Disable cloud failover")
|
||||
flag.BoolVar(&cfg.MetricsEnabled, "metrics", cfg.MetricsEnabled, "Enable Prometheus metrics exporter")
|
||||
flag.BoolVar(&cfg.OTLPEnabled, "otlp", cfg.OTLPEnabled, "Enable OTLP exporters (metrics/traces) to OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
flag.StringVar(&cfg.AdminAddr, "metrics-admin-addr", cfg.AdminAddr, "Admin/metrics bind address")
|
||||
flag.BoolVar(&cfg.MetricsAsyncBytes, "metrics-async-bytes", cfg.MetricsAsyncBytes, "Enable async bytes counting (background flush; lower hot path overhead)")
|
||||
flag.BoolVar(&cfg.PprofEnabled, "pprof", cfg.PprofEnabled, "Enable pprof debug endpoints on admin server")
|
||||
flag.StringVar(&cfg.Region, "region", cfg.Region, "Optional region resource attribute (also NEWT_REGION)")
|
||||
flag.StringVar(&cfg.AuthDaemonKey, "ad-pre-shared-key", cfg.AuthDaemonKey, "Pre-shared key for auth daemon authentication")
|
||||
flag.StringVar(&cfg.AuthDaemonPrincipalsFile, "ad-principals-file", cfg.AuthDaemonPrincipalsFile, "Path to the principals file for auth daemon")
|
||||
flag.StringVar(&cfg.AuthDaemonCACertPath, "ad-ca-cert-path", cfg.AuthDaemonCACertPath, "Path to the CA certificate file for auth daemon")
|
||||
flag.BoolVar(&cfg.AuthDaemonGenerateRandomPassword, "ad-generate-random-password", cfg.AuthDaemonGenerateRandomPassword, "Generate a random password for authenticated users")
|
||||
|
||||
version := fs.Bool("version", false, "Print the version")
|
||||
showConfig := fs.Bool("show-config", false, "Show configuration values and their sources, then exit")
|
||||
version := flag.Bool("version", false, "Print the version")
|
||||
showConfig := flag.Bool("show-config", false, "Show configuration values and their sources, then exit")
|
||||
|
||||
fs.Parse(opts.Args)
|
||||
flag.Parse()
|
||||
|
||||
// ---- post-parse processing ----
|
||||
|
||||
@@ -576,7 +517,6 @@ func Load(opts Options) (newtpkg.Config, error) {
|
||||
markCLI("blueprint-file", cfg.BlueprintFile != origBlueprintFile)
|
||||
markCLI("provisioning-blueprint-file", cfg.ProvisioningBlueprintFile != origProvBlueprintFile)
|
||||
markCLI("no-cloud", cfg.NoCloud != origNoCloud)
|
||||
markCLI("local-endpoint-interfaces", localEndpointInterfacesStr != origLocalEndpointInterfaces)
|
||||
markCLI("metrics", cfg.MetricsEnabled != origMetrics)
|
||||
markCLI("otlp", cfg.OTLPEnabled != origOTLP)
|
||||
markCLI("metrics-admin-addr", cfg.AdminAddr != origAdminAddr)
|
||||
@@ -593,16 +533,16 @@ func Load(opts Options) (newtpkg.Config, error) {
|
||||
|
||||
// Version check (exits process)
|
||||
if *version {
|
||||
fmt.Println("Newt version " + opts.Version)
|
||||
fmt.Println("Newt version " + newtVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *showConfig {
|
||||
printShowConfig(cfg, sources, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr, localEndpointInterfacesStr)
|
||||
printShowConfig(cfg, sources, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
logger.Info("Newt version %s", opts.Version)
|
||||
logger.Info("Newt version %s", newtVersion)
|
||||
|
||||
// Parse port
|
||||
if portStr != "" {
|
||||
@@ -614,19 +554,6 @@ func Load(opts Options) (newtpkg.Config, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse local endpoint interface allowlist (after flag parsing so CLI takes effect)
|
||||
if localEndpointInterfacesStr != "" {
|
||||
var names []string
|
||||
for _, n := range strings.Split(localEndpointInterfacesStr, ",") {
|
||||
if t := strings.TrimSpace(n); t != "" {
|
||||
names = append(names, t)
|
||||
}
|
||||
}
|
||||
cfg.LocalEndpointInterfaces = names
|
||||
} else {
|
||||
cfg.LocalEndpointInterfaces = nil
|
||||
}
|
||||
|
||||
// Parse MTU
|
||||
if mtuStr == "" {
|
||||
mtuStr = "1280"
|
||||
@@ -645,20 +572,16 @@ func Load(opts Options) (newtpkg.Config, error) {
|
||||
cfg.DockerEnforceNetworkValidation = false
|
||||
}
|
||||
|
||||
// Parse durations (after flag parsing so CLI flags take effect)
|
||||
// Parse durations (after flag.Parse so CLI flags take effect)
|
||||
cfg.PingInterval = parseDurationEnvOrFlag(pingIntervalStr, 15*time.Second, "PING_INTERVAL")
|
||||
cfg.PingTimeout = parseDurationEnvOrFlag(pingTimeoutStr, 7*time.Second, "PING_TIMEOUT")
|
||||
cfg.UDPProxyIdleTimeout = parseDurationEnvOrFlag(udpProxyIdleTimeoutStr, 90*time.Second, "NEWT_UDP_PROXY_IDLE_TIMEOUT")
|
||||
|
||||
if err := validateTLSConfig(cfg); err != nil {
|
||||
return newtpkg.Config{}, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
return cfg
|
||||
}
|
||||
|
||||
// printShowConfig prints the resolved configuration and the source of each value
|
||||
func printShowConfig(cfg newtpkg.Config, sources map[string]string, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr, localEndpointInterfacesStr string) {
|
||||
func printShowConfig(cfg newtpkg.Config, sources map[string]string, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr string) {
|
||||
getSource := func(key string) string {
|
||||
if s, ok := sources[key]; ok && s != "" {
|
||||
return s
|
||||
@@ -706,7 +629,6 @@ func printShowConfig(cfg newtpkg.Config, sources map[string]string, configPath,
|
||||
fmt.Printf(" native-main = %v [%s]\n", cfg.UseNativeMainInterface, getSource("native-main"))
|
||||
fmt.Printf(" interface-main = %s [%s]\n", cfg.NativeMainInterfaceName, getSource("interface-main"))
|
||||
fmt.Printf(" no-cloud = %v [%s]\n", cfg.NoCloud, getSource("no-cloud"))
|
||||
fmt.Printf(" local-endpoint-interfaces = %s [%s]\n", mask("local-endpoint-interfaces", localEndpointInterfacesStr), getSource("local-endpoint-interfaces"))
|
||||
|
||||
fmt.Println("\nLogging:")
|
||||
fmt.Printf(" log-level = %s [%s]\n", cfg.LogLevel, getSource("log-level"))
|
||||
@@ -1,67 +1,42 @@
|
||||
package newtconfig
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// resetFlags allows flag.Parse() to be called again in each test, since
|
||||
// loadNewtConfig registers flags on the global flag.CommandLine.
|
||||
func resetFlags(t *testing.T) {
|
||||
t.Helper()
|
||||
oldArgs := os.Args
|
||||
oldCommandLine := flag.CommandLine
|
||||
t.Cleanup(func() {
|
||||
os.Args = oldArgs
|
||||
flag.CommandLine = oldCommandLine
|
||||
})
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
}
|
||||
|
||||
func clearNewtEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
for _, k := range []string{
|
||||
"PANGOLIN_ENDPOINT", "NEWT_ID", "NEWT_SECRET", "DNS", "LOG_LEVEL",
|
||||
"MTU", "CONFIG_FILE", "NEWT_PROVISIONING_KEY", "NEWT_NAME",
|
||||
"DISABLE_SSH", "DISABLE_CLIENTS", "SITE_ID", "SITE_SECRET",
|
||||
"DISABLE_SSH", "DISABLE_CLIENTS",
|
||||
} {
|
||||
t.Setenv(k, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNewtConfig_SiteIDSecretEnvAliases(t *testing.T) {
|
||||
clearNewtEnv(t)
|
||||
t.Setenv("SITE_ID", "from-site-id")
|
||||
t.Setenv("SITE_SECRET", "from-site-secret")
|
||||
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", filepath.Join(t.TempDir(), "missing.json")}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.ID != "from-site-id" {
|
||||
t.Errorf("expected id from SITE_ID, got %q", cfg.ID)
|
||||
}
|
||||
if cfg.Secret != "from-site-secret" {
|
||||
t.Errorf("expected secret from SITE_SECRET, got %q", cfg.Secret)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNewtConfig_NewtIDSecretWinOverSiteAliases(t *testing.T) {
|
||||
clearNewtEnv(t)
|
||||
t.Setenv("SITE_ID", "from-site-id")
|
||||
t.Setenv("SITE_SECRET", "from-site-secret")
|
||||
t.Setenv("NEWT_ID", "from-newt-id")
|
||||
t.Setenv("NEWT_SECRET", "from-newt-secret")
|
||||
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", filepath.Join(t.TempDir(), "missing.json")}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.ID != "from-newt-id" {
|
||||
t.Errorf("expected NEWT_ID to win over SITE_ID, got %q", cfg.ID)
|
||||
}
|
||||
if cfg.Secret != "from-newt-secret" {
|
||||
t.Errorf("expected NEWT_SECRET to win over SITE_SECRET, got %q", cfg.Secret)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNewtConfig_Defaults(t *testing.T) {
|
||||
resetFlags(t)
|
||||
clearNewtEnv(t)
|
||||
os.Args = []string{"newt", "--config-file", filepath.Join(t.TempDir(), "missing.json")}
|
||||
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", filepath.Join(t.TempDir(), "missing.json")}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
cfg := loadNewtConfig()
|
||||
|
||||
if cfg.DNS != "9.9.9.9" {
|
||||
t.Errorf("expected default dns, got %q", cfg.DNS)
|
||||
@@ -75,17 +50,16 @@ func TestLoadNewtConfig_Defaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadNewtConfig_FileOverridesDefault(t *testing.T) {
|
||||
resetFlags(t)
|
||||
clearNewtEnv(t)
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
if err := os.WriteFile(configPath, []byte(`{"dns":"1.1.1.1","mtu":1300,"disableSsh":true}`), 0o644); err != nil {
|
||||
t.Fatalf("failed to write config file: %v", err)
|
||||
}
|
||||
os.Args = []string{"newt", "--config-file", configPath}
|
||||
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", configPath}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
cfg := loadNewtConfig()
|
||||
|
||||
if cfg.DNS != "1.1.1.1" {
|
||||
t.Errorf("expected dns from file, got %q", cfg.DNS)
|
||||
@@ -99,6 +73,7 @@ func TestLoadNewtConfig_FileOverridesDefault(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadNewtConfig_EnvOverridesFile(t *testing.T) {
|
||||
resetFlags(t)
|
||||
clearNewtEnv(t)
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
@@ -106,11 +81,9 @@ func TestLoadNewtConfig_EnvOverridesFile(t *testing.T) {
|
||||
t.Fatalf("failed to write config file: %v", err)
|
||||
}
|
||||
t.Setenv("DNS", "8.8.4.4")
|
||||
os.Args = []string{"newt", "--config-file", configPath}
|
||||
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", configPath}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
cfg := loadNewtConfig()
|
||||
|
||||
if cfg.DNS != "8.8.4.4" {
|
||||
t.Errorf("expected env to override file dns, got %q", cfg.DNS)
|
||||
@@ -118,6 +91,7 @@ func TestLoadNewtConfig_EnvOverridesFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadNewtConfig_CLIOverridesEnv(t *testing.T) {
|
||||
resetFlags(t)
|
||||
clearNewtEnv(t)
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
@@ -125,11 +99,9 @@ func TestLoadNewtConfig_CLIOverridesEnv(t *testing.T) {
|
||||
t.Fatalf("failed to write config file: %v", err)
|
||||
}
|
||||
t.Setenv("DNS", "8.8.4.4")
|
||||
os.Args = []string{"newt", "--config-file", configPath, "--dns", "4.2.2.2"}
|
||||
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", configPath, "--dns", "4.2.2.2"}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
cfg := loadNewtConfig()
|
||||
|
||||
if cfg.DNS != "4.2.2.2" {
|
||||
t.Errorf("expected cli to override env dns, got %q", cfg.DNS)
|
||||
@@ -137,28 +109,22 @@ func TestLoadNewtConfig_CLIOverridesEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadNewtConfig_TLSClientCAMergesAcrossSources(t *testing.T) {
|
||||
resetFlags(t)
|
||||
clearNewtEnv(t)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
caFromFile := filepath.Join(tmpDir, "file-ca.pem")
|
||||
caFromEnv := filepath.Join(tmpDir, "env-ca.pem")
|
||||
caFromCLI := filepath.Join(tmpDir, "cli-ca.pem")
|
||||
for _, ca := range []string{caFromFile, caFromEnv, caFromCLI} {
|
||||
if err := os.WriteFile(ca, []byte("test"), 0o644); err != nil {
|
||||
t.Fatalf("failed to write CA file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tmpDir, "config.json")
|
||||
if err := os.WriteFile(configPath, []byte(`{"tlsClientCa":["`+caFromFile+`"]}`), 0o644); err != nil {
|
||||
t.Fatalf("failed to write config file: %v", err)
|
||||
}
|
||||
t.Setenv("TLS_CLIENT_CAS", caFromEnv)
|
||||
os.Args = []string{"newt", "--config-file", configPath, "--tls-client-ca", caFromCLI}
|
||||
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", configPath, "--tls-client-ca", caFromCLI}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
cfg := loadNewtConfig()
|
||||
|
||||
want := map[string]bool{caFromFile: true, caFromEnv: true, caFromCLI: true}
|
||||
if len(cfg.TLSClientCAs) != len(want) {
|
||||
@@ -87,18 +87,6 @@ func attrsWithSite(extra ...attribute.KeyValue) []attribute.KeyValue {
|
||||
return attrs
|
||||
}
|
||||
|
||||
// EnsureInstruments registers this package's instruments against whichever
|
||||
// MeterProvider is globally active (the no-op provider if Init has not been
|
||||
// called), so that the Inc*/Observe* recording functions below are always
|
||||
// safe to call. Idempotent and cheap to call repeatedly or before Init - Init
|
||||
// calls it too, and OTel's global API transparently upgrades instruments
|
||||
// created this way once a real MeterProvider is later installed via
|
||||
// otel.SetMeterProvider, so calling it early does not affect metrics
|
||||
// exported once Init runs.
|
||||
func EnsureInstruments() error {
|
||||
return registerInstruments()
|
||||
}
|
||||
|
||||
func registerInstruments() error {
|
||||
var err error
|
||||
initOnce.Do(func() {
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/fosrl/newt/internal/telemetry"
|
||||
"github.com/fosrl/newt/logger"
|
||||
newtpkg "github.com/fosrl/newt/newt"
|
||||
"github.com/fosrl/newt/newtconfig"
|
||||
"github.com/fosrl/newt/updates"
|
||||
"github.com/fosrl/newt/websocket"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
@@ -63,14 +62,7 @@ func main() {
|
||||
func runNewtMain(ctx context.Context) {
|
||||
logger.Init(nil)
|
||||
|
||||
cfg, err := newtconfig.Load(newtconfig.Options{
|
||||
Args: os.Args[1:],
|
||||
Version: newtVersion,
|
||||
Platform: newtPlatform,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatal("Configuration error: %v", err)
|
||||
}
|
||||
cfg := loadNewtConfig()
|
||||
|
||||
if cfg.UseNativeMainInterface {
|
||||
if err := permissions.CheckNativeInterfacePermissions(); err != nil {
|
||||
@@ -78,6 +70,10 @@ func runNewtMain(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateTLSConfig(cfg); err != nil {
|
||||
logger.Fatal("TLS configuration error: %v", err)
|
||||
}
|
||||
|
||||
logger.Debug("Endpoint: %v", cfg.Endpoint)
|
||||
logger.Debug("Log Level: %v", cfg.LogLevel)
|
||||
logger.Debug("Docker Network Validation Enabled: %v", cfg.DockerEnforceNetworkValidation)
|
||||
|
||||
+30
-16
@@ -496,28 +496,42 @@ func (h *ICMPHandler) handleICMPPacket(id stack.TransportEndpointID, pkt *stack.
|
||||
logger.Info("ICMP Handler: Echo Request from %s to %s (ident=%d, seq=%d)",
|
||||
srcIP, dstIP, icmpHdr.Ident(), icmpHdr.Sequence())
|
||||
|
||||
// Convert to netip.Addr for subnet matching
|
||||
srcAddr, err := netip.ParseAddr(srcIP)
|
||||
if err != nil {
|
||||
logger.Debug("ICMP Handler: Failed to parse source IP %s: %v", srcIP, err)
|
||||
return false
|
||||
}
|
||||
dstAddr, err := netip.ParseAddr(dstIP)
|
||||
if err != nil {
|
||||
logger.Debug("ICMP Handler: Failed to parse dest IP %s: %v", dstIP, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Check subnet rules (use port 0 for ICMP since it doesn't have ports)
|
||||
if h.proxyHandler == nil {
|
||||
logger.Debug("ICMP Handler: No proxy handler configured")
|
||||
return false
|
||||
}
|
||||
|
||||
// This packet only reached the proxy stack because it already matched a
|
||||
// subnet rule during injection (ProxyHandler.HandleIncomingPacket), so
|
||||
// there's no need to re-run subnet matching here for permission - doing
|
||||
// so used to re-derive the DNAT target from a *fresh* rule lookup keyed
|
||||
// on dstIP, but dstIP here is ambiguous: for a loopback rewrite target
|
||||
// it's still the original (unrewritten) destination, while for a
|
||||
// non-loopback rewrite target it's already the post-DNAT address. In
|
||||
// the latter case (and always for a domain-name RewriteTo, which has no
|
||||
// subnet rule of its own for the resolved IP) that re-lookup could find
|
||||
// no rule and silently drop the ping even though the connection is
|
||||
// legitimately allowed. Instead, resolve the same way the TCP/UDP
|
||||
// handlers do: via destRewriteTable, which HandleIncomingPacket already
|
||||
// populated for this exact flow keyed by the original destination.
|
||||
matchedRule := h.proxyHandler.subnetLookup.Match(srcAddr, dstAddr, 0, header.ICMPv4ProtocolNumber)
|
||||
if matchedRule == nil {
|
||||
logger.Debug("ICMP Handler: No matching subnet rule for %s -> %s", srcIP, dstIP)
|
||||
return false
|
||||
}
|
||||
|
||||
logger.Info("ICMP Handler: Matched subnet rule for %s -> %s", srcIP, dstIP)
|
||||
|
||||
// Determine actual destination (with possible rewrite)
|
||||
actualDstIP := dstIP
|
||||
if rewrittenAddr, ok := h.proxyHandler.LookupDestinationRewrite(srcIP, dstIP, 0, uint8(header.ICMPv4ProtocolNumber)); ok {
|
||||
actualDstIP = rewrittenAddr.String()
|
||||
logger.Info("ICMP Handler: Using rewritten destination %s (original: %s)", actualDstIP, dstIP)
|
||||
if matchedRule.RewriteTo != "" {
|
||||
resolvedAddr, err := h.proxyHandler.resolveRewriteAddress(matchedRule.RewriteTo)
|
||||
if err != nil {
|
||||
logger.Info("ICMP Handler: Failed to resolve rewrite address %s: %v", matchedRule.RewriteTo, err)
|
||||
} else {
|
||||
actualDstIP = resolvedAddr.String()
|
||||
logger.Info("ICMP Handler: Using rewritten destination %s (original: %s)", actualDstIP, dstIP)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the full ICMP payload (including the data after the header)
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
package netstack2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fosrl/newt/logger"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
)
|
||||
|
||||
// buildICMPEchoRequest builds a minimal, checksummed IPv4 ICMP echo request
|
||||
// packet from src to dst.
|
||||
func buildICMPEchoRequest(t *testing.T, src, dst netip.Addr) []byte {
|
||||
t.Helper()
|
||||
|
||||
const icmpSize = header.ICMPv4MinimumSize
|
||||
totalLen := header.IPv4MinimumSize + icmpSize
|
||||
pkt := make([]byte, totalLen)
|
||||
|
||||
ip := header.IPv4(pkt)
|
||||
ip.Encode(&header.IPv4Fields{
|
||||
TotalLength: uint16(totalLen),
|
||||
TTL: 64,
|
||||
Protocol: uint8(header.ICMPv4ProtocolNumber),
|
||||
SrcAddr: tcpip.AddrFrom4(src.As4()),
|
||||
DstAddr: tcpip.AddrFrom4(dst.As4()),
|
||||
})
|
||||
ip.SetChecksum(0)
|
||||
ip.SetChecksum(^ip.CalculateChecksum())
|
||||
|
||||
icmp := header.ICMPv4(pkt[header.IPv4MinimumSize:])
|
||||
icmp.SetType(header.ICMPv4Echo)
|
||||
icmp.SetCode(0)
|
||||
icmp.SetIdent(1)
|
||||
icmp.SetSequence(1)
|
||||
icmp.SetChecksum(0)
|
||||
icmp.SetChecksum(header.ICMPv4Checksum(icmp, checksum.Checksum(icmp.Payload(), 0)))
|
||||
|
||||
return pkt
|
||||
}
|
||||
|
||||
// noopNotification is a no-op channel.Notification for tests that don't
|
||||
// care about read-availability notifications.
|
||||
type noopNotification struct{}
|
||||
|
||||
func (noopNotification) WriteNotify() {}
|
||||
|
||||
// captureLogOutput redirects the package logger to a pipe for the duration
|
||||
// of fn, and returns everything written to it. Needed here because
|
||||
// ICMPHandler.handleICMPPacket runs on its own goroutine off of
|
||||
// HandleIncomingPacket and reports its outcome only via log lines.
|
||||
func captureLogOutput(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("os.Pipe: %v", err)
|
||||
}
|
||||
|
||||
logger.SetOutput(w)
|
||||
defer logger.SetOutput(os.Stdout)
|
||||
|
||||
done := make(chan string, 1)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
done <- buf.String()
|
||||
}()
|
||||
|
||||
fn()
|
||||
|
||||
// handleICMPPacket runs asynchronously (go h.proxyPing(...)); give it a
|
||||
// moment to log its outcome before we stop capturing.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
w.Close()
|
||||
return <-done
|
||||
}
|
||||
|
||||
// A DNAT target whose RewriteTo isn't independently reachable as its own
|
||||
// destination (e.g. a resolved domain name, or - as here - just an IP with
|
||||
// no mirrored direct subnet rule) used to make ICMP echo requests get
|
||||
// silently dropped: the old ICMPHandler re-derived the DNAT target by
|
||||
// running a fresh SubnetLookup.Match() against whatever address the packet
|
||||
// carried by the time it reached the handler, which for a non-loopback
|
||||
// rewrite is already the post-DNAT address - and no rule exists for that
|
||||
// address on its own. The fix resolves the real target via
|
||||
// destRewriteTable (LookupDestinationRewrite) instead, exactly like the
|
||||
// TCP/UDP handlers already did, so this no longer depends on a mirrored
|
||||
// direct rule existing for the rewritten address.
|
||||
func TestICMPHandleIncomingPacket_DNATWithoutMirroredDirectRule(t *testing.T) {
|
||||
ph, err := NewProxyHandler(ProxyHandlerOptions{EnableICMP: true, MTU: 1500})
|
||||
if err != nil {
|
||||
t.Fatalf("NewProxyHandler: %v", err)
|
||||
}
|
||||
if err := ph.Initialize(noopNotification{}); err != nil {
|
||||
t.Fatalf("Initialize: %v", err)
|
||||
}
|
||||
defer ph.Close()
|
||||
|
||||
srcAddr := netip.MustParseAddr("10.0.0.5")
|
||||
aliasAddr := netip.MustParseAddr("10.20.20.9")
|
||||
realAddr := netip.MustParseAddr("203.0.113.50")
|
||||
|
||||
ph.AddSubnetRule(SubnetRule{
|
||||
SourcePrefix: netip.MustParsePrefix("10.0.0.0/24"),
|
||||
DestPrefix: netip.PrefixFrom(aliasAddr, 32),
|
||||
RewriteTo: realAddr.String() + "/32",
|
||||
})
|
||||
|
||||
pkt := buildICMPEchoRequest(t, srcAddr, aliasAddr)
|
||||
|
||||
var injected bool
|
||||
logs := captureLogOutput(t, func() {
|
||||
injected = ph.HandleIncomingPacket(pkt)
|
||||
})
|
||||
|
||||
if !injected {
|
||||
t.Fatal("expected ICMP echo request to be matched and injected")
|
||||
}
|
||||
|
||||
// destRewriteTable is populated by HandleIncomingPacket itself, so this
|
||||
// much holds regardless of the bug - included here to pin the mechanism
|
||||
// the fix relies on.
|
||||
got, ok := ph.LookupDestinationRewrite(srcAddr.String(), aliasAddr.String(), 0, uint8(header.ICMPv4ProtocolNumber))
|
||||
if !ok {
|
||||
t.Fatal("expected destRewriteTable to have an entry for this ICMP flow")
|
||||
}
|
||||
if got != realAddr {
|
||||
t.Fatalf("expected rewritten destination %s, got %s", realAddr, got)
|
||||
}
|
||||
|
||||
// This is the actual regression check: with the bug, handleICMPPacket
|
||||
// re-matches on the already-rewritten address, finds no rule for it,
|
||||
// and drops the echo request before ever attempting to proxy it.
|
||||
if strings.Contains(logs, "No matching subnet rule") {
|
||||
t.Errorf("ICMP handler dropped the echo request instead of proxying it; logs:\n%s", logs)
|
||||
}
|
||||
if !strings.Contains(logs, "Proxying ping from") {
|
||||
t.Errorf("expected ICMP handler to proxy the ping to the rewritten destination; logs:\n%s", logs)
|
||||
}
|
||||
if !strings.Contains(logs, realAddr.String()) {
|
||||
t.Errorf("expected logs to reference the rewritten destination %s; logs:\n%s", realAddr, logs)
|
||||
}
|
||||
}
|
||||
@@ -98,27 +98,15 @@ func interfaceScore(name string) int {
|
||||
// name of our own WireGuard/TUN interface, whose address is the tunnel IP
|
||||
// and not a useful endpoint to advertise.
|
||||
//
|
||||
// allowedInterfaces, if non-empty, restricts the result to only those
|
||||
// interface names (an allowlist), letting callers report a single known-good
|
||||
// interface instead of every candidate on the host.
|
||||
//
|
||||
// If interfaces cannot be enumerated (e.g. insufficient OS permissions),
|
||||
// an info message is logged and an empty slice is returned.
|
||||
func GetLocalEndpoints(port uint16, excludeInterface string, allowedInterfaces []string) []string {
|
||||
func GetLocalEndpoints(port uint16, excludeInterface string) []string {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
logger.Info("Unable to enumerate local network interfaces, localEndpoints will not be reported: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var allowedSet map[string]struct{}
|
||||
if len(allowedInterfaces) > 0 {
|
||||
allowedSet = make(map[string]struct{}, len(allowedInterfaces))
|
||||
for _, name := range allowedInterfaces {
|
||||
allowedSet[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
type candidate struct {
|
||||
score int
|
||||
ip string
|
||||
@@ -129,11 +117,6 @@ func GetLocalEndpoints(port uint16, excludeInterface string, allowedInterfaces [
|
||||
if excludeInterface != "" && iface.Name == excludeInterface {
|
||||
continue
|
||||
}
|
||||
if allowedSet != nil {
|
||||
if _, ok := allowedSet[iface.Name]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ func (n *Newt) setupClients() {
|
||||
n.client,
|
||||
n.config.DNS,
|
||||
n.config.UseNativeInterface,
|
||||
n.config.LocalEndpointInterfaces,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to create WireGuard service: %v", err)
|
||||
|
||||
@@ -29,7 +29,6 @@ type Config struct {
|
||||
NativeMainInterfaceName string
|
||||
NoCloud bool
|
||||
PreferEndpoint string
|
||||
LocalEndpointInterfaces []string
|
||||
|
||||
// Timing
|
||||
PingInterval time.Duration
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
wgclients "github.com/fosrl/newt/clients"
|
||||
"github.com/fosrl/newt/docker"
|
||||
"github.com/fosrl/newt/healthcheck"
|
||||
"github.com/fosrl/newt/internal/telemetry"
|
||||
"github.com/fosrl/newt/logger"
|
||||
"github.com/fosrl/newt/nativessh"
|
||||
"github.com/fosrl/newt/proxy"
|
||||
@@ -83,14 +82,6 @@ type Newt struct {
|
||||
func Init(ctx context.Context, cfg Config) (*Newt, error) {
|
||||
n := &Newt{config: cfg}
|
||||
|
||||
// Metric-recording calls throughout the websocket/proxy/tunnel code are
|
||||
// unconditional, not gated on cfg.MetricsEnabled, so the instruments
|
||||
// must exist even when the caller never sets up telemetry exporters
|
||||
// (e.g. an embedder that only calls Init/Start, like the Pangolin CLI).
|
||||
if err := telemetry.EnsureInstruments(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize telemetry instruments: %w", err)
|
||||
}
|
||||
|
||||
n.loggerLevel = util.ParseLogLevel(cfg.LogLevel)
|
||||
|
||||
if !cfg.DisableSSH {
|
||||
|
||||
@@ -342,17 +342,6 @@ func TestParseTargetStringNetDialCompatibility(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestPingNilNetstackReturnsError is the regression guard for fosrl/newt#439:
|
||||
// a still-running ping goroutine calling ping() with a nil *netstack.Net
|
||||
// (after closeWgTunnel clears it during teardown/reconnect) must return an
|
||||
// error instead of panicking with a nil pointer dereference.
|
||||
func TestPingNilNetstackReturnsError(t *testing.T) {
|
||||
_, err := ping(nil, "127.0.0.1", 100*time.Millisecond)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when tnet is nil, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestShouldFireRecovery is the regression guard for the broken trigger gate
|
||||
// that prevented data-plane recovery from ever firing under default settings
|
||||
// (fosrl/newt#284, #310, pangolin#1004).
|
||||
|
||||
@@ -47,10 +47,6 @@ func pingNative(dst string, timeout time.Duration) (time.Duration, error) {
|
||||
}
|
||||
|
||||
func ping(tnet *netstack.Net, dst string, timeout time.Duration) (time.Duration, error) {
|
||||
if tnet == nil {
|
||||
return 0, fmt.Errorf("netstack not initialized")
|
||||
}
|
||||
|
||||
socket, err := tnet.Dial("ping4", dst)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create ICMP socket: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user