| From 03c8255d7a503d3131ca6ba7be97bbd762fdc73b Mon Sep 17 00:00:00 2001 |
| From: Paul Stewart <pstew@chromium.org> |
| Date: Thu, 27 May 2021 11:29:02 +0000 |
| Subject: [PATCH 02/19] 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 ("--unicast") 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 "--unicast" option set after |
| a previous run with or without the "-R" option set. |
| |
| Reviewed-on: https://gerrit.chromium.org/gerrit/22643 |
| --- |
| src/Makefile | 2 +- |
| src/arp.c | 19 ++++-- |
| src/arp.h | 5 +- |
| src/bpf.c | 7 +- |
| src/bpf.h | 3 +- |
| src/dhcp.c | 163 +++++++++++++++++++++++++++++++++++++++++++---- |
| src/dhcp.h | 9 +++ |
| src/if-options.c | 5 ++ |
| src/if-options.h | 1 + |
| src/ipv4ll.c | 4 +- |
| 10 files changed, 194 insertions(+), 24 deletions(-) |
| |
| diff --git a/src/Makefile b/src/Makefile |
| index 665f5ff6..fe78631d 100644 |
| --- a/src/Makefile |
| +++ b/src/Makefile |
| @@ -50,7 +50,7 @@ all: ${TOP}/config.h ${PROG} ${SCRIPTS} ${MAN5} ${MAN8} |
| dev: |
| cd dev && ${MAKE} |
| |
| -.c.o: Makefile ${TOP}/config.mk |
| +%.o: %.c Makefile ${TOP}/config.mk |
| ${CC} ${CFLAGS} ${CPPFLAGS} -c $< -o $@ |
| |
| CLEANFILES+= dhcpcd-embedded.h dhcpcd-embedded.c |
| diff --git a/src/arp.c b/src/arp.c |
| index 84c501aa..98081865 100644 |
| --- a/src/arp.c |
| +++ b/src/arp.c |
| @@ -63,7 +63,8 @@ |
| __CTASSERT(sizeof(struct arphdr) == 8); |
| |
| 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; |
| @@ -94,11 +95,15 @@ 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)); |
| |
| state = ARP_CSTATE(ifp); |
| - return bpf_send(ifp, state->bpf_fd, ETHERTYPE_ARP, arp_buffer, len); |
| + return bpf_send(ifp, state->bpf_fd, ETHERTYPE_ARP, arp_buffer, |
| + len, dest_hw_addr); |
| |
| eexit: |
| errno = ENOBUFS; |
| @@ -274,6 +279,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; |
| @@ -290,8 +296,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) |
| logerr(__func__); |
| } |
| |
| @@ -343,7 +351,8 @@ arp_announce1(void *arg) |
| logdebugx("%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) |
| logerr(__func__); |
| eloop_timeout_add_sec(ifp->ctx->eloop, ANNOUNCE_WAIT, |
| astate->claims < ANNOUNCE_NUM ? arp_announce1 : arp_announced, |
| diff --git a/src/arp.h b/src/arp.h |
| index 86448c0c..ef4a0a1f 100644 |
| --- a/src/arp.h |
| +++ b/src/arp.h |
| @@ -73,6 +73,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); |
| |
| @@ -89,7 +91,8 @@ struct iarp_state { |
| |
| #ifdef ARP |
| int arp_open(struct interface *); |
| -ssize_t arp_request(const struct interface *, in_addr_t, in_addr_t); |
| +ssize_t arp_request(const struct interface *, in_addr_t, in_addr_t, |
| + const uint8_t *dest_hw_addr); |
| void arp_probe(struct arp_state *); |
| void arp_report_conflicted(const struct arp_state *, const struct arp_msg *); |
| struct arp_state *arp_new(struct interface *, const struct in_addr *); |
| diff --git a/src/bpf.c b/src/bpf.c |
| index e85cd4f3..0e9277d0 100644 |
| --- a/src/bpf.c |
| +++ b/src/bpf.c |
| @@ -264,14 +264,17 @@ bpf_attach(int fd, void *filter, unsigned int filter_len) |
| /* SunOS is special too - sending via BPF goes nowhere. */ |
| ssize_t |
| bpf_send(const struct interface *ifp, int fd, 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 eh; |
| |
| switch(ifp->family) { |
| case ARPHRD_ETHER: |
| - memset(&eh.ether_dhost, 0xff, sizeof(eh.ether_dhost)); |
| + if (dest_hw_addr) |
| + memcpy(&eh.ether_dhost, dest_hw_addr, ETHER_ADDR_LEN); |
| + else |
| + memset(&eh.ether_dhost, 0xff, sizeof(eh.ether_dhost)); |
| memcpy(&eh.ether_shost, ifp->hwaddr, sizeof(eh.ether_shost)); |
| eh.ether_type = htons(protocol); |
| iov[0].iov_base = &eh; |
| diff --git a/src/bpf.h b/src/bpf.h |
| index 91ca16e2..cd1707eb 100644 |
| --- a/src/bpf.h |
| +++ b/src/bpf.h |
| @@ -39,7 +39,8 @@ size_t bpf_frame_header_len(const struct interface *); |
| int bpf_open(struct interface *, int (*)(struct interface *, int)); |
| int bpf_close(struct interface *, int); |
| int bpf_attach(int, void *, unsigned int); |
| -ssize_t bpf_send(const struct interface *, int, uint16_t, const void *, size_t); |
| +ssize_t bpf_send(const struct interface *, int, uint16_t, |
| + const void *, size_t, const uint8_t *); |
| ssize_t bpf_read(struct interface *, int, void *, size_t, unsigned int *); |
| int bpf_arp(struct interface *, int); |
| int bpf_bootp(struct interface *, int); |
| diff --git a/src/dhcp.c b/src/dhcp.c |
| index 1365957d..3591ce1f 100644 |
| --- a/src/dhcp.c |
| +++ b/src/dhcp.c |
| @@ -207,8 +207,14 @@ get_option(struct dhcpcd_ctx *ctx, |
| overl = (uint8_t)(overl & ~2); |
| p = bootp->sname; |
| e = p + sizeof(bootp->sname); |
| - } else |
| + } else { |
| + /* Explicit search for DHO_END so do not fail |
| + * and return pointer to option itself (there is |
| + * no length/data) */ |
| + if (opt == DHO_END) |
| + op = p - 1; |
| goto exit; |
| + } |
| /* No length to read */ |
| continue; |
| } |
| @@ -1148,13 +1154,57 @@ write_lease(const struct interface *ifp, const struct bootp *bootp, size_t len) |
| int fd; |
| ssize_t bytes; |
| const struct dhcp_state *state = D_CSTATE(ifp); |
| + const uint8_t *ptr; |
| |
| logdebugx("%s: writing lease `%s'", ifp->name, state->leasefile); |
| |
| fd = open(state->leasefile, O_WRONLY | O_CREAT | O_TRUNC, 0644); |
| if (fd == -1) |
| return -1; |
| + |
| + /* We would like to save GW HW addrs together with the lease, so we'll |
| + * use private DHO_GWHWADDR extension for that. The idea is to insert |
| + * this extra option at the end - if original offer ends with DHO_END |
| + * then squeze it just before pushing out DHO_END. Let's first find the |
| + * end of original offer: it is either DHO_GWHWADDR (if present) or |
| + * DHO_END (if present) or just the end of message. */ |
| + if (state->server_info.gw_hwlen != 0) { |
| + ptr = get_option(ifp->ctx, bootp, len, DHO_GWHWADDR, NULL); |
| + if (ptr) |
| + /* Point to the beginning of GWADDR option */ |
| + ptr -= 2; |
| + else |
| + ptr = get_option(ifp->ctx, bootp, len, DHO_END, |
| + NULL); |
| + if (ptr) |
| + len = (size_t)(ptr - (const uint8_t*)bootp); |
| + else { |
| + /* Just in case we get an offer with options not |
| + * terminated by DHO_END strip possible padding at the |
| + * end. */ |
| + ptr = (const uint8_t*)bootp + len - 1; |
| + while (*ptr-- == DHO_PAD) |
| + --len; |
| + } |
| + } |
| + |
| bytes = write(fd, bootp, len); |
| + |
| + if (state->server_info.gw_hwlen != 0) { |
| + uint8_t gw_len = state->server_info.gw_hwlen; |
| + uint8_t opt_buf[2 + HWADDR_LEN + 1] = { DHO_GWHWADDR, gw_len }; |
| + |
| + memcpy(opt_buf + 2, state->server_info.gw_hwaddr, gw_len); |
| + /* Include option & length in gw_len. */ |
| + gw_len += 2; |
| + /* Make sure opstions end with DHO_END (even if not present in |
| + * the original offer). */ |
| + opt_buf[gw_len] = DHO_END; |
| + gw_len += 1; |
| + if (write(fd, opt_buf, gw_len) < 0) |
| + return -1; |
| + bytes += gw_len; |
| + } |
| close(fd); |
| return bytes; |
| } |
| @@ -1168,6 +1218,8 @@ read_lease(struct interface *ifp, struct bootp **bootp) |
| struct bootp *lease; |
| size_t bytes; |
| uint8_t type; |
| + const uint8_t *gw_hwaddr; |
| + size_t gw_len; |
| #ifdef AUTH |
| const uint8_t *auth; |
| size_t auth_len; |
| @@ -1248,6 +1300,21 @@ read_lease(struct interface *ifp, struct bootp **bootp) |
| return 0; |
| } |
| #endif |
| + /* Check for our DHO_GWHWADDR extension present but do not load values |
| + * into state->server_info because the offer is not yet accepted (e.g it |
| + * might be expired). The value is used at the last moment - see |
| + * start_unicast_arp(). The code below only checks validity. */ |
| + gw_hwaddr = get_option(ifp->ctx, (struct bootp *)lease, bytes, |
| + DHO_GWHWADDR, &gw_len); |
| + if (gw_hwaddr) { |
| + logdebugx("%s: found server info in lease '%s'", |
| + ifp->name, state->leasefile); |
| + if (gw_len != ifp->hwlen) |
| + logerrx("%s: lease file %s has incompatible" |
| + "MAC address length %zu (expected %hhu)", |
| + ifp->name, state->leasefile, |
| + gw_len, ifp->hwlen); |
| + } |
| |
| out: |
| *bootp = (struct bootp *)lease; |
| @@ -1821,7 +1888,7 @@ send_message(struct interface *ifp, uint8_t type, |
| r = 0; |
| } else { |
| r = bpf_send(ifp, state->bpf_fd, |
| - ETHERTYPE_IP, (uint8_t *)udp, ulen); |
| + ETHERTYPE_IP, (uint8_t *)udp, ulen, NULL); |
| free(udp); |
| } |
| /* If we failed to send a raw packet this normally means |
| @@ -2047,11 +2114,23 @@ dhcp_rebind(void *arg) |
| } |
| |
| #ifdef ARP |
| +static void |
| +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 */ |
| logerrx("%s: Probe gateway %s timed out ", |
| astate->iface->name, inet_ntoa(astate->addr)); |
| astate->iface->options->options &= ~DHCPCD_ARPGW; |
| @@ -2078,14 +2157,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); |
| + 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); |
| + ipv4_finaliseaddr(astate->iface); |
| #else |
| - dhcp_bind(astate->iface); |
| + dhcp_bind(astate->iface); |
| #endif |
| + } |
| arp_free(astate); |
| } |
| } |
| @@ -2280,7 +2367,7 @@ dhcp_arp_announced(struct arp_state *state) |
| arp_free(state); |
| //#endif |
| } |
| -#endif |
| +#endif /* ARP */ |
| |
| void |
| dhcp_bind(struct interface *ifp) |
| @@ -2688,6 +2775,55 @@ dhcp_activeaddr(const struct interface *ifp, const struct in_addr *addr) |
| } |
| #endif |
| |
| +#ifdef ARP |
| +static void |
| +start_unicast_arp(struct interface *ifp) |
| +{ |
| + struct dhcp_state *state = D_STATE(ifp); |
| + struct in_addr gwa; |
| + struct arp_state *astate; |
| + const uint8_t *gw_hwaddr; |
| + size_t gw_hwlen; |
| + |
| + if (!state->offer) |
| + return; |
| + |
| + if (!state->lease.frominfo) |
| + return; |
| + |
| + /* We have a valid offer - check for the appended private extention |
| + * keeping GW HW address from previous probes. */ |
| + gw_hwaddr = get_option(ifp->ctx, state->offer, state->offer_len, |
| + DHO_GWHWADDR, &gw_hwlen); |
| + if (! gw_hwaddr || gw_hwlen != ifp->hwlen) |
| + return; |
| + |
| + if (get_option_addr(ifp->ctx, &gwa, state->offer, state->offer_len, |
| + 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 = ifp->hwlen; |
| + memcpy(astate->dest_hwaddr, gw_hwaddr, ifp->hwlen); |
| + |
| + arp_probe(astate); |
| + |
| + /* Note that we don't have to invalidate our gateway address right now |
| + * since we don't touch state->server_info upon reading of the lease and |
| + * rebooting. It should be invalid until we get the reply and store |
| + * values from it. |
| + */ |
| +} |
| +#endif |
| + |
| static void |
| dhcp_reboot(struct interface *ifp) |
| { |
| @@ -2717,12 +2853,15 @@ dhcp_reboot(struct interface *ifp) |
| dhcp_inform(ifp); |
| return; |
| } |
| - if (ifo->reboot == 0 || state->offer == NULL) { |
| + if (ifo->reboot == 0) { |
| dhcp_discover(ifp); |
| return; |
| } |
| - if (!IS_DHCP(state->offer)) |
| - return; |
| +#ifdef ARP |
| + if (ifo->options & DHCPCD_UNICAST_ARP) { |
| + start_unicast_arp(ifp); |
| + } |
| +#endif |
| |
| loginfox("%s: rebinding lease of %s", |
| ifp->name, inet_ntoa(state->lease.addr)); |
| diff --git a/src/dhcp.h b/src/dhcp.h |
| index 192d046b..e54671e1 100644 |
| --- a/src/dhcp.h |
| +++ b/src/dhcp.h |
| @@ -123,6 +123,8 @@ enum DHO { |
| DHO_MUDURL = 161, /* draft-ietf-opsawg-mud */ |
| DHO_SIXRD = 212, /* RFC 5969 */ |
| DHO_MSCSR = 249, /* MS code for RFC 3442 */ |
| + DHO_GWHWADDR = 254, /* 224-254 are reserved for private use |
| + so use one for storing of GW HW address */ |
| DHO_END = 255 |
| }; |
| |
| @@ -184,6 +186,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_NONE, |
| DHS_INIT, |
| @@ -229,6 +237,7 @@ struct dhcp_state { |
| #ifdef ARPING |
| ssize_t arping_index; |
| #endif |
| + struct dhcp_server_info server_info; |
| }; |
| |
| #ifdef INET |
| diff --git a/src/if-options.c b/src/if-options.c |
| index ae3b4415..80d1efda 100644 |
| --- a/src/if-options.c |
| +++ b/src/if-options.c |
| @@ -105,6 +105,7 @@ |
| #define O_LASTLEASE_EXTEND O_BASE + 46 |
| #define O_INACTIVE O_BASE + 47 |
| #define O_MUDURL O_BASE + 48 |
| +#define O_UNICASTGW O_BASE + 49 |
| |
| const struct option cf_options[] = { |
| {"background", no_argument, NULL, 'b'}, |
| @@ -205,6 +206,7 @@ const struct option cf_options[] = { |
| {"lastleaseextend", no_argument, NULL, O_LASTLEASE_EXTEND}, |
| {"inactive", no_argument, NULL, O_INACTIVE}, |
| {"mudurl", required_argument, NULL, O_MUDURL}, |
| + {"unicast", no_argument, NULL, O_UNICASTGW}, |
| {NULL, 0, NULL, '\0'} |
| }; |
| |
| @@ -2172,6 +2174,9 @@ err_sla: |
| } |
| *ifo->mudurl = (uint8_t)s; |
| break; |
| + case O_UNICASTGW: |
| + ifo->options |= DHCPCD_UNICAST_ARP; |
| + break; |
| default: |
| return 0; |
| } |
| diff --git a/src/if-options.h b/src/if-options.h |
| index 0e55e38b..8f820bc5 100644 |
| --- a/src/if-options.h |
| +++ b/src/if-options.h |
| @@ -119,6 +119,7 @@ |
| #define DHCPCD_ONESHOT (1ULL << 60) |
| #define DHCPCD_INACTIVE (1ULL << 61) |
| #define DHCPCD_ARPGW (1ULL << 62) |
| +#define DHCPCD_UNICAST_ARP (1ULL << 63) |
| |
| #define DHCPCD_NODROP (DHCPCD_EXITING | DHCPCD_PERSISTENT) |
| |
| diff --git a/src/ipv4ll.c b/src/ipv4ll.c |
| index 29eaf1df..7dbd7d26 100644 |
| --- a/src/ipv4ll.c |
| +++ b/src/ipv4ll.c |
| @@ -292,8 +292,8 @@ ipv4ll_conflicted(struct arp_state *astate, const struct arp_msg *amsg) |
| if (timespeccmp(&defend, &now, >)) |
| logwarnx("%s: IPv4LL %d second defence failed for %s", |
| ifp->name, DEFEND_INTERVAL, state->addr->saddr); |
| - else if (arp_request(ifp, |
| - state->addr->addr.s_addr, state->addr->addr.s_addr) == -1) |
| + else if (arp_request(ifp, state->addr->addr.s_addr, |
| + state->addr->addr.s_addr, NULL) == -1) |
| logerr(__func__); |
| else { |
| logdebugx("%s: defended IPv4LL address %s", |
| -- |
| 2.33.0.800.g4c38ced690-goog |
| |