On Fri, Dec 09, 2016 at 11:55:17PM +0100, Reyk Floeter wrote: > On Fri, Dec 09, 2016 at 10:08:09AM +0100, Rafael Zalamena wrote: > > On Thu, Dec 08, 2016 at 08:43:20PM +0100, Rafael Zalamena wrote: > > > This diff implements layer 2 relaying support for dhcrelay with further > > > support for Relay Agent Info (RFC 3046). This feature is mostly used by > > > switched networks that might not be using IP addresses when in the edge > > > with the customer. > > > > > > Basically this diff allows you to run dhcrelay on interfaces without > > > addresses and doesn't require you to specify an DHCP server address. > > > Instead you just need to specify the output port. > > > > > > I also updated the man page to show the new options for layer 2 relaying > > > Relay Agent Info knobs, since you might want to let the remote DHCP > > > server know where the DHCP packet is coming from. > > > > I forgot to add the man page in the last diff, here is a new one with > > the man page modifications. > > > > ok? > > > > See comments below. >
Thanks for the in-depth review, however see comments for the following snippets: > For the circuit-id, you could default to the interface name or index > where the packet was received on. The remote-id could even default to > the an hostname or address. And how does it differ from the existing > -o (see below)? Yes we could do that, but then we also have to define a way to make dhcrelay not use Relay Agent Information (L2 without packet modifications). Can we fix this in another diff? > The encoding is different to the DHO_RELAY_AGENT_INFORMATION (option > 82) that I added for enc0/IPsec where the circuit-id is just the > interface index and the remote id an IP address. I know this is > related to the other standard, but could this be merged with > relay_agentinfo() somehow or documented that there is a difference? It is actually the same standard you just added different information, normally you can use the 'giaddr' for layer 3 identification, but I guess you stumbled on something else and you added relay_agentinfo(). I'm happy to make Layer 3 Relay Agent Information use the same code, but can we do that in another diff? Beside those two points mentioned above, I fixed everything else you commented. ok? Index: bpf.c =================================================================== RCS file: /home/obsdcvs/src/usr.sbin/dhcrelay/bpf.c,v retrieving revision 1.13 diff -u -p -r1.13 bpf.c --- bpf.c 8 Dec 2016 19:18:15 -0000 1.13 +++ bpf.c 10 Dec 2016 01:52:46 -0000 @@ -93,6 +93,38 @@ if_register_send(struct interface_info * } /* + * Packet filter program: 'ip and udp and dst port CLIENT_PORT' + */ +struct bpf_insn dhcp_bpf_sfilter[] = { + /* Make sure this is an IP packet... */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 12), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ETHERTYPE_IP, 0, 8), + + /* Make sure it's a UDP packet... */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 23), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_UDP, 0, 6), + + /* Make sure this isn't a fragment... */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 20), + BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, 0x1fff, 4, 0), + + /* Get the IP header length... */ + BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 14), + + /* Make sure it's to the right port... */ + BPF_STMT(BPF_LD + BPF_H + BPF_IND, 16), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, CLIENT_PORT, 0, 1), + + /* If we passed all the tests, ask for the whole packet. */ + BPF_STMT(BPF_RET+BPF_K, (u_int)-1), + + /* Otherwise, drop it. */ + BPF_STMT(BPF_RET+BPF_K, 0), +}; + +int dhcp_bpf_sfilter_len = sizeof(dhcp_bpf_sfilter) / sizeof(struct bpf_insn); + +/* * Packet filter program: 'ip and udp and dst port SERVER_PORT' */ struct bpf_insn dhcp_bpf_filter[] = { @@ -161,6 +193,38 @@ struct bpf_insn dhcp_bpf_efilter[] = { int dhcp_bpf_efilter_len = sizeof(dhcp_bpf_efilter) / sizeof(struct bpf_insn); /* + * Packet write filter program: 'ip and udp and src port CLIENT_PORT' + */ +struct bpf_insn dhcp_bpf_swfilter[] = { + /* Make sure this is an IP packet... */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 12), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ETHERTYPE_IP, 0, 8), + + /* Make sure it's a UDP packet... */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 23), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_UDP, 0, 6), + + /* Make sure this isn't a fragment... */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 20), + BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, 0x1fff, 4, 0), + + /* Get the IP header length... */ + BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 14), + + /* Make sure it's from the right port... */ + BPF_STMT(BPF_LD + BPF_H + BPF_IND, 14), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, CLIENT_PORT, 0, 1), + + /* If we passed all the tests, ask for the whole packet. */ + BPF_STMT(BPF_RET+BPF_K, (u_int)-1), + + /* Otherwise, drop it. */ + BPF_STMT(BPF_RET+BPF_K, 0), +}; + +int dhcp_bpf_swfilter_len = sizeof(dhcp_bpf_swfilter) / sizeof(struct bpf_insn); + +/* * Packet write filter program: 'ip and udp and src port SERVER_PORT' */ struct bpf_insn dhcp_bpf_wfilter[] = { @@ -193,7 +257,7 @@ struct bpf_insn dhcp_bpf_wfilter[] = { int dhcp_bpf_wfilter_len = sizeof(dhcp_bpf_wfilter) / sizeof(struct bpf_insn); void -if_register_receive(struct interface_info *info) +if_register_receive(struct interface_info *info, int isserver) { struct bpf_version v; struct bpf_program p; @@ -234,7 +298,10 @@ if_register_receive(struct interface_inf info->rbuf_len = 0; /* Set up the bpf filter program structure. */ - if (info->hw_address.htype == HTYPE_IPSEC_TUNNEL) { + if (isserver) { + p.bf_len = dhcp_bpf_sfilter_len; + p.bf_insns = dhcp_bpf_sfilter; + } else if (info->hw_address.htype == HTYPE_IPSEC_TUNNEL) { p.bf_len = dhcp_bpf_efilter_len; p.bf_insns = dhcp_bpf_efilter; } else { @@ -245,8 +312,13 @@ if_register_receive(struct interface_inf error("Can't install packet filter program: %m"); /* Set up the bpf write filter program structure. */ - p.bf_len = dhcp_bpf_wfilter_len; - p.bf_insns = dhcp_bpf_wfilter; + if (isserver) { + p.bf_len = dhcp_bpf_swfilter_len; + p.bf_insns = dhcp_bpf_swfilter; + } else { + p.bf_len = dhcp_bpf_wfilter_len; + p.bf_insns = dhcp_bpf_wfilter; + } if (ioctl(info->rfdesc, BIOCSETWF, &p) == -1) error("Can't install write filter program: %m"); Index: dhcpd.h =================================================================== RCS file: /home/obsdcvs/src/usr.sbin/dhcrelay/dhcpd.h,v retrieving revision 1.17 diff -u -p -r1.17 dhcpd.h --- dhcpd.h 8 Dec 2016 19:18:15 -0000 1.17 +++ dhcpd.h 10 Dec 2016 01:54:23 -0000 @@ -77,6 +77,12 @@ enum dhcp_state { S_REBINDING }; +/* DHCP relaying modes. */ +enum dhcp_relay_mode { + DRM_UNKNOWN, + DRM_LAYER2, + DRM_LAYER3, +}; struct interface_info { struct interface_info *next; @@ -123,7 +129,7 @@ int debug(char *, ...) __attribute__ ((_ /* bpf.c */ int if_register_bpf(struct interface_info *); void if_register_send(struct interface_info *); -void if_register_receive(struct interface_info *); +void if_register_receive(struct interface_info *, int); ssize_t send_packet(struct interface_info *, struct dhcp_packet *, size_t, struct packet_ctx *); ssize_t receive_packet(struct interface_info *, unsigned char *, size_t, @@ -133,7 +139,7 @@ ssize_t receive_packet(struct interface_ extern void (*bootp_packet_handler)(struct interface_info *, struct dhcp_packet *, int, struct packet_ctx *); struct interface_info *get_interface(const char *, - void (*)(struct protocol *)); + void (*)(struct protocol *), int isserver); void dispatch(void); void got_one(struct protocol *); void add_protocol(char *, int, void (*)(struct protocol *), void *); Index: dhcrelay.8 =================================================================== RCS file: /home/obsdcvs/src/usr.sbin/dhcrelay/dhcrelay.8,v retrieving revision 1.12 diff -u -p -r1.12 dhcrelay.8 --- dhcrelay.8 16 Jul 2013 11:13:33 -0000 1.12 +++ dhcrelay.8 10 Dec 2016 02:12:53 -0000 @@ -45,8 +45,10 @@ .Sh SYNOPSIS .Nm .Op Fl do +.Op Fl C Ar circuit-id +.Op Fl R Ar remote-id .Fl i Ar interface -.Ar server1 Op Ar ... serverN +.Ar destination1 Op Ar ... destinationN .Sh DESCRIPTION The .Nm @@ -58,10 +60,15 @@ other subnets. listens for DHCP requests on a given interface. When a query is received, .Nm -forwards it to the list of DHCP servers specified on the command line. +forwards it to the list of DHCP destinations specified on the command line. When a reply is received, it is broadcast or unicast on the network from whence the original request came. .Pp +The server might be a name, address or interface. +.Nm +will operate in layer 2 mode when the specified servers are interfaces, +otherwise it will operate in layer 3 mode. +.Pp The name of at least one DHCP server to which DHCP and BOOTP requests should be relayed, as well as the name of the network interface that @@ -73,12 +80,16 @@ must be specified on the command line. supports relaying of DHCP traffic to configure IPsec tunnel mode clients when listening on the .Xr enc 4 -interface. +interface using layer 3 mode only. The DHCP server has to support RFC 3046 to echo back the relay agent information to allow stateless DHCP reply to IPsec tunnel mapping. .Pp The options are as follows: .Bl -tag -width Ds +.It Fl C Ar circuit-id +The circuit-id Relay Agent Information sub-option value that +.Nm +should append on layer 2 relayed packets. .It Fl d .Nm normally runs in the foreground until it has configured @@ -96,6 +107,10 @@ Add the relay agent information option. By default, this is only enabled for the .Xr enc 4 interface. +.It Fl R Ar remote-id +The remote-id Relay Agent Information sub-option value that +.Nm +should append on layer 2 relayed packets. .El .Sh SEE ALSO .Xr dhclient 8 , Index: dhcrelay.c =================================================================== RCS file: /home/obsdcvs/src/usr.sbin/dhcrelay/dhcrelay.c,v retrieving revision 1.49 diff -u -p -r1.49 dhcrelay.c --- dhcrelay.c 8 Dec 2016 19:18:15 -0000 1.49 +++ dhcrelay.c 11 Dec 2016 00:18:42 -0000 @@ -66,6 +66,8 @@ void usage(void); int rdaemon(int); void relay(struct interface_info *, struct dhcp_packet *, int, struct packet_ctx *); +void l2relay(struct interface_info *, struct dhcp_packet *, int, + struct packet_ctx *); char *print_hw_addr(int, int, unsigned char *); void got_response(struct protocol *); int get_rdomain(char *); @@ -84,7 +86,12 @@ struct interface_info *interfaces = NULL int server_fd; int oflag; +enum dhcp_relay_mode drm = DRM_UNKNOWN; +const char *rai_circuit = NULL; +const char *rai_remote = NULL; + struct server_list { + struct interface_info *intf; struct server_list *next; struct sockaddr_in to; int fd; @@ -98,6 +105,7 @@ main(int argc, char *argv[]) struct server_list *sp = NULL; struct passwd *pw; struct sockaddr_in laddr; + int optslen; daemonize = 1; @@ -105,8 +113,11 @@ main(int argc, char *argv[]) openlog(__progname, LOG_NDELAY, DHCPD_LOG_FACILITY); setlogmask(LOG_UPTO(LOG_INFO)); - while ((ch = getopt(argc, argv, "adi:o")) != -1) { + while ((ch = getopt(argc, argv, "aC:di:oR:")) != -1) { switch (ch) { + case 'C': + rai_circuit = optarg; + break; case 'd': daemonize = 0; break; @@ -114,7 +125,7 @@ main(int argc, char *argv[]) if (interfaces != NULL) usage(); - interfaces = get_interface(optarg, got_one); + interfaces = get_interface(optarg, got_one, 0); if (interfaces == NULL) error("interface '%s' not found", optarg); break; @@ -122,6 +133,9 @@ main(int argc, char *argv[]) /* add the relay agent information option */ oflag++; break; + case 'R': + rai_remote = optarg; + break; default: usage(); @@ -135,10 +149,42 @@ main(int argc, char *argv[]) if (argc < 1) usage(); + if (rai_remote != NULL && rai_circuit == NULL) + error("you must specify a circuit-id with a remote-id"); + + /* Validate that we have space for all suboptions. */ + if (rai_circuit != NULL) { + optslen = 2 + strlen(rai_circuit); + if (rai_remote != NULL) + optslen += 2 + strlen(rai_remote); + + if (optslen > 255) + error("relay agent information is too long"); + } + while (argc > 0) { struct hostent *he; struct in_addr ia, *iap = NULL; + if ((sp = calloc(1, sizeof(*sp))) == NULL) + error("calloc"); + + if ((sp->intf = get_interface(argv[0], got_one, 1)) != NULL) { + if (drm == DRM_LAYER3) + error("don't mix interfaces with hosts"); + + if (sp->intf->hw_address.htype == HTYPE_IPSEC_TUNNEL) + error("can't use IPSec with layer 2"); + + sp->next = servers; + servers = sp; + + drm = DRM_LAYER2; + argc--; + argv++; + continue; + } + if (inet_aton(argv[0], &ia)) iap = &ia; else { @@ -149,12 +195,16 @@ main(int argc, char *argv[]) iap = ((struct in_addr *)he->h_addr_list[0]); } if (iap) { - if ((sp = calloc(1, sizeof *sp)) == NULL) - error("calloc"); + if (drm == DRM_LAYER2) + error("don't mix interfaces with hosts"); + + drm = DRM_LAYER3; sp->next = servers; servers = sp; memcpy(&sp->to.sin_addr, iap, sizeof *iap); - } + } else + free(sp); + argc--; argv++; } @@ -167,7 +217,9 @@ main(int argc, char *argv[]) if (interfaces == NULL) error("no interface given"); - if (interfaces->primary_address.s_addr == 0) + /* We need an address for running layer 3 mode. */ + if (drm == DRM_LAYER3 && + interfaces->primary_address.s_addr == 0) error("interface '%s' does not have an address", interfaces->name); @@ -192,6 +244,9 @@ main(int argc, char *argv[]) laddr.sin_addr.s_addr = interfaces->primary_address.s_addr; /* Set up the server sockaddrs. */ for (sp = servers; sp; sp = sp->next) { + if (sp->intf != NULL) + break; + sp->to.sin_port = server_port; sp->to.sin_family = AF_INET; sp->to.sin_len = sizeof sp->to; @@ -234,7 +289,10 @@ main(int argc, char *argv[]) tzset(); time(&cur_time); - bootp_packet_handler = relay; + if (drm == DRM_LAYER3) + bootp_packet_handler = relay; + else + bootp_packet_handler = l2relay; if ((pw = getpwnam("_dhcp")) == NULL) error("user \"_dhcp\" not found"); @@ -374,7 +432,8 @@ usage(void) { extern char *__progname; - fprintf(stderr, "usage: %s [-do] -i interface server1 [... serverN]\n", + fprintf(stderr, "usage: %s [-do] [-C circuit-id] [-R remote-id] " + "-i interface\n\tdestination1 [... destinationN]\n", __progname); exit(1); } @@ -598,4 +657,267 @@ get_rdomain(char *name) close(s); return rv; +} + +ssize_t +relayagentinfo_append(struct dhcp_packet *dp, size_t dplen) +{ + uint8_t *p; + ssize_t newtotal = dplen; + int opttotal, optlen, i, hasinfo = 0; + int maxlen, circuitlen, remotelen; + + if (rai_circuit == NULL) + return (dplen); + + p = (uint8_t *)&dp->options; + if (memcmp(p, DHCP_OPTIONS_COOKIE, DHCP_OPTIONS_COOKIE_LEN)) { + note("invalid dhcp options cookie"); + return (-1); + } + + p += DHCP_OPTIONS_COOKIE_LEN; + opttotal = dplen - DHCP_FIXED_NON_UDP - DHCP_OPTIONS_COOKIE_LEN; + maxlen = DHCP_MTU_MAX - DHCP_FIXED_LEN - DHCP_OPTIONS_COOKIE_LEN - 1; + if (maxlen < 1 || opttotal < 1) + return (dplen); + + for (i = 0; i < opttotal && *p != DHO_END;) { + if (*p == DHO_PAD) + optlen = 1; + else + optlen = p[1] + 2; + + if ((i + optlen) > opttotal) { + note("truncated dhcp options"); + return (-1); + } + + if (*p == DHO_RELAY_AGENT_INFORMATION) { + hasinfo = 1; + continue; + } + + p += optlen; + i += optlen; + + /* We reached the end, append the relay agent info. */ + if (*p == DHO_END || i >= opttotal) { + /* We already have the Relay Agent Info, skip it. */ + if (hasinfo) + continue; + + circuitlen = strlen(rai_circuit); + if (rai_remote != NULL) + remotelen = strlen(rai_remote); + else + remotelen = 0; + + *p++ = DHO_RELAY_AGENT_INFORMATION; + if (rai_remote != NULL) + *p++ = (2 + circuitlen) + (2 + remotelen); + else + *p++ = (2 + circuitlen); + + if ((newtotal + (2 * 3) + circuitlen + remotelen) > + maxlen) { + warning("no space for relay agent info"); + return (newtotal); + } + + /* New option header: 2 bytes. */ + newtotal += 2; + + /* Sub-option circuit-id header plus value. */ + *p++ = RAI_CIRCUIT_ID; + *p++ = circuitlen; + memcpy(p, rai_circuit, circuitlen); + p += circuitlen; + newtotal += 2 + circuitlen; + + /* Optionally sub-option remote-id header plus value. */ + if (rai_remote != NULL) { + *p++ = RAI_REMOTE_ID; + *p++ = remotelen; + memcpy(p, rai_remote, remotelen); + p += remotelen; + newtotal += 2 + remotelen; + } + + *p = DHO_END; + } + } + + return (newtotal); +} + +int +relayagentinfo_cmp(uint8_t *p, int plen) +{ + int len; + char buf[256]; + + len = *(p + 1); + if (len > plen) + return (-1); + + switch (*p) { + case RAI_CIRCUIT_ID: + if (rai_circuit == NULL) + return (-1); + + memcpy(buf, p + 2, len); + buf[len] = 0; + + return (strcmp(rai_circuit, buf)); + + case RAI_REMOTE_ID: + if (rai_remote == NULL) + return (-1); + + memcpy(buf, p + 2, len); + buf[len] = 0; + + return (strcmp(rai_remote, buf)); + + default: + /* Unmatched type */ + note("unmatched relay info %d", *p); + return (0); + } +} + +ssize_t +relayagentinfo_remove(struct dhcp_packet *dp, size_t dplen) +{ + uint8_t *p, *np, *startp, *endp; + int opttotal, optleft; + int suboptlen, optlen, i; + int remaining, matched = 0; + + startp = (uint8_t *)dp; + p = (uint8_t *)&dp->options; + if (memcmp(p, DHCP_OPTIONS_COOKIE, DHCP_OPTIONS_COOKIE_LEN)) { + note("invalid dhcp options cookie"); + return (-1); + } + + opttotal = dplen - DHCP_FIXED_NON_UDP - DHCP_OPTIONS_COOKIE_LEN; + optleft = opttotal; + + p += DHCP_OPTIONS_COOKIE_LEN; + endp = p + opttotal; + + for (i = 0; i < opttotal && *p != DHO_END;) { + if (*p == DHO_PAD) + optlen = 1; + else + optlen = p[1] + 2; + + if ((i + optlen) > opttotal) { + note("truncated dhcp options"); + return (-1); + } + + if (*p == DHO_RELAY_AGENT_INFORMATION) { + /* Fast case: there is no next option. */ + np = p + optlen; + if (*np == DHO_END) { + *p = *np; + endp = p + 1; + return (endp - startp); + } + + remaining = optlen; + while (remaining > 0) { + suboptlen = *(p + 1); + remaining -= 2 + suboptlen; + + matched = 1; + if (relayagentinfo_cmp(p, suboptlen) == 0) + continue; + + matched = 0; + break; + } + /* It is not ours Relay Agent Info, don't remove it. */ + if (matched == 0) + break; + + /* Move the other options on top of this one. */ + optleft -= optlen; + endp -= optlen; + + /* Replace the old agent relay info. */ + memmove(p, dp, optleft); + + return (endp - p); + } + + p += optlen; + i += optlen; + optleft -= optlen; + } + + return (endp - startp); +} + +void +l2relay(struct interface_info *ip, struct dhcp_packet *dp, int length, + struct packet_ctx *pc) +{ + struct server_list *sp; + ssize_t dplen; + + if (dp->hlen > sizeof(dp->chaddr)) { + note("Discarding packet with invalid hlen."); + return; + } + + switch (dp->op) { + case BOOTREQUEST: + /* Add the relay agent info asked by the user. */ + if ((dplen = relayagentinfo_append(dp, length)) == -1) + return; + + /* + * Re-send the packet to every interface except the one + * it came in. + */ + for (sp = servers; sp != NULL; sp = sp->next) { + if (sp->intf == ip) + continue; + + debug("forwarded BOOTREQUEST for %s to %s", + print_hw_addr(pc->pc_htype, pc->pc_hlen, + pc->pc_smac), sp->intf->name); + + send_packet(sp->intf, dp, dplen, pc); + } + if (ip != interfaces) { + debug("forwarded BOOTREQUEST for %s to %s", + print_hw_addr(pc->pc_htype, pc->pc_hlen, + pc->pc_smac), interfaces->name); + + send_packet(interfaces, dp, dplen, pc); + } + break; + + case BOOTREPLY: + /* Remove relay agent info on offer. */ + if ((dplen = relayagentinfo_remove(dp, length)) == -1) + return; + + if (ip != interfaces) { + debug("forwarded BOOTREPLY for %s to %s", + print_hw_addr(pc->pc_htype, pc->pc_hlen, + pc->pc_dmac), interfaces->name); + send_packet(interfaces, dp, dplen, pc); + } + break; + + default: + debug("invalid operation type '%d'", dp->op); + return; + } } Index: dispatch.c =================================================================== RCS file: /home/obsdcvs/src/usr.sbin/dhcrelay/dispatch.c,v retrieving revision 1.14 diff -u -p -r1.14 dispatch.c --- dispatch.c 8 Dec 2016 19:18:15 -0000 1.14 +++ dispatch.c 10 Dec 2016 01:52:46 -0000 @@ -74,7 +74,8 @@ void (*bootp_packet_handler)(struct inte static int interface_status(struct interface_info *ifinfo); struct interface_info * -get_interface(const char *ifname, void (*handler)(struct protocol *)) +get_interface(const char *ifname, void (*handler)(struct protocol *), + int isserver) { struct interface_info *iface; struct ifaddrs *ifap, *ifa; @@ -145,7 +146,7 @@ get_interface(const char *ifname, void ( error("interface name '%s' too long", ifname); /* Register the interface... */ - if_register_receive(iface); + if_register_receive(iface, isserver); if_register_send(iface); add_protocol(iface->name, iface->rfdesc, handler, iface);