From 7040fd381a60959076c166cc8d38156bfc42aedf Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Thu, 29 Jun 2023 15:53:02 -0700 Subject: [PATCH 1/9] dhcp-client: parse RFC8910 captive portal dhcp option --- src/basic/string-util.h | 3 ++ src/libsystemd-network/dhcp-lease-internal.h | 1 + src/libsystemd-network/sd-dhcp-lease.c | 34 ++++++++++++++++++++ src/systemd/sd-dhcp-lease.h | 1 + 4 files changed, 39 insertions(+) diff --git a/src/basic/string-util.h b/src/basic/string-util.h index 0ff5b46bba..22ae777fd5 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -22,6 +22,9 @@ #define ALPHANUMERICAL LETTERS DIGITS #define HEXDIGITS DIGITS "abcdefABCDEF" #define LOWERCASE_HEXDIGITS DIGITS "abcdef" +#define URI_RESERVED ":/?#[]@!$&'()*+;=" /* [RFC3986] */ +#define URI_UNRESERVED ALPHANUMERICAL "-._~" /* [RFC3986] */ +#define URI_VALID URI_RESERVED URI_UNRESERVED /* [RFC3986] */ static inline char* strstr_ptr(const char *haystack, const char *needle) { if (!haystack || !needle) diff --git a/src/libsystemd-network/dhcp-lease-internal.h b/src/libsystemd-network/dhcp-lease-internal.h index a660e52201..6503e3ce2d 100644 --- a/src/libsystemd-network/dhcp-lease-internal.h +++ b/src/libsystemd-network/dhcp-lease-internal.h @@ -60,6 +60,7 @@ struct sd_dhcp_lease { char **search_domains; char *hostname; char *root_path; + char *captive_portal; void *client_id; size_t client_id_len; diff --git a/src/libsystemd-network/sd-dhcp-lease.c b/src/libsystemd-network/sd-dhcp-lease.c index fcc3f55154..03fd80064b 100644 --- a/src/libsystemd-network/sd-dhcp-lease.c +++ b/src/libsystemd-network/sd-dhcp-lease.c @@ -168,6 +168,17 @@ int sd_dhcp_lease_get_root_path(sd_dhcp_lease *lease, const char **root_path) { return 0; } +int sd_dhcp_lease_get_captive_portal(sd_dhcp_lease *lease, const char **ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + if (!lease->captive_portal) + return -ENODATA; + + *ret = lease->captive_portal; + return 0; +} + int sd_dhcp_lease_get_router(sd_dhcp_lease *lease, const struct in_addr **addr) { assert_return(lease, -EINVAL); assert_return(addr, -EINVAL); @@ -322,6 +333,7 @@ static sd_dhcp_lease *dhcp_lease_free(sd_dhcp_lease *lease) { free(lease->timezone); free(lease->hostname); free(lease->domainname); + free(lease->captive_portal); for (sd_dhcp_lease_server_type_t i = 0; i < _SD_DHCP_LEASE_SERVER_TYPE_MAX; i++) free(lease->servers[i].addr); @@ -406,6 +418,22 @@ static int lease_parse_domain(const uint8_t *option, size_t len, char **ret) { return 0; } +static int lease_parse_captive_portal(const uint8_t *option, size_t len, char **ret) { + _cleanup_free_ char *uri = NULL; + int r; + + assert(option); + assert(ret); + + r = dhcp_option_parse_string(option, len, &uri); + if (r < 0) + return r; + if (uri && !in_charset(uri, URI_VALID)) + return -EINVAL; + + return free_and_replace(*ret, uri); +} + static int lease_parse_in_addrs(const uint8_t *option, size_t len, struct in_addr **ret, size_t *n_ret) { assert(option || len == 0); assert(ret); @@ -675,6 +703,12 @@ int dhcp_lease_parse_options(uint8_t code, uint8_t len, const void *option, void log_debug_errno(r, "Failed to parse LPR server, ignoring: %m"); break; + case SD_DHCP_OPTION_DHCP_CAPTIVE_PORTAL: + r = lease_parse_captive_portal(option, len, &lease->captive_portal); + if (r < 0) + log_debug_errno(r, "Failed to parse captive portal, ignoring: %m"); + break; + case SD_DHCP_OPTION_STATIC_ROUTE: r = lease_parse_static_routes(lease, option, len); if (r < 0) diff --git a/src/systemd/sd-dhcp-lease.h b/src/systemd/sd-dhcp-lease.h index 3f21826b8a..a0e09e52ac 100644 --- a/src/systemd/sd-dhcp-lease.h +++ b/src/systemd/sd-dhcp-lease.h @@ -67,6 +67,7 @@ int sd_dhcp_lease_get_domainname(sd_dhcp_lease *lease, const char **domainname); int sd_dhcp_lease_get_search_domains(sd_dhcp_lease *lease, char ***domains); int sd_dhcp_lease_get_hostname(sd_dhcp_lease *lease, const char **hostname); int sd_dhcp_lease_get_root_path(sd_dhcp_lease *lease, const char **root_path); +int sd_dhcp_lease_get_captive_portal(sd_dhcp_lease *lease, const char **captive_portal); int sd_dhcp_lease_get_static_routes(sd_dhcp_lease *lease, sd_dhcp_route ***ret); int sd_dhcp_lease_get_classless_routes(sd_dhcp_lease *lease, sd_dhcp_route ***ret); int sd_dhcp_lease_get_vendor_specific(sd_dhcp_lease *lease, const void **data, size_t *data_len); From fde788601be8e4ae9f7cd9c61eff760175ba77ef Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Thu, 29 Jun 2023 15:55:21 -0700 Subject: [PATCH 2/9] dhcp6-client: parse RFC8910 captive portal dhcp6 option --- src/libsystemd-network/dhcp6-lease-internal.h | 2 ++ src/libsystemd-network/dhcp6-option.c | 20 +++++++++++ src/libsystemd-network/dhcp6-option.h | 1 + src/libsystemd-network/sd-dhcp6-lease.c | 35 +++++++++++++++++++ src/systemd/sd-dhcp6-lease.h | 1 + 5 files changed, 59 insertions(+) diff --git a/src/libsystemd-network/dhcp6-lease-internal.h b/src/libsystemd-network/dhcp6-lease-internal.h index 1f10dccbbb..f4c12ca7c4 100644 --- a/src/libsystemd-network/dhcp6-lease-internal.h +++ b/src/libsystemd-network/dhcp6-lease-internal.h @@ -43,6 +43,7 @@ struct sd_dhcp6_lease { struct in6_addr *sntp; size_t sntp_count; char *fqdn; + char *captive_portal; }; int dhcp6_lease_get_lifetime(sd_dhcp6_lease *lease, usec_t *ret_t1, usec_t *ret_t2, usec_t *ret_valid); @@ -60,6 +61,7 @@ int dhcp6_lease_add_domains(sd_dhcp6_lease *lease, const uint8_t *optval, size_t int dhcp6_lease_add_ntp(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen); int dhcp6_lease_add_sntp(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen); int dhcp6_lease_set_fqdn(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen); +int dhcp6_lease_set_captive_portal(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen); int dhcp6_lease_new(sd_dhcp6_lease **ret); int dhcp6_lease_new_from_message( diff --git a/src/libsystemd-network/dhcp6-option.c b/src/libsystemd-network/dhcp6-option.c index a6b74e07b2..3409485e11 100644 --- a/src/libsystemd-network/dhcp6-option.c +++ b/src/libsystemd-network/dhcp6-option.c @@ -524,6 +524,26 @@ int dhcp6_option_parse_status(const uint8_t *data, size_t data_len, char **ret_s return status; } +/* parse a string from dhcp option field. *ret must be initialized */ +int dhcp6_option_parse_string(const uint8_t *data, size_t data_len, char **ret) { + _cleanup_free_ char *string = NULL; + int r; + + assert(data); + assert(ret); + + if (data_len <= 0) { + *ret = mfree(*ret); + return 0; + } + + r = make_cstring((const char *) data, data_len, MAKE_CSTRING_REFUSE_TRAILING_NUL, &string); + if (r < 0) + return r; + + return free_and_replace(*ret, string); +} + static int dhcp6_option_parse_ia_options(sd_dhcp6_client *client, const uint8_t *buf, size_t buflen) { int r; diff --git a/src/libsystemd-network/dhcp6-option.h b/src/libsystemd-network/dhcp6-option.h index 36841dd270..614b4f8a43 100644 --- a/src/libsystemd-network/dhcp6-option.h +++ b/src/libsystemd-network/dhcp6-option.h @@ -88,6 +88,7 @@ int dhcp6_option_parse( size_t *ret_option_data_len, const uint8_t **ret_option_data); int dhcp6_option_parse_status(const uint8_t *data, size_t data_len, char **ret_status_message); +int dhcp6_option_parse_string(const uint8_t *data, size_t data_len, char **ret); int dhcp6_option_parse_ia( sd_dhcp6_client *client, be32_t iaid, diff --git a/src/libsystemd-network/sd-dhcp6-lease.c b/src/libsystemd-network/sd-dhcp6-lease.c index d14c412c1f..89e5a608f8 100644 --- a/src/libsystemd-network/sd-dhcp6-lease.c +++ b/src/libsystemd-network/sd-dhcp6-lease.c @@ -445,6 +445,34 @@ int sd_dhcp6_lease_get_fqdn(sd_dhcp6_lease *lease, const char **ret) { return 0; } +int dhcp6_lease_set_captive_portal(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) { + _cleanup_free_ char *uri = NULL; + int r; + + assert(lease); + assert(optval || optlen == 0); + + r = dhcp6_option_parse_string(optval, optlen, &uri); + if (r < 0) + return r; + + if (uri && !in_charset(uri, URI_VALID)) + return -EINVAL; + + return free_and_replace(lease->captive_portal, uri); +} + +int sd_dhcp6_lease_get_captive_portal(sd_dhcp6_lease *lease, const char **ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + if (!lease->captive_portal) + return -ENODATA; + + *ret = lease->captive_portal; + return 0; +} + static int dhcp6_lease_parse_message( sd_dhcp6_client *client, sd_dhcp6_lease *lease, @@ -605,6 +633,12 @@ static int dhcp6_lease_parse_message( break; + case SD_DHCP6_OPTION_CAPTIVE_PORTAL: + r = dhcp6_lease_set_captive_portal(lease, optval, optlen); + if (r < 0) + log_dhcp6_client_errno(client, r, "Failed to parse captive portal option, ignoring: %m"); + break; + case SD_DHCP6_OPTION_CLIENT_FQDN: r = dhcp6_lease_set_fqdn(lease, optval, optlen); if (r < 0) @@ -665,6 +699,7 @@ static sd_dhcp6_lease *dhcp6_lease_free(sd_dhcp6_lease *lease) { dhcp6_ia_free(lease->ia_pd); free(lease->dns); free(lease->fqdn); + free(lease->captive_portal); strv_free(lease->domains); free(lease->ntp); strv_free(lease->ntp_fqdn); diff --git a/src/systemd/sd-dhcp6-lease.h b/src/systemd/sd-dhcp6-lease.h index 716f6fc17c..9f52496985 100644 --- a/src/systemd/sd-dhcp6-lease.h +++ b/src/systemd/sd-dhcp6-lease.h @@ -48,6 +48,7 @@ int sd_dhcp6_lease_get_domains(sd_dhcp6_lease *lease, char ***ret); int sd_dhcp6_lease_get_ntp_addrs(sd_dhcp6_lease *lease, const struct in6_addr **ret); int sd_dhcp6_lease_get_ntp_fqdn(sd_dhcp6_lease *lease, char ***ret); int sd_dhcp6_lease_get_fqdn(sd_dhcp6_lease *lease, const char **ret); +int sd_dhcp6_lease_get_captive_portal(sd_dhcp6_lease *lease, const char **ret); sd_dhcp6_lease *sd_dhcp6_lease_ref(sd_dhcp6_lease *lease); sd_dhcp6_lease *sd_dhcp6_lease_unref(sd_dhcp6_lease *lease); From 9747955d2d60b818d008c7a3c255aedf8de1c673 Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Thu, 29 Jun 2023 16:22:45 -0700 Subject: [PATCH 3/9] ndisc: parse RFC8910 captive portal ipv6ra option --- src/libsystemd-network/ndisc-router.c | 42 +++++++++++++++++++++++++++ src/network/networkd-link.c | 1 + src/network/networkd-link.h | 1 + src/network/networkd-ndisc.c | 40 +++++++++++++++++++++++++ src/network/networkd-network.c | 1 + src/network/networkd-network.h | 1 + src/systemd/sd-ndisc.h | 3 ++ 7 files changed, 89 insertions(+) diff --git a/src/libsystemd-network/ndisc-router.c b/src/libsystemd-network/ndisc-router.c index e4cbf714b9..997ac57586 100644 --- a/src/libsystemd-network/ndisc-router.c +++ b/src/libsystemd-network/ndisc-router.c @@ -715,3 +715,45 @@ int sd_ndisc_router_dnssl_get_lifetime(sd_ndisc_router *rt, uint32_t *ret_sec) { *ret_sec = be32toh(*(uint32_t*) (ri + 4)); return 0; } + +int sd_ndisc_router_captive_portal_get_uri(sd_ndisc_router *rt, const char **ret_uri, size_t *ret_size) { + int r; + const char *nd_opt_captive_portal; + size_t length; + + assert_return(rt, -EINVAL); + assert_return(ret_uri, -EINVAL); + + r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_CAPTIVE_PORTAL); + if (r < 0) + return r; + if (r == 0) + return -EMEDIUMTYPE; + + r = sd_ndisc_router_option_get_raw(rt, (void *)&nd_opt_captive_portal, &length); + if (r < 0) + return r; + + /* The length field has units of 8 octets */ + assert(length % 8 == 0); + if (length == 0) + return -EBADMSG; + + /* Check that the message is not truncated by an embedded NUL. + * NUL padding to a multiple of 8 is expected. */ + size_t size = strnlen(nd_opt_captive_portal + 2, length - 2); + if (DIV_ROUND_UP(size + 2, 8) != length / 8) + return -EBADMSG; + + /* Let's not return an empty buffer */ + if (size == 0) { + *ret_uri = NULL; + *ret_size = 0; + return 0; + } + + *ret_uri = nd_opt_captive_portal + 2; + *ret_size = size; + + return 0; +} diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c index 62dd892afa..7d9ea6f8e7 100644 --- a/src/network/networkd-link.c +++ b/src/network/networkd-link.c @@ -196,6 +196,7 @@ static Link *link_free(Link *link) { free(link->ssid); free(link->previous_ssid); free(link->driver); + free(link->ndisc_captive_portal); unlink_and_free(link->lease_file); unlink_and_free(link->lldp_file); diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h index 0d601ab548..bc8fe08374 100644 --- a/src/network/networkd-link.h +++ b/src/network/networkd-link.h @@ -154,6 +154,7 @@ typedef struct Link { sd_event_source *ndisc_expire; Set *ndisc_rdnss; Set *ndisc_dnssl; + char *ndisc_captive_portal; unsigned ndisc_messages; bool ndisc_configured:1; diff --git a/src/network/networkd-ndisc.c b/src/network/networkd-ndisc.c index 99a07e16fc..da5312c5ff 100644 --- a/src/network/networkd-ndisc.c +++ b/src/network/networkd-ndisc.c @@ -716,6 +716,43 @@ DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( ndisc_dnssl_compare_func, free); +static int ndisc_router_process_captive_portal(Link *link, sd_ndisc_router *rt) { + const char *uri; + _cleanup_free_ char *captive_portal = NULL; + size_t len; + int r; + + assert(link); + assert(link->network); + assert(rt); + + if (!link->network->ipv6_accept_ra_use_captive_portal) + return 0; + + r = sd_ndisc_router_captive_portal_get_uri(rt, &uri, &len); + if (r < 0) + return r; + + if (len == 0) { + mfree(link->ndisc_captive_portal); + return 0; + } + + r = make_cstring(uri, len, MAKE_CSTRING_REFUSE_TRAILING_NUL, &captive_portal); + if (r < 0) + return r; + + if (!in_charset(captive_portal, URI_VALID)) + return -EINVAL; + + if (!streq_ptr(link->ndisc_captive_portal, captive_portal)) { + free_and_replace(link->ndisc_captive_portal, captive_portal); + link_dirty(link); + } + + return 0; +} + static int ndisc_router_process_dnssl(Link *link, sd_ndisc_router *rt) { _cleanup_strv_free_ char **l = NULL; usec_t lifetime_usec, timestamp_usec; @@ -832,6 +869,9 @@ static int ndisc_router_process_options(Link *link, sd_ndisc_router *rt) { case SD_NDISC_OPTION_DNSSL: r = ndisc_router_process_dnssl(link, rt); break; + case SD_NDISC_OPTION_CAPTIVE_PORTAL: + r = ndisc_router_process_captive_portal(link, rt); + break; } if (r < 0 && r != -EBADMSG) return r; diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index 9a0511eeef..2423d891d3 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -476,6 +476,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi .ipv6_accept_ra = -1, .ipv6_accept_ra_use_dns = true, .ipv6_accept_ra_use_gateway = true, + .ipv6_accept_ra_use_captive_portal = true, .ipv6_accept_ra_use_route_prefix = true, .ipv6_accept_ra_use_autonomous_prefix = true, .ipv6_accept_ra_use_onlink_prefix = true, diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index 7685c98f65..c692fad991 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -315,6 +315,7 @@ struct Network { bool ipv6_accept_ra_use_onlink_prefix; bool ipv6_accept_ra_use_mtu; bool ipv6_accept_ra_quickack; + bool ipv6_accept_ra_use_captive_portal; bool active_slave; bool primary_slave; DHCPUseDomains ipv6_accept_ra_use_domains; diff --git a/src/systemd/sd-ndisc.h b/src/systemd/sd-ndisc.h index ee309a4253..b4faa4428e 100644 --- a/src/systemd/sd-ndisc.h +++ b/src/systemd/sd-ndisc.h @@ -123,6 +123,9 @@ int sd_ndisc_router_rdnss_get_lifetime(sd_ndisc_router *rt, uint32_t *ret); int sd_ndisc_router_dnssl_get_domains(sd_ndisc_router *rt, char ***ret); int sd_ndisc_router_dnssl_get_lifetime(sd_ndisc_router *rt, uint32_t *ret); +/* Specific option access: SD_NDISC_OPTION_CAPTIVE_PORTAL */ +int sd_ndisc_router_captive_portal_get_uri(sd_ndisc_router *rt, const char **uri, size_t *size); + _SD_DEFINE_POINTER_CLEANUP_FUNC(sd_ndisc, sd_ndisc_unref); _SD_DEFINE_POINTER_CLEANUP_FUNC(sd_ndisc_router, sd_ndisc_router_unref); From edb88a7201f5dfe11ca83cfb26b833cee80bd845 Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Thu, 29 Jun 2023 16:30:31 -0700 Subject: [PATCH 4/9] network: Introduce UseCaptivePortal DHCPv4 option Accepts a boolean. When enabled, UseCaptivePortal will request and retain the captive portal configuration from the DHCP server. --- man/systemd.network.xml | 8 ++++++++ src/libsystemd/sd-network/sd-network.c | 4 ++++ src/network/networkd-dhcp4.c | 5 +++++ src/network/networkd-network-gperf.gperf | 1 + src/network/networkd-network.c | 1 + src/network/networkd-network.h | 1 + src/network/networkd-state-file.c | 14 +++++++++++++- src/systemd/sd-network.h | 3 +++ 8 files changed, 36 insertions(+), 1 deletion(-) diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 1b34000052..452d2835b0 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -1969,6 +1969,14 @@ allow my_server_t localnet_peer_t:peer recv; + + UseCaptivePortal= + + When true (the default), the captive portal advertised by the DHCP server will be recorded + and made available to client programs and displayed in the networkctl status output per-link. + + + UseMTU= diff --git a/src/libsystemd/sd-network/sd-network.c b/src/libsystemd/sd-network/sd-network.c index dd440a5d17..cf3c400dbc 100644 --- a/src/libsystemd/sd-network/sd-network.c +++ b/src/libsystemd/sd-network/sd-network.c @@ -258,6 +258,10 @@ int sd_network_link_get_sip(int ifindex, char ***ret) { return network_link_get_strv(ifindex, "SIP", ret); } +int sd_network_link_get_captive_portal(int ifindex, char **ret) { + return network_link_get_string(ifindex, "CAPTIVE_PORTAL", ret); +} + int sd_network_link_get_search_domains(int ifindex, char ***ret) { return network_link_get_strv(ifindex, "DOMAINS", ret); } diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c index d4b4942173..ce6ce230bc 100644 --- a/src/network/networkd-dhcp4.c +++ b/src/network/networkd-dhcp4.c @@ -1426,6 +1426,11 @@ static int dhcp4_configure(Link *link) { if (r < 0) return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for SIP server: %m"); } + if (link->network->dhcp_use_captive_portal) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_DHCP_CAPTIVE_PORTAL); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for captive portal: %m"); + } if (link->network->dhcp_use_timezone) { r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_TZDB_TIMEZONE); diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 716904cc34..92ea9eaeef 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -216,6 +216,7 @@ DHCPv4.RoutesToDNS, config_parse_bool, DHCPv4.UseNTP, config_parse_dhcp_use_ntp, AF_INET, 0 DHCPv4.RoutesToNTP, config_parse_bool, 0, offsetof(Network, dhcp_routes_to_ntp) DHCPv4.UseSIP, config_parse_bool, 0, offsetof(Network, dhcp_use_sip) +DHCPv4.UseCaptivePortal, config_parse_bool, 0, offsetof(Network, dhcp_use_captive_portal) DHCPv4.UseMTU, config_parse_bool, 0, offsetof(Network, dhcp_use_mtu) DHCPv4.UseHostname, config_parse_bool, 0, offsetof(Network, dhcp_use_hostname) DHCPv4.UseDomains, config_parse_dhcp_use_domains, AF_INET, 0 diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index 2423d891d3..b2a2d8052c 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -395,6 +395,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi .dhcp_use_ntp = true, .dhcp_routes_to_ntp = true, .dhcp_use_sip = true, + .dhcp_use_captive_portal = true, .dhcp_use_dns = true, .dhcp_routes_to_dns = true, .dhcp_use_hostname = true, diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index c692fad991..b4666f1d63 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -143,6 +143,7 @@ struct Network { bool dhcp_use_ntp_set; bool dhcp_routes_to_ntp; bool dhcp_use_sip; + bool dhcp_use_captive_portal; bool dhcp_use_mtu; bool dhcp_use_routes; int dhcp_use_gateway; diff --git a/src/network/networkd-state-file.c b/src/network/networkd-state-file.c index 6e962c03f6..e313d9e918 100644 --- a/src/network/networkd-state-file.c +++ b/src/network/networkd-state-file.c @@ -464,7 +464,8 @@ static void link_save_domains(Link *link, FILE *f, OrderedSet *static_domains, D } int link_save(Link *link) { - const char *admin_state, *oper_state, *carrier_state, *address_state, *ipv4_address_state, *ipv6_address_state; + const char *admin_state, *oper_state, *carrier_state, *address_state, *ipv4_address_state, *ipv6_address_state, + *dhcp_captive_portal = NULL; _cleanup_(unlink_and_freep) char *temp_path = NULL; _cleanup_fclose_ FILE *f = NULL; int r; @@ -606,6 +607,17 @@ int link_save(Link *link) { /************************************************************/ + if (link->dhcp_lease && link->network->dhcp_use_captive_portal) { + r = sd_dhcp_lease_get_captive_portal(link->dhcp_lease, &dhcp_captive_portal); + if (r < 0 && r != -ENODATA) + return r; + } + + if (dhcp_captive_portal) + fprintf(f, "CAPTIVE_PORTAL=%s\n", dhcp_captive_portal); + + /************************************************************/ + fputs("DOMAINS=", f); if (link->search_domains) link_save_domains(link, f, link->search_domains, DHCP_USE_DOMAINS_NO); diff --git a/src/systemd/sd-network.h b/src/systemd/sd-network.h index 9cc2cbaa6e..d292719a3e 100644 --- a/src/systemd/sd-network.h +++ b/src/systemd/sd-network.h @@ -134,6 +134,9 @@ int sd_network_link_get_ntp(int ifindex, char ***ret); * representations of IP addresses */ int sd_network_link_get_sip(int ifindex, char ***ret); +/* Get the captive portal address for a given link. */ +int sd_network_link_get_captive_portal(int ifindex, char **ret); + /* Indicates whether or not LLMNR should be enabled for the link * Possible levels of support: yes, no, resolve * Possible return codes: From a75feb554b9b3278744f3594475cd1d3c93f111b Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Thu, 29 Jun 2023 16:33:57 -0700 Subject: [PATCH 5/9] network: Introduce UseCaptivePortal DHCPv6 option Acepts a boolean. When enabled requests and retains captive portal configuration from the DHCPv6 server. --- man/systemd.network.xml | 8 ++++++++ src/network/networkd-dhcp6.c | 6 ++++++ src/network/networkd-network-gperf.gperf | 1 + src/network/networkd-network.c | 1 + src/network/networkd-network.h | 1 + src/network/networkd-state-file.c | 15 ++++++++++++++- 6 files changed, 31 insertions(+), 1 deletion(-) diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 452d2835b0..b623a4aa75 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2290,6 +2290,14 @@ allow my_server_t localnet_peer_t:peer recv; + + UseCaptivePortal= + + When true (the default), the captive portal advertised by the DHCPv6 server will be recorded + and made available to client programs and displayed in the networkctl status output per-link. + + + UseDelegatedPrefix= diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c index 43be988377..57272e7bf6 100644 --- a/src/network/networkd-dhcp6.c +++ b/src/network/networkd-dhcp6.c @@ -635,6 +635,12 @@ static int dhcp6_configure(Link *link) { return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request domains: %m"); } + if (link->network->dhcp6_use_captive_portal > 0) { + r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_CAPTIVE_PORTAL); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request captive portal: %m"); + } + if (link->network->dhcp6_use_ntp) { r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_NTP_SERVER); if (r < 0) diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 92ea9eaeef..ab0d252b94 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -258,6 +258,7 @@ DHCPv6.UseDNS, config_parse_dhcp_use_dns, DHCPv6.UseHostname, config_parse_bool, 0, offsetof(Network, dhcp6_use_hostname) DHCPv6.UseDomains, config_parse_dhcp_use_domains, AF_INET6, 0 DHCPv6.UseNTP, config_parse_dhcp_use_ntp, AF_INET6, 0 +DHCPv6.UseCaptivePortal, config_parse_bool, 0, offsetof(Network, dhcp6_use_captive_portal) DHCPv6.MUDURL, config_parse_mud_url, 0, offsetof(Network, dhcp6_mudurl) DHCPv6.RequestOptions, config_parse_dhcp_request_options, AF_INET6, 0 DHCPv6.UserClass, config_parse_dhcp_user_or_vendor_class, AF_INET6, offsetof(Network, dhcp6_user_class) diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index b2a2d8052c..a92c229d4f 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -414,6 +414,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi .dhcp6_use_dns = true, .dhcp6_use_hostname = true, .dhcp6_use_ntp = true, + .dhcp6_use_captive_portal = true, .dhcp6_use_rapid_commit = true, .dhcp6_duid.type = _DUID_TYPE_INVALID, .dhcp6_client_start_mode = _DHCP6_CLIENT_START_MODE_INVALID, diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index b4666f1d63..e473a72500 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -170,6 +170,7 @@ struct Network { bool dhcp6_use_hostname; bool dhcp6_use_ntp; bool dhcp6_use_ntp_set; + bool dhcp6_use_captive_portal; bool dhcp6_use_rapid_commit; DHCPUseDomains dhcp6_use_domains; bool dhcp6_use_domains_set; diff --git a/src/network/networkd-state-file.c b/src/network/networkd-state-file.c index e313d9e918..5f5cb3239d 100644 --- a/src/network/networkd-state-file.c +++ b/src/network/networkd-state-file.c @@ -465,7 +465,7 @@ static void link_save_domains(Link *link, FILE *f, OrderedSet *static_domains, D int link_save(Link *link) { const char *admin_state, *oper_state, *carrier_state, *address_state, *ipv4_address_state, *ipv6_address_state, - *dhcp_captive_portal = NULL; + *dhcp_captive_portal = NULL, *dhcp6_captive_portal = NULL; _cleanup_(unlink_and_freep) char *temp_path = NULL; _cleanup_fclose_ FILE *f = NULL; int r; @@ -613,8 +613,21 @@ int link_save(Link *link) { return r; } + if (link->dhcp6_lease && link->network->dhcp6_use_captive_portal) { + r = sd_dhcp6_lease_get_captive_portal(link->dhcp6_lease, &dhcp6_captive_portal); + if (r < 0 && r != -ENODATA) + return r; + } + + if (dhcp6_captive_portal && dhcp_captive_portal && !streq(dhcp_captive_portal, dhcp6_captive_portal)) + log_link_warning(link, "DHCPv6 Captive Portal (%s) does not match DHCPv4 (%s). Ignoring DHCPv6 portal.", + dhcp6_captive_portal, dhcp_captive_portal); + + if (dhcp_captive_portal) fprintf(f, "CAPTIVE_PORTAL=%s\n", dhcp_captive_portal); + else if (dhcp6_captive_portal) + fprintf(f, "CAPTIVE_PORTAL=%s\n", dhcp6_captive_portal); /************************************************************/ From d74c4ce103fb12bebbaa369219fb64707e27aaca Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Thu, 29 Jun 2023 16:58:03 -0700 Subject: [PATCH 6/9] network: Introduce UseCaptivePortal IPv6RA option Accepts a boolean. When enabled retains captive portal configuration advertised by the router. --- man/systemd.network.xml | 8 ++++++++ src/network/networkd-network-gperf.gperf | 1 + src/network/networkd-state-file.c | 10 ++++++++++ 3 files changed, 19 insertions(+) diff --git a/man/systemd.network.xml b/man/systemd.network.xml index b623a4aa75..0d3805fa8a 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2612,6 +2612,14 @@ Token=prefixstable:2002:da8:1:: + + UseCaptivePortal= + + When true (the default), the captive portal received in the Router Advertisement will be recorded + and made available to client programs and displayed in the networkctl status output per-link. + + + UseAutonomousPrefix= diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index ab0d252b94..6a7ef24651 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -284,6 +284,7 @@ IPv6AcceptRA.DHCPv6Client, config_parse_ipv6_accept_ra_start_d IPv6AcceptRA.RouteTable, config_parse_dhcp_or_ra_route_table, AF_INET6, 0 IPv6AcceptRA.RouteMetric, config_parse_ipv6_accept_ra_route_metric, 0, 0 IPv6AcceptRA.QuickAck, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_quickack) +IPv6AcceptRA.UseCaptivePortal, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_captive_portal) IPv6AcceptRA.RouterAllowList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_allow_listed_router) IPv6AcceptRA.RouterDenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_router) IPv6AcceptRA.PrefixAllowList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_allow_listed_prefix) diff --git a/src/network/networkd-state-file.c b/src/network/networkd-state-file.c index 5f5cb3239d..c9366e97d8 100644 --- a/src/network/networkd-state-file.c +++ b/src/network/networkd-state-file.c @@ -623,11 +623,21 @@ int link_save(Link *link) { log_link_warning(link, "DHCPv6 Captive Portal (%s) does not match DHCPv4 (%s). Ignoring DHCPv6 portal.", dhcp6_captive_portal, dhcp_captive_portal); + if (link->network->ipv6_accept_ra_use_captive_portal && link->ndisc_captive_portal) { + if (dhcp_captive_portal && !streq(dhcp_captive_portal, link->ndisc_captive_portal)) + log_link_warning(link, "IPv6RA captive portal (%s) does not match DHCPv4 (%s). Ignorning IPv6RA portal.", + link->ndisc_captive_portal, dhcp_captive_portal); + if (dhcp6_captive_portal && !streq(dhcp6_captive_portal, link->ndisc_captive_portal)) + log_link_warning(link, "IPv6RA captive portal (%s) does not match DHCPv6 (%s). Ignorning IPv6RA portal.", + link->ndisc_captive_portal, dhcp6_captive_portal); + } if (dhcp_captive_portal) fprintf(f, "CAPTIVE_PORTAL=%s\n", dhcp_captive_portal); else if (dhcp6_captive_portal) fprintf(f, "CAPTIVE_PORTAL=%s\n", dhcp6_captive_portal); + else if (link->ndisc_captive_portal) + fprintf(f, "CAPTIVE_PORTAL=%s\n", link->ndisc_captive_portal); /************************************************************/ From 8628267f31b291e91d707da91813885bcb70946f Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Thu, 29 Jun 2023 16:36:50 -0700 Subject: [PATCH 7/9] networkd: include captive portal information in link json description --- src/network/networkd-json.c | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/network/networkd-json.c b/src/network/networkd-json.c index 15cd1a93f6..31ccb9679a 100644 --- a/src/network/networkd-json.c +++ b/src/network/networkd-json.c @@ -873,6 +873,41 @@ finalize: return r; } +static int captive_portal_build_json(Link *link, JsonVariant **ret) { + int r; + const char *captive_portal = NULL; + + assert(link); + assert(ret); + + if (!link->network) { + *ret = NULL; + return 0; + } + + if (link->network->dhcp_use_captive_portal && link->dhcp_lease) { + r = sd_dhcp_lease_get_captive_portal(link->dhcp_lease, &captive_portal); + if (r < 0 && r != -ENODATA) + return r; + } + + if (link->network->dhcp6_use_captive_portal && link->dhcp6_lease && !captive_portal) { + r = sd_dhcp6_lease_get_captive_portal(link->dhcp6_lease, &captive_portal); + if (r < 0 && r != -ENODATA) + return r; + } + + if (link->network->ipv6_accept_ra_use_captive_portal && !captive_portal) + captive_portal = link->ndisc_captive_portal; + + if (!captive_portal) { + *ret = NULL; + return 0; + } + + return json_build(ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("CaptivePortal", captive_portal))); +} + static int domain_build_json(int family, const char *domain, NetworkConfigSource s, const union in_addr_union *p, JsonVariant **ret) { assert(IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6)); assert(domain); @@ -1390,6 +1425,16 @@ int link_build_json(Link *link, JsonVariant **ret) { w = json_variant_unref(w); + r = captive_portal_build_json(link, &w); + if (r < 0) + return r; + + r = json_variant_merge(&v, w); + if (r < 0) + return r; + + w = json_variant_unref(w); + r = domains_build_json(link, /* is_route = */ false, &w); if (r < 0) return r; From e469d2a2ed79362f89248c5f3ceb4c13275f78bb Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Thu, 29 Jun 2023 16:38:26 -0700 Subject: [PATCH 8/9] networkctl: show captive portal configuration in link status --- src/network/networkctl.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/network/networkctl.c b/src/network/networkctl.c index 0037c8e8ce..ec60c5e6b8 100644 --- a/src/network/networkctl.c +++ b/src/network/networkctl.c @@ -1678,7 +1678,7 @@ static int link_status_one( _cleanup_strv_free_ char **dns = NULL, **ntp = NULL, **sip = NULL, **search_domains = NULL, **route_domains = NULL, **link_dropins = NULL, **network_dropins = NULL; - _cleanup_free_ char *t = NULL, *network = NULL, *iaid = NULL, *duid = NULL, + _cleanup_free_ char *t = NULL, *network = NULL, *iaid = NULL, *duid = NULL, *captive_portal = NULL, *setup_state = NULL, *operational_state = NULL, *online_state = NULL, *activation_policy = NULL; const char *driver = NULL, *path = NULL, *vendor = NULL, *model = NULL, *link = NULL, *on_color_operational, *off_color_operational, *on_color_setup, *off_color_setup, *on_color_online; @@ -1706,6 +1706,7 @@ static int link_status_one( (void) sd_network_link_get_route_domains(info->ifindex, &route_domains); (void) sd_network_link_get_ntp(info->ifindex, &ntp); (void) sd_network_link_get_sip(info->ifindex, &sip); + (void) sd_network_link_get_captive_portal(info->ifindex, &captive_portal); (void) sd_network_link_get_network_file(info->ifindex, &network); (void) sd_network_link_get_network_file_dropins(info->ifindex, &network_dropins); (void) sd_network_link_get_carrier_bound_to(info->ifindex, &carrier_bound_to); @@ -2358,6 +2359,9 @@ static int link_status_one( return table_log_add_error(r); } + if (captive_portal) + table_add_string_line(table, "Captive Portal:", captive_portal); + if (lease) { const void *client_id; size_t client_id_len; From dbe960f07fbb86f6fd614368db4cc860d468d44e Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Thu, 29 Jun 2023 16:38:55 -0700 Subject: [PATCH 9/9] test-network: add tests for captive portal dhcp options --- test/test-network/systemd-networkd-tests.py | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index b5ef83a9c0..681b14732a 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -5135,6 +5135,45 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): check(self, False, True) check(self, False, False) + def test_dhcp_client_use_captive_portal(self): + def check(self, ipv4, ipv6): + os.makedirs(os.path.join(network_unit_dir, '25-dhcp-client.network.d'), exist_ok=True) + with open(os.path.join(network_unit_dir, '25-dhcp-client.network.d/override.conf'), mode='w', encoding='utf-8') as f: + f.write('[DHCPv4]\nUseCaptivePortal=') + f.write('yes' if ipv4 else 'no') + f.write('\n[DHCPv6]\nUseCaptivePortal=') + f.write('yes' if ipv6 else 'no') + f.write('\n[IPv6AcceptRA]\nUseCaptivePortal=no') + + networkctl_reload() + self.wait_online(['veth99:routable']) + + # link becomes 'routable' when at least one protocol provide an valid address. Hence, we need to explicitly wait for both addresses. + self.wait_address('veth99', r'inet 192.168.5.[0-9]*/24 metric 1024 brd 192.168.5.255 scope global dynamic', ipv='-4') + self.wait_address('veth99', r'inet6 2600::[0-9a-f]*/128 scope global (dynamic noprefixroute|noprefixroute dynamic)', ipv='-6') + + output = check_output(*networkctl_cmd, 'status', 'veth99', env=env) + print(output) + if ipv4 or ipv6: + self.assertIn('Captive Portal: http://systemd.io', output) + else: + self.assertNotIn('Captive Portal: http://systemd.io', output) + + # TODO: check json string + check_output(*networkctl_cmd, '--json=short', 'status', env=env) + + copy_network_unit('25-veth.netdev', '25-dhcp-server-veth-peer.network', '25-dhcp-client.network', copy_dropins=False) + + start_networkd() + self.wait_online(['veth-peer:carrier']) + start_dnsmasq('--dhcp-option=114,http://systemd.io', + '--dhcp-option=option6:103,http://systemd.io') + + check(self, True, True) + check(self, True, False) + check(self, False, True) + check(self, False, False) + class NetworkdDHCPPDTests(unittest.TestCase, Utilities): def setUp(self):