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
dhcp_request(struct virtio_dev * dev,char * buf,size_t buflen,char ** obuf)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], <ime, 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