mirror of
https://github.com/fosrl/newt.git
synced 2026-09-18 11:59:06 +02:00
@@ -34,6 +34,14 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: AI Disclosure
|
||||
description: |
|
||||
If you used AI to help write this issue, please disclose it here. This is important for transparency and helps maintain the integrity of the issue tracking process.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
|
||||
@@ -4,6 +4,10 @@ perpetual license to use, modify, and redistribute these contributions under any
|
||||
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
|
||||
represent that I have the right to grant this license for all contributed content.
|
||||
|
||||
## AI Disclosure
|
||||
|
||||
> Please disclose how AI was used in this pull request. The use of AI does not preclude this from being merged but is an important factor in how we review your request.
|
||||
|
||||
## Description
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.25
|
||||
1.26
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# FROM golang:1.25-alpine AS builder
|
||||
FROM public.ecr.aws/docker/library/golang:1.25-alpine AS builder
|
||||
FROM public.ecr.aws/docker/library/golang:1.26-alpine AS builder
|
||||
|
||||
# Install git and ca-certificates
|
||||
RUN apk --no-cache add ca-certificates git tzdata
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
[](https://github.com/fosrl/newt/blob/main/LICENSE)
|
||||
[](https://goreportcard.com/report/github.com/fosrl/newt)
|
||||
|
||||
> [!NOTE]
|
||||
> Newt is being phased out in favor of the [Pangolin CLI](https://github.com/fosrl/cli). Use `pangolin site up`
|
||||
|
||||
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.
|
||||
|
||||
### Installation and Documentation
|
||||
|
||||
+29
-27
@@ -98,20 +98,21 @@ type PeerReading struct {
|
||||
}
|
||||
|
||||
type WireGuardService struct {
|
||||
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
|
||||
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
|
||||
// Netstack fields
|
||||
tun tun.Device
|
||||
tnet *netstack2.Net
|
||||
@@ -154,7 +155,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) (*WireGuardService, error) {
|
||||
func NewWireGuardService(interfaceName string, port uint16, mtu int, host string, newtId string, wsClient *websocket.Client, dns string, useNativeInterface bool, localEndpointInterfaces []string) (*WireGuardService, error) {
|
||||
key, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate private key: %v", err)
|
||||
@@ -195,17 +196,18 @@ func NewWireGuardService(interfaceName string, port uint16, mtu int, host string
|
||||
dnsAddrs := []netip.Addr{netip.MustParseAddr(dns)}
|
||||
|
||||
service := &WireGuardService{
|
||||
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,
|
||||
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,
|
||||
}
|
||||
|
||||
// Create the holepunch manager
|
||||
@@ -532,7 +534,7 @@ func (s *WireGuardService) LoadRemoteConfig() error {
|
||||
"publicKey": s.key.PublicKey().String(),
|
||||
"port": s.Port,
|
||||
"chainId": chainId,
|
||||
"localEndpoints": network.GetLocalEndpoints(s.Port, s.interfaceName),
|
||||
"localEndpoints": network.GetLocalEndpoints(s.Port, s.interfaceName, s.localEndpointInterfaces),
|
||||
}, 2*time.Second)
|
||||
|
||||
logger.Debug("Requesting WireGuard configuration from remote server")
|
||||
|
||||
@@ -87,6 +87,18 @@ 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,6 +16,7 @@ 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"
|
||||
@@ -62,7 +63,18 @@ func main() {
|
||||
func runNewtMain(ctx context.Context) {
|
||||
logger.Init(nil)
|
||||
|
||||
cfg := loadNewtConfig()
|
||||
cfg, err := newtconfig.Load(newtconfig.Options{
|
||||
Args: os.Args[1:],
|
||||
Version: newtVersion,
|
||||
Agent: "newt",
|
||||
AgentVersion: newtVersion,
|
||||
Platform: newtPlatform,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatal("Configuration error: %v", err)
|
||||
}
|
||||
|
||||
logger.Info("Newt version %s", cfg.Version)
|
||||
|
||||
if cfg.UseNativeMainInterface {
|
||||
if err := permissions.CheckNativeInterfacePermissions(); err != nil {
|
||||
@@ -70,10 +82,6 @@ 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)
|
||||
@@ -190,6 +198,7 @@ func runNewtMain(ctx context.Context) {
|
||||
CurrentVersion: newtVersion,
|
||||
Platform: newtPlatform,
|
||||
TLSConfig: selfUpdateTLS,
|
||||
Agent: "newt",
|
||||
}); err != nil {
|
||||
if errors.Is(err, updates.ErrAutoUpdateUnsupportedInOfficialContainer) {
|
||||
logger.Debug("checkAndSelfUpdate: auto-update skipped: %v", err)
|
||||
@@ -199,7 +208,8 @@ func runNewtMain(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(2 * time.Minute)
|
||||
time.Sleep(2 * time.Minute) // for production
|
||||
// time.Sleep(10 * time.Second) // for testing, check for updates after 10 seconds
|
||||
doUpdate()
|
||||
ticker := time.NewTicker(6 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
+16
-30
@@ -496,42 +496,28 @@ 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
|
||||
}
|
||||
|
||||
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)
|
||||
// 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.
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// Get the full ICMP payload (including the data after the header)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
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,15 +98,27 @@ 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) []string {
|
||||
func GetLocalEndpoints(port uint16, excludeInterface string, allowedInterfaces []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
|
||||
@@ -117,6 +129,11 @@ func GetLocalEndpoints(port uint16, excludeInterface string) []string {
|
||||
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,6 +42,7 @@ 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)
|
||||
|
||||
+5
-2
@@ -5,8 +5,10 @@ import "time"
|
||||
// Config holds all runtime configuration for a Newt instance.
|
||||
type Config struct {
|
||||
// Build info
|
||||
Version string
|
||||
Platform string
|
||||
Version string
|
||||
Platform string
|
||||
Agent string
|
||||
AgentVersion string
|
||||
|
||||
// Logging
|
||||
LogLevel string
|
||||
@@ -29,6 +31,7 @@ type Config struct {
|
||||
NativeMainInterfaceName string
|
||||
NoCloud bool
|
||||
PreferEndpoint string
|
||||
LocalEndpointInterfaces []string
|
||||
|
||||
// Timing
|
||||
PingInterval time.Duration
|
||||
|
||||
+8
-4
@@ -145,10 +145,12 @@ func (n *Newt) registerHandlers(ctx context.Context) {
|
||||
chainId := generateChainId()
|
||||
n.pendingRegisterChainId = chainId
|
||||
n.stopFunc = n.client.SendMessageInterval(topicWGRegister, map[string]interface{}{
|
||||
"publicKey": n.publicKey.String(),
|
||||
"pingResults": pingResults,
|
||||
"newtVersion": n.config.Version,
|
||||
"chainId": chainId,
|
||||
"publicKey": n.publicKey.String(),
|
||||
"pingResults": pingResults,
|
||||
"newtVersion": n.config.Version,
|
||||
"agent": n.config.Agent,
|
||||
"agentVersion": n.config.AgentVersion,
|
||||
"chainId": chainId,
|
||||
}, 2*time.Second)
|
||||
|
||||
logger.Debug("Sent exit node ping results to cloud for selection: pingResults=%+v", pingResults)
|
||||
@@ -894,6 +896,8 @@ func (n *Newt) registerHandlers(ctx context.Context) {
|
||||
if err := n.client.SendMessage(topicWGRegister, map[string]interface{}{
|
||||
"publicKey": n.publicKey.String(),
|
||||
"newtVersion": n.config.Version,
|
||||
"agent": n.config.Agent,
|
||||
"agentVersion": n.config.AgentVersion,
|
||||
"backwardsCompatible": true,
|
||||
"chainId": bcChainId,
|
||||
}); err != nil {
|
||||
|
||||
@@ -13,6 +13,7 @@ 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"
|
||||
@@ -82,6 +83,14 @@ 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,6 +342,17 @@ 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,6 +47,10 @@ 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)
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
package main
|
||||
// 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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -53,11 +59,12 @@ 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"`
|
||||
UseNativeInterface *bool `json:"native"`
|
||||
UseNativeMainInterface *bool `json:"nativeMain"`
|
||||
NativeMainInterfaceName *string `json:"interfaceMain"`
|
||||
NoCloud *bool `json:"noCloud"`
|
||||
PreferEndpoint *string `json:"preferEndpoint"`
|
||||
LocalEndpointInterfaces []string `json:"localEndpointInterfaces"`
|
||||
|
||||
PingInterval *string `json:"pingInterval"`
|
||||
PingTimeout *string `json:"pingTimeout"`
|
||||
@@ -92,10 +99,16 @@ 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 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 {
|
||||
// the same precedence as every other setting: CLI > env > caller default >
|
||||
// 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.
|
||||
//
|
||||
// defaultConfigFile, when non-empty, overrides the standalone newt-client OS
|
||||
// default below - it lets a caller that embeds newtconfig as a library (such
|
||||
// as the Pangolin CLI) point new installs at its own config directory
|
||||
// instead of newt's, while --config-file/CONFIG_FILE still take precedence.
|
||||
func resolveConfigFilePath(args []string, defaultConfigFile string) string {
|
||||
for i, a := range args {
|
||||
if a == "--config-file" || a == "-config-file" {
|
||||
if i+1 < len(args) {
|
||||
@@ -114,6 +127,15 @@ func resolveConfigFilePath(args []string) string {
|
||||
return v
|
||||
}
|
||||
|
||||
if defaultConfigFile != "" {
|
||||
if dir := filepath.Dir(defaultConfigFile); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
fmt.Printf("Warning: Failed to create config directory: %v\n", err)
|
||||
}
|
||||
}
|
||||
return defaultConfigFile
|
||||
}
|
||||
|
||||
var configDir string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
@@ -180,6 +202,34 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
// applyEnvBoolAlias behaves like applyEnvBool, but checks a preferred env var
|
||||
// first and only falls back to an alias name when the preferred one is unset.
|
||||
func applyEnvBoolAlias(dst *bool, envName, aliasEnvName, key string, sources map[string]string) {
|
||||
if v := os.Getenv(envName); v != "" {
|
||||
*dst = v == "true"
|
||||
sources[key] = string(sourceEnv)
|
||||
return
|
||||
}
|
||||
if v := os.Getenv(aliasEnvName); v != "" {
|
||||
*dst = v == "true"
|
||||
sources[key] = string(sourceEnv)
|
||||
}
|
||||
}
|
||||
|
||||
// validateTLSConfig validates that TLS config fields are consistent and that
|
||||
// referenced files exist.
|
||||
func validateTLSConfig(cfg newtpkg.Config) error {
|
||||
@@ -234,14 +284,37 @@ func parseDurationEnvOrFlag(s string, defaultVal time.Duration, label string) ti
|
||||
return d
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
Agent string
|
||||
AgentVersion string
|
||||
Platform string
|
||||
// DefaultConfigFile overrides the OS-default config file path used when
|
||||
// neither --config-file nor CONFIG_FILE is set. Leave empty to use the
|
||||
// standalone newt binary's own default (e.g.
|
||||
// ~/.config/newt-client/config.json on Linux); callers embedding
|
||||
// newtconfig as a library (such as the Pangolin CLI) should set this to
|
||||
// a path under their own config directory instead.
|
||||
DefaultConfigFile 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) {
|
||||
sources := make(map[string]string)
|
||||
|
||||
configPath := resolveConfigFilePath(os.Args[1:])
|
||||
configPath := resolveConfigFilePath(opts.Args, opts.DefaultConfigFile)
|
||||
fileCfg, err := loadFileSettings(configPath)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to load config file: %v", err)
|
||||
@@ -249,13 +322,15 @@ func loadNewtConfig() newtpkg.Config {
|
||||
|
||||
// ---- defaults ----
|
||||
cfg := newtpkg.Config{
|
||||
Version: newtVersion,
|
||||
Platform: newtPlatform,
|
||||
Version: opts.Version,
|
||||
Platform: opts.Platform,
|
||||
Agent: opts.Agent,
|
||||
AgentVersion: opts.AgentVersion,
|
||||
|
||||
DNS: "9.9.9.9",
|
||||
LogLevel: "INFO",
|
||||
InterfaceName: "newt",
|
||||
NativeMainInterfaceName: "newt",
|
||||
InterfaceName: "pangolin",
|
||||
NativeMainInterfaceName: "pangolin",
|
||||
AuthDaemonPrincipalsFile: "/var/run/auth-daemon/principals",
|
||||
AuthDaemonCACertPath: "/etc/ssh/ca.pem",
|
||||
AdminAddr: "127.0.0.1:2112",
|
||||
@@ -294,6 +369,10 @@ func loadNewtConfig() newtpkg.Config {
|
||||
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)
|
||||
@@ -335,10 +414,14 @@ func loadNewtConfig() newtpkg.Config {
|
||||
|
||||
// ---- layer 2: environment variables ----
|
||||
applyEnvStr(&cfg.Endpoint, "PANGOLIN_ENDPOINT", "endpoint", 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)
|
||||
// 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)
|
||||
applyEnvStrAlias(&cfg.ProvisioningKey, "NEWT_PROVISIONING_KEY", "SITE_PROVISIONING_KEY", "provisioning-key", sources)
|
||||
applyEnvStrAlias(&cfg.NewtName, "NEWT_NAME", "SITE_NAME", "name", sources)
|
||||
|
||||
applyEnvStr(&cfg.DNS, "DNS", "dns", sources)
|
||||
applyEnvStr(&cfg.LogLevel, "LOG_LEVEL", "log-level", sources)
|
||||
@@ -351,10 +434,20 @@ func loadNewtConfig() newtpkg.Config {
|
||||
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)
|
||||
applyEnvStr(&udpProxyIdleTimeoutStr, "NEWT_UDP_PROXY_IDLE_TIMEOUT", "udp-proxy-idle-timeout", sources)
|
||||
applyEnvStrAlias(&udpProxyIdleTimeoutStr, "NEWT_UDP_PROXY_IDLE_TIMEOUT", "SITE_UDP_PROXY_IDLE_TIMEOUT", "udp-proxy-idle-timeout", sources)
|
||||
|
||||
applyEnvBool(&cfg.DisableClients, "DISABLE_CLIENTS", "disable-clients", sources)
|
||||
applyEnvBool(&cfg.DisableSSH, "DISABLE_SSH", "disable-ssh", sources)
|
||||
@@ -391,7 +484,11 @@ func loadNewtConfig() newtpkg.Config {
|
||||
sources["tls-client-cert"] = sources["tls-client-cert-file"]
|
||||
}
|
||||
|
||||
if metricsEnabledEnv := os.Getenv("NEWT_METRICS_PROMETHEUS_ENABLED"); metricsEnabledEnv != "" {
|
||||
metricsEnabledEnv := os.Getenv("NEWT_METRICS_PROMETHEUS_ENABLED")
|
||||
if metricsEnabledEnv == "" {
|
||||
metricsEnabledEnv = os.Getenv("SITE_METRICS_PROMETHEUS_ENABLED")
|
||||
}
|
||||
if metricsEnabledEnv != "" {
|
||||
if v, err := strconv.ParseBool(metricsEnabledEnv); err == nil {
|
||||
cfg.MetricsEnabled = v
|
||||
} else {
|
||||
@@ -399,11 +496,11 @@ func loadNewtConfig() newtpkg.Config {
|
||||
}
|
||||
sources["metrics"] = string(sourceEnv)
|
||||
}
|
||||
applyEnvBool(&cfg.OTLPEnabled, "NEWT_METRICS_OTLP_ENABLED", "otlp", sources)
|
||||
applyEnvStr(&cfg.AdminAddr, "NEWT_ADMIN_ADDR", "metrics-admin-addr", sources)
|
||||
applyEnvStr(&cfg.Region, "NEWT_REGION", "region", sources)
|
||||
applyEnvBool(&cfg.MetricsAsyncBytes, "NEWT_METRICS_ASYNC_BYTES", "metrics-async-bytes", sources)
|
||||
applyEnvBool(&cfg.PprofEnabled, "NEWT_PPROF_ENABLED", "pprof", sources)
|
||||
applyEnvBoolAlias(&cfg.OTLPEnabled, "NEWT_METRICS_OTLP_ENABLED", "SITE_METRICS_OTLP_ENABLED", "otlp", sources)
|
||||
applyEnvStrAlias(&cfg.AdminAddr, "NEWT_ADMIN_ADDR", "SITE_ADMIN_ADDR", "metrics-admin-addr", sources)
|
||||
applyEnvStrAlias(&cfg.Region, "NEWT_REGION", "SITE_REGION", "region", sources)
|
||||
applyEnvBoolAlias(&cfg.MetricsAsyncBytes, "NEWT_METRICS_ASYNC_BYTES", "SITE_METRICS_ASYNC_BYTES", "metrics-async-bytes", sources)
|
||||
applyEnvBoolAlias(&cfg.PprofEnabled, "NEWT_PPROF_ENABLED", "SITE_PPROF_ENABLED", "pprof", sources)
|
||||
|
||||
// ---- layer 3: CLI flags (always registered; default = file/env-resolved value) ----
|
||||
origEndpoint, origID, origSecret := cfg.Endpoint, cfg.ID, cfg.Secret
|
||||
@@ -416,64 +513,69 @@ func loadNewtConfig() newtpkg.Config {
|
||||
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
|
||||
|
||||
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)")
|
||||
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)")
|
||||
// making this the same as above should prevent them from running together
|
||||
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)")
|
||||
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)")
|
||||
// Backward-compat dummy flag (auth daemon is always enabled now)
|
||||
flag.Bool("auth-daemon", false, "Enable auth daemon mode (deprecated, always enabled)")
|
||||
fs.Bool("auth-daemon", false, "Enable auth daemon mode (deprecated, always enabled)")
|
||||
|
||||
var tlsClientCAsFlag stringSlice
|
||||
flag.Var(&tlsClientCAsFlag, "tls-client-ca", "Path to CA certificate file for validating remote certificates (can be specified multiple times)")
|
||||
fs.Var(&tlsClientCAsFlag, "tls-client-ca", "Path to CA certificate file for validating remote certificates (can be specified multiple times)")
|
||||
|
||||
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")
|
||||
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")
|
||||
|
||||
version := flag.Bool("version", false, "Print the version")
|
||||
showConfig := flag.Bool("show-config", false, "Show configuration values and their sources, then exit")
|
||||
version := fs.Bool("version", false, "Print the version")
|
||||
showConfig := fs.Bool("show-config", false, "Show configuration values and their sources, then exit")
|
||||
|
||||
flag.Parse()
|
||||
fs.Parse(opts.Args)
|
||||
|
||||
// ---- post-parse processing ----
|
||||
|
||||
@@ -517,6 +619,7 @@ func loadNewtConfig() newtpkg.Config {
|
||||
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)
|
||||
@@ -533,17 +636,15 @@ func loadNewtConfig() newtpkg.Config {
|
||||
|
||||
// Version check (exits process)
|
||||
if *version {
|
||||
fmt.Println("Newt version " + newtVersion)
|
||||
fmt.Println("Newt version " + opts.Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *showConfig {
|
||||
printShowConfig(cfg, sources, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr)
|
||||
printShowConfig(cfg, sources, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr, localEndpointInterfacesStr)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
logger.Info("Newt version %s", newtVersion)
|
||||
|
||||
// Parse port
|
||||
if portStr != "" {
|
||||
portInt, err := strconv.Atoi(portStr)
|
||||
@@ -554,6 +655,19 @@ func loadNewtConfig() newtpkg.Config {
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
@@ -572,16 +686,20 @@ func loadNewtConfig() newtpkg.Config {
|
||||
cfg.DockerEnforceNetworkValidation = false
|
||||
}
|
||||
|
||||
// Parse durations (after flag.Parse so CLI flags take effect)
|
||||
// Parse durations (after flag parsing 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")
|
||||
|
||||
return cfg
|
||||
if err := validateTLSConfig(cfg); err != nil {
|
||||
return newtpkg.Config{}, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// 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 string) {
|
||||
func printShowConfig(cfg newtpkg.Config, sources map[string]string, configPath, mtuStr, portStr, pingIntervalStr, pingTimeoutStr, udpProxyIdleTimeoutStr, dockerEnforceStr, localEndpointInterfacesStr string) {
|
||||
getSource := func(key string) string {
|
||||
if s, ok := sources[key]; ok && s != "" {
|
||||
return s
|
||||
@@ -629,6 +747,7 @@ 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,42 +1,67 @@
|
||||
package main
|
||||
package newtconfig
|
||||
|
||||
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",
|
||||
"DISABLE_SSH", "DISABLE_CLIENTS", "SITE_ID", "SITE_SECRET",
|
||||
} {
|
||||
t.Setenv(k, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNewtConfig_Defaults(t *testing.T) {
|
||||
resetFlags(t)
|
||||
func TestLoadNewtConfig_SiteIDSecretEnvAliases(t *testing.T) {
|
||||
clearNewtEnv(t)
|
||||
os.Args = []string{"newt", "--config-file", filepath.Join(t.TempDir(), "missing.json")}
|
||||
t.Setenv("SITE_ID", "from-site-id")
|
||||
t.Setenv("SITE_SECRET", "from-site-secret")
|
||||
|
||||
cfg := loadNewtConfig()
|
||||
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) {
|
||||
clearNewtEnv(t)
|
||||
|
||||
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.DNS != "9.9.9.9" {
|
||||
t.Errorf("expected default dns, got %q", cfg.DNS)
|
||||
@@ -50,16 +75,17 @@ 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 := loadNewtConfig()
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", configPath}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.DNS != "1.1.1.1" {
|
||||
t.Errorf("expected dns from file, got %q", cfg.DNS)
|
||||
@@ -73,7 +99,6 @@ func TestLoadNewtConfig_FileOverridesDefault(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadNewtConfig_EnvOverridesFile(t *testing.T) {
|
||||
resetFlags(t)
|
||||
clearNewtEnv(t)
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
@@ -81,9 +106,11 @@ 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 := loadNewtConfig()
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", configPath}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.DNS != "8.8.4.4" {
|
||||
t.Errorf("expected env to override file dns, got %q", cfg.DNS)
|
||||
@@ -91,7 +118,6 @@ func TestLoadNewtConfig_EnvOverridesFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadNewtConfig_CLIOverridesEnv(t *testing.T) {
|
||||
resetFlags(t)
|
||||
clearNewtEnv(t)
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
@@ -99,9 +125,11 @@ 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 := loadNewtConfig()
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", configPath, "--dns", "4.2.2.2"}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.DNS != "4.2.2.2" {
|
||||
t.Errorf("expected cli to override env dns, got %q", cfg.DNS)
|
||||
@@ -109,22 +137,28 @@ 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 := loadNewtConfig()
|
||||
cfg, err := Load(Options{Args: []string{"--config-file", configPath, "--tls-client-ca", caFromCLI}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
want := map[string]bool{caFromFile: true, caFromEnv: true, caFromCLI: true}
|
||||
if len(cfg.TLSClientCAs) != len(want) {
|
||||
@@ -143,15 +177,15 @@ func TestResolveConfigFilePath_Precedence(t *testing.T) {
|
||||
|
||||
// CLI flag wins over env.
|
||||
t.Setenv("CONFIG_FILE", "/env/path/config.json")
|
||||
if got := resolveConfigFilePath([]string{"--config-file", "/cli/path/config.json"}); got != "/cli/path/config.json" {
|
||||
if got := resolveConfigFilePath([]string{"--config-file", "/cli/path/config.json"}, ""); got != "/cli/path/config.json" {
|
||||
t.Errorf("expected cli path to win, got %q", got)
|
||||
}
|
||||
if got := resolveConfigFilePath([]string{"--config-file=/cli/eq/config.json"}); got != "/cli/eq/config.json" {
|
||||
if got := resolveConfigFilePath([]string{"--config-file=/cli/eq/config.json"}, ""); got != "/cli/eq/config.json" {
|
||||
t.Errorf("expected cli = path to win, got %q", got)
|
||||
}
|
||||
|
||||
// Env wins over default when no CLI flag given.
|
||||
if got := resolveConfigFilePath([]string{}); got != "/env/path/config.json" {
|
||||
if got := resolveConfigFilePath([]string{}, ""); got != "/env/path/config.json" {
|
||||
t.Errorf("expected env path, got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@ type SelfUpdateConfig struct {
|
||||
Platform string
|
||||
// TLSConfig is an optional TLS configuration for the HTTP client (may be nil).
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// cli or newt depending on where we are
|
||||
Agent string
|
||||
}
|
||||
|
||||
// versionResponse mirrors the JSON returned by POST /api/v1/auth/newt/version
|
||||
@@ -156,6 +159,7 @@ func CheckAndSelfUpdate(cfg SelfUpdateConfig) error {
|
||||
"newtId": cfg.NewtID,
|
||||
"secret": cfg.Secret,
|
||||
"platform": plat,
|
||||
"agent": cfg.Agent,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal version request: %w", err)
|
||||
@@ -211,7 +215,7 @@ func CheckAndSelfUpdate(cfg SelfUpdateConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debug("checkAndSelfUpdate: update available %s → %s", cfg.CurrentVersion, verResp.Data.LatestVersion)
|
||||
logger.Debug("checkAndSelfUpdate: newt package update available %s → %s", cfg.CurrentVersion, verResp.Data.LatestVersion)
|
||||
|
||||
// --- Pre-download: verify we can write to the binary's directory ---
|
||||
// Do this before downloading so a permission failure doesn't waste bandwidth.
|
||||
@@ -225,7 +229,7 @@ func CheckAndSelfUpdate(cfg SelfUpdateConfig) error {
|
||||
_ = os.Remove(writeTestFile.Name())
|
||||
|
||||
// --- Step 2: Download the new binary ---
|
||||
logger.Debug("checkAndSelfUpdate: beginning download of new binary")
|
||||
logger.Debug("checkAndSelfUpdate: beginning download of new binary from %s", verResp.Data.DownloadUrl)
|
||||
dlCtx, dlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer dlCancel()
|
||||
|
||||
|
||||
+1
-5
@@ -133,11 +133,7 @@ func CheckForUpdate(owner, repo, currentVersion string) error {
|
||||
|
||||
// Check if update is available
|
||||
if currentVer.isNewer(latestVer) {
|
||||
releaseNotes := componentVersion.ReleaseNotes
|
||||
if releaseNotes == "" {
|
||||
releaseNotes = "curl -fsSL https://static.pangolin.net/get-newt.sh | bash"
|
||||
}
|
||||
printUpdateBanner(currentVer.String(), latestVer.String(), releaseNotes)
|
||||
printUpdateBanner(currentVer.String(), latestVer.String(), "curl -fsSL https://static.pangolin.net/get-newt.sh | bash")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user