From 2600050acd74bf6cea320e8d5f3c7ae09568b062 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Thu, 27 Aug 2026 13:34:17 +0300 Subject: ipv6_nd: do not overrun the advertisement buffer with prefix information The prefix information options are written into a fixed 1024 byte pool buffer with no bound at all, one 32 byte option per address on the session. A session carrying more than about thirty addresses therefore writes past the end of the buffer. Skip the prefix information of the addresses which no longer fit and warn about them. The loop also installs the addresses on the interface, so keep doing that regardless of how much room is left in the advertisement. --- accel-pppd/ipv6/nd.c | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/accel-pppd/ipv6/nd.c b/accel-pppd/ipv6/nd.c index 8b22da14..815b822a 100644 --- a/accel-pppd/ipv6/nd.c +++ b/accel-pppd/ipv6/nd.c @@ -109,6 +109,8 @@ static void ipv6_nd_send_ra(struct ipv6_nd_handler_t *h, struct sockaddr_in6 *ds struct ipv6db_addr_t *a; struct in6_addr addr, peer_addr; struct in6_addr dns[MAX_DNS_COUNT]; + char str[INET6_ADDRSTRLEN]; + void *bufend; int i, prefix_len, dns_count; if (!buf) { @@ -132,20 +134,31 @@ static void ipv6_nd_send_ra(struct ipv6_nd_handler_t *h, struct sockaddr_in6 *ds adv->nd_ra_retransmit = htonl(conf_AdvRetransTimer); pinfo = (struct nd_opt_prefix_info *)(adv + 1); + bufend = (uint8_t *)buf + BUF_SIZE; list_for_each_entry(a, &ses->ipv6->addr_list, entry) { prefix_len = a->prefix_len == 128 ? 64 : a->prefix_len; - memset(pinfo, 0, sizeof(*pinfo)); - pinfo->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION; - pinfo->nd_opt_pi_len = 4; - pinfo->nd_opt_pi_prefix_len = prefix_len; - pinfo->nd_opt_pi_flags_reserved = - ((a->flag_onlink || conf_AdvPrefixOnLinkFlag) ? ND_OPT_PI_FLAG_ONLINK : 0) | - ((a->flag_auto || (conf_AdvPrefixAutonomousFlag && prefix_len == 64)) ? ND_OPT_PI_FLAG_AUTO : 0); - pinfo->nd_opt_pi_valid_time = htonl(conf_AdvPrefixValidLifetime); - pinfo->nd_opt_pi_preferred_time = htonl(conf_AdvPrefixPreferredLifetime); - memcpy(&pinfo->nd_opt_pi_prefix, &a->addr, (prefix_len + 7) / 8); - pinfo->nd_opt_pi_prefix.s6_addr[prefix_len / 8] &= ~(0xff >> (prefix_len % 8)); - pinfo++; + + /* Addresses are installed even when the advertisement is + already full, only their prefix information is dropped */ + if ((void *)(pinfo + 1) > bufend) { + log_ppp_warn("ipv6_nd: prefix %s/%i does not fit into the" + " router advertisement, not advertising it\n", + inet_ntop(AF_INET6, &a->addr, str, sizeof(str)), + prefix_len); + } else { + memset(pinfo, 0, sizeof(*pinfo)); + pinfo->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION; + pinfo->nd_opt_pi_len = 4; + pinfo->nd_opt_pi_prefix_len = prefix_len; + pinfo->nd_opt_pi_flags_reserved = + ((a->flag_onlink || conf_AdvPrefixOnLinkFlag) ? ND_OPT_PI_FLAG_ONLINK : 0) | + ((a->flag_auto || (conf_AdvPrefixAutonomousFlag && prefix_len == 64)) ? ND_OPT_PI_FLAG_AUTO : 0); + pinfo->nd_opt_pi_valid_time = htonl(conf_AdvPrefixValidLifetime); + pinfo->nd_opt_pi_preferred_time = htonl(conf_AdvPrefixPreferredLifetime); + memcpy(&pinfo->nd_opt_pi_prefix, &a->addr, (prefix_len + 7) / 8); + pinfo->nd_opt_pi_prefix.s6_addr[prefix_len / 8] &= ~(0xff >> (prefix_len % 8)); + pinfo++; + } if (!a->installed) { if (a->prefix_len == 128) { -- cgit v1.2.3 From d8084108cc750962dafcd3716f5d243de2e56418 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Thu, 27 Aug 2026 09:14:05 +0300 Subject: ipv6: use a dedicated type for per-session DNS DNS-Server-IPv6-Address carries a full IPv6 address, so storing it in ipv6db_addr_t also carried an unused prefix length and flags. Introduce dedicated DNS list and address types so the representation matches the data. Suggested-by: Vladislav Grishenko <566150+themiron@users.noreply.github.com> Link: https://github.com/accel-ppp/accel-ppp/commit/4f562467dbdf819395e138617c2a057e02595b9e#r197215804 --- accel-pppd/include/ap_session.h | 8 ++++---- accel-pppd/include/ipv6_dns.h | 12 ++++++++++-- accel-pppd/ipv6/ipv6_dns_test.c | 7 +++---- accel-pppd/radius/radius.c | 11 +++++------ accel-pppd/radius/radius_p.h | 3 ++- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/accel-pppd/include/ap_session.h b/accel-pppd/include/ap_session.h index a0733986..3bc8360b 100644 --- a/accel-pppd/include/ap_session.h +++ b/accel-pppd/include/ap_session.h @@ -41,6 +41,7 @@ struct ap_session; struct backup_data; struct rtnl_link_stats; +struct ipv6_dns_t; struct ap_ctrl { struct triton_context_t *ctx; @@ -87,10 +88,9 @@ struct ap_session struct ipv6db_prefix_t *ipv6_dp; /* Per session IPv6 DNS servers, NULL when none were assigned and the globally configured ones ([ipv6-dns]) should be advertised instead. - Only addr_list, and only the addr member of its entries, is - meaningful here. Owned by whoever sets it, currently the radius - module from the DNS-Server-IPv6-Address attribute (RFC 6911) */ - struct ipv6db_item_t *ipv6_dns; + Owned by whoever sets it, currently the radius module from the + DNS-Server-IPv6-Address attribute (RFC 6911) */ + struct ipv6_dns_t *ipv6_dns; char *ipv4_pool_name; char *ipv6_pool_name; char *dpv6_pool_name; diff --git a/accel-pppd/include/ipv6_dns.h b/accel-pppd/include/ipv6_dns.h index b604d006..b1d74708 100644 --- a/accel-pppd/include/ipv6_dns.h +++ b/accel-pppd/include/ipv6_dns.h @@ -4,9 +4,17 @@ #include #include "list.h" -#include "ipdb.h" #include "ap_session.h" +struct ipv6_dns_addr_t { + struct list_head entry; + struct in6_addr addr; +}; + +struct ipv6_dns_t { + struct list_head addr_list; +}; + /* * Pick the IPv6 DNS servers to advertise to a session. * @@ -22,7 +30,7 @@ static inline int ipv6_dns_get(const struct ap_session *ses, const struct in6_addr *conf_dns, int conf_dns_count, struct in6_addr *dns, int max) { - struct ipv6db_addr_t *a; + struct ipv6_dns_addr_t *a; int count = 0; if (ses && ses->ipv6_dns) { diff --git a/accel-pppd/ipv6/ipv6_dns_test.c b/accel-pppd/ipv6/ipv6_dns_test.c index 72b4751c..9452e5a1 100644 --- a/accel-pppd/ipv6/ipv6_dns_test.c +++ b/accel-pppd/ipv6/ipv6_dns_test.c @@ -44,15 +44,14 @@ static int is(const struct in6_addr *addr, const char *str) static struct ap_session *session_with(const char **str, int count) { struct ap_session *ses = calloc(1, sizeof(*ses)); - struct ipv6db_item_t *item = calloc(1, sizeof(*item)); + struct ipv6_dns_t *item = calloc(1, sizeof(*item)); int i; INIT_LIST_HEAD(&item->addr_list); for (i = 0; i < count; i++) { - struct ipv6db_addr_t *a = calloc(1, sizeof(*a)); + struct ipv6_dns_addr_t *a = calloc(1, sizeof(*a)); a->addr = a6(str[i]); - a->prefix_len = 128; list_add_tail(&a->entry, &item->addr_list); } @@ -65,7 +64,7 @@ static void session_free(struct ap_session *ses) { if (ses->ipv6_dns) { while (!list_empty(&ses->ipv6_dns->addr_list)) { - struct ipv6db_addr_t *a = list_entry(ses->ipv6_dns->addr_list.next, + struct ipv6_dns_addr_t *a = list_entry(ses->ipv6_dns->addr_list.next, typeof(*a), entry); list_del(&a->entry); diff --git a/accel-pppd/radius/radius.c b/accel-pppd/radius/radius.c index 89f1e398..544be09f 100644 --- a/accel-pppd/radius/radius.c +++ b/accel-pppd/radius/radius.c @@ -497,7 +497,7 @@ err: static void free_ipv6_dns(struct radius_pd_t *rpd) { - struct ipv6db_addr_t *a; + struct ipv6_dns_addr_t *a; while (!list_empty(&rpd->ipv6_dns.addr_list)) { a = list_entry(rpd->ipv6_dns.addr_list.next, typeof(*a), entry); @@ -512,6 +512,7 @@ int rad_proc_attrs(struct rad_req_t *req) struct ev_dns_t dns = {}; struct rad_attr_t *attr; struct ipv6db_addr_t *a; + struct ipv6_dns_addr_t *dns6; int dns6_count = -1; int res = 0; struct radius_pd_t *rpd = req->rpd; @@ -643,11 +644,9 @@ int rad_proc_attrs(struct rad_req_t *req) dns6_count++; break; } - a = _malloc(sizeof(*a)); - memset(a, 0, sizeof(*a)); - a->prefix_len = 128; - a->addr = attr->val.ipv6addr; - list_add_tail(&a->entry, &rpd->ipv6_dns.addr_list); + dns6 = _malloc(sizeof(*dns6)); + dns6->addr = attr->val.ipv6addr; + list_add_tail(&dns6->entry, &rpd->ipv6_dns.addr_list); dns6_count++; break; case NAS_Port: diff --git a/accel-pppd/radius/radius_p.h b/accel-pppd/radius/radius_p.h index e4b84740..354d54fc 100644 --- a/accel-pppd/radius/radius_p.h +++ b/accel-pppd/radius/radius_p.h @@ -9,6 +9,7 @@ #include "radius.h" #include "ppp.h" #include "ipdb.h" +#include "ipv6_dns.h" #include "pwdb.h" struct rad_server_t; @@ -65,7 +66,7 @@ struct radius_pd_t { struct ipv4db_item_t ipv4_addr; struct ipv6db_item_t ipv6_addr; struct ipv6db_prefix_t ipv6_dp; - struct ipv6db_item_t ipv6_dns; + struct ipv6_dns_t ipv6_dns; int acct_interim_interval; int acct_interim_jitter; -- cgit v1.2.3 From 2823a07df0c88e9d98472b9248f2e9c3179f6920 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Thu, 27 Aug 2026 09:16:31 +0300 Subject: ipv6: remove obsolete three-server DNS limit The legacy limit matches RFC 6106's recommendation that hosts treat three learned RDNSS addresses as sufficient. That was host-side repository guidance, not a limit in the RADIUS, DHCPv6, or RDNSS encodings. RFC 8106 obsoletes RFC 6106 and explicitly removes the recommendation to limit learned RDNSS addresses to three, leaving the number to local policy. It instead recommends the ability to store at least three addresses when DNS information comes from multiple sources. Store both configured and per-session servers in reusable contiguous arrays. Start with a four-address, 64-byte allocation, grow geometrically, retain capacity across reload or reauthorization, and free session storage only at teardown. Bound advertised lists by actual DHCPv6 and router-advertisement packet space. Warn when servers have to be dropped for lack of room, as the fixed limit used to, skip the DNS search list rather than write it past the end of a full advertisement, and drop the stale three-server wording from the [ipv6-dns] documentation. Link: https://github.com/accel-ppp/accel-ppp/commit/4f562467dbdf819395e138617c2a057e02595b9e#r197212083 Link: https://www.rfc-editor.org/rfc/rfc6106.html#section-5.3.1 Link: https://www.rfc-editor.org/rfc/rfc8106.html#section-5.3.1 Link: https://www.rfc-editor.org/rfc/rfc8106.html#appendix-A --- .github/workflows/run-tests-asan-ubsan.yml | 3 +- accel-pppd/CMakeLists.txt | 2 +- accel-pppd/accel-ppp.conf.5 | 6 ++- accel-pppd/include/ipv6_dns.h | 46 +++++++------------ accel-pppd/ipv6/dhcpv6.c | 41 ++++++++++------- accel-pppd/ipv6/dhcpv6.h | 1 + accel-pppd/ipv6/dhcpv6_packet.c | 27 +++++++++-- accel-pppd/ipv6/ipv6_dns_test.c | 74 +++++++++++++++--------------- accel-pppd/ipv6/nd.c | 51 ++++++++++++++------ accel-pppd/ipv6_dns.c | 33 +++++++++++++ accel-pppd/radius/radius.c | 61 ++++++++---------------- 11 files changed, 199 insertions(+), 146 deletions(-) create mode 100644 accel-pppd/ipv6_dns.c diff --git a/.github/workflows/run-tests-asan-ubsan.yml b/.github/workflows/run-tests-asan-ubsan.yml index 90074463..0ee2c755 100644 --- a/.github/workflows/run-tests-asan-ubsan.yml +++ b/.github/workflows/run-tests-asan-ubsan.yml @@ -75,7 +75,8 @@ jobs: gcc -O2 -Wall -D_GNU_SOURCE -DAP_SESSIONID_LEN=16 \ -fsanitize=${{ matrix.sanitizer }} -fno-sanitize-recover=all \ -I accel-pppd/include -I accel-pppd/triton -I build \ - -o /tmp/ipv6_dns_test accel-pppd/ipv6/ipv6_dns_test.c + -o /tmp/ipv6_dns_test \ + accel-pppd/ipv6/ipv6_dns_test.c accel-pppd/ipv6_dns.c /tmp/ipv6_dns_test gcc -O2 -Wall -o /tmp/bitpool_test accel-pppd/extra/bitpool_test.c /tmp/bitpool_test diff --git a/accel-pppd/CMakeLists.txt b/accel-pppd/CMakeLists.txt index 49c2ff61..c9283dfd 100644 --- a/accel-pppd/CMakeLists.txt +++ b/accel-pppd/CMakeLists.txt @@ -90,6 +90,7 @@ int main(void) ADD_EXECUTABLE(accel-pppd memdebug.c + ipv6_dns.c session.c session_backup.c ifcfg.c @@ -165,4 +166,3 @@ IF (NOT DEFINED CPACK_TYPE) INSTALL(DIRECTORY DESTINATION "${CMAKE_INSTALL_LOCALSTATEDIR}/log/accel-ppp") INSTALL(DIRECTORY DESTINATION "${CMAKE_INSTALL_LOCALSTATEDIR}/lib/accel-ppp") ENDIF (NOT DEFINED CPACK_TYPE) - diff --git a/accel-pppd/accel-ppp.conf.5 b/accel-pppd/accel-ppp.conf.5 index 5ee7fd15..b6e8528c 100644 --- a/accel-pppd/accel-ppp.conf.5 +++ b/accel-pppd/accel-ppp.conf.5 @@ -673,14 +673,16 @@ Specifies primary NBNS to be sent to peer. Specifies secondary NBNS to be sent to peer. .SH [ipv6-dns] These options apply to sessions which were not assigned DNS servers of their -own. A RADIUS server may assign per session ones by returning up to 3 +own. A RADIUS server may assign per session ones by returning DNS-Server-IPv6-Address attributes (RFC 6911) in the Access-Accept; those replace, rather than extend, the servers configured here, for that session only. Both the ipv6_nd module (RDNSS option of the router advertisements) and the ipv6_dhcp module (DNS_SERVERS option) honour them. .TP .BI "dns=" IPv6_address -Specifies IPv6 DNS to be sent to peer. You may specify up to 3 dns options. +Specifies IPv6 DNS to be sent to peer. You may specify multiple dns options. +Servers which do not fit into a router advertisement or a DHCPv6 reply are +dropped from it and a warning is logged. .TP .BI "dnssl=" name Specify DNS Search List. You may specify multiple dns and dnssl options. diff --git a/accel-pppd/include/ipv6_dns.h b/accel-pppd/include/ipv6_dns.h index b1d74708..e82812ce 100644 --- a/accel-pppd/include/ipv6_dns.h +++ b/accel-pppd/include/ipv6_dns.h @@ -3,18 +3,18 @@ #include -#include "list.h" #include "ap_session.h" -struct ipv6_dns_addr_t { - struct list_head entry; - struct in6_addr addr; -}; +#define IPV6_DNS_INITIAL_CAPACITY 4 struct ipv6_dns_t { - struct list_head addr_list; + struct in6_addr *addr; + unsigned int count; + unsigned int capacity; }; +int ipv6_dns_reserve(struct ipv6_dns_t *dns, unsigned int count); + /* * Pick the IPv6 DNS servers to advertise to a session. * @@ -23,34 +23,20 @@ struct ipv6_dns_t { * precedence. Sessions without any fall back to the globally configured ones, * which is what every session got before per session servers existed. * - * Up to 'max' addresses are written to 'dns', the number written is returned. - * Callers advertise nothing when that is 0. + * The selected array is returned and its length is written to 'count'. + * Callers advertise nothing when the returned count is 0. */ -static inline int ipv6_dns_get(const struct ap_session *ses, - const struct in6_addr *conf_dns, int conf_dns_count, - struct in6_addr *dns, int max) +static inline const struct in6_addr *ipv6_dns_get(const struct ap_session *ses, + const struct in6_addr *conf_dns, + int conf_dns_count, int *count) { - struct ipv6_dns_addr_t *a; - int count = 0; - - if (ses && ses->ipv6_dns) { - list_for_each_entry(a, &ses->ipv6_dns->addr_list, entry) { - if (count == max) - break; - dns[count++] = a->addr; - } - - /* An empty list means "nothing assigned", not "no DNS at all" */ - if (count) - return count; - } - - while (count < conf_dns_count && count < max) { - dns[count] = conf_dns[count]; - count++; + if (ses && ses->ipv6_dns && ses->ipv6_dns->count) { + *count = ses->ipv6_dns->count; + return ses->ipv6_dns->addr; } - return count; + *count = conf_dns_count; + return conf_dns; } #endif diff --git a/accel-pppd/ipv6/dhcpv6.c b/accel-pppd/ipv6/dhcpv6.c index 9b957f0f..5bbb1e36 100644 --- a/accel-pppd/ipv6/dhcpv6.c +++ b/accel-pppd/ipv6/dhcpv6.c @@ -30,7 +30,6 @@ #include "memdebug.h" #define BUF_SIZE 65536 -#define MAX_DNS_COUNT 3 static struct { struct dhcpv6_opt_serverid hdr; @@ -46,8 +45,7 @@ static uint8_t *conf_aftr_gw; static int conf_aftr_gw_size; -static struct in6_addr conf_dns[MAX_DNS_COUNT]; -static int conf_dns_count; +static struct ipv6_dns_t conf_dns; static uint8_t *conf_dnssl; static int conf_dnssl_size; @@ -226,24 +224,30 @@ static int insert_status(struct dhcpv6_packet *pkt, struct dhcpv6_option *opt, i static int insert_oro(struct dhcpv6_packet *reply, struct dhcpv6_option *opt) { struct dhcpv6_option *opt1; - int i, j, dns_count; + int i, dns_count; uint8_t *ptr; uint16_t code; - struct in6_addr addr; - uint8_t *addr_ptr; - struct in6_addr dns[MAX_DNS_COUNT]; + const struct in6_addr *dns; + size_t max_dns_count; for (i = ntohs(opt->hdr->len) / 2, ptr = opt->hdr->data; i; i--, ptr += sizeof(code)) { code = u_read_be16(ptr); if (code == D6_OPTION_DNS_SERVERS) { - dns_count = ipv6_dns_get(reply->ses, conf_dns, conf_dns_count, - dns, MAX_DNS_COUNT); + dns = ipv6_dns_get(reply->ses, conf_dns.addr, conf_dns.count, + &dns_count); + max_dns_count = dhcpv6_option_space(reply) / sizeof(*dns); + if ((size_t)dns_count > max_dns_count) { + log_ppp_warn("dhcpv6: sending %zu of %i DNS servers," + " the rest does not fit into the reply\n", + max_dns_count, dns_count); + dns_count = (int)max_dns_count; + } if (dns_count) { - opt1 = dhcpv6_option_alloc(reply, D6_OPTION_DNS_SERVERS, dns_count * sizeof(addr)); + opt1 = dhcpv6_option_alloc(reply, D6_OPTION_DNS_SERVERS, + dns_count * sizeof(*dns)); if (!opt1) return -1; - for (j = 0, addr_ptr = opt1->hdr->data; j < dns_count; j++, addr_ptr += sizeof(addr)) - memcpy(addr_ptr, dns + j, sizeof(addr)); + memcpy(opt1->hdr->data, dns, dns_count * sizeof(*dns)); } } else if (code == D6_OPTION_DOMAIN_LIST) { if (conf_dnssl_size) { @@ -1030,7 +1034,7 @@ static void load_dns(void) if (!s) return; - conf_dns_count = 0; + conf_dns.count = 0; if (conf_dnssl) _free(conf_dnssl); @@ -1044,14 +1048,17 @@ static void load_dns(void) } if (!strcmp(opt->name, "dns") || !opt->val) { - if (conf_dns_count == MAX_DNS_COUNT) - continue; + if (ipv6_dns_reserve(&conf_dns, conf_dns.count + 1)) { + log_emerg("dhcpv6: out of memory allocating IPv6 DNS servers\n"); + break; + } - if (inet_pton(AF_INET6, opt->val ? opt->val : opt->name, &conf_dns[conf_dns_count]) == 0) { + if (inet_pton(AF_INET6, opt->val ? opt->val : opt->name, + &conf_dns.addr[conf_dns.count]) == 0) { log_error("dnsv6: failed to parse '%s'\n", opt->name); continue; } - conf_dns_count++; + conf_dns.count++; } } } diff --git a/accel-pppd/ipv6/dhcpv6.h b/accel-pppd/ipv6/dhcpv6.h index 7dfc71a1..705870d9 100644 --- a/accel-pppd/ipv6/dhcpv6.h +++ b/accel-pppd/ipv6/dhcpv6.h @@ -196,6 +196,7 @@ void dhcpv6_packet_free(struct dhcpv6_packet *pkt); void dhcpv6_packet_print(struct dhcpv6_packet *pkt, void (*print)(const char *fmt, ...)); struct dhcpv6_packet *dhcpv6_packet_alloc_reply(struct dhcpv6_packet *req, int type); struct dhcpv6_option *dhcpv6_option_alloc(struct dhcpv6_packet *pkt, int code, int len); +size_t dhcpv6_option_space(const struct dhcpv6_packet *pkt); struct dhcpv6_option *dhcpv6_nested_option_alloc(struct dhcpv6_packet *pkt, struct dhcpv6_option *opt, int code, int len); void dhcpv6_fill_relay_info(struct dhcpv6_packet *pkt); diff --git a/accel-pppd/ipv6/dhcpv6_packet.c b/accel-pppd/ipv6/dhcpv6_packet.c index 9190c505..5685b3d3 100644 --- a/accel-pppd/ipv6/dhcpv6_packet.c +++ b/accel-pppd/ipv6/dhcpv6_packet.c @@ -1,4 +1,5 @@ #include +#include #include #include @@ -261,12 +262,31 @@ error: return NULL; } +static size_t dhcpv6_packet_tailroom(const struct dhcpv6_packet *pkt) +{ + /* The message, preceded by the relay headers when there are any, lives + in the BUF_SIZE bytes allocated right behind the packet. Note that + pkt->hdr is not the start of that buffer once relays are involved */ + ptrdiff_t room = (uint8_t *)(pkt + 1) + BUF_SIZE - (uint8_t *)pkt->endptr; + + return room > 0 ? (size_t)room : 0; +} + +size_t dhcpv6_option_space(const struct dhcpv6_packet *pkt) +{ + size_t room = dhcpv6_packet_tailroom(pkt); + + if (room <= sizeof(struct dhcpv6_opt_hdr)) + return 0; + + return room - sizeof(struct dhcpv6_opt_hdr); +} + struct dhcpv6_option *dhcpv6_option_alloc(struct dhcpv6_packet *pkt, int code, int len) { struct dhcpv6_option *opt; - if (len < 0 || len > BUF_SIZE || - (char *)(pkt + 1) + BUF_SIZE - (char *)pkt->endptr < sizeof(struct dhcpv6_opt_hdr) + (size_t)len) + if (len < 0 || dhcpv6_packet_tailroom(pkt) < sizeof(struct dhcpv6_opt_hdr) + (size_t)len) return NULL; opt = _malloc(sizeof(*opt)); @@ -293,8 +313,7 @@ struct dhcpv6_option *dhcpv6_nested_option_alloc(struct dhcpv6_packet *pkt, stru { struct dhcpv6_option *opt; - if (len < 0 || len > BUF_SIZE || - (char *)(pkt + 1) + BUF_SIZE - (char *)pkt->endptr < sizeof(struct dhcpv6_opt_hdr) + (size_t)len) + if (len < 0 || dhcpv6_packet_tailroom(pkt) < sizeof(struct dhcpv6_opt_hdr) + (size_t)len) return NULL; opt = _malloc(sizeof(*opt)); diff --git a/accel-pppd/ipv6/ipv6_dns_test.c b/accel-pppd/ipv6/ipv6_dns_test.c index 9452e5a1..3a5c7090 100644 --- a/accel-pppd/ipv6/ipv6_dns_test.c +++ b/accel-pppd/ipv6/ipv6_dns_test.c @@ -6,7 +6,8 @@ * a configured build directory around for config.h: * gcc -O2 -Wall -D_GNU_SOURCE -DAP_SESSIONID_LEN=16 \ * -I accel-pppd/include -I accel-pppd/triton -I build \ - * -o /tmp/ipv6_dns_test accel-pppd/ipv6/ipv6_dns_test.c && /tmp/ipv6_dns_test + * -o /tmp/ipv6_dns_test accel-pppd/ipv6/ipv6_dns_test.c \ + * accel-pppd/ipv6_dns.c && /tmp/ipv6_dns_test */ #include #include @@ -19,8 +20,6 @@ static int failures; #define CHECK(cond) do { if (!(cond)) { \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); failures++; } } while (0) -#define MAX_DNS_COUNT 3 /* as in nd.c and dhcpv6.c */ - static struct in6_addr a6(const char *str) { struct in6_addr addr; @@ -47,13 +46,11 @@ static struct ap_session *session_with(const char **str, int count) struct ipv6_dns_t *item = calloc(1, sizeof(*item)); int i; - INIT_LIST_HEAD(&item->addr_list); - for (i = 0; i < count; i++) { - struct ipv6_dns_addr_t *a = calloc(1, sizeof(*a)); - - a->addr = a6(str[i]); - list_add_tail(&a->entry, &item->addr_list); - } + item->addr = calloc(count, sizeof(*item->addr)); + for (i = 0; i < count; i++) + item->addr[i] = a6(str[i]); + item->count = count; + item->capacity = count; ses->ipv6_dns = item; @@ -63,13 +60,7 @@ static struct ap_session *session_with(const char **str, int count) static void session_free(struct ap_session *ses) { if (ses->ipv6_dns) { - while (!list_empty(&ses->ipv6_dns->addr_list)) { - struct ipv6_dns_addr_t *a = list_entry(ses->ipv6_dns->addr_list.next, - typeof(*a), entry); - - list_del(&a->entry); - free(a); - } + free(ses->ipv6_dns->addr); free(ses->ipv6_dns); } @@ -80,68 +71,77 @@ int main(void) { static const char *four[] = { "2001:db8::1", "2001:db8::2", "2001:db8::3", "2001:db8::4" }; - struct in6_addr conf_dns[MAX_DNS_COUNT]; - struct in6_addr dns[MAX_DNS_COUNT]; + struct in6_addr conf_dns[4]; + struct ipv6_dns_t dynamic_dns = {}; + const struct in6_addr *dns; struct ap_session *ses; int n; conf_dns[0] = a6("fc00::53"); conf_dns[1] = a6("fc00::54"); + conf_dns[2] = a6("fc00::55"); + conf_dns[3] = a6("fc00::56"); + + /* Shared storage grows geometrically and retains existing entries. */ + CHECK(ipv6_dns_reserve(&dynamic_dns, 1) == 0); + CHECK(dynamic_dns.capacity == IPV6_DNS_INITIAL_CAPACITY); + dynamic_dns.addr[0] = a6("2001:db8::53"); + CHECK(ipv6_dns_reserve(&dynamic_dns, IPV6_DNS_INITIAL_CAPACITY + 1) == 0); + CHECK(dynamic_dns.capacity == IPV6_DNS_INITIAL_CAPACITY * 2); + CHECK(is(&dynamic_dns.addr[0], "2001:db8::53")); + free(dynamic_dns.addr); /* No session at all: the configured servers, as before the feature */ - n = ipv6_dns_get(NULL, conf_dns, 2, dns, MAX_DNS_COUNT); + dns = ipv6_dns_get(NULL, conf_dns, 2, &n); CHECK(n == 2); CHECK(is(&dns[0], "fc00::53")); CHECK(is(&dns[1], "fc00::54")); /* Session without assigned servers: same fallback */ ses = calloc(1, sizeof(*ses)); - n = ipv6_dns_get(ses, conf_dns, 2, dns, MAX_DNS_COUNT); + dns = ipv6_dns_get(ses, conf_dns, 2, &n); CHECK(n == 2); CHECK(is(&dns[0], "fc00::53")); free(ses); /* Nothing configured and nothing assigned: advertise nothing */ - n = ipv6_dns_get(NULL, conf_dns, 0, dns, MAX_DNS_COUNT); + dns = ipv6_dns_get(NULL, conf_dns, 0, &n); CHECK(n == 0); /* Assigned servers win over the configured ones */ ses = session_with(four, 2); - n = ipv6_dns_get(ses, conf_dns, 2, dns, MAX_DNS_COUNT); + dns = ipv6_dns_get(ses, conf_dns, 2, &n); CHECK(n == 2); CHECK(is(&dns[0], "2001:db8::1")); CHECK(is(&dns[1], "2001:db8::2")); /* ... and win even when nothing is configured */ - n = ipv6_dns_get(ses, conf_dns, 0, dns, MAX_DNS_COUNT); + dns = ipv6_dns_get(ses, conf_dns, 0, &n); CHECK(n == 2); CHECK(is(&dns[0], "2001:db8::1")); session_free(ses); - /* An empty assigned list is "nothing assigned", not "no DNS" */ + /* An empty assigned set is "nothing assigned", not "no DNS" */ ses = session_with(four, 0); - n = ipv6_dns_get(ses, conf_dns, 2, dns, MAX_DNS_COUNT); + dns = ipv6_dns_get(ses, conf_dns, 2, &n); CHECK(n == 2); CHECK(is(&dns[0], "fc00::53")); session_free(ses); - /* More assigned than fit: keep the first max, never overrun */ + /* Assigned sets are not restricted to the configured-server limit */ ses = session_with(four, 4); - memset(dns, 0, sizeof(dns)); - n = ipv6_dns_get(ses, conf_dns, 2, dns, MAX_DNS_COUNT); - CHECK(n == MAX_DNS_COUNT); + dns = ipv6_dns_get(ses, conf_dns, 2, &n); + CHECK(n == 4); CHECK(is(&dns[0], "2001:db8::1")); CHECK(is(&dns[1], "2001:db8::2")); CHECK(is(&dns[2], "2001:db8::3")); - /* Same for the configured ones, should they ever exceed max */ - n = ipv6_dns_get(NULL, conf_dns, 99, dns, MAX_DNS_COUNT); - CHECK(n == MAX_DNS_COUNT); - - /* A caller with no room gets nothing rather than a stomped buffer */ - n = ipv6_dns_get(ses, conf_dns, 2, dns, 0); - CHECK(n == 0); + /* Configured sets are not restricted to three servers either */ + dns = ipv6_dns_get(NULL, conf_dns, 4, &n); + CHECK(n == 4); + CHECK(is(&dns[2], "fc00::55")); + CHECK(is(&dns[3], "fc00::56")); session_free(ses); diff --git a/accel-pppd/ipv6/nd.c b/accel-pppd/ipv6/nd.c index 815b822a..674dff53 100644 --- a/accel-pppd/ipv6/nd.c +++ b/accel-pppd/ipv6/nd.c @@ -22,13 +22,10 @@ #include "memdebug.h" -#define MAX_DNS_COUNT 3 - static int conf_init_ra = 5; static int conf_init_ra_interval = 3; static int conf_rdnss_lifetime; -static struct in6_addr conf_dns[MAX_DNS_COUNT]; -static int conf_dns_count; +static struct ipv6_dns_t conf_dns; static uint8_t *conf_dnssl; static int conf_dnssl_size; @@ -107,11 +104,12 @@ static void ipv6_nd_send_ra(struct ipv6_nd_handler_t *h, struct sockaddr_in6 *ds struct nd_opt_dnssl_info_local *dnsslinfo; //struct nd_opt_mtu *mtu; struct ipv6db_addr_t *a; + const struct in6_addr *dns; struct in6_addr addr, peer_addr; - struct in6_addr dns[MAX_DNS_COUNT]; char str[INET6_ADDRSTRLEN]; void *bufend; int i, prefix_len, dns_count; + size_t avail, dnssl_size, max_dns; if (!buf) { log_emerg("out of memory\n"); @@ -189,7 +187,31 @@ static void ipv6_nd_send_ra(struct ipv6_nd_handler_t *h, struct sockaddr_in6 *ds rinfo++; }*/ - dns_count = ipv6_dns_get(ses, conf_dns, conf_dns_count, dns, MAX_DNS_COUNT); + dns = ipv6_dns_get(ses, conf_dns.addr, conf_dns.count, &dns_count); + + /* Room left behind the prefix information options, shared by the RDNSS + and DNSSL options which follow. The search list is sized by the + configuration alone, so give it its share first */ + avail = (uint8_t *)bufend - (uint8_t *)pinfo; + dnssl_size = conf_dnssl ? (1 + (conf_dnssl_size - 1) / 8 + 1) * 8 : 0; + if (dnssl_size > avail) { + log_ppp_warn("ipv6_nd: DNS search list does not fit into the router" + " advertisement, not advertising it\n"); + dnssl_size = 0; + } + avail -= dnssl_size; + + /* nd_opt_rdnssi_len counts 8 byte units in a single octet, so an + advertisement carries at most 127 addresses however large it is */ + max_dns = avail > sizeof(*rdnssinfo) ? (avail - sizeof(*rdnssinfo)) / sizeof(*dns) : 0; + if (max_dns > 127) + max_dns = 127; + if ((size_t)dns_count > max_dns) { + log_ppp_warn("ipv6_nd: advertising %zu of %i DNS servers, the rest" + " does not fit into the router advertisement\n", + max_dns, dns_count); + dns_count = (int)max_dns; + } if (dns_count) { rdnssinfo = (struct nd_opt_rdnss_info_local *)pinfo; @@ -199,13 +221,13 @@ static void ipv6_nd_send_ra(struct ipv6_nd_handler_t *h, struct sockaddr_in6 *ds rdnssinfo->nd_opt_rdnssi_lifetime = htonl(conf_rdnss_lifetime); rdnss_addr = (struct in6_addr *)rdnssinfo->nd_opt_rdnssi; for (i = 0; i < dns_count; i++) { - memcpy(rdnss_addr, &dns[i], sizeof(*rdnss_addr)); + memcpy(rdnss_addr, dns + i, sizeof(*rdnss_addr)); rdnss_addr++; } } else rdnss_addr = (struct in6_addr *)pinfo; - if (conf_dnssl) { + if (dnssl_size) { dnsslinfo = (struct nd_opt_dnssl_info_local *)rdnss_addr; memset(dnsslinfo, 0, sizeof(*dnsslinfo)); dnsslinfo->nd_opt_dnssli_type = ND_OPT_DNSSL_INFORMATION; @@ -484,7 +506,7 @@ static void load_dns(void) if (!s) return; - conf_dns_count = 0; + conf_dns.count = 0; if (conf_dnssl) _free(conf_dnssl); @@ -504,14 +526,17 @@ static void load_dns(void) } if (!strcmp(opt->name, "dns") || !opt->val) { - if (conf_dns_count == MAX_DNS_COUNT) - continue; + if (ipv6_dns_reserve(&conf_dns, conf_dns.count + 1)) { + log_emerg("ipv6_nd: out of memory allocating IPv6 DNS servers\n"); + break; + } - if (inet_pton(AF_INET6, opt->val ? opt->val : opt->name, &conf_dns[conf_dns_count]) == 0) { + if (inet_pton(AF_INET6, opt->val ? opt->val : opt->name, + &conf_dns.addr[conf_dns.count]) == 0) { log_error("dnsv6: failed to parse '%s'\n", opt->name); continue; } - conf_dns_count++; + conf_dns.count++; } } } diff --git a/accel-pppd/ipv6_dns.c b/accel-pppd/ipv6_dns.c new file mode 100644 index 00000000..bb1d13f3 --- /dev/null +++ b/accel-pppd/ipv6_dns.c @@ -0,0 +1,33 @@ +#include + +#include "triton.h" +#include "ipv6_dns.h" + +#include "memdebug.h" + +int __export ipv6_dns_reserve(struct ipv6_dns_t *dns, unsigned int count) +{ + struct in6_addr *addr; + unsigned int capacity; + + if (count <= dns->capacity) + return 0; + + capacity = dns->capacity; + if (!capacity) + capacity = IPV6_DNS_INITIAL_CAPACITY; + while (capacity < count) { + /* Doubling past this would wrap around and spin forever */ + if (capacity > UINT_MAX / 2) + return -1; + capacity *= 2; + } + + addr = _realloc(dns->addr, capacity * sizeof(*addr)); + if (!addr) + return -1; + + dns->addr = addr; + dns->capacity = capacity; + return 0; +} diff --git a/accel-pppd/radius/radius.c b/accel-pppd/radius/radius.c index 544be09f..cb26132e 100644 --- a/accel-pppd/radius/radius.c +++ b/accel-pppd/radius/radius.c @@ -488,38 +488,30 @@ err: return -1; } -/* - * Number of IPv6 DNS servers kept per session. Matches the number of dns= - * options the ipv6_nd and ipv6_dhcp modules accept in [ipv6-dns], and keeps - * the RDNSS option of a router advertisement to a sane size. - */ -#define MAX_DNS6_COUNT 3 - -static void free_ipv6_dns(struct radius_pd_t *rpd) -{ - struct ipv6_dns_addr_t *a; - - while (!list_empty(&rpd->ipv6_dns.addr_list)) { - a = list_entry(rpd->ipv6_dns.addr_list.next, typeof(*a), entry); - list_del(&a->entry); - _free(a); - } -} - int rad_proc_attrs(struct rad_req_t *req) { struct ev_wins_t wins = {}; struct ev_dns_t dns = {}; struct rad_attr_t *attr; struct ipv6db_addr_t *a; - struct ipv6_dns_addr_t *dns6; - int dns6_count = -1; + unsigned int dns6_count = 0; + unsigned int dns6_index = 0; int res = 0; struct radius_pd_t *rpd = req->rpd; req->rpd->acct_interim_interval = conf_acct_interim_interval; req->rpd->acct_interim_jitter = conf_acct_interim_jitter; + list_for_each_entry(attr, &req->reply->attrs, entry) { + if (!attr->vendor && attr->attr->id == DNS_Server_IPv6_Address) + dns6_count++; + } + + if (dns6_count && ipv6_dns_reserve(&rpd->ipv6_dns, dns6_count)) { + log_emerg("radius: out of memory allocating IPv6 DNS servers\n"); + return -1; + } + list_for_each_entry(attr, &req->reply->attrs, entry) { if (attr->vendor) { if (attr->vendor->id == VENDOR_Microsoft) { @@ -630,24 +622,7 @@ int rad_proc_attrs(struct rad_req_t *req) list_add_tail(&a->entry, &rpd->ipv6_dp.prefix_list); break; case DNS_Server_IPv6_Address: - if (dns6_count < 0) { - /* This reply carries a DNS server list of - its own, it replaces whatever a previous - one assigned */ - free_ipv6_dns(rpd); - dns6_count = 0; - } - if (dns6_count >= MAX_DNS6_COUNT) { - if (dns6_count == MAX_DNS6_COUNT) - log_ppp_warn("radius: ignoring DNS-Server-IPv6-Address" - " beyond the first %i\n", MAX_DNS6_COUNT); - dns6_count++; - break; - } - dns6 = _malloc(sizeof(*dns6)); - dns6->addr = attr->val.ipv6addr; - list_add_tail(&dns6->entry, &rpd->ipv6_dns.addr_list); - dns6_count++; + rpd->ipv6_dns.addr[dns6_index++] = attr->val.ipv6addr; break; case NAS_Port: rpd->ses->unit_idx = attr->val.integer; @@ -684,7 +659,9 @@ int rad_proc_attrs(struct rad_req_t *req) /* Like the IPv4 DNS servers, absent attributes leave whatever a previous reply assigned in place */ - if (!list_empty(&rpd->ipv6_dns.addr_list)) + if (dns6_count) + rpd->ipv6_dns.count = dns6_count; + if (rpd->ipv6_dns.count) rpd->ses->ipv6_dns = &rpd->ipv6_dns; return res; @@ -851,7 +828,6 @@ static void ses_starting(struct ap_session *ses) INIT_LIST_HEAD(&rpd->plugin_list); INIT_LIST_HEAD(&rpd->ipv6_addr.addr_list); INIT_LIST_HEAD(&rpd->ipv6_dp.prefix_list); - INIT_LIST_HEAD(&rpd->ipv6_dns.addr_list); rpd->ipv4_addr.owner = &ipdb; rpd->ipv6_addr.owner = &ipdb; @@ -1035,7 +1011,10 @@ static void ses_finished(struct ap_session *ses) } ses->ipv6_dns = NULL; - free_ipv6_dns(rpd); + _free(rpd->ipv6_dns.addr); + rpd->ipv6_dns.addr = NULL; + rpd->ipv6_dns.count = 0; + rpd->ipv6_dns.capacity = 0; fr6 = rpd->fr6; while (fr6) { -- cgit v1.2.3