diff --git a/man/systemd.network.xml b/man/systemd.network.xml
index f527f31111..1ab28cde4b 100644
--- a/man/systemd.network.xml
+++ b/man/systemd.network.xml
@@ -1971,6 +1971,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=
@@ -2284,6 +2292,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=
@@ -2598,6 +2614,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/basic/string-util.h b/src/basic/string-util.h
index ce6ea64bb9..52eab27fa3 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/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/ndisc-router.c b/src/libsystemd-network/ndisc-router.c
index 36deba757b..43a36e4b14 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/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/libsystemd-network/sd-dhcp6-lease.c b/src/libsystemd-network/sd-dhcp6-lease.c
index 1c6b231db6..6dcc25888d 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,
@@ -610,6 +638,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)
@@ -670,6 +704,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/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/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;
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-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-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;
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-gperf.gperf b/src/network/networkd-network-gperf.gperf
index 716904cc34..6a7ef24651 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
@@ -257,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)
@@ -282,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-network.c b/src/network/networkd-network.c
index 7eef3d5b52..d4f4a9ed97 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,
@@ -413,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,
@@ -476,6 +478,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..e473a72500 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;
@@ -169,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;
@@ -315,6 +317,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/network/networkd-state-file.c b/src/network/networkd-state-file.c
index 6e962c03f6..c9366e97d8 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, *dhcp6_captive_portal = NULL;
_cleanup_(unlink_and_freep) char *temp_path = NULL;
_cleanup_fclose_ FILE *f = NULL;
int r;
@@ -606,6 +607,40 @@ 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 (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 (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);
+
+ /************************************************************/
+
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-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);
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);
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);
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:
diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py
index 1d8125c115..9f55456fa4 100755
--- a/test/test-network/systemd-networkd-tests.py
+++ b/test/test-network/systemd-networkd-tests.py
@@ -5150,6 +5150,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):