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