fix: auto-detect primary IP for admin init --bind default

Bare `spx admin init` used a 127.0.0.1 default that kept awsgw on loopback
only, so system VMs on br-mgmt could never reach it. Auto-detect the
default-route source IPv4 instead, which keeps TLS cert SANs specific
(still not a wildcard). Dev-install scripts now pass --bind 127.0.0.1
explicitly to preserve loopback-only behavior on laptops.

Closes mulga-siv-7.
This commit is contained in:
joshsiv-mulga
2026-04-21 21:20:38 +10:00
parent 74b6e2dc2a
commit b88dfdfdd9
6 changed files with 46 additions and 4 deletions
+10 -1
View File
@@ -239,7 +239,7 @@ func init() {
adminInitCmd.Flags().Int("nodes", 3, "Number of nodes to expect for cluster") adminInitCmd.Flags().Int("nodes", 3, "Number of nodes to expect for cluster")
adminInitCmd.Flags().String("host", "", "Leader node to join (if not specified, tries multicast discovery)") adminInitCmd.Flags().String("host", "", "Leader node to join (if not specified, tries multicast discovery)")
adminInitCmd.Flags().Int("port", 4432, "Port to bind cluster services on") adminInitCmd.Flags().Int("port", 4432, "Port to bind cluster services on")
adminInitCmd.Flags().String("bind", "127.0.0.1", "IP address to bind services to (e.g., 10.11.12.1 for multi-node)") adminInitCmd.Flags().String("bind", "", "IP address to bind services to (default: auto-detect primary non-loopback IPv4; e.g., 10.11.12.1 for multi-node)")
adminInitCmd.Flags().String("cluster-bind", "", "IP address to bind NATS cluster services to (e.g., 10.11.12.1 for multi-node)") adminInitCmd.Flags().String("cluster-bind", "", "IP address to bind NATS cluster services to (e.g., 10.11.12.1 for multi-node)")
adminInitCmd.Flags().String("cluster-routes", "", "NATS cluster hosts for routing specify multiple with comma (e.g., 10.11.12.1:4248,10.11.12.2:4248 for multi-node)") adminInitCmd.Flags().String("cluster-routes", "", "NATS cluster hosts for routing specify multiple with comma (e.g., 10.11.12.1:4248,10.11.12.2:4248 for multi-node)")
adminInitCmd.Flags().String("predastore-nodes", "", "Comma-separated IPs for multi-node Predastore cluster (e.g., 10.11.12.1,10.11.12.2,10.11.12.3). Requires >= 3 nodes.") adminInitCmd.Flags().String("predastore-nodes", "", "Comma-separated IPs for multi-node Predastore cluster (e.g., 10.11.12.1,10.11.12.2,10.11.12.3). Requires >= 3 nodes.")
@@ -607,6 +607,15 @@ func runAdminInit(cmd *cobra.Command, args []string) {
nodes, _ := cmd.Flags().GetInt("nodes") nodes, _ := cmd.Flags().GetInt("nodes")
port, _ := cmd.Flags().GetInt("port") port, _ := cmd.Flags().GetInt("port")
bindIP, _ := cmd.Flags().GetString("bind") bindIP, _ := cmd.Flags().GetString("bind")
if bindIP == "" {
bindIP = admin.DiscoverPrimaryIP()
if bindIP == "" {
fmt.Fprintf(os.Stderr, "❌ Error: could not auto-detect primary IP. Pass --bind explicitly.\n")
fmt.Fprintf(os.Stderr, " Detected local IPs: %v\n", admin.DiscoverLocalIPs())
os.Exit(1)
}
fmt.Printf("Auto-detected --bind=%s (pass --bind to override)\n", bindIP)
}
clusterBind, _ := cmd.Flags().GetString("cluster-bind") clusterBind, _ := cmd.Flags().GetString("cluster-bind")
clusterRoutesStr, _ := cmd.Flags().GetString("cluster-routes") clusterRoutesStr, _ := cmd.Flags().GetString("cluster-routes")
var clusterRoutes []string var clusterRoutes []string
+1 -1
View File
@@ -58,7 +58,7 @@ echo "=== Setting up OVN ==="
sudo /usr/local/share/spinifex/setup-ovn.sh --management sudo /usr/local/share/spinifex/setup-ovn.sh --management
echo "=== Initializing ===" echo "=== Initializing ==="
sudo spx admin init --force --region ap-southeast-2 --az ap-southeast-2a --node node1 --nodes 1 sudo spx admin init --force --bind 127.0.0.1 --region ap-southeast-2 --az ap-southeast-2a --node node1 --nodes 1
echo "=== Installing CA certificate ===" echo "=== Installing CA certificate ==="
sudo cp /etc/spinifex/ca.pem /usr/local/share/ca-certificates/spinifex-ca.crt sudo cp /etc/spinifex/ca.pem /usr/local/share/ca-certificates/spinifex-ca.crt
+1 -1
View File
@@ -176,5 +176,5 @@ echo " make reinstall # Changed a .service file? Re-run full setup"
echo "" echo ""
echo "Legacy workflow (deprecated, retained for pseudo-multinode CI only):" echo "Legacy workflow (deprecated, retained for pseudo-multinode CI only):"
echo " ./scripts/setup-ovn.sh --management" echo " ./scripts/setup-ovn.sh --management"
echo " sudo ./bin/spx admin init --region ap-southeast-2 --az ap-southeast-2a --node node1 --nodes 1" echo " sudo ./bin/spx admin init --bind 127.0.0.1 --region ap-southeast-2 --az ap-southeast-2a --node node1 --nodes 1"
echo " ./scripts/start-dev.sh" echo " ./scripts/start-dev.sh"
+1 -1
View File
@@ -168,7 +168,7 @@ echo "Re-initializing OVN"
./scripts/setup-ovn.sh --management $SETUP_OVN_FLAGS ./scripts/setup-ovn.sh --management $SETUP_OVN_FLAGS
echo "Initializing platform" echo "Initializing platform"
ADMIN_INIT_ARGS="--region $REGION --az ${REGION}a --node node1 --nodes 1" ADMIN_INIT_ARGS="--bind 127.0.0.1 --region $REGION --az ${REGION}a --node node1 --nodes 1"
# External networking mode: set EXTERNAL_MODE=nat for NAT/DHCP mode (outbound-only), # External networking mode: set EXTERNAL_MODE=nat for NAT/DHCP mode (outbound-only),
# or leave unset / EXTERNAL_MODE=pool for pool mode (per-VM public IPs). # or leave unset / EXTERNAL_MODE=pool for pool mode (per-VM public IPs).
+18
View File
@@ -517,6 +517,24 @@ func GenerateCACert(caCertPath, caKeyPath string) error {
return nil return nil
} }
// DiscoverPrimaryIP returns the host's default-route source IPv4, determined
// via a UDP-dial probe against TEST-NET-1 (RFC 5737). No packet is sent — the
// kernel picks the outbound interface for the given destination and we read
// its source address back off the socket. Returns "" when no usable default
// route exists or the resolved source is a loopback.
func DiscoverPrimaryIP() string {
conn, err := net.Dial("udp4", "192.0.2.1:9")
if err != nil {
return ""
}
defer conn.Close()
addr, ok := conn.LocalAddr().(*net.UDPAddr)
if !ok || addr.IP == nil || addr.IP.IsLoopback() {
return ""
}
return addr.IP.String()
}
// DiscoverLocalIPs enumerates all non-loopback network interface addresses on // DiscoverLocalIPs enumerates all non-loopback network interface addresses on
// this machine and returns them as strings. Link-local addresses are excluded. // this machine and returns them as strings. Link-local addresses are excluded.
func DiscoverLocalIPs() []string { func DiscoverLocalIPs() []string {
+15
View File
@@ -443,6 +443,21 @@ func TestDiscoverLocalIPs(t *testing.T) {
} }
} }
func TestDiscoverPrimaryIP(t *testing.T) {
t.Parallel()
ip := DiscoverPrimaryIP()
if ip == "" {
t.Skip("no default-route interface available in this environment")
}
parsed := net.ParseIP(ip)
require.NotNil(t, parsed, "returned IP should be parseable: %s", ip)
assert.False(t, parsed.IsLoopback(), "should not return loopback: %s", ip)
assert.False(t, parsed.IsUnspecified(), "should not return 0.0.0.0: %s", ip)
locals := DiscoverLocalIPs()
assert.Contains(t, locals, ip, "primary IP should appear in local interface list")
}
func TestDiscoverHostname(t *testing.T) { func TestDiscoverHostname(t *testing.T) {
t.Parallel() t.Parallel()
hostname := DiscoverHostname() hostname := DiscoverHostname()