xref: /openbsd-src/usr.sbin/vmd/dhcp.c (revision 65bbee46cad7861cd5a570f338df9e976422e3ab)
1 /*	$OpenBSD: dhcp.c,v 1.14 2024/09/26 01:45:13 jsg Exp $	*/
2 
3 /*
4  * Copyright (c) 2017 Reyk Floeter <reyk@openbsd.org>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 
19 #include <sys/types.h>
20 
21 #include <net/if.h>
22 #include <netinet/in.h>
23 #include <netinet/ip.h>
24 #include <netinet/udp.h>
25 #include <netinet/if_ether.h>
26 #include <arpa/inet.h>
27 
28 #include <resolv.h>
29 #include <stdlib.h>
30 #include <string.h>
31 #include <stddef.h>
32 
33 #include "dhcp.h"
34 #include "virtio.h"
35 #include "vmd.h"
36 
37 #define OPTIONS_OFFSET	offsetof(struct dhcp_packet, options)
38 #define OPTIONS_MAX_LEN	\
39 	(1500 - sizeof(struct ip) - sizeof(struct udphdr) - OPTIONS_OFFSET)
40 
41 static const uint8_t broadcast[6] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
42 
43 ssize_t
44 dhcp_request(struct virtio_dev *dev, char *buf, size_t buflen, char **obuf)
45 {
46 	struct vionet_dev	*vionet = NULL;
47 	unsigned char		*respbuf = NULL, *op, *oe, dhcptype = 0;
48 	unsigned char		*opts = NULL;
49 	ssize_t			 offset, optslen, respbuflen = 0;
50 	struct packet_ctx	 pc;
51 	struct dhcp_packet	 req, resp;
52 	struct in_addr		 server_addr, mask, client_addr, requested_addr;
53 	size_t			 len, resplen, o;
54 	uint32_t		 ltime;
55 	struct vmd_vm		*vm;
56 	const char		*hostname = NULL;
57 
58 	if (dev->dev_type != VMD_DEVTYPE_NET)
59 		fatalx("%s: not a network device", __func__);
60 	vionet = &dev->vionet;
61 
62 	if (buflen < BOOTP_MIN_LEN + ETHER_HDR_LEN ||
63 	    buflen > 1500 + ETHER_HDR_LEN)
64 		return (-1);
65 
66 	memset(&pc, 0, sizeof(pc));
67 	if ((offset = decode_hw_header(buf, buflen, 0, &pc, HTYPE_ETHER)) < 0)
68 		return (-1);
69 
70 	if (memcmp(pc.pc_dmac, broadcast, ETHER_ADDR_LEN) != 0 &&
71 	    memcmp(pc.pc_dmac, vionet->hostmac, ETHER_ADDR_LEN) != 0)
72 		return (-1);
73 
74 	if (memcmp(pc.pc_smac, vionet->mac, ETHER_ADDR_LEN) != 0)
75 		return (-1);
76 
77 	if ((offset = decode_udp_ip_header(buf, buflen, offset, &pc)) < 0)
78 		return (-1);
79 
80 	if (ntohs(ss2sin(&pc.pc_src)->sin_port) != CLIENT_PORT ||
81 	    ntohs(ss2sin(&pc.pc_dst)->sin_port) != SERVER_PORT)
82 		return (-1);
83 
84 	/* Only populate the base DHCP fields. Options are parsed separately. */
85 	if ((size_t)offset + OPTIONS_OFFSET > buflen)
86 		return (-1);
87 	memset(&req, 0, sizeof(req));
88 	memcpy(&req, buf + offset, OPTIONS_OFFSET);
89 
90 	if (req.op != BOOTREQUEST ||
91 	    req.htype != pc.pc_htype ||
92 	    req.hlen != ETHER_ADDR_LEN ||
93 	    memcmp(vionet->mac, req.chaddr, req.hlen) != 0)
94 		return (-1);
95 
96 	/* Ignore unsupported requests for now */
97 	if (req.ciaddr.s_addr != 0 || req.file[0] != '\0' || req.hops != 0)
98 		return (-1);
99 
100 	/*
101 	 * If packet has data that could be DHCP options, check for the cookie
102 	 * and then see if the region is still long enough to contain at least
103 	 * one variable length option (3 bytes). If not, fallback to BOOTP.
104 	 */
105 	optslen = buflen - offset - OPTIONS_OFFSET;
106 	if (optslen > DHCP_OPTIONS_COOKIE_LEN + 3 &&
107 	    optslen < (ssize_t)OPTIONS_MAX_LEN) {
108 		opts = buf + offset + OPTIONS_OFFSET;
109 
110 		if (memcmp(opts, DHCP_OPTIONS_COOKIE,
111 			DHCP_OPTIONS_COOKIE_LEN) == 0) {
112 			memset(&requested_addr, 0, sizeof(requested_addr));
113 			op = opts + DHCP_OPTIONS_COOKIE_LEN;
114 			oe = opts + optslen;
115 			while (*op != DHO_END && op + 1 < oe) {
116 				if (op[0] == DHO_PAD) {
117 					op++;
118 					continue;
119 				}
120 				if (op + 2 + op[1] > oe)
121 					break;
122 				if (op[0] == DHO_DHCP_MESSAGE_TYPE &&
123 				    op[1] == 1)
124 					dhcptype = op[2];
125 				else if (op[0] == DHO_DHCP_REQUESTED_ADDRESS &&
126 				    op[1] == sizeof(requested_addr))
127 					memcpy(&requested_addr, &op[2],
128 					    sizeof(requested_addr));
129 				op += 2 + op[1];
130 			}
131 		}
132 	}
133 
134 	memset(&resp, 0, sizeof(resp));
135 	resp.op = BOOTREPLY;
136 	resp.htype = req.htype;
137 	resp.hlen = req.hlen;
138 	resp.xid = req.xid;
139 
140 	if (vionet->pxeboot) {
141 		strlcpy(resp.file, "auto_install", sizeof resp.file);
142 		vm = vm_getbyvmid(dev->vm_vmid);
143 		if (vm && res_hnok(vm->vm_params.vmc_params.vcp_name))
144 			hostname = vm->vm_params.vmc_params.vcp_name;
145 	}
146 
147 	if ((client_addr.s_addr = vm_priv_addr(&vionet->local_prefix,
148 	    dev->vm_vmid, vionet->idx, 1)) == 0)
149 		return (-1);
150 	memcpy(&resp.yiaddr, &client_addr,
151 	    sizeof(client_addr));
152 	memcpy(&ss2sin(&pc.pc_dst)->sin_addr, &client_addr,
153 	    sizeof(client_addr));
154 	ss2sin(&pc.pc_dst)->sin_port = htons(CLIENT_PORT);
155 
156 	if ((server_addr.s_addr = vm_priv_addr(&vionet->local_prefix,
157 	    dev->vm_vmid, vionet->idx, 0)) == 0)
158 		return (-1);
159 	memcpy(&resp.siaddr, &server_addr, sizeof(server_addr));
160 	memcpy(&ss2sin(&pc.pc_src)->sin_addr, &server_addr,
161 	    sizeof(server_addr));
162 	ss2sin(&pc.pc_src)->sin_port = htons(SERVER_PORT);
163 
164 	/* Packet is already allocated */
165 	if (*obuf != NULL)
166 		goto fail;
167 
168 	respbuflen = sizeof(resp);
169 	if ((respbuf = calloc(1, respbuflen)) == NULL)
170 		goto fail;
171 
172 	memcpy(&pc.pc_dmac, vionet->mac, sizeof(pc.pc_dmac));
173 	memcpy(&resp.chaddr, vionet->mac, resp.hlen);
174 	memcpy(&pc.pc_smac, vionet->mac, sizeof(pc.pc_smac));
175 	pc.pc_smac[5]++;
176 	if ((offset = assemble_hw_header(respbuf, respbuflen, 0,
177 	    &pc, HTYPE_ETHER)) < 0) {
178 		log_debug("%s: assemble_hw_header failed", __func__);
179 		goto fail;
180 	}
181 
182 	/* Add BOOTP Vendor Extensions (DHCP options) */
183 	memcpy(&resp.options, DHCP_OPTIONS_COOKIE, DHCP_OPTIONS_COOKIE_LEN);
184 	o = DHCP_OPTIONS_COOKIE_LEN;
185 
186 	/* Did we receive a DHCP request or was it just BOOTP? */
187 	if (dhcptype) {
188 		/*
189 		 * There is no need for a real state machine as we always
190 		 * answer with the same client IP and options for the VM.
191 		 */
192 		if (dhcptype == DHCPDISCOVER)
193 			dhcptype = DHCPOFFER;
194 		else if (dhcptype == DHCPREQUEST &&
195 		    (requested_addr.s_addr == 0 ||
196 		    client_addr.s_addr == requested_addr.s_addr))
197 			dhcptype = DHCPACK;
198 		else
199 			dhcptype = DHCPNAK;
200 
201 		resp.options[o++] = DHO_DHCP_MESSAGE_TYPE;
202 		resp.options[o++] = sizeof(dhcptype);
203 		memcpy(&resp.options[o], &dhcptype, sizeof(dhcptype));
204 		o += sizeof(dhcptype);
205 
206 		/* Our lease never changes, use the maximum lease time */
207 		resp.options[o++] = DHO_DHCP_LEASE_TIME;
208 		resp.options[o++] = sizeof(ltime);
209 		ltime = ntohl(0xffffffff);
210 		memcpy(&resp.options[o], &ltime, sizeof(ltime));
211 		o += sizeof(ltime);
212 
213 		resp.options[o++] = DHO_DHCP_SERVER_IDENTIFIER;
214 		resp.options[o++] = sizeof(server_addr);
215 		memcpy(&resp.options[o], &server_addr, sizeof(server_addr));
216 		o += sizeof(server_addr);
217 	}
218 
219 	resp.options[o++] = DHO_SUBNET_MASK;
220 	resp.options[o++] = sizeof(mask);
221 	mask.s_addr = htonl(0xfffffffe);
222 	memcpy(&resp.options[o], &mask, sizeof(mask));
223 	o += sizeof(mask);
224 
225 	resp.options[o++] = DHO_ROUTERS;
226 	resp.options[o++] = sizeof(server_addr);
227 	memcpy(&resp.options[o], &server_addr, sizeof(server_addr));
228 	o += sizeof(server_addr);
229 
230 	resp.options[o++] = DHO_DOMAIN_NAME_SERVERS;
231 	resp.options[o++] = sizeof(server_addr);
232 	memcpy(&resp.options[o], &server_addr, sizeof(server_addr));
233 	o += sizeof(server_addr);
234 
235 	if (hostname != NULL && (len = strlen(hostname)) > 1) {
236 		/* Check if there's still room for the option type and len (2),
237 		 * hostname, and a final to-be-added DHO_END (1). */
238 		if (o + 2 + len + 1 > sizeof(resp.options)) {
239 			log_debug("%s: hostname too long", __func__);
240 			goto fail;
241 		}
242 		resp.options[o++] = DHO_HOST_NAME;
243 		resp.options[o++] = len;
244 		memcpy(&resp.options[o], hostname, len);
245 		o += len;
246 	}
247 
248 	resp.options[o++] = DHO_END;
249 
250 	resplen = OPTIONS_OFFSET + o;
251 
252 	/* Minimum packet size */
253 	if (resplen < BOOTP_MIN_LEN)
254 		resplen = BOOTP_MIN_LEN;
255 
256 	if ((offset = assemble_udp_ip_header(respbuf, respbuflen, offset, &pc,
257 	    (unsigned char *)&resp, resplen)) < 0) {
258 		log_debug("%s: assemble_udp_ip_header failed", __func__);
259 		goto fail;
260 	}
261 
262 	memcpy(respbuf + offset, &resp, resplen);
263 	respbuflen = offset + resplen;
264 
265 	*obuf = respbuf;
266 	return (respbuflen);
267  fail:
268 	free(respbuf);
269 	return (-1);
270 }
271