mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
fix(vpcd/dhcp): enable AF_PACKET promisc on bridge for DORA (mulga-siv-33)
Real-world DHCP servers commonly ignore the BOOTP BROADCAST flag and
reply with a unicast OFFER addressed to the synthetic per-tenant chaddr
(e.g. 02:f3:...). 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 vpcd's AF_PACKET socket on br-wan misses the OFFER and DORA times
out at 15s ("unable to receive an offer: context deadline exceeded").
Dev/CI didn't hit this:
- CI uses spx_wan_mode=static — no DORA via vpcd.
- QEMU dev DHCP servers (SLIRP/dnsmasq) honor the BROADCAST flag, so
their replies are L2 broadcast which the bridge always passes up.
Replace the three nclient4.New(iface, ...) call sites in Acquire/Renew/
Release with nclient4.NewWithConn(...) wrapping a custom AF_PACKET
DGRAM conn that calls SetPromiscuous(true). Promisc is per-socket
(per-DORA, ~15s) and refcounted by the kernel — auto-clears on close.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() }()
|
||||
|
||||
Reference in New Issue
Block a user