| From ccd28aa8243a7a53862560a66f924fb1c6f048d0 Mon Sep 17 00:00:00 2001 |
| From: Paul Stewart <pstew@chromium.org> |
| Date: Mon, 11 May 2015 14:42:05 -0700 |
| Subject: [PATCH] dhcpcd: Teach DHCP client to do unicast-ARP for gateway |
| |
| Implement RFC-4436 ("Detecting Network Attachment in IPv4 (DNAv4)"). |
| Keep track of the MAC address of the default gateway in a file as |
| a part of the arpgw change ("-R" option), and append this info to |
| the end of the saved lease. |
| |
| Implement a separate command line option ("-P") which will use |
| the MAC address stored in the lease to do a unicast ARP to the |
| default gateway saved in the lease. If this succeeds, notify |
| listeners that this succeeded, along with the contents of the lease, |
| but do not stop the normal DHCP process. This returns immediate, |
| fast feedback that our lease will likely work, but continues the |
| DHCP process so we know for sure. |
| |
| BUG=chromium-os:25717 |
| TEST=Manual: Hex dumps of the lease file with or without the "-R" |
| option set. Ensure gateway MAC address appears in the lease when run |
| to success with "-R", and that a successive run without "-R" casuses |
| the MAC to disappear. tcpdump packet captures of DHCP and ARP traffic |
| when running with and without the "-P" option set after a previous run |
| with or without the "-R" option set. |
| |
| Reviewed-on: https://gerrit.chromium.org/gerrit/22643 |
| |
| --- |
| arp.c | 19 ++++++-- |
| arp.h | 2 + |
| dhcp.c | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++------- |
| dhcp.h | 7 +++ |
| if-bsd.c | 7 ++- |
| if-linux.c | 10 ++-- |
| if-options.c | 4 ++ |
| if-options.h | 3 +- |
| if.h | 2 +- |
| 9 files changed, 180 insertions(+), 30 deletions(-) |
| |
| diff --git a/arp.c b/arp.c |
| index abb6d4e..5fb23a3 100644 |
| --- a/arp.c |
| +++ b/arp.c |
| @@ -55,7 +55,8 @@ |
| (sizeof(struct arphdr) + (2 * sizeof(uint32_t)) + (2 * HWADDR_LEN)) |
| |
| static ssize_t |
| -arp_request(const struct interface *ifp, in_addr_t sip, in_addr_t tip) |
| +arp_request(const struct interface *ifp, in_addr_t sip, in_addr_t tip, |
| + const uint8_t *dest_hw_addr) |
| { |
| uint8_t arp_buffer[ARP_LEN]; |
| struct arphdr ar; |
| @@ -85,9 +86,13 @@ arp_request(const struct interface *ifp, in_addr_t sip, in_addr_t tip) |
| APPEND(&ar, sizeof(ar)); |
| APPEND(ifp->hwaddr, ifp->hwlen); |
| APPEND(&sip, sizeof(sip)); |
| - ZERO(ifp->hwlen); |
| + if (dest_hw_addr) |
| + APPEND(dest_hw_addr, ifp->hwlen); |
| + else |
| + ZERO(ifp->hwlen); |
| APPEND(&tip, sizeof(tip)); |
| - return if_sendrawpacket(ifp, ETHERTYPE_ARP, arp_buffer, len); |
| + return if_sendrawpacket(ifp, ETHERTYPE_ARP, arp_buffer, len, |
| + dest_hw_addr); |
| |
| eexit: |
| errno = ENOBUFS; |
| @@ -232,7 +237,8 @@ arp_announce1(void *arg) |
| "%s: ARP announcing %s (%d of %d)", |
| ifp->name, inet_ntoa(astate->addr), |
| astate->claims, ANNOUNCE_NUM); |
| - if (arp_request(ifp, astate->addr.s_addr, astate->addr.s_addr) == -1) |
| + if (arp_request(ifp, astate->addr.s_addr, astate->addr.s_addr, |
| + NULL) == -1) |
| logger(ifp->ctx, LOG_ERR, "send_arp: %m"); |
| eloop_timeout_add_sec(ifp->ctx->eloop, ANNOUNCE_WAIT, |
| astate->claims < ANNOUNCE_NUM ? arp_announce1 : arp_announced, |
| @@ -262,6 +268,7 @@ arp_probe1(void *arg) |
| struct arp_state *astate = arg; |
| struct interface *ifp = astate->iface; |
| struct timespec tv; |
| + uint8_t *dest_hwaddr = NULL; |
| |
| if (++astate->probes < PROBE_NUM) { |
| tv.tv_sec = PROBE_MIN; |
| @@ -279,8 +286,10 @@ arp_probe1(void *arg) |
| ifp->name, inet_ntoa(astate->addr), |
| astate->probes ? astate->probes : PROBE_NUM, PROBE_NUM, |
| timespec_to_double(&tv)); |
| + if (astate->dest_hwlen == ifp->hwlen) |
| + dest_hwaddr = astate->dest_hwaddr; |
| if (arp_request(ifp, astate->src_addr.s_addr, |
| - astate->addr.s_addr) == -1) |
| + astate->addr.s_addr, dest_hwaddr) == -1) |
| logger(ifp->ctx, LOG_ERR, "send_arp: %m"); |
| } |
| |
| diff --git a/arp.h b/arp.h |
| index 8c50092..0daf615 100644 |
| --- a/arp.h |
| +++ b/arp.h |
| @@ -63,6 +63,8 @@ struct arp_state { |
| int probes; |
| int claims; |
| struct in_addr failed; |
| + uint8_t dest_hwlen; |
| + unsigned char dest_hwaddr[HWADDR_LEN]; |
| }; |
| TAILQ_HEAD(arp_statehead, arp_state); |
| |
| diff --git a/dhcp.c b/dhcp.c |
| index 9cb50a7..93a329f 100644 |
| --- a/dhcp.c |
| +++ b/dhcp.c |
| @@ -1070,6 +1070,8 @@ write_lease(const struct interface *ifp, const struct dhcp_message *dhcp) |
| uint8_t l; |
| uint8_t o = 0; |
| const struct dhcp_state *state = D_CSTATE(ifp); |
| + uint8_t write_buffer[sizeof(*dhcp) + sizeof(state->server_info) + 1]; |
| + uint8_t *w; |
| |
| /* We don't write BOOTP leases */ |
| if (IS_BOOTP(ifp, dhcp)) { |
| @@ -1100,7 +1102,18 @@ write_lease(const struct interface *ifp, const struct dhcp_message *dhcp) |
| p += l; |
| } |
| } |
| - bytes = write(fd, dhcp, len); |
| + |
| + memcpy(write_buffer, dhcp, len); |
| + w = write_buffer + len; |
| + |
| + /* Copy in server info if this is available. */ |
| + if (state->server_info.gw_hwlen != 0) { |
| + *w++ = DHO_END; |
| + memcpy(w, &state->server_info, sizeof(state->server_info)); |
| + len += sizeof(state->server_info) + 1; |
| + } |
| + |
| + bytes = write(fd, write_buffer, len); |
| close(fd); |
| return bytes; |
| } |
| @@ -1111,11 +1124,18 @@ read_lease(struct interface *ifp) |
| int fd; |
| struct dhcp_message *dhcp; |
| struct dhcp_state *state = D_STATE(ifp); |
| + uint8_t read_buffer[sizeof(*dhcp) + sizeof(state->server_info) + 1]; |
| + const uint8_t *options_startp = |
| + read_buffer + offsetof(struct dhcp_message, options); |
| + const uint8_t *options_endp = options_startp + sizeof(dhcp->options); |
| + uint8_t option_len; |
| + uint8_t option_type = 0; |
| ssize_t bytes; |
| const uint8_t *auth; |
| uint8_t type; |
| size_t auth_len; |
| |
| + memset(&state->server_info, 0, sizeof(state->server_info)); |
| fd = open(state->leasefile, O_RDONLY); |
| if (fd == -1) { |
| if (errno != ENOENT) |
| @@ -1125,17 +1145,31 @@ read_lease(struct interface *ifp) |
| } |
| logger(ifp->ctx, LOG_DEBUG, "%s: reading lease `%s'", |
| ifp->name, state->leasefile); |
| + bytes = read(fd, read_buffer, sizeof(read_buffer)); |
| + close(fd); |
| + |
| + /* Lease file should at minimum contain all fields before options. */ |
| + if (read_buffer + bytes < options_startp) |
| + return NULL; |
| + |
| dhcp = calloc(1, sizeof(*dhcp)); |
| if (dhcp == NULL) { |
| - close(fd); |
| return NULL; |
| } |
| - bytes = read(fd, dhcp, sizeof(*dhcp)); |
| - close(fd); |
| - if (bytes < 0) { |
| - free(dhcp); |
| - return NULL; |
| + |
| + if (options_endp > read_buffer + bytes) |
| + options_endp = read_buffer + bytes; |
| + |
| + while (options_startp < options_endp) { |
| + option_type = *options_startp++; |
| + if (option_type == DHO_END) |
| + break; |
| + if (option_type != DHO_PAD) { |
| + option_len = *options_startp++; |
| + options_startp += option_len; |
| + } |
| } |
| + memcpy(dhcp, read_buffer, options_startp - read_buffer); |
| |
| /* We may have found a BOOTP server */ |
| if (get_option_uint8(ifp->ctx, &type, dhcp, DHO_MESSAGETYPE) == -1) |
| @@ -1162,6 +1196,27 @@ read_lease(struct interface *ifp) |
| "%s: accepted reconfigure key", ifp->name); |
| } |
| |
| + /* |
| + * DHCP server information is stored after the DHO_END character |
| + * in the lease file. The first byte of the server information |
| + * is the length of the gateway hardware address. |
| + */ |
| + options_endp = read_buffer + bytes; |
| + if (options_startp >= options_endp || |
| + options_startp + sizeof(state->server_info) > options_endp) |
| + return dhcp; |
| + |
| + logger(ifp->ctx, LOG_DEBUG, "%s: found server info in lease '%s'", |
| + ifp->name, state->leasefile); |
| + |
| + memcpy(&state->server_info, options_startp, sizeof(state->server_info)); |
| + if (state->server_info.gw_hwlen != ifp->hwlen) { |
| + logger(ifp->ctx, LOG_ERR, "%s: lease file %s has incompatible" |
| + "MAC address length %d (expected %zd)", |
| + ifp->name, state->leasefile, |
| + state->server_info.gw_hwlen, ifp->hwlen); |
| + memset(&state->server_info, 0, sizeof(state->server_info)); |
| + } |
| return dhcp; |
| } |
| |
| @@ -1681,7 +1736,7 @@ send_message(struct interface *ifp, uint8_t type, |
| logger(ifp->ctx, LOG_ERR, "dhcp_makeudppacket: %m"); |
| } else { |
| r = if_sendrawpacket(ifp, ETHERTYPE_IP, |
| - (uint8_t *)udp, ulen); |
| + (uint8_t *)udp, ulen, NULL); |
| free(udp); |
| } |
| /* If we failed to send a raw packet this normally means |
| @@ -2129,6 +2184,8 @@ dhcp_reboot_newopts(struct interface *ifp, unsigned long long oldopts) |
| } |
| } |
| |
| +static void start_unicast_arp(struct interface *ifp); |
| + |
| static void |
| dhcp_reboot(struct interface *ifp) |
| { |
| @@ -2150,6 +2207,9 @@ dhcp_reboot(struct interface *ifp) |
| dhcp_static(ifp); |
| return; |
| } |
| + if (ifo->options & DHCPCD_UNICAST_ARP) { |
| + start_unicast_arp(ifp); |
| + } |
| if (ifo->options & DHCPCD_INFORM) { |
| logger(ifp->ctx, LOG_INFO, "%s: informing address of %s", |
| ifp->name, inet_ntoa(state->lease.addr)); |
| @@ -2342,10 +2402,23 @@ whitelisted_ip(const struct if_options *ifo, in_addr_t addr) |
| } |
| |
| static void |
| -dhcp_probe_gw_timeout(struct arp_state *astate) { |
| +save_gateway_addr(struct interface *ifp, const uint8_t *gw_hwaddr) |
| +{ |
| + struct dhcp_state *state = D_STATE(ifp); |
| + memcpy(state->server_info.gw_hwaddr, gw_hwaddr, ifp->hwlen); |
| + state->server_info.gw_hwlen = ifp->hwlen; |
| +} |
| + |
| +static void |
| +dhcp_probe_gw_timeout(struct arp_state *astate) |
| +{ |
| struct dhcp_state *state = D_STATE(astate->iface); |
| |
| - /* Allow ourselves to fail only once this way */ |
| + /* Ignore unicast ARP failures. */ |
| + if (astate->dest_hwlen) |
| + return; |
| + |
| + /* Probegw failure, allow ourselves to fail only once this way */ |
| logger(astate->iface->ctx, LOG_ERR, |
| "%s: Probe gateway %s timed out ", |
| astate->iface->name, inet_ntoa(astate->addr)); |
| @@ -2373,14 +2446,22 @@ dhcp_probe_gw_response(struct arp_state *astate, const struct arp_msg *amsg) |
| amsg && |
| amsg->tip.s_addr == astate->src_addr.s_addr && |
| amsg->sip.s_addr == astate->addr.s_addr) { |
| - dhcp_close(astate->iface); |
| - eloop_timeout_delete(astate->iface->ctx->eloop, |
| - NULL, astate->iface); |
| -#ifdef IN_IFF_TENTATIVE |
| - ipv4_finaliseaddr(astate->iface); |
| -#else |
| - dhcp_bind(astate->iface, NULL); |
| -#endif |
| + if (astate->dest_hwlen) { |
| + /* Response to unicast ARP. */ |
| + /* TODO(zqiu): notify listener. */ |
| + } else { |
| + /* Response to arpgw request. */ |
| + save_gateway_addr(astate->iface, amsg->sha); |
| + |
| + dhcp_close(astate->iface); |
| + eloop_timeout_delete(astate->iface->ctx->eloop, |
| + NULL, astate->iface); |
| + #ifdef IN_IFF_TENTATIVE |
| + ipv4_finaliseaddr(astate->iface); |
| + #else |
| + dhcp_bind(astate->iface, NULL); |
| + #endif |
| + } |
| arp_close(astate->iface); |
| } |
| } |
| @@ -2407,6 +2488,45 @@ dhcp_probe_gw(struct interface *ifp) |
| } |
| |
| static void |
| +start_unicast_arp(struct interface *ifp) |
| +{ |
| + struct dhcp_state *state = D_STATE(ifp); |
| + struct in_addr gwa; |
| + struct in_addr src_addr; |
| + struct arp_state *astate; |
| + |
| + if (!state->offer) |
| + return; |
| + |
| + if (!state->lease.frominfo) |
| + return; |
| + |
| + if (state->server_info.gw_hwlen != ifp->hwlen) |
| + return; |
| + |
| + if (get_option_addr(ifp->ctx, &gwa, state->offer, DHO_ROUTER)) |
| + return; |
| + |
| + astate = arp_new(ifp, &gwa); |
| + if (!astate) |
| + return; |
| + if (state->offer->yiaddr) |
| + astate->src_addr.s_addr = state->offer->yiaddr; |
| + else |
| + astate->src_addr.s_addr = state->offer->ciaddr; |
| + astate->probed_cb = dhcp_probe_gw_timeout; |
| + astate->conflicted_cb = dhcp_probe_gw_response; |
| + astate->dest_hwlen = state->server_info.gw_hwlen; |
| + memcpy(astate->dest_hwaddr, state->server_info.gw_hwaddr, |
| + state->server_info.gw_hwlen); |
| + |
| + arp_probe(astate); |
| + |
| + /* Invalidate our gateway address until the next successful PROBEGW. */ |
| + state->server_info.gw_hwlen = 0; |
| +} |
| + |
| +static void |
| dhcp_arp_probed(struct arp_state *astate) |
| { |
| struct dhcp_state *state; |
| diff --git a/dhcp.h b/dhcp.h |
| index 85ce06f..acef896 100644 |
| --- a/dhcp.h |
| +++ b/dhcp.h |
| @@ -185,6 +185,12 @@ struct dhcp_lease { |
| uint32_t cookie; |
| }; |
| |
| +/* Extra data about servers stored in the lease file after the dhcp_message */ |
| +struct dhcp_server_info { |
| + uint8_t gw_hwlen; |
| + unsigned char gw_hwaddr[HWADDR_LEN]; |
| +}; |
| + |
| enum DHS { |
| DHS_INIT, |
| DHS_DISCOVER, |
| @@ -236,6 +242,7 @@ struct dhcp_state { |
| unsigned int conflicts; |
| time_t defend; |
| char randomstate[128]; |
| + struct dhcp_server_info server_info; |
| }; |
| |
| #define D_STATE(ifp) \ |
| diff --git a/if-bsd.c b/if-bsd.c |
| index 14ea913..ec4d4ba 100644 |
| --- a/if-bsd.c |
| +++ b/if-bsd.c |
| @@ -376,7 +376,7 @@ eexit: |
| |
| ssize_t |
| if_sendrawpacket(const struct interface *ifp, uint16_t protocol, |
| - const void *data, size_t len) |
| + const void *data, size_t len, const uint8_t *dest_hw_addr) |
| { |
| struct iovec iov[2]; |
| struct ether_header hw; |
| @@ -384,7 +384,10 @@ if_sendrawpacket(const struct interface *ifp, uint16_t protocol, |
| const struct dhcp_state *state; |
| |
| memset(&hw, 0, ETHER_HDR_LEN); |
| - memset(&hw.ether_dhost, 0xff, ETHER_ADDR_LEN); |
| + if (dest_hw_addr) |
| + memcpy(&hw.ether_dhost, dest_hw_addr, ETHER_ADDR_LEN); |
| + else |
| + memset(&hw.ether_dhost, 0xff, ETHER_ADDR_LEN); |
| hw.ether_type = htons(protocol); |
| iov[0].iov_base = &hw; |
| iov[0].iov_len = ETHER_HDR_LEN; |
| diff --git a/if-linux.c b/if-linux.c |
| index 9871d6b..e4847c6 100644 |
| --- a/if-linux.c |
| +++ b/if-linux.c |
| @@ -1194,7 +1194,7 @@ eexit: |
| |
| ssize_t |
| if_sendrawpacket(const struct interface *ifp, uint16_t protocol, |
| - const void *data, size_t len) |
| + const void *data, size_t len, const uint8_t *dest_hw_addr) |
| { |
| const struct dhcp_state *state; |
| union sockunion { |
| @@ -1213,8 +1213,12 @@ if_sendrawpacket(const struct interface *ifp, uint16_t protocol, |
| if (ifp->family == ARPHRD_INFINIBAND) |
| memcpy(&su.sll.sll_addr, |
| &ipv4_bcast_addr, sizeof(ipv4_bcast_addr)); |
| - else |
| - memset(&su.sll.sll_addr, 0xff, ifp->hwlen); |
| + else { |
| + if (dest_hw_addr) |
| + memcpy(&su.sll.sll_addr, dest_hw_addr, ifp->hwlen); |
| + else |
| + memset(&su.sll.sll_addr, 0xff, ifp->hwlen); |
| + } |
| state = D_CSTATE(ifp); |
| if (protocol == ETHERTYPE_ARP) |
| fd = state->arp_fd; |
| diff --git a/if-options.c b/if-options.c |
| index c6226dd..8680b5b 100644 |
| --- a/if-options.c |
| +++ b/if-options.c |
| @@ -141,6 +141,7 @@ const struct option cf_options[] = { |
| {"noipv4ll", no_argument, NULL, 'L'}, |
| {"master", no_argument, NULL, 'M'}, |
| {"nooption", optional_argument, NULL, 'O'}, |
| + {"unicast", no_argument, NULL, 'P'}, |
| {"require", required_argument, NULL, 'Q'}, |
| {"arpgw", no_argument, NULL, 'R'}, |
| {"static", required_argument, NULL, 'S'}, |
| @@ -988,6 +989,9 @@ parse_option(struct dhcpcd_ctx *ctx, const char *ifname, struct if_options *ifo, |
| return -1; |
| } |
| break; |
| + case 'P': |
| + ifo->options |= DHCPCD_UNICAST_ARP; |
| + break; |
| case 'Q': |
| arg = set_option_space(ctx, arg, &d, &dl, &od, &odl, ifo, |
| &request, &require, &no, &reject); |
| diff --git a/if-options.h b/if-options.h |
| index 153f9b1..c6fdb11 100644 |
| --- a/if-options.h |
| +++ b/if-options.h |
| @@ -42,7 +42,7 @@ |
| /* Don't set any optional arguments here so we retain POSIX |
| * compatibility with getopt */ |
| #define IF_OPTS "46bc:de:f:gh:i:j:kl:m:no:pqr:s:t:u:v:wxy:z:" \ |
| - "ABC:DEF:GHI:JKLMO:Q:RS:TUVW:X:Z:" |
| + "ABC:DEF:GHI:JKLMO:PQ:RS:TUVW:X:Z:" |
| |
| #define DEFAULT_TIMEOUT 30 |
| #define DEFAULT_REBOOT 5 |
| @@ -112,6 +112,7 @@ |
| #define DHCPCD_IPV6RA_ACCEPT_NOPUBLIC (1ULL << 56) |
| #define DHCPCD_BOOTP (1ULL << 57) |
| #define DHCPCD_ARPGW (1ULL << 58) |
| +#define DHCPCD_UNICAST_ARP (1ULL << 59) |
| |
| #define DHCPCD_WARNINGS (DHCPCD_CSR_WARNED | \ |
| DHCPCD_ROUTER_HOST_ROUTE_WARNED) |
| diff --git a/if.h b/if.h |
| index d9ef718..cda4c01 100644 |
| --- a/if.h |
| +++ b/if.h |
| @@ -128,7 +128,7 @@ int if_managelink(struct dhcpcd_ctx *); |
| extern const char *if_pfname; |
| int if_openrawsocket(struct interface *, uint16_t); |
| ssize_t if_sendrawpacket(const struct interface *, |
| - uint16_t, const void *, size_t); |
| + uint16_t, const void *, size_t, const uint8_t *dest_hw_addr); |
| ssize_t if_readrawpacket(struct interface *, uint16_t, void *, size_t, int *); |
| |
| int if_address(const struct interface *, |
| -- |
| 2.2.0.rc0.207.ga3a616c |
| |