https://www.rfc-editor.org/rfc/rfc8910.html
RFC8908 defines a captive portal client (CPC) that handles the API URI returned.
The captive portal (CP) option presence signals a portal that requires
authentication beyond what odhcp6c currently handles.
It's possible that a user connecting through an openwrt gateway encounters the
portal anyway (though this behaviour depends on the portal), but if this is not
the case, with this addition we can:
- surface a message in a UI as to presence of the CP
- signal that users install a CPC (a chicken/egg problem behind a CP)
- provide the API URI for downstream consumers.
This should ease the use of travel router scenarios.
Downstream consumers of the API URI can find it via the CAPTIVE_PORTAL_URI
environment/ubus property.
The strengths of having this option handled means downstream consumers get one
unified environment variable since ra.c does not yet handle CUSTOM_* the way
DHCPv6 does.
Signed-off-by: Paul Donald <[email protected]>
Link: https://github.com/openwrt/odhcp6c/pull/127
Signed-off-by: Álvaro Fernández Rojas <[email protected]>
| `RA_RETRANSMIT` | ND Retransmit time |
| `AFTR` | The DS-Lite AFTR domain name |
| `MAPE` / `MAPT` / `LW4O6` | Softwire rules for MAPE, MAPT and LW4O6 |
+| `CAPTIVE_PORTAL_URI` | RFC8910 captive portal API URI received from upstream |
| `PASSTHRU` | The content of the last packet relayed |
htons(DHCPV6_OPT_SNTP_SERVERS),
htons(DHCPV6_OPT_NTP_SERVER),
htons(DHCPV6_OPT_PD_EXCLUDE),
+ /* RFC8910: Clients that support this option SHOULD include it */
+ htons(DHCPV6_OPT_CAPTIVE_PORTAL),
};
odhcp6c_add_state(STATE_ORO, oro, sizeof(oro));
}
odhcp6c_clear_state(STATE_S46_MAPT);
odhcp6c_clear_state(STATE_S46_MAPE);
odhcp6c_clear_state(STATE_S46_LW);
+ odhcp6c_clear_state(STATE_CAPT_PORT);
odhcp6c_clear_state(STATE_PASSTHRU);
odhcp6c_clear_state(STATE_CUSTOM_OPTS);
odhcp6c_add_state(STATE_S46_LW, odata, olen);
break;
+ case DHCPV6_OPT_CAPTIVE_PORTAL: /* RFC8910 §2.2 */
+ size_t ref_len = sizeof(URN_IETF_CAPT_PORT_UNRESTR) - 1;
+ /* RFC8910 §2:
+ * Networks with no captive portals may explicitly indicate this
+ * condition by using this option with the IANA-assigned URI for
+ * this purpose. Clients observing the URI value ... may forego
+ * time-consuming forms of captive portal detection. */
+ if (memcmp(odata, URN_IETF_CAPT_PORT_UNRESTR, ref_len)) {
+ /* RFC8910 §2.2:
+ * Note that the URI parameter is not null terminated.
+ * Allocate new buffer including room for '\0' */
+ size_t uri_len = olen + 1;
+ uint8_t *copy = malloc(uri_len);
+ if (!copy)
+ continue;
+ memcpy(copy, odata, olen);
+ copy[uri_len] = '\0';
+ odhcp6c_add_state(STATE_CAPT_PORT, odata, olen);
+ free(copy);
+ }
+ break;
+
default:
odhcp6c_add_state(STATE_CUSTOM_OPTS, &odata[-DHCPV6_OPT_HDR_SIZE], olen + DHCPV6_OPT_HDR_SIZE);
break;
odhcp6c_clear_state(STATE_NTP_FQDN);
odhcp6c_clear_state(STATE_SIP_IP);
odhcp6c_clear_state(STATE_SIP_FQDN);
+ odhcp6c_clear_state(STATE_CAPT_PORT);
bound = false;
size_t oro_len = 0;
#define RA_MIN_ADV_INTERVAL 3 /* RFC 4861 paragraph 6.2.1 */
+/* RFC8910 §2 */
+static const uint8_t URN_IETF_CAPT_PORT_UNRESTR[] = "urn:ietf:params:capport:unrestricted";
+#define CAPT_PORT_URI_STR "CAPTIVE_PORTAL_URI"
+
enum dhcvp6_opt {
/* RFC8415(bis) */
DHCPV6_OPT_CLIENTID = 1,
DHCPV6_OPT_LQ_BASE_TIME = 100,
DHCPV6_OPT_LQ_START_TIME = 101,
DHCPV6_OPT_LQ_END_TIME = 102,
+ /* RFC8910 */
+ DHCPV6_OPT_CAPTIVE_PORTAL = 103,
/* RFC7839 */
DHCPV6_OPT_ANI_ATT = 105,
DHCPV6_OPT_ANI_NETWORK_NAME = 106,
STATE_S46_MAPT,
STATE_S46_MAPE,
STATE_S46_LW,
+ STATE_CAPT_PORT,
STATE_PASSTHRU,
_STATE_MAX
};
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/ioctl.h>
ra_holdoff_interval);
entry->auxlen = 0;
}
+ } else if (opt->type == ND_OPT_CAPTIVE_PORTAL) {
+ /* RFC8910 Captive-Portal §2.3 */
+ if (opt->len <= 1)
+ continue;
+
+ struct icmpv6_opt_captive_portal *capt_port = (struct icmpv6_opt_captive_portal*)opt;
+ uint8_t *buf = &capt_port->data[0];
+ size_t ref_len = sizeof(URN_IETF_CAPT_PORT_UNRESTR) - 1;
+
+ /* RFC8910 §2:
+ * Networks with no captive portals may explicitly indicate this
+ * condition by using this option with the IANA-assigned URI for
+ * this purpose. Clients observing the URI value ... may forego
+ * time-consuming forms of captive portal detection. */
+ if (memcmp(buf, URN_IETF_CAPT_PORT_UNRESTR, ref_len)) {
+ /* URI are not guaranteed to be \0 terminated if data is unpadded */
+ size_t uri_len = (capt_port->len * 8) - 2;
+ /* Allocate new buffer including room for '\0' */
+ uint8_t *copy = malloc(uri_len + 1);
+ if (!copy)
+ continue;
+
+ memcpy(copy, buf, uri_len);
+ copy[uri_len] = '\0';
+ odhcp6c_clear_state(STATE_CAPT_PORT);
+ odhcp6c_add_state(STATE_CAPT_PORT, copy, uri_len);
+ free(copy);
+ }
}
}
uint8_t data[6];
};
+/* RFC8910 Captive-Portal §2.3 */
+struct icmpv6_opt_captive_portal {
+ uint8_t type; /* 37 */
+ uint8_t len; /* includes the Type and Length fields, in units of 8 bytes */
+ uint8_t data[]; /* padded with NUL (0x00) to make length multiple of 8 */
+};
+
struct icmpv6_opt_route_info {
uint8_t type;
uint8_t len;
};
#define ND_OPT_ROUTE_INFORMATION 24
+#define ND_OPT_CAPTIVE_PORTAL 37
#define icmpv6_for_each_option(opt, start, end)\
putenv(buf);
}
+static void string_to_env(const char *name, const uint8_t *string, size_t len)
+{
+ size_t buf_len = strlen(name);
+ const uint8_t *string_end = string + len;
+ char *buf = realloc(NULL, len + buf_len + 2);
+
+ memcpy(buf, name, buf_len);
+ buf[buf_len++] = '=';
+
+ while (string < string_end) {
+ int l = strlen((const char *)string);
+ if (l <= 0)
+ break;
+ string += l;
+ buf_len += strlen(&buf[buf_len]);
+ buf[buf_len++] = ' ';
+ }
+
+ if (buf[buf_len - 1] == ' ')
+ buf_len--;
+
+ buf[buf_len] = '\0';
+ putenv(buf);
+}
+
static void bin_to_env(uint8_t *opts, size_t len)
{
uint8_t *oend = opts + len, *odata;
} else if (pid == 0) {
size_t dns_len, search_len, custom_len, sntp_ip_len, ntp_ip_len, ntp_dns_len;
size_t sip_ip_len, sip_fqdn_len, aftr_name_len, addr_len;
- size_t s46_mapt_len, s46_mape_len, s46_lw_len, passthru_len;
+ size_t s46_mapt_len, s46_mape_len, s46_lw_len, capt_port_len, passthru_len;
signal(SIGTERM, SIG_DFL);
if (delay > 0) {
uint8_t *s46_mapt = odhcp6c_get_state(STATE_S46_MAPT, &s46_mapt_len);
uint8_t *s46_mape = odhcp6c_get_state(STATE_S46_MAPE, &s46_mape_len);
uint8_t *s46_lw = odhcp6c_get_state(STATE_S46_LW, &s46_lw_len);
+ uint8_t *capt_port = odhcp6c_get_state(STATE_CAPT_PORT, &capt_port_len);
uint8_t *passthru = odhcp6c_get_state(STATE_PASSTHRU, &passthru_len);
size_t prefix_len, address_len, ra_pref_len,
s46_to_env(STATE_S46_MAPE, s46_mape, s46_mape_len);
s46_to_env(STATE_S46_MAPT, s46_mapt, s46_mapt_len);
s46_to_env(STATE_S46_LW, s46_lw, s46_lw_len);
+ string_to_env(CAPT_PORT_URI_STR, capt_port, capt_port_len);
bin_to_env(custom, custom_len);
if (odhcp6c_is_bound()) {
char *buf = NULL;
size_t dns_len, search_len, custom_len, sntp_ip_len, ntp_ip_len, ntp_dns_len;
size_t sip_ip_len, sip_fqdn_len, aftr_name_len, addr_len;
- size_t s46_mapt_len, s46_mape_len, s46_lw_len, passthru_len;
+ size_t s46_mapt_len, s46_mape_len, s46_lw_len, capt_port_len, passthru_len;
struct in6_addr *addr = odhcp6c_get_state(STATE_SERVER_ADDR, &addr_len);
struct in6_addr *dns = odhcp6c_get_state(STATE_DNS, &dns_len);
uint8_t *search = odhcp6c_get_state(STATE_SEARCH, &search_len);
uint8_t *s46_mapt = odhcp6c_get_state(STATE_S46_MAPT, &s46_mapt_len);
uint8_t *s46_mape = odhcp6c_get_state(STATE_S46_MAPE, &s46_mape_len);
uint8_t *s46_lw = odhcp6c_get_state(STATE_S46_LW, &s46_lw_len);
+ uint8_t *capt_port = odhcp6c_get_state(STATE_CAPT_PORT, &capt_port_len);
uint8_t *passthru = odhcp6c_get_state(STATE_PASSTHRU, &passthru_len);
size_t prefix_len, address_len, ra_pref_len,
CHECK(s46_to_blob(STATE_S46_MAPE, s46_mape, s46_mape_len));
CHECK(s46_to_blob(STATE_S46_MAPT, s46_mapt, s46_mapt_len));
CHECK(s46_to_blob(STATE_S46_LW, s46_lw, s46_lw_len));
+ blobmsg_add_string(&b, CAPT_PORT_URI_STR, (char *)capt_port);
CHECK(bin_to_blob(custom, custom_len));
if (odhcp6c_is_bound()) {