mirror of
https://passt.top/passt
synced 2024-12-22 13:45:32 +00:00
c0fbc7ef2a
It's widely considered a legacy option nowadays, and I've haven't seen clients setting it since Windows 95, but it's convenient for a minimal DHCP client not using raw IP sockets such as what I'm playing with for muvm. Signed-off-by: Stefano Brivio <sbrivio@redhat.com> Reviewed-by: David Gibson <david@gibson.dropbear.id.au>
415 lines
9.6 KiB
C
415 lines
9.6 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
/* PASST - Plug A Simple Socket Transport
|
|
* for qemu/UNIX domain socket mode
|
|
*
|
|
* PASTA - Pack A Subtle Tap Abstraction
|
|
* for network namespace/tap device mode
|
|
*
|
|
* dhcp.c - Minimalistic DHCP server for PASST
|
|
*
|
|
* Copyright (c) 2020-2021 Red Hat GmbH
|
|
* Author: Stefano Brivio <sbrivio@redhat.com>
|
|
*/
|
|
|
|
#include <arpa/inet.h>
|
|
#include <net/if.h>
|
|
#include <netinet/if_ether.h>
|
|
#include <netinet/ip.h>
|
|
#include <netinet/udp.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include <limits.h>
|
|
|
|
#include "util.h"
|
|
#include "ip.h"
|
|
#include "checksum.h"
|
|
#include "packet.h"
|
|
#include "passt.h"
|
|
#include "tap.h"
|
|
#include "log.h"
|
|
#include "dhcp.h"
|
|
|
|
/**
|
|
* struct opt - DHCP option
|
|
* @sent: Convenience flag, set while filling replies
|
|
* @slen: Length of option defined for server, -1 if not going to be sent
|
|
* @s: Option payload from server
|
|
* @clen: Length of option received from client, -1 if not received
|
|
* @c: Option payload from client
|
|
*/
|
|
struct opt {
|
|
int sent;
|
|
int slen;
|
|
uint8_t s[255];
|
|
int clen;
|
|
uint8_t c[255];
|
|
};
|
|
|
|
static struct opt opts[255];
|
|
|
|
#define DHCPDISCOVER 1
|
|
#define DHCPOFFER 2
|
|
#define DHCPREQUEST 3
|
|
#define DHCPDECLINE 4
|
|
#define DHCPACK 5
|
|
#define DHCPNAK 6
|
|
#define DHCPRELEASE 7
|
|
#define DHCPINFORM 8
|
|
#define DHCPFORCERENEW 9
|
|
|
|
#define OPT_MIN 60 /* RFC 951 */
|
|
|
|
/**
|
|
* dhcp_init() - Initialise DHCP options
|
|
*/
|
|
void dhcp_init(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(opts); i++)
|
|
opts[i].slen = -1;
|
|
|
|
opts[1] = (struct opt) { 0, 4, { 0 }, 0, { 0 }, }; /* Mask */
|
|
opts[3] = (struct opt) { 0, 4, { 0 }, 0, { 0 }, }; /* Router */
|
|
opts[51] = (struct opt) { 0, 4, { 0xff,
|
|
0xff,
|
|
0xff,
|
|
0xff }, 0, { 0 }, }; /* Lease time */
|
|
opts[53] = (struct opt) { 0, 1, { 0 }, 0, { 0 }, }; /* Type */
|
|
opts[54] = (struct opt) { 0, 4, { 0 }, 0, { 0 }, }; /* Server ID */
|
|
}
|
|
|
|
/**
|
|
* struct msg - BOOTP/DHCP message
|
|
* @op: BOOTP message type
|
|
* @htype: Hardware address type
|
|
* @hlen: Hardware address length
|
|
* @hops: DHCP relay hops
|
|
* @xid: Transaction ID randomly chosen by client
|
|
* @secs: Seconds elapsed since beginning of acquisition or renewal
|
|
* @flags: DHCP message flags
|
|
* @ciaddr: Client IP address in BOUND, RENEW, REBINDING
|
|
* @yiaddr: IP address being offered or assigned
|
|
* @siaddr: Next server to use in bootstrap
|
|
* @giaddr: Relay agent IP address
|
|
* @chaddr: Client hardware address
|
|
* @sname: Server host name
|
|
* @file: Boot file name
|
|
* @magic: Magic cookie prefix before options
|
|
* @o: Options
|
|
*/
|
|
struct msg {
|
|
uint8_t op;
|
|
#define BOOTREQUEST 1
|
|
#define BOOTREPLY 2
|
|
uint8_t htype;
|
|
uint8_t hlen;
|
|
uint8_t hops;
|
|
uint32_t xid;
|
|
uint16_t secs;
|
|
uint16_t flags;
|
|
#define FLAG_BROADCAST htons_constant(0x8000)
|
|
|
|
uint32_t ciaddr;
|
|
struct in_addr yiaddr;
|
|
uint32_t siaddr;
|
|
uint32_t giaddr;
|
|
uint8_t chaddr[16];
|
|
uint8_t sname[64];
|
|
uint8_t file[128];
|
|
uint32_t magic;
|
|
uint8_t o[308];
|
|
} __attribute__((__packed__));
|
|
|
|
/**
|
|
* fill_one() - Fill a single option in message
|
|
* @m: Message to fill
|
|
* @o: Option number
|
|
* @offset: Current offset within options field, updated on insertion
|
|
*/
|
|
static void fill_one(struct msg *m, int o, int *offset)
|
|
{
|
|
m->o[*offset] = o;
|
|
m->o[*offset + 1] = opts[o].slen;
|
|
memcpy(&m->o[*offset + 2], opts[o].s, opts[o].slen);
|
|
|
|
opts[o].sent = 1;
|
|
*offset += 2 + opts[o].slen;
|
|
}
|
|
|
|
/**
|
|
* fill() - Fill options in message
|
|
* @m: Message to fill
|
|
*
|
|
* Return: current size of options field
|
|
*/
|
|
static int fill(struct msg *m)
|
|
{
|
|
int i, o, offset = 0;
|
|
|
|
m->op = BOOTREPLY;
|
|
m->secs = 0;
|
|
|
|
for (o = 0; o < 255; o++)
|
|
opts[o].sent = 0;
|
|
|
|
/* Some clients (wattcp32, mTCP, maybe some others) expect
|
|
* option 53 at the beginning of the list.
|
|
* Put it there explicitly, unless requested via option 55.
|
|
*/
|
|
if (opts[55].clen > 0 && !memchr(opts[55].c, 53, opts[55].clen))
|
|
fill_one(m, 53, &offset);
|
|
|
|
for (i = 0; i < opts[55].clen; i++) {
|
|
o = opts[55].c[i];
|
|
if (opts[o].slen != -1)
|
|
fill_one(m, o, &offset);
|
|
}
|
|
|
|
for (o = 0; o < 255; o++) {
|
|
if (opts[o].slen != -1 && !opts[o].sent)
|
|
fill_one(m, o, &offset);
|
|
}
|
|
|
|
m->o[offset++] = 255;
|
|
m->o[offset++] = 0;
|
|
|
|
if (offset < OPT_MIN) {
|
|
memset(&m->o[offset], 0, OPT_MIN - offset);
|
|
offset = OPT_MIN;
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
/**
|
|
* opt_dns_search_dup_ptr() - Look for possible domain name compression pointer
|
|
* @buf: Current option buffer with existing labels
|
|
* @cmp: Portion of domain name being added
|
|
* @len: Length of current option buffer
|
|
*
|
|
* Return: offset to corresponding compression pointer if any, -1 if not found
|
|
*/
|
|
static int opt_dns_search_dup_ptr(unsigned char *buf, const char *cmp,
|
|
size_t len)
|
|
{
|
|
unsigned int i;
|
|
|
|
for (i = 0; i < len; i++) {
|
|
if (buf[i] == 0 &&
|
|
len - i - 1 >= strlen(cmp) &&
|
|
!memcmp(buf + i + 1, cmp, strlen(cmp)))
|
|
return i;
|
|
|
|
if ((buf[i] & 0xc0) == 0xc0 &&
|
|
len - i - 2 >= strlen(cmp) &&
|
|
!memcmp(buf + i + 2, cmp, strlen(cmp)))
|
|
return i + 1;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* opt_set_dns_search() - Fill data and set length for Domain Search option
|
|
* @c: Execution context
|
|
* @max_len: Maximum total length of option buffer
|
|
*/
|
|
static void opt_set_dns_search(const struct ctx *c, size_t max_len)
|
|
{
|
|
char buf[NS_MAXDNAME];
|
|
int i;
|
|
|
|
opts[119].slen = 0;
|
|
|
|
for (i = 0; i < 255; i++)
|
|
max_len -= opts[i].slen;
|
|
|
|
for (i = 0; *c->dns_search[i].n; i++) {
|
|
unsigned int n;
|
|
int count = -1;
|
|
const char *p;
|
|
|
|
buf[0] = 0;
|
|
for (p = c->dns_search[i].n, n = 1; *p; p++) {
|
|
if (*p == '.') {
|
|
/* RFC 1035 4.1.4 Message compression */
|
|
count = opt_dns_search_dup_ptr(opts[119].s,
|
|
p + 1,
|
|
opts[119].slen);
|
|
|
|
if (count >= 0) {
|
|
buf[n++] = '\xc0';
|
|
buf[n++] = count;
|
|
break;
|
|
}
|
|
buf[n++] = '.';
|
|
} else {
|
|
buf[n++] = *p;
|
|
}
|
|
}
|
|
|
|
/* The compression pointer is also an end of label */
|
|
if (count < 0)
|
|
buf[n++] = 0;
|
|
|
|
if (n >= max_len)
|
|
break;
|
|
|
|
memcpy(opts[119].s + opts[119].slen, buf, n);
|
|
opts[119].slen += n;
|
|
max_len -= n;
|
|
}
|
|
|
|
for (i = 0; i < opts[119].slen; i++) {
|
|
if (!opts[119].s[i] || opts[119].s[i] == '.') {
|
|
opts[119].s[i] = strcspn((char *)opts[119].s + i + 1,
|
|
".\xc0");
|
|
}
|
|
}
|
|
|
|
if (!opts[119].slen)
|
|
opts[119].slen = -1;
|
|
}
|
|
|
|
/**
|
|
* dhcp() - Check if this is a DHCP message, reply as needed
|
|
* @c: Execution context
|
|
* @p: Packet pool, single packet with Ethernet buffer
|
|
*
|
|
* Return: 0 if it's not a DHCP message, 1 if handled, -1 on failure
|
|
*/
|
|
int dhcp(const struct ctx *c, const struct pool *p)
|
|
{
|
|
size_t mlen, dlen, offset = 0, opt_len, opt_off = 0;
|
|
char macstr[ETH_ADDRSTRLEN];
|
|
struct in_addr mask, dst;
|
|
const struct ethhdr *eh;
|
|
const struct iphdr *iph;
|
|
const struct udphdr *uh;
|
|
unsigned int i;
|
|
struct msg *m;
|
|
|
|
eh = packet_get(p, 0, offset, sizeof(*eh), NULL);
|
|
offset += sizeof(*eh);
|
|
|
|
iph = packet_get(p, 0, offset, sizeof(*iph), NULL);
|
|
if (!eh || !iph)
|
|
return -1;
|
|
|
|
offset += iph->ihl * 4UL;
|
|
uh = packet_get(p, 0, offset, sizeof(*uh), &mlen);
|
|
offset += sizeof(*uh);
|
|
|
|
if (!uh)
|
|
return -1;
|
|
|
|
if (uh->dest != htons(67))
|
|
return 0;
|
|
|
|
if (c->no_dhcp)
|
|
return 1;
|
|
|
|
m = packet_get(p, 0, offset, offsetof(struct msg, o), &opt_len);
|
|
if (!m ||
|
|
mlen != ntohs(uh->len) - sizeof(*uh) ||
|
|
mlen < offsetof(struct msg, o) ||
|
|
m->op != BOOTREQUEST)
|
|
return -1;
|
|
|
|
offset += offsetof(struct msg, o);
|
|
|
|
for (i = 0; i < ARRAY_SIZE(opts); i++)
|
|
opts[i].clen = -1;
|
|
|
|
while (opt_off + 2 < opt_len) {
|
|
const uint8_t *olen, *val;
|
|
uint8_t *type;
|
|
|
|
type = packet_get(p, 0, offset + opt_off, 1, NULL);
|
|
olen = packet_get(p, 0, offset + opt_off + 1, 1, NULL);
|
|
if (!type || !olen)
|
|
return -1;
|
|
|
|
val = packet_get(p, 0, offset + opt_off + 2, *olen, NULL);
|
|
if (!val)
|
|
return -1;
|
|
|
|
memcpy(&opts[*type].c, val, *olen);
|
|
opts[*type].clen = *olen;
|
|
opt_off += *olen + 2;
|
|
}
|
|
|
|
opts[80].slen = -1;
|
|
if (opts[53].clen > 0 && opts[53].c[0] == DHCPDISCOVER) {
|
|
if (opts[80].clen == -1) {
|
|
info("DHCP: offer to discover");
|
|
opts[53].s[0] = DHCPOFFER;
|
|
} else {
|
|
info("DHCP: ack to discover (Rapid Commit)");
|
|
opts[53].s[0] = DHCPACK;
|
|
opts[80].slen = 0;
|
|
}
|
|
} else if (opts[53].clen <= 0 || opts[53].c[0] == DHCPREQUEST) {
|
|
info("%s: ack to request", /* DHCP needs a valid message type */
|
|
(opts[53].clen <= 0) ? "BOOTP" : "DHCP");
|
|
opts[53].s[0] = DHCPACK;
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
info(" from %s", eth_ntop(m->chaddr, macstr, sizeof(macstr)));
|
|
|
|
m->yiaddr = c->ip4.addr;
|
|
mask.s_addr = htonl(0xffffffff << (32 - c->ip4.prefix_len));
|
|
memcpy(opts[1].s, &mask, sizeof(mask));
|
|
memcpy(opts[3].s, &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
|
|
memcpy(opts[54].s, &c->ip4.our_tap_addr, sizeof(c->ip4.our_tap_addr));
|
|
|
|
/* If the gateway is not on the assigned subnet, send an option 121
|
|
* (Classless Static Routing) adding a dummy route to it.
|
|
*/
|
|
if ((c->ip4.addr.s_addr & mask.s_addr)
|
|
!= (c->ip4.guest_gw.s_addr & mask.s_addr)) {
|
|
/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
|
|
opts[121].slen = 14;
|
|
opts[121].s[0] = 32;
|
|
memcpy(opts[121].s + 1,
|
|
&c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
|
|
memcpy(opts[121].s + 10,
|
|
&c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
|
|
}
|
|
|
|
if (c->mtu != -1) {
|
|
opts[26].slen = 2;
|
|
opts[26].s[0] = c->mtu / 256;
|
|
opts[26].s[1] = c->mtu % 256;
|
|
}
|
|
|
|
for (i = 0, opts[6].slen = 0;
|
|
!c->no_dhcp_dns && !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns[i]); i++) {
|
|
((struct in_addr *)opts[6].s)[i] = c->ip4.dns[i];
|
|
opts[6].slen += sizeof(uint32_t);
|
|
}
|
|
if (!opts[6].slen)
|
|
opts[6].slen = -1;
|
|
|
|
if (!c->no_dhcp_dns_search)
|
|
opt_set_dns_search(c, sizeof(m->o));
|
|
|
|
dlen = offsetof(struct msg, o) + fill(m);
|
|
|
|
if (m->flags & FLAG_BROADCAST)
|
|
dst = in4addr_broadcast;
|
|
else
|
|
dst = c->ip4.addr;
|
|
|
|
tap_udp4_send(c, c->ip4.our_tap_addr, 67, dst, 68, m, dlen);
|
|
|
|
return 1;
|
|
}
|