pf does not filter dhcp

2017/01/15

Introduction / Synopsis

While reading The Book of PF and experimenting with the different configuration I noticed that pf does not block dhcp traffic.

int_if="vr0"
block quick on $int_if inet proto { tcp, udp } to $int_if port { bootps, bootpc }

Digging into the code

Further investigation showed that dhcpd accesses the socket before pf via bpf (Berkley Packet Filter) and sees the unfiltered traffic, the traffic before pf.

       +-- BPF -- dhcpd
       |
NIC ---+ --------- pf

I won’t dissect the code in every detail, but the people from the #openbsd channel kindly requested that I share more details. I looked at bpf.c, pf.c and ip_input.c. From what I understand — correct me if I’m wrong — the following snippets are relevant:

/*
 * From sys/netinet/ip_input.c
 */

#if NPF > 0
	/*
	 * Packet filter
	 */
	pfrdr = ip->ip_dst.s_addr;
	if (pf_test(AF_INET, PF_IN, m->m_pkthdr.rcvif, &m, NULL) != PF_PASS)
		goto bad;
	if (m == NULL)
		return;

	ip = mtod(m, struct ip *);
	hlen = ip->ip_hl << 2;
	pfrdr = (pfrdr != ip->ip_dst.s_addr);
#endif

This is where pf gets the IP packets passed meaning that we are already at layer 3 here. While dhcpd works with bpf and thus access directly raw ethernet frames:

/*
 * From usr.sbin/dhcpd/bpf.c
 */

void
if_register_receive(struct interface_info *info)
{
	struct bpf_version v;
	struct bpf_program p;
	int flag = 1, sz, cmplt = 0;

	/* Open a BPF device and hang it on this interface... */
	info->rfdesc = if_register_bpf(info);

	/* Make sure the BPF version is in range... */
	if (ioctl(info->rfdesc, BIOCVERSION, &v) == -1)
		error("Can't get BPF version: %m");

	if (v.bv_major != BPF_MAJOR_VERSION ||
	    v.bv_minor < BPF_MINOR_VERSION)
		error("Kernel BPF version out of range - recompile dhcpd!");

	/*
	 * Set immediate mode so that reads return as soon as a packet
	 * comes in, rather than waiting for the input buffer to fill
	 * with packets.
	 */
	if (ioctl(info->rfdesc, BIOCIMMEDIATE, &flag) == -1)
		error("Can't set immediate mode on bpf device: %m");

	if (ioctl(info->rfdesc, BIOCSFILDROP, &flag) == -1)
		error("Can't set filter-drop mode on bpf device: %m");

	/* make sure kernel fills in the source ethernet address */
	if (ioctl(info->rfdesc, BIOCSHDRCMPLT, &cmplt) == -1)
		error("Can't set header complete flag on bpf device: %m");

	/* Get the required BPF buffer length from the kernel. */
	if (ioctl(info->rfdesc, BIOCGBLEN, &sz) == -1)
		error("Can't get bpf buffer length: %m");
	info->rbuf_max = sz;
	info->rbuf = malloc(info->rbuf_max);
	if (!info->rbuf)
		error("Can't allocate %lu bytes for bpf input buffer.",
		    (unsigned long)info->rbuf_max);
	info->rbuf_offset = 0;
	info->rbuf_len = 0;

	/* Set up the bpf filter program structure. */
	p.bf_len = dhcp_bpf_filter_len;
	p.bf_insns = dhcp_bpf_filter;

	if (ioctl(info->rfdesc, BIOCSETF, &p) == -1)
		error("Can't install packet filter program: %m");

	/* Set up the bpf write filter program structure. */
	p.bf_len = dhcp_bpf_wfilter_len;
	p.bf_insns = dhcp_bpf_wfilter;

	if (ioctl(info->rfdesc, BIOCSETWF, &p) == -1)
		error("Can't install write filter program: %m");

	/* make sure these settings cannot be changed after dropping privs */
	if (ioctl(info->rfdesc, BIOCLOCK) == -1)
		error("Failed to lock bpf descriptor: %m");
}

Also the manpage of bpf says «Each descriptor that accepts the packet receives its own copy.» meaning that despite an active pf dhcpd receives an unbiased, unfiltered frame

Conclusion

So this renders the rule completely useless when the dhcpd runs on the same machine as the pf.

This syntax and test were made under OpenBSD 4.5 but should apply also to the newest version according to the people in the IRC-channel on #openbsd and I looked at the newest code versions.