xref: /openbsd-src/usr.sbin/vmd/dhcp.c (revision 65bbee46cad7861cd5a570f338df9e976422e3ab)
1*65bbee46Sjsg /*	$OpenBSD: dhcp.c,v 1.14 2024/09/26 01:45:13 jsg Exp $	*/
2470adcf5Sreyk 
3470adcf5Sreyk /*
4470adcf5Sreyk  * Copyright (c) 2017 Reyk Floeter <reyk@openbsd.org>
5470adcf5Sreyk  *
6470adcf5Sreyk  * Permission to use, copy, modify, and distribute this software for any
7470adcf5Sreyk  * purpose with or without fee is hereby granted, provided that the above
8470adcf5Sreyk  * copyright notice and this permission notice appear in all copies.
9470adcf5Sreyk  *
10470adcf5Sreyk  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11470adcf5Sreyk  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12470adcf5Sreyk  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13470adcf5Sreyk  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14470adcf5Sreyk  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15470adcf5Sreyk  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16470adcf5Sreyk  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17470adcf5Sreyk  */
18470adcf5Sreyk 
19470adcf5Sreyk #include <sys/types.h>
20470adcf5Sreyk 
21470adcf5Sreyk #include <net/if.h>
22470adcf5Sreyk #include <netinet/in.h>
23067df316Sdv #include <netinet/ip.h>
24067df316Sdv #include <netinet/udp.h>
25470adcf5Sreyk #include <netinet/if_ether.h>
26610f4e31Sreyk #include <arpa/inet.h>
27470adcf5Sreyk 
28c16b9d5aSanton #include <resolv.h>
29470adcf5Sreyk #include <stdlib.h>
30470adcf5Sreyk #include <string.h>
31470adcf5Sreyk #include <stddef.h>
32470adcf5Sreyk 
33470adcf5Sreyk #include "dhcp.h"
34470adcf5Sreyk #include "virtio.h"
356eb4c859Sdv #include "vmd.h"
36470adcf5Sreyk 
37067df316Sdv #define OPTIONS_OFFSET	offsetof(struct dhcp_packet, options)
38067df316Sdv #define OPTIONS_MAX_LEN	\
39067df316Sdv 	(1500 - sizeof(struct ip) - sizeof(struct udphdr) - OPTIONS_OFFSET)
40067df316Sdv 
41470adcf5Sreyk static const uint8_t broadcast[6] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
42470adcf5Sreyk 
43470adcf5Sreyk ssize_t
443481ecdfSdv dhcp_request(struct virtio_dev *dev, char *buf, size_t buflen, char **obuf)
45470adcf5Sreyk {
463481ecdfSdv 	struct vionet_dev	*vionet = NULL;
47610f4e31Sreyk 	unsigned char		*respbuf = NULL, *op, *oe, dhcptype = 0;
48067df316Sdv 	unsigned char		*opts = NULL;
49067df316Sdv 	ssize_t			 offset, optslen, respbuflen = 0;
50470adcf5Sreyk 	struct packet_ctx	 pc;
51470adcf5Sreyk 	struct dhcp_packet	 req, resp;
52610f4e31Sreyk 	struct in_addr		 server_addr, mask, client_addr, requested_addr;
53c16b9d5aSanton 	size_t			 len, resplen, o;
54610f4e31Sreyk 	uint32_t		 ltime;
55c16b9d5aSanton 	struct vmd_vm		*vm;
56c16b9d5aSanton 	const char		*hostname = NULL;
57470adcf5Sreyk 
583481ecdfSdv 	if (dev->dev_type != VMD_DEVTYPE_NET)
593481ecdfSdv 		fatalx("%s: not a network device", __func__);
603481ecdfSdv 	vionet = &dev->vionet;
613481ecdfSdv 
62067df316Sdv 	if (buflen < BOOTP_MIN_LEN + ETHER_HDR_LEN ||
63067df316Sdv 	    buflen > 1500 + ETHER_HDR_LEN)
64470adcf5Sreyk 		return (-1);
65470adcf5Sreyk 
66470adcf5Sreyk 	memset(&pc, 0, sizeof(pc));
67470adcf5Sreyk 	if ((offset = decode_hw_header(buf, buflen, 0, &pc, HTYPE_ETHER)) < 0)
68470adcf5Sreyk 		return (-1);
69470adcf5Sreyk 
7097f33f1dSdv 	if (memcmp(pc.pc_dmac, broadcast, ETHER_ADDR_LEN) != 0 &&
713481ecdfSdv 	    memcmp(pc.pc_dmac, vionet->hostmac, ETHER_ADDR_LEN) != 0)
7297f33f1dSdv 		return (-1);
7397f33f1dSdv 
743481ecdfSdv 	if (memcmp(pc.pc_smac, vionet->mac, ETHER_ADDR_LEN) != 0)
75470adcf5Sreyk 		return (-1);
76470adcf5Sreyk 
77470adcf5Sreyk 	if ((offset = decode_udp_ip_header(buf, buflen, offset, &pc)) < 0)
78470adcf5Sreyk 		return (-1);
79470adcf5Sreyk 
80470adcf5Sreyk 	if (ntohs(ss2sin(&pc.pc_src)->sin_port) != CLIENT_PORT ||
81470adcf5Sreyk 	    ntohs(ss2sin(&pc.pc_dst)->sin_port) != SERVER_PORT)
82470adcf5Sreyk 		return (-1);
83470adcf5Sreyk 
84067df316Sdv 	/* Only populate the base DHCP fields. Options are parsed separately. */
85067df316Sdv 	if ((size_t)offset + OPTIONS_OFFSET > buflen)
86067df316Sdv 		return (-1);
87470adcf5Sreyk 	memset(&req, 0, sizeof(req));
88067df316Sdv 	memcpy(&req, buf + offset, OPTIONS_OFFSET);
89470adcf5Sreyk 
90470adcf5Sreyk 	if (req.op != BOOTREQUEST ||
91470adcf5Sreyk 	    req.htype != pc.pc_htype ||
92470adcf5Sreyk 	    req.hlen != ETHER_ADDR_LEN ||
933481ecdfSdv 	    memcmp(vionet->mac, req.chaddr, req.hlen) != 0)
94470adcf5Sreyk 		return (-1);
95470adcf5Sreyk 
96470adcf5Sreyk 	/* Ignore unsupported requests for now */
97470adcf5Sreyk 	if (req.ciaddr.s_addr != 0 || req.file[0] != '\0' || req.hops != 0)
98470adcf5Sreyk 		return (-1);
99470adcf5Sreyk 
100067df316Sdv 	/*
101067df316Sdv 	 * If packet has data that could be DHCP options, check for the cookie
102067df316Sdv 	 * and then see if the region is still long enough to contain at least
103067df316Sdv 	 * one variable length option (3 bytes). If not, fallback to BOOTP.
104067df316Sdv 	 */
105067df316Sdv 	optslen = buflen - offset - OPTIONS_OFFSET;
106067df316Sdv 	if (optslen > DHCP_OPTIONS_COOKIE_LEN + 3 &&
107067df316Sdv 	    optslen < (ssize_t)OPTIONS_MAX_LEN) {
108067df316Sdv 		opts = buf + offset + OPTIONS_OFFSET;
109067df316Sdv 
110067df316Sdv 		if (memcmp(opts, DHCP_OPTIONS_COOKIE,
111067df316Sdv 			DHCP_OPTIONS_COOKIE_LEN) == 0) {
112610f4e31Sreyk 			memset(&requested_addr, 0, sizeof(requested_addr));
113067df316Sdv 			op = opts + DHCP_OPTIONS_COOKIE_LEN;
114067df316Sdv 			oe = opts + optslen;
115067df316Sdv 			while (*op != DHO_END && op + 1 < oe) {
116610f4e31Sreyk 				if (op[0] == DHO_PAD) {
117610f4e31Sreyk 					op++;
118610f4e31Sreyk 					continue;
119610f4e31Sreyk 				}
120067df316Sdv 				if (op + 2 + op[1] > oe)
121610f4e31Sreyk 					break;
122610f4e31Sreyk 				if (op[0] == DHO_DHCP_MESSAGE_TYPE &&
123610f4e31Sreyk 				    op[1] == 1)
124610f4e31Sreyk 					dhcptype = op[2];
125610f4e31Sreyk 				else if (op[0] == DHO_DHCP_REQUESTED_ADDRESS &&
126610f4e31Sreyk 				    op[1] == sizeof(requested_addr))
127610f4e31Sreyk 					memcpy(&requested_addr, &op[2],
128610f4e31Sreyk 					    sizeof(requested_addr));
129610f4e31Sreyk 				op += 2 + op[1];
130610f4e31Sreyk 			}
131610f4e31Sreyk 		}
132067df316Sdv 	}
133610f4e31Sreyk 
134470adcf5Sreyk 	memset(&resp, 0, sizeof(resp));
135470adcf5Sreyk 	resp.op = BOOTREPLY;
136470adcf5Sreyk 	resp.htype = req.htype;
137470adcf5Sreyk 	resp.hlen = req.hlen;
138470adcf5Sreyk 	resp.xid = req.xid;
139470adcf5Sreyk 
1403481ecdfSdv 	if (vionet->pxeboot) {
141cc104512Sclaudio 		strlcpy(resp.file, "auto_install", sizeof resp.file);
142c16b9d5aSanton 		vm = vm_getbyvmid(dev->vm_vmid);
143c16b9d5aSanton 		if (vm && res_hnok(vm->vm_params.vmc_params.vcp_name))
144c16b9d5aSanton 			hostname = vm->vm_params.vmc_params.vcp_name;
145c16b9d5aSanton 	}
146cc104512Sclaudio 
1472272e586Sdv 	if ((client_addr.s_addr = vm_priv_addr(&vionet->local_prefix,
1483481ecdfSdv 	    dev->vm_vmid, vionet->idx, 1)) == 0)
149470adcf5Sreyk 		return (-1);
150610f4e31Sreyk 	memcpy(&resp.yiaddr, &client_addr,
151610f4e31Sreyk 	    sizeof(client_addr));
152610f4e31Sreyk 	memcpy(&ss2sin(&pc.pc_dst)->sin_addr, &client_addr,
153610f4e31Sreyk 	    sizeof(client_addr));
154470adcf5Sreyk 	ss2sin(&pc.pc_dst)->sin_port = htons(CLIENT_PORT);
155470adcf5Sreyk 
1562272e586Sdv 	if ((server_addr.s_addr = vm_priv_addr(&vionet->local_prefix,
1572272e586Sdv 	    dev->vm_vmid, vionet->idx, 0)) == 0)
158470adcf5Sreyk 		return (-1);
159cc104512Sclaudio 	memcpy(&resp.siaddr, &server_addr, sizeof(server_addr));
160610f4e31Sreyk 	memcpy(&ss2sin(&pc.pc_src)->sin_addr, &server_addr,
161610f4e31Sreyk 	    sizeof(server_addr));
162470adcf5Sreyk 	ss2sin(&pc.pc_src)->sin_port = htons(SERVER_PORT);
163470adcf5Sreyk 
164470adcf5Sreyk 	/* Packet is already allocated */
165470adcf5Sreyk 	if (*obuf != NULL)
166470adcf5Sreyk 		goto fail;
167470adcf5Sreyk 
168067df316Sdv 	respbuflen = sizeof(resp);
169470adcf5Sreyk 	if ((respbuf = calloc(1, respbuflen)) == NULL)
170470adcf5Sreyk 		goto fail;
171470adcf5Sreyk 
1723481ecdfSdv 	memcpy(&pc.pc_dmac, vionet->mac, sizeof(pc.pc_dmac));
1733481ecdfSdv 	memcpy(&resp.chaddr, vionet->mac, resp.hlen);
1743481ecdfSdv 	memcpy(&pc.pc_smac, vionet->mac, sizeof(pc.pc_smac));
175470adcf5Sreyk 	pc.pc_smac[5]++;
176470adcf5Sreyk 	if ((offset = assemble_hw_header(respbuf, respbuflen, 0,
177470adcf5Sreyk 	    &pc, HTYPE_ETHER)) < 0) {
178470adcf5Sreyk 		log_debug("%s: assemble_hw_header failed", __func__);
179470adcf5Sreyk 		goto fail;
180470adcf5Sreyk 	}
181470adcf5Sreyk 
182470adcf5Sreyk 	/* Add BOOTP Vendor Extensions (DHCP options) */
183067df316Sdv 	memcpy(&resp.options, DHCP_OPTIONS_COOKIE, DHCP_OPTIONS_COOKIE_LEN);
184067df316Sdv 	o = DHCP_OPTIONS_COOKIE_LEN;
185470adcf5Sreyk 
186610f4e31Sreyk 	/* Did we receive a DHCP request or was it just BOOTP? */
187610f4e31Sreyk 	if (dhcptype) {
188610f4e31Sreyk 		/*
189610f4e31Sreyk 		 * There is no need for a real state machine as we always
190610f4e31Sreyk 		 * answer with the same client IP and options for the VM.
191610f4e31Sreyk 		 */
192610f4e31Sreyk 		if (dhcptype == DHCPDISCOVER)
193610f4e31Sreyk 			dhcptype = DHCPOFFER;
194610f4e31Sreyk 		else if (dhcptype == DHCPREQUEST &&
195610f4e31Sreyk 		    (requested_addr.s_addr == 0 ||
196610f4e31Sreyk 		    client_addr.s_addr == requested_addr.s_addr))
197610f4e31Sreyk 			dhcptype = DHCPACK;
198610f4e31Sreyk 		else
199610f4e31Sreyk 			dhcptype = DHCPNAK;
200610f4e31Sreyk 
201610f4e31Sreyk 		resp.options[o++] = DHO_DHCP_MESSAGE_TYPE;
202610f4e31Sreyk 		resp.options[o++] = sizeof(dhcptype);
203610f4e31Sreyk 		memcpy(&resp.options[o], &dhcptype, sizeof(dhcptype));
204610f4e31Sreyk 		o += sizeof(dhcptype);
205610f4e31Sreyk 
206610f4e31Sreyk 		/* Our lease never changes, use the maximum lease time */
207610f4e31Sreyk 		resp.options[o++] = DHO_DHCP_LEASE_TIME;
208610f4e31Sreyk 		resp.options[o++] = sizeof(ltime);
209610f4e31Sreyk 		ltime = ntohl(0xffffffff);
210610f4e31Sreyk 		memcpy(&resp.options[o], &ltime, sizeof(ltime));
211610f4e31Sreyk 		o += sizeof(ltime);
212610f4e31Sreyk 
213610f4e31Sreyk 		resp.options[o++] = DHO_DHCP_SERVER_IDENTIFIER;
214610f4e31Sreyk 		resp.options[o++] = sizeof(server_addr);
215610f4e31Sreyk 		memcpy(&resp.options[o], &server_addr, sizeof(server_addr));
216610f4e31Sreyk 		o += sizeof(server_addr);
217610f4e31Sreyk 	}
218610f4e31Sreyk 
219470adcf5Sreyk 	resp.options[o++] = DHO_SUBNET_MASK;
220470adcf5Sreyk 	resp.options[o++] = sizeof(mask);
221470adcf5Sreyk 	mask.s_addr = htonl(0xfffffffe);
222470adcf5Sreyk 	memcpy(&resp.options[o], &mask, sizeof(mask));
223470adcf5Sreyk 	o += sizeof(mask);
224470adcf5Sreyk 
225470adcf5Sreyk 	resp.options[o++] = DHO_ROUTERS;
226610f4e31Sreyk 	resp.options[o++] = sizeof(server_addr);
227610f4e31Sreyk 	memcpy(&resp.options[o], &server_addr, sizeof(server_addr));
228610f4e31Sreyk 	o += sizeof(server_addr);
229470adcf5Sreyk 
230470adcf5Sreyk 	resp.options[o++] = DHO_DOMAIN_NAME_SERVERS;
231610f4e31Sreyk 	resp.options[o++] = sizeof(server_addr);
232610f4e31Sreyk 	memcpy(&resp.options[o], &server_addr, sizeof(server_addr));
233610f4e31Sreyk 	o += sizeof(server_addr);
234470adcf5Sreyk 
235067df316Sdv 	if (hostname != NULL && (len = strlen(hostname)) > 1) {
236067df316Sdv 		/* Check if there's still room for the option type and len (2),
237067df316Sdv 		 * hostname, and a final to-be-added DHO_END (1). */
238067df316Sdv 		if (o + 2 + len + 1 > sizeof(resp.options)) {
239067df316Sdv 			log_debug("%s: hostname too long", __func__);
240067df316Sdv 			goto fail;
241067df316Sdv 		}
242c16b9d5aSanton 		resp.options[o++] = DHO_HOST_NAME;
243c16b9d5aSanton 		resp.options[o++] = len;
244c16b9d5aSanton 		memcpy(&resp.options[o], hostname, len);
245c16b9d5aSanton 		o += len;
246c16b9d5aSanton 	}
247c16b9d5aSanton 
248470adcf5Sreyk 	resp.options[o++] = DHO_END;
249470adcf5Sreyk 
250067df316Sdv 	resplen = OPTIONS_OFFSET + o;
251470adcf5Sreyk 
252470adcf5Sreyk 	/* Minimum packet size */
253470adcf5Sreyk 	if (resplen < BOOTP_MIN_LEN)
254470adcf5Sreyk 		resplen = BOOTP_MIN_LEN;
255470adcf5Sreyk 
256470adcf5Sreyk 	if ((offset = assemble_udp_ip_header(respbuf, respbuflen, offset, &pc,
257470adcf5Sreyk 	    (unsigned char *)&resp, resplen)) < 0) {
258470adcf5Sreyk 		log_debug("%s: assemble_udp_ip_header failed", __func__);
259470adcf5Sreyk 		goto fail;
260470adcf5Sreyk 	}
261470adcf5Sreyk 
262bfa31a59Sreyk 	memcpy(respbuf + offset, &resp, resplen);
263470adcf5Sreyk 	respbuflen = offset + resplen;
264470adcf5Sreyk 
265470adcf5Sreyk 	*obuf = respbuf;
266470adcf5Sreyk 	return (respbuflen);
267470adcf5Sreyk  fail:
268470adcf5Sreyk 	free(respbuf);
269067df316Sdv 	return (-1);
270470adcf5Sreyk }
271