blob: b93f9932dcb5d465914e03676ce4e2dbe56d09ac [file] [log] [blame]
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