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.

ok?

Index: bpf.c
===================================================================
RCS file: /cvs/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       8 Dec 2016 19:34: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: /cvs/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     8 Dec 2016 19:34: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.c
===================================================================
RCS file: /cvs/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  8 Dec 2016 19:34: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: /cvs/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  8 Dec 2016 19:34: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);
 

Reply via email to