From addc36b71a7231019994f1199d2fcb3af3b2eff9 Mon Sep 17 00:00:00 2001 From: Qifan Lu Date: Fri, 13 Mar 2026 16:45:26 -0700 Subject: [PATCH] Extended prefix_filter to work in relay mode for RA and NDP --- src/config.c | 4 +- src/dhcpv6-ia.c | 10 +-- src/ndp.c | 11 ++- src/odhcpd.c | 51 ++++++++++- src/odhcpd.h | 28 ++++-- src/router.c | 222 ++++++++++++++++++++++++++++++------------------ 6 files changed, 226 insertions(+), 100 deletions(-) diff --git a/src/config.c b/src/config.c index 29384482..52413252 100644 --- a/src/config.c +++ b/src/config.c @@ -1707,8 +1707,8 @@ int config_parse_interface(void *data, size_t len, const char *name, bool overwr if ((c = tb[IFACE_ATTR_PREFIX_FILTER])) odhcpd_parse_addr6_prefix(blobmsg_get_string(c), - &iface->pio_filter_addr, - &iface->pio_filter_length); + &iface->prefix_filter_addr, + &iface->prefix_filter_length); if (overwrite && (c = tb[IFACE_ATTR_NTP])) { struct blob_attr *cur; diff --git a/src/dhcpv6-ia.c b/src/dhcpv6-ia.c index 2ad628d9..caf80402 100644 --- a/src/dhcpv6-ia.c +++ b/src/dhcpv6-ia.c @@ -295,7 +295,7 @@ static void __apply_lease(struct dhcpv6_lease *a, for (ssize_t i = 0; i < addr_len; ++i) { struct in6_addr prefix; - if (ADDR_MATCH_PIO_FILTER(&addrs[i], a->iface)) + if (ADDR_MATCH_PREFIX_FILTER(&addrs[i], a->iface)) continue; prefix = addrs[i].addr.in6; @@ -324,7 +324,7 @@ static void set_border_assignment_size(struct interface *iface, struct dhcpv6_le for (size_t i = 0; i < iface->addr6_len; ++i) { struct odhcpd_ipaddr *addr = &iface->addr6[i]; - if (ADDR_MATCH_PIO_FILTER(addr, iface)) + if (ADDR_MATCH_PREFIX_FILTER(addr, iface)) continue; if (addr->preferred_lt > (uint32_t)now && @@ -634,7 +634,7 @@ static size_t build_ia(uint8_t *buf, size_t buflen, uint16_t status, continue; /* Filter Out Prefixes */ - if (ADDR_MATCH_PIO_FILTER(&addrs[i], iface)) { + if (ADDR_MATCH_PREFIX_FILTER(&addrs[i], iface)) { char addrbuf[INET6_ADDRSTRLEN]; info("Address %s filtered out on %s", inet_ntop(AF_INET6, &addrs[i].addr.in6, addrbuf, sizeof(addrbuf)), @@ -761,7 +761,7 @@ static size_t build_ia(uint8_t *buf, size_t buflen, uint16_t status, if (!valid_prefix_length(a, addrs[i].prefix_len)) continue; - if (ADDR_MATCH_PIO_FILTER(&addrs[i], iface)) + if (ADDR_MATCH_PREFIX_FILTER(&addrs[i], iface)) continue; if (ia->type == htons(DHCPV6_OPT_IA_PD)) { @@ -934,7 +934,7 @@ static bool dhcpv6_ia_on_link(const struct dhcpv6_ia_hdr *ia, struct dhcpv6_leas if (!valid_addr(&addrs[i], now)) continue; - if (ADDR_MATCH_PIO_FILTER(&addrs[i], iface)) + if (ADDR_MATCH_PREFIX_FILTER(&addrs[i], iface)) continue; if (ia->type == htons(DHCPV6_OPT_IA_PD)) { diff --git a/src/ndp.c b/src/ndp.c index 92578697..0bfde5c0 100644 --- a/src/ndp.c +++ b/src/ndp.c @@ -278,12 +278,18 @@ static void ndp_netevent_cb(unsigned long event, struct netevent_handler_info *i netlink_dump_neigh_table(false); _o_fallthrough; case NETEV_ADDR6_ADD: + if (IN6_MATCH_PREFIX_FILTER(info->addr.in6, iface)) + break; /* Address filtered out */ + setup_addr_for_relaying(&info->addr.in6, iface, add); break; case NETEV_NEIGH6_DEL: add = false; _o_fallthrough; case NETEV_NEIGH6_ADD: + if (IN6_MATCH_PREFIX_FILTER(info->neigh.dst.in6, iface)) + break; /* Address filtered out */ + if (info->neigh.flags & NTF_PROXY) { if (add) { netlink_setup_proxy_neigh(&info->neigh.dst.in6, iface->ifindex, false); @@ -380,13 +386,16 @@ static void handle_solicit(void *addr, void *data, size_t len, return; if (len < sizeof(*ip6) + sizeof(*req)) - return; // Invalid total length + return; /* Invalid total length */ if (IN6_IS_ADDR_LINKLOCAL(&req->nd_ns_target) || IN6_IS_ADDR_LOOPBACK(&req->nd_ns_target) || IN6_IS_ADDR_MULTICAST(&req->nd_ns_target)) return; /* Invalid target */ + if (IN6_MATCH_PREFIX_FILTER(req->nd_ns_target, iface)) + return; /* Address filtered out */ + inet_ntop(AF_INET6, &req->nd_ns_target, ipbuf, sizeof(ipbuf)); debug("Got a NS for %s on %s", ipbuf, iface->name); diff --git a/src/odhcpd.c b/src/odhcpd.c index 9921e1b0..45717cd2 100644 --- a/src/odhcpd.c +++ b/src/odhcpd.c @@ -680,7 +680,7 @@ void odhcpd_enum_addr6(struct interface *iface, struct dhcpv6_lease *lease, continue; /* Filter Out Prefixes */ - if (ADDR_MATCH_PIO_FILTER(&addrs[i], iface)) { + if (ADDR_MATCH_PREFIX_FILTER(&addrs[i], iface)) { char addrbuf[INET6_ADDRSTRLEN]; info("Address %s filtered out on %s", inet_ntop(AF_INET6, &addrs[i].addr.in6, addrbuf, sizeof(addrbuf)), @@ -789,3 +789,52 @@ bool odhcpd_hostname_valid(const char *name) return (label_sz && label_sz <= DNS_MAX_LABEL_LEN ? true : false); } + +void odhcpd_iov_builder_init(struct iov_builder *builder, uint8_t *data, struct iovec* iov_buf, size_t iov_capacity) { + builder->iov_count = 0; + builder->current_iov_base = data; + builder->current_iov_len = 0; + builder->include_iov = false; + + if (iov_buf) { + builder->iov_buf = iov_buf; + builder->iov_capacity = iov_capacity; + } +} + +int odhcpd_iov_builder_advance(struct iov_builder *builder, size_t chunk_len, bool include_chunk) { + if (include_chunk ^ builder->include_iov) { + if (builder->include_iov) { + int error = odhcpd_iov_builder_append(builder, builder->current_iov_base, builder->current_iov_len); + if (error) + return error; + } + + builder->current_iov_base += builder->current_iov_len; + builder->current_iov_len = chunk_len; + builder->include_iov = include_chunk; + } else { + builder->current_iov_len += chunk_len; + } + + return 0; +} + +int odhcpd_iov_builder_append(struct iov_builder *builder, uint8_t *iov_base, size_t iov_len) { + if (iov_len == 0) + return 0; + if (builder->iov_count >= builder->iov_capacity) + return 1; + + builder->iov_buf[builder->iov_count] = (struct iovec) { + .iov_base = iov_base, + .iov_len = iov_len + }; + builder->iov_count++; + + return 0; +} + +int odhcpd_iov_builder_finalize(struct iov_builder *builder) { + return odhcpd_iov_builder_advance(builder, 0, false); +} diff --git a/src/odhcpd.h b/src/odhcpd.h index 9298d62a..3b2ba422 100644 --- a/src/odhcpd.h +++ b/src/odhcpd.h @@ -82,10 +82,12 @@ #define IN6_IS_ADDR_ULA(a) (((a)->s6_addr32[0] & htonl(0xfe000000)) == htonl(0xfc000000)) -#define ADDR_MATCH_PIO_FILTER(_addr, iface) (odhcpd_bmemcmp(&(_addr)->addr, \ - &(iface)->pio_filter_addr, \ - (iface)->pio_filter_length) != 0 || \ - (_addr)->prefix_len < (iface)->pio_filter_length) +#define IN6_MATCH_PREFIX_FILTER(_addr, iface) (odhcpd_bmemcmp(&(_addr), \ + &(iface)->prefix_filter_addr, \ + (iface)->prefix_filter_length) != 0) + +#define ADDR_MATCH_PREFIX_FILTER(_addr, iface) (IN6_MATCH_PREFIX_FILTER((_addr)->addr, iface) || \ + (_addr)->prefix_len < (iface)->prefix_filter_length) struct interface; struct nl_sock; @@ -423,8 +425,8 @@ struct interface { uint32_t pref64_prefix[3]; bool no_dynamic_dhcp; bool have_link_local; - uint8_t pio_filter_length; - struct in6_addr pio_filter_addr; + uint8_t prefix_filter_length; + struct in6_addr prefix_filter_addr; int default_router; int route_preference; uint32_t ra_maxinterval; @@ -666,4 +668,18 @@ void reload_services(struct interface *iface); void odhcpd_reload(void); +struct iov_builder { + struct iovec *iov_buf; + size_t iov_capacity; + size_t iov_count; + uint8_t *current_iov_base; + size_t current_iov_len; + bool include_iov; +}; + +void odhcpd_iov_builder_init(struct iov_builder *builder, uint8_t *data, struct iovec* iov_buf, size_t iov_capacity); +int odhcpd_iov_builder_advance(struct iov_builder *builder, size_t chunk_len, bool include_chunk); +int odhcpd_iov_builder_append(struct iov_builder *builder, uint8_t *iov_base, size_t iov_len); +int odhcpd_iov_builder_finalize(struct iov_builder *builder); + #endif /* _ODHCPD_H_ */ diff --git a/src/router.c b/src/router.c index 731a6dc0..3451326c 100644 --- a/src/router.c +++ b/src/router.c @@ -760,7 +760,7 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr continue; } - if (ADDR_MATCH_PIO_FILTER(addr, iface)) { + if (ADDR_MATCH_PREFIX_FILTER(addr, iface)) { info("Address %s filtered out as RA prefix on %s", inet_ntop(AF_INET6, &addr->addr.in6, buf, sizeof(buf)), iface->name); @@ -1018,7 +1018,7 @@ static int send_router_advert(struct interface *iface, const struct in6_addr *fr continue; } - if (ADDR_MATCH_PIO_FILTER(addr, iface)) { + if (ADDR_MATCH_PREFIX_FILTER(addr, iface)) { debug("Address %s filtered out as RA route on %s", inet_ntop(AF_INET6, &addr->addr.in6, buf, sizeof(buf)), iface->name); @@ -1169,22 +1169,17 @@ static void forward_router_advertisement(const struct interface *iface, uint8_t struct sockaddr_in6 all_nodes; struct icmpv6_opt *opt; struct interface *c; - struct iovec iov = { .iov_base = data, .iov_len = len }; /* Rewrite options */ uint8_t *end = data + len; - uint8_t *mac_ptr = NULL; - struct in6_addr *dns_addrs6 = NULL; - size_t dns_addrs6_cnt = 0; - // MTU option - struct nd_opt_mtu *mtu_opt = NULL; - uint32_t ingress_mtu_val = 0; /* PIO L/A/R/P flag and RA M/O Flags */ uint8_t ra_flags; size_t pio_count = 0; - struct fwd_pio_flags { - uint8_t *ptr; - uint8_t flags; - } *pio_flags = NULL; + size_t pio_index = 0; + uint8_t *pio_flags_all = NULL; + + size_t opt_count = 0; + size_t iov_capacity; + struct iov_builder ra_builder; icmpv6_for_each_option(opt, &adv[1], end) { /* check our packet content is not truncated */ @@ -1193,6 +1188,8 @@ static void forward_router_advertisement(const struct interface *iface, uint8_t return; } + opt_count++; + switch(opt->type) { case ND_OPT_PREFIX_INFORMATION: pio_count++; @@ -1200,36 +1197,23 @@ static void forward_router_advertisement(const struct interface *iface, uint8_t } } - if (pio_count > 0) { - pio_flags = alloca(sizeof(*pio_flags) * pio_count); - pio_count = 0; - } + if (pio_count > 0) + pio_flags_all = alloca(pio_count); + + /* Capacity is computed as an upper bound of the number of chunks to sent, + * which is when options alternate between being kept and deleted. + */ + iov_capacity = opt_count / 2 + 1; + struct iovec iov_buf[iov_capacity]; /* Parse existing options */ + pio_index = 0; icmpv6_for_each_option(opt, &adv[1], end) { switch (opt->type) { - case ND_OPT_SOURCE_LINKADDR: - mac_ptr = opt->data; - break; - - case ND_OPT_RECURSIVE_DNS: - if (opt->len > 1) { - dns_addrs6 = (struct in6_addr *)&opt->data[6]; - dns_addrs6_cnt = (opt->len - 1) / 2; - } - break; - - case ND_OPT_MTU: - if (opt->len == 1 && (uint8_t *)opt + sizeof(struct nd_opt_mtu) <= end) { - mtu_opt = (struct nd_opt_mtu *)opt; - ingress_mtu_val = ntohl(mtu_opt->nd_opt_mtu_mtu); - } - break; case ND_OPT_PREFIX_INFORMATION: - /* Store options for each PIO */ - pio_flags[pio_count].ptr = &opt->data[1]; - pio_flags[pio_count].flags = opt->data[1]; - pio_count++; + /* Store original flags for each PIO */ + pio_flags_all[pio_index] = opt->data[1]; + pio_index++; break; } } @@ -1250,34 +1234,9 @@ static void forward_router_advertisement(const struct interface *iface, uint8_t if (c->ra != MODE_RELAY || c->master) continue; - /* Fixup source hardware address option */ - if (mac_ptr) - odhcpd_get_mac(c, mac_ptr); - if (pio_count > 0) debug("RA forward: Rewriting RA PIO flags"); - for (size_t i = 0; i < pio_count; i++) { - /* restore the flags byte to its upstream state before applying per-interface policy */ - *pio_flags[i].ptr = pio_flags[i].flags; - /* ensure L flag (on-link) cleared; relayed == not on-link */ - *pio_flags[i].ptr &= ~ND_OPT_PI_FLAG_ONLINK; - /* upstream no SLAAC, downstream no SLAAC: no change - * upstream no SLAAC, downstream SLAAC: no change - * upstream SLAAC, downstream SLAAC: no change - * upstream SLAAC, downstream no SLAAC: clear flag - * Why? We shall not SLAAC downstream if upstream disables it. Sometimes - * we just inform about a prefix for DHCPv6 and routing info. - */ - if (!c->ra_slaac) - *pio_flags[i].ptr &= ~ND_OPT_PI_FLAG_AUTO;/* ensure A flag cleared */ - - /* we have no opinion on the R flag - it can be forwarded */ - - if (c->dhcpv6 == MODE_DISABLED || !c->dhcpv6_pd || !c->dhcpv6_pd_preferred) - *pio_flags[i].ptr &= ~ND_OPT_PI_FLAG_PD_PREFERRED;/* ensure P flag (DHCPv6-PD) cleared */ - } - /* Apply per-interface modifications of upstream RA state */ adv->nd_ra_flags_reserved = ra_flags; /* Rewrite M/O flags unless we relay DHCPv6 */ @@ -1288,37 +1247,130 @@ static void forward_router_advertisement(const struct interface *iface, uint8_t adv->nd_ra_flags_reserved |= c->ra_flags & (ND_RA_FLAG_MANAGED | ND_RA_FLAG_OTHER); } - /* If we have to rewrite DNS entries */ - if (c->always_rewrite_dns && dns_addrs6 && dns_addrs6_cnt > 0) { - const struct in6_addr *rewrite = c->dns_addrs6; - struct in6_addr addr; - size_t rewrite_cnt = c->dns_addrs6_cnt; + pio_index = 0; + + odhcpd_iov_builder_init(&ra_builder, data, iov_buf, iov_capacity); + /* Router advertisement header */ + odhcpd_iov_builder_advance(&ra_builder, sizeof(struct nd_router_advert), true); - if (rewrite_cnt == 0) { - if (odhcpd_get_interface_dns_addr6(c, &addr)) - continue; /* Unable to comply */ + icmpv6_for_each_option(opt, &adv[1], end) { + bool include_opt = true; - rewrite = &addr; - rewrite_cnt = 1; + switch (opt->type) { + case ND_OPT_SOURCE_LINKADDR: { + odhcpd_get_mac(c, opt->data); + break; } + case ND_OPT_RECURSIVE_DNS: { + /* DNS rewriting is disabled or the option is too short to contain one address */ + if (!c->always_rewrite_dns || opt->len < 3) + break; + + struct in6_addr *cur_dns_addrs6 = (struct in6_addr *)&opt->data[6]; + size_t cur_dns_addrs6_cnt = (opt->len - 1) / 2; + const struct in6_addr *rewrite = c->dns_addrs6; + struct in6_addr addr; + size_t rewrite_cnt = c->dns_addrs6_cnt; - /* Copy over any other addresses */ - for (size_t i = 0; i < dns_addrs6_cnt; ++i) { - size_t j = (i < rewrite_cnt) ? i : rewrite_cnt - 1; - dns_addrs6[i] = rewrite[j]; + if (rewrite_cnt == 0) { + if (odhcpd_get_interface_dns_addr6(c, &addr)) { + include_opt = false; /* Unable to comply */ + break; + } + + rewrite = &addr; + rewrite_cnt = 1; + } + + /* Copy over any other addresses */ + for (size_t j = 0; j < cur_dns_addrs6_cnt; ++j) { + size_t k = (j < rewrite_cnt) ? j : rewrite_cnt - 1; + cur_dns_addrs6[j] = rewrite[k]; + } + + break; } - } + case ND_OPT_MTU: { + /* MTU rewriting is disabled, or invalid or incomplete MTU option */ + if (!c->ra_mtu || opt->len != 1 || (uint8_t *)opt + sizeof(struct nd_opt_mtu) > end) + break; + + struct nd_opt_mtu *cur_mtu = (struct nd_opt_mtu *)opt; + uint32_t ingress_mtu_val = ntohl(cur_mtu->nd_opt_mtu_mtu); + + if (ingress_mtu_val != c->ra_mtu) { + debug("Rewriting RA MTU from %u to %u on %s", + ingress_mtu_val, c->ra_mtu, c->name); + cur_mtu->nd_opt_mtu_mtu = htonl(c->ra_mtu); + } - /* Rewrite MTU option if local RA MTU is configured */ - if (c->ra_mtu && mtu_opt) { - if (ingress_mtu_val != c->ra_mtu) { - debug("Rewriting RA MTU from %u to %u on %s", - ingress_mtu_val, c->ra_mtu, c->name); - mtu_opt->nd_opt_mtu_mtu = htonl(c->ra_mtu); + break; } + case ND_OPT_PREFIX_INFORMATION: { + uint8_t pio_flags_orig = pio_flags_all[pio_index]; + pio_index++; + + struct nd_opt_prefix_info *pio = (struct nd_opt_prefix_info *)opt; + struct odhcpd_ipaddr pio_prefix = { + .addr.in6 = pio->nd_opt_pi_prefix, + .prefix_len = pio->nd_opt_pi_prefix_len, + }; + + /* Filtered PIO prefix */ + if (ADDR_MATCH_PREFIX_FILTER(&pio_prefix, c)) { + include_opt = false; + break; + } + + uint8_t *pio_flags = &pio->nd_opt_pi_flags_reserved; + /* restore the flags byte to its upstream state before applying per-interface policy */ + *pio_flags = pio_flags_orig; + /* ensure L flag (on-link) cleared; relayed == not on-link */ + *pio_flags &= ~ND_OPT_PI_FLAG_ONLINK; + /* upstream no SLAAC, downstream no SLAAC: no change + * upstream no SLAAC, downstream SLAAC: no change + * upstream SLAAC, downstream SLAAC: no change + * upstream SLAAC, downstream no SLAAC: clear flag + * Why? We shall not SLAAC downstream if upstream disables it. Sometimes + * we just inform about a prefix for DHCPv6 and routing info. + */ + if (!c->ra_slaac) + *pio_flags &= ~ND_OPT_PI_FLAG_AUTO; /* ensure A flag cleared */ + + /* we have no opinion on the R flag - it can be forwarded */ + if (c->dhcpv6 == MODE_DISABLED || !c->dhcpv6_pd || !c->dhcpv6_pd_preferred) + *pio_flags &= ~ND_OPT_PI_FLAG_PD_PREFERRED; /* ensure P flag (DHCPv6-PD) cleared */ + + break; + } + case ND_OPT_ROUTE_INFO: { + struct nd_opt_route_info *rio = (struct nd_opt_route_info *)opt; + struct odhcpd_ipaddr rio_prefix = { + .addr.in6 = IN6ADDR_ANY_INIT, + .prefix_len = rio->prefix_len, + }; + + if (opt->len > 1) { + size_t rio_prefix_bytes = (size_t)(opt->len - 1) * 8; + size_t copy_len = min(sizeof(rio_prefix.addr.in6), rio_prefix_bytes); + memcpy(&rio_prefix.addr.in6, &rio->addr[0], copy_len); + } + + /* Filtered RIO prefix */ + if (ADDR_MATCH_PREFIX_FILTER(&rio_prefix, c)) + include_opt = false; + + break; + } + } + + /* Current ICMPv6 option */ + odhcpd_iov_builder_advance(&ra_builder, (size_t)(opt->len) * 8, include_opt); } + odhcpd_iov_builder_finalize(&ra_builder); + info("Forward a RA on %s", c->name); - odhcpd_send(c->router_event.uloop.fd, &all_nodes, &iov, 1, c); + odhcpd_send(c->router_event.uloop.fd, &all_nodes, iov_buf, ra_builder.iov_count, c); } }