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], <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