a73x

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
}