internal/agent/netsnoop/netsnoop.go
Ref: Size: 3.1 KiB History
// Package netsnoop discovers the address a guest's named-network NIC was
// granted, by watching that NIC's own tap for its DHCP exchange. On a named
// network the site's own server assigns the address, so the agent is not the
// authority it is on the eitri bridge (internal/agent/dhcp) and cannot simply
// report what it handed out. With no in-guest presence — a boundary this
// project holds, the guest owns the guest — the tap is the only place the
// answer appears: it carries every frame that NIC sends or receives, so
// initial leases and unicast renewals are both visible without touching the
// guest or the site's server.
//
// The snoop reads plain untagged Ethernet frames. A guest that does its own
// 802.1Q tagging DHCPs inside the tag and is not discovered here; it keeps
// whatever address it negotiates, and eitri reports none — the guest owns the
// guest, and a wrong address would be worse than no address.
package netsnoop
import (
"net"
"net/netip"
"github.com/insomniacslk/dhcp/dhcpv4"
)
// ParseACK inspects one Ethernet frame and, when it is a DHCP ACK, returns
// the client MAC it is addressed to and the address it grants. ok=false for
// every other frame — this is called on all tap traffic, including whatever a
// hostile guest chooses to emit, so it must be cheap and unshockable: it
// indexes only after a length check and treats a parse failure as "not an
// ACK", never as an error worth reporting.
func ParseACK(frame []byte) (mac net.HardwareAddr, ip netip.Addr, ok bool) {
const ethHdr = 14
if len(frame) < ethHdr+20+8 {
return nil, netip.Addr{}, false
}
if frame[12] != 0x08 || frame[13] != 0x00 { // not IPv4
return nil, netip.Addr{}, false
}
ipb := frame[ethHdr:]
if ipb[0]>>4 != 4 {
return nil, netip.Addr{}, false
}
ihl := int(ipb[0]&0x0f) * 4
if ihl < 20 || len(ipb) < ihl+8 || ipb[9] != 17 { // not UDP
return nil, netip.Addr{}, false
}
udp := ipb[ihl:]
srcPort := int(udp[0])<<8 | int(udp[1])
dstPort := int(udp[2])<<8 | int(udp[3])
if srcPort != 67 || dstPort != 68 { // not server->client DHCP
return nil, netip.Addr{}, false
}
// The opcode check sorts frames, it does not defend against them. A guest
// can write a boot reply on these ports as easily as a server can, so
// nothing read out of the frame says who sent it. What makes an address
// believable is the direction the frame was travelling on the tap, which
// the receive path settles before it calls here — see lease in
// listen_linux.go, which is also honest about what direction does not
// cover.
msg, err := dhcpv4.FromBytes(udp[8:])
if err != nil || msg.OpCode != dhcpv4.OpcodeBootReply ||
msg.MessageType() != dhcpv4.MessageTypeAck {
return nil, netip.Addr{}, false
}
// A wire yiaddr is four bytes, so To4 is non-nil in practice; the
// AddrFromSlice check is the belt to that braces. The live arm is
// IsUnspecified: a reply that grants 0.0.0.0 is not an address.
granted, okAddr := netip.AddrFromSlice(msg.YourIPAddr.To4())
if !okAddr || granted.IsUnspecified() {
return nil, netip.Addr{}, false
}
return msg.ClientHWAddr, granted, true
}