diff --git a/go.mod b/go.mod index 602326933..abba016b2 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/insomniacslk/dhcp v0.0.0-20260407060928-11b94ed970f2 github.com/kdomanski/iso9660 v0.4.0 github.com/klauspost/cpuid/v2 v2.3.0 + github.com/mdlayher/packet v1.1.2 github.com/mulgadc/predastore v1.1.6 github.com/mulgadc/viperblock v1.2.1 github.com/nats-io/nats-server/v2 v2.12.7 @@ -23,6 +24,7 @@ require ( github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.50.0 + golang.org/x/sys v0.43.0 gopkg.in/ini.v1 v1.67.1 ) @@ -86,7 +88,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.21 // indirect - github.com/mdlayher/packet v1.1.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/highwayhash v1.0.4 // indirect @@ -123,7 +124,6 @@ require ( golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect golang.org/x/term v0.42.0 // indirect golang.org/x/text v0.36.0 // indirect diff --git a/spinifex/services/vpcd/dhcp/nclient4.go b/spinifex/services/vpcd/dhcp/nclient4.go index 70ca30f92..8ab50860d 100644 --- a/spinifex/services/vpcd/dhcp/nclient4.go +++ b/spinifex/services/vpcd/dhcp/nclient4.go @@ -3,12 +3,46 @@ package dhcp import ( "context" "fmt" + "net" "time" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/nclient4" + "github.com/mdlayher/packet" + "golang.org/x/sys/unix" ) +// dhcpClientPort is the IANA-assigned UDP port for DHCP clients. +const dhcpClientPort = 68 + +// openPromiscBridgeConn opens an AF_PACKET DGRAM socket on iface filtered +// to IPv4 and enables PACKET_MR_PROMISC on it. Equivalent to nclient4's +// own NewRawUDPConn but with promisc on. +// +// Why promisc: vpcd uses a synthetic per-tenant chaddr (e.g. 02:f3:...) +// that never matches the bridge's MAC. Many real-world DHCP servers +// ignore the BOOTP BROADCAST flag and reply with a unicast OFFER addressed +// to that chaddr. A Linux bridge floods unknown unicast to slave ports +// but does NOT pass it up to the bridge netdev unless IFF_PROMISC is set, +// so the AF_PACKET socket misses the OFFER and DORA times out. Promisc +// flips IFF_PROMISC for the lifetime of the socket (per-DORA), and the +// kernel refcounts it so it auto-clears on Close. +func openPromiscBridgeConn(iface string) (net.PacketConn, net.HardwareAddr, error) { + ifc, err := net.InterfaceByName(iface) + if err != nil { + return nil, nil, fmt.Errorf("lookup iface %s: %w", iface, err) + } + raw, err := packet.Listen(ifc, packet.Datagram, unix.ETH_P_IP, nil) + if err != nil { + return nil, nil, fmt.Errorf("packet.Listen on %s: %w", iface, err) + } + if err := raw.SetPromiscuous(true); err != nil { + _ = raw.Close() + return nil, nil, fmt.Errorf("set promisc on %s: %w", iface, err) + } + return nclient4.NewBroadcastUDPConn(raw, &net.UDPAddr{Port: dhcpClientPort}), ifc.HardwareAddr, nil +} + // NClient4Client is the production DHCP client backed by // github.com/insomniacslk/dhcp/dhcpv4/nclient4. Each Acquire/Renew/Release // opens an AF_PACKET socket on the target bridge for the duration of the @@ -39,12 +73,17 @@ func (c *NClient4Client) Acquire(ctx context.Context, req AcquireRequest) (*Leas return nil, fmt.Errorf("dhcp acquire: hw_addr is required") } - client, err := nclient4.New(req.Bridge, + conn, ifaceMAC, err := openPromiscBridgeConn(req.Bridge) + if err != nil { + return nil, fmt.Errorf("open promisc conn on %s: %w", req.Bridge, err) + } + client, err := nclient4.NewWithConn(conn, ifaceMAC, nclient4.WithHWAddr(req.HWAddr), nclient4.WithTimeout(c.timeout), nclient4.WithRetry(c.retry), ) if err != nil { + _ = conn.Close() return nil, fmt.Errorf("open nclient4 on %s: %w", req.Bridge, err) } defer func() { _ = client.Close() }() @@ -65,12 +104,17 @@ func (c *NClient4Client) Renew(ctx context.Context, lease *Lease) (*Lease, error return nil, fmt.Errorf("dhcp renew: %w", err) } - client, err := nclient4.New(lease.Bridge, + conn, ifaceMAC, err := openPromiscBridgeConn(lease.Bridge) + if err != nil { + return nil, fmt.Errorf("open promisc conn on %s for renew: %w", lease.Bridge, err) + } + client, err := nclient4.NewWithConn(conn, ifaceMAC, nclient4.WithHWAddr(lease.HWAddr), nclient4.WithTimeout(c.timeout), nclient4.WithRetry(c.retry), ) if err != nil { + _ = conn.Close() return nil, fmt.Errorf("open nclient4 on %s for renew: %w", lease.Bridge, err) } defer func() { _ = client.Close() }() @@ -99,11 +143,16 @@ func (c *NClient4Client) Release(ctx context.Context, lease *Lease) error { return fmt.Errorf("dhcp release: %w", err) } - client, err := nclient4.New(lease.Bridge, + conn, ifaceMAC, err := openPromiscBridgeConn(lease.Bridge) + if err != nil { + return fmt.Errorf("open promisc conn on %s for release: %w", lease.Bridge, err) + } + client, err := nclient4.NewWithConn(conn, ifaceMAC, nclient4.WithHWAddr(lease.HWAddr), nclient4.WithTimeout(c.timeout), ) if err != nil { + _ = conn.Close() return fmt.Errorf("open nclient4 on %s for release: %w", lease.Bridge, err) } defer func() { _ = client.Close() }()