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? 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 9 Dec 2016 09:03:44 -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 9 Dec 2016 09:03:44 -0000 @@ -39,6 +39,12 @@ * Enterprises, see ``http://www.vix.com''. */ +enum dhcp_relay_mode { + DRM_UNKNOWN, + DRM_LAYER2, + DRM_LAYER3, +}; + #define SERVER_PORT 67 #define CLIENT_PORT 68 @@ -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 9 Dec 2016 09:05:13 -0000 @@ -45,6 +45,8 @@ .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 .Sh DESCRIPTION @@ -62,6 +64,11 @@ forwards it to the list of DHCP servers 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,7 +80,7 @@ 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 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 9 Dec 2016 09:03:44 -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 [-CdoR] " + "-i interface server1 [... serverN]\n", __progname); exit(1); } @@ -598,4 +657,259 @@ 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; + size_t opttotal; + int optlen, i, hasinfo = 0; + int 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; + + 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); + + newtotal += 2; + + *p++ = RAI_CIRCUIT_ID; + *p++ = circuitlen; + memcpy(p, rai_circuit, circuitlen); + p += circuitlen; + newtotal += 2 + circuitlen; + + 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]; + + switch (*p) { + case RAI_CIRCUIT_ID: + len = *(p + 1); + memcpy(buf, p + 2, len); + buf[len + 1] = 0; + + return (strcmp(rai_circuit, buf)); + + case RAI_REMOTE_ID: + len = *(p + 1); + memcpy(buf, p + 2, len); + buf[len + 1] = 0; + + return (strcmp(rai_circuit, 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; + size_t 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; + + /* + * VMware PXE "ROMs" confuse the DHCP gateway address + * with the IP gateway address. This is a problem if your + * DHCP relay is running on something that's not your + * network gateway. + * + * It is purely informational from the relay to the client + * so we can safely clear it. + */ + dp->giaddr.s_addr = 0x0; + + 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 9 Dec 2016 09:03:44 -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);