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