Add an "ia_pd" DHCPv6 option that lets the CMS supply a delegated prefix in a
logical port's DHCP_Options for OVN's native DHCPv6 server to hand out, with no
pool or lease state kept in the dataplane. This lets OVN delegate a prefix
(for example to a VM that in turn sub-delegates to its own workloads) while
remaining the stateless responder it already is for IA_NA.
The server is request-aware per RFC 8415: it parses the client's IA_PD,
captures its IAID, and emits an IA_PD -> IA_PREFIX carrying the configured
prefix only when the client actually solicited one, echoing the client's IAID.
Prefixes are advertised with infinite lifetimes, matching the existing IA_NA
emit. Only Solicit and Request elicit a delegated prefix; an IA_PD in Confirm,
Decline or Information-request is ignored, and only the first IA_PD from a
client is honored (a single prefix is delegated).
A port must also receive a managed IPv6 address (ia_addr), which is what makes
northd intercept its DHCPv6 traffic; PD-only ports are not yet supported.
- lib/ovn-l7.h, northd/ovn-northd.c: register DHCPV6_OPT_IA_PD_PREFIX
("ia_pd", code 25, type "ia_pd") and add it to supported_dhcpv6_opts[].
- lib/actions.c: encode the "ia_pd" type into the action buffer as
[plen][prefix], deriving plen from the value's mask. parse_gen_opt
rejects a value that is not a single masked IPv6 prefix, since the mask
supplies the on-wire prefix length (a bare address would silently encode
a /0, and a set would drop all but the first).
- controller/pinctrl.c: parse the client IA_PD and emit the delegated
prefix, copying it with memcpy to avoid an unaligned struct in6_addr
access on strict-alignment architectures.
- ovn-sb.ovsschema: allow "ia_pd" in the DHCPv6_Options "type" column enum
and bump the schema version. northd writes the supported option into the
Southbound DHCPv6_Options table, so without this every northd SB commit
failed with a constraint violation.
- ovn-nb.xml, ovn-sb.xml: document the new option and the SB type.
- tests: register ia_pd in the test harness, add put_dhcpv6_opts parse and
encode cases, and extend the dhcpv6 system test to exercise the
request-aware path (a sibling IA_NA-only port must not receive an IA_PD).
Signed-off-by: Dax Kelson <[email protected]>
---
controller/pinctrl.c | 84 +++++++++++++++++++++++++++++++++++++++++---
lib/actions.c | 20 +++++++++++
lib/ovn-l7.h | 3 ++
northd/ovn-northd.c | 1 +
ovn-nb.xml | 33 +++++++++++++++++
ovn-sb.ovsschema | 7 ++--
ovn-sb.xml | 18 ++++++++++
tests/ovn.at | 68 ++++++++++++++++++++++++++++++++++-
tests/test-ovn.c | 1 +
9 files changed, 227 insertions(+), 8 deletions(-)
diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index fce1c4be0..ff5004746 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -2962,7 +2962,8 @@ encode_dhcpv6_server_id_opt(struct ofpbuf *opts, void
*user_data)
static bool
compose_out_dhcpv6_opts(struct ofpbuf *userdata,
struct ofpbuf *out_dhcpv6_opts,
- ovs_be32 iaid, bool ipxe_req, uint8_t fqdn_flags)
+ ovs_be32 iaid, bool ipxe_req, uint8_t fqdn_flags,
+ ovs_be32 pd_iaid, bool pd_requested)
{
while (userdata->size) {
struct dhcpv6_opt_header *userdata_opt = ofpbuf_try_pull(
@@ -3025,6 +3026,54 @@ compose_out_dhcpv6_opts(struct ofpbuf *userdata,
break;
}
+ case DHCPV6_OPT_IA_PD:
+ {
+ if (size != sizeof(uint8_t) + sizeof(struct in6_addr)) {
+ return false;
+ }
+
+ if (!pd_requested) {
+ /* The client did not ask for a delegated prefix, so don't
+ * put an IA_PD option in the response. */
+ break;
+ }
+
+ /* The IA Prefix option carries a delegated prefix and must be
+ * encapsulated in the Options field of an IA_PD option. Please
+ * see RFC 8415 sections 21.21 and 21.22. */
+ uint8_t plen = *(uint8_t *) userdata_opt_data;
+
+ struct dhcpv6_opt_ia_na *opt_ia_pd = ofpbuf_put_zeros(
+ out_dhcpv6_opts, sizeof *opt_ia_pd);
+ opt_ia_pd->opt.code = htons(DHCPV6_OPT_IA_PD);
+ /* IA_PD length (in bytes)-
+ * IAID - 4
+ * T1 - 4
+ * T2 - 4
+ * IA Prefix - sizeof(struct dhcpv6_opt_ia_prefix)
+ */
+ opt_ia_pd->opt.len =
+ htons(12 + sizeof(struct dhcpv6_opt_ia_prefix));
+ opt_ia_pd->iaid = pd_iaid;
+ /* Set the lifetime of the prefix to infinity. */
+ opt_ia_pd->t1 = OVS_BE32_MAX;
+ opt_ia_pd->t2 = OVS_BE32_MAX;
+
+ struct dhcpv6_opt_ia_prefix *opt_ia_prefix = ofpbuf_put_zeros(
+ out_dhcpv6_opts, sizeof *opt_ia_prefix);
+ opt_ia_prefix->opt.code = htons(DHCPV6_OPT_IA_PREFIX);
+ opt_ia_prefix->opt.len =
+ htons(sizeof(struct dhcpv6_opt_ia_prefix) -
+ DHCP6_OPT_HEADER_LEN);
+ opt_ia_prefix->plife_time = OVS_BE32_MAX;
+ opt_ia_prefix->vlife_time = OVS_BE32_MAX;
+ opt_ia_prefix->plen = plen;
+ memcpy(&opt_ia_prefix->ipv6,
+ (uint8_t *) userdata_opt_data + sizeof plen,
+ sizeof opt_ia_prefix->ipv6);
+ break;
+ }
+
case DHCPV6_OPT_DNS_SERVER_CODE:
{
struct dhcpv6_opt_header *opt_dns = ofpbuf_put_zeros(
@@ -3209,6 +3258,8 @@ pinctrl_handle_put_dhcpv6_opts(
* option. So we don't need to copy that in the Server's response.
* */
ovs_be32 iaid = 0;
+ ovs_be32 pd_iaid = 0;
+ bool pd_requested = false;
struct dhcpv6_opt_header const *in_opt_client_id = NULL;
bool ipxe_req = false;
uint8_t fqdn_flags = DHCPV6_FQDN_FLAGS_UNDEFINED;
@@ -3234,6 +3285,23 @@ pinctrl_handle_put_dhcpv6_opts(
break;
}
+ case DHCPV6_OPT_IA_PD:
+ {
+ if (opt_len < sizeof(struct dhcpv6_opt_ia_na)) {
+ break;
+ }
+
+ /* IA_PD shares the IAID/T1/T2 header layout with IA_NA. OVN
+ * delegates a single prefix, so honor only the first IA_PD. */
+ if (!pd_requested) {
+ struct dhcpv6_opt_ia_na *opt_ia_pd =
+ (struct dhcpv6_opt_ia_na *) in_opt;
+ pd_iaid = opt_ia_pd->iaid;
+ pd_requested = true;
+ }
+ break;
+ }
+
case DHCPV6_OPT_CLIENT_ID_CODE:
in_opt_client_id = in_opt;
break;
@@ -3271,8 +3339,15 @@ pinctrl_handle_put_dhcpv6_opts(
goto exit;
}
- if (!iaid && hdr->msg_type != DHCPV6_MSG_TYPE_INFO_REQ) {
- VLOG_WARN_RL(&rl, "DHCPv6 option - IA NA not present in the "
+ /* A delegated prefix is only offered in reply to a Solicit or Request. */
+ if (hdr->msg_type != DHCPV6_MSG_TYPE_SOLICIT &&
+ hdr->msg_type != DHCPV6_MSG_TYPE_REQUEST) {
+ pd_requested = false;
+ }
+
+ if (!iaid && !pd_requested &&
+ hdr->msg_type != DHCPV6_MSG_TYPE_INFO_REQ) {
+ VLOG_WARN_RL(&rl, "DHCPv6 option - no IA_NA or IA_PD present in the "
"DHCPv6 packet");
goto exit;
}
@@ -3286,7 +3361,8 @@ pinctrl_handle_put_dhcpv6_opts(
compose = compose_dhcpv6_status(userdata, &out_dhcpv6_opts);
} else {
compose = compose_out_dhcpv6_opts(userdata, &out_dhcpv6_opts, iaid,
- ipxe_req, fqdn_flags);
+ ipxe_req, fqdn_flags, pd_iaid,
+ pd_requested);
}
if (!compose) {
diff --git a/lib/actions.c b/lib/actions.c
index 0367b330c..725674b0c 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -2694,6 +2694,20 @@ parse_gen_opt(struct action_context *ctx, struct
ovnact_gen_option *o,
return;
}
}
+
+ /* The ia_pd value is a single IPv6 prefix: its mask supplies the
+ * delegated prefix length on the wire, so reject a bare address (which
+ * would encode a meaningless /0) or a set of addresses. */
+ if (!strcmp(o->option->type, "ia_pd")) {
+ const struct expr_constant *c = vector_get_ptr(&o->value.values, 0);
+ if (vector_len(&o->value.values) != 1 ||
+ c->format != LEX_F_IPV6 || !c->masked) {
+ lexer_error(ctx->lexer, "%s option %s requires a single IPv6 "
+ "prefix value, e.g. aef0:1::/64.",
+ opts_type, o->option->name);
+ return;
+ }
+ }
}
static const struct ovnact_gen_option *
@@ -3221,6 +3235,12 @@ encode_put_dhcpv6_option(const struct ovnact_gen_option
*o,
ofpbuf_put(ofpacts, encoded, size);
free(encoded);
+ } else if (!strcmp(o->option->type, "ia_pd")) {
+ uint8_t plen = ipv6_count_cidr_bits(&c->mask.ipv6);
+ size = sizeof plen + sizeof c->value.ipv6;
+ opt->len = htons(size);
+ ofpbuf_put(ofpacts, &plen, sizeof plen);
+ ofpbuf_put(ofpacts, &c->value.ipv6, sizeof c->value.ipv6);
}
}
diff --git a/lib/ovn-l7.h b/lib/ovn-l7.h
index 5f52e1791..b23e3ca29 100644
--- a/lib/ovn-l7.h
+++ b/lib/ovn-l7.h
@@ -286,6 +286,9 @@ BUILD_ASSERT_DECL(DHCP_OPT_HEADER_LEN == sizeof(struct
dhcp_opt_header));
#define DHCPV6_OPT_IA_ADDR \
DHCP_OPTION("ia_addr", DHCPV6_OPT_IA_ADDR_CODE, "ipv6")
+#define DHCPV6_OPT_IA_PD_PREFIX \
+ DHCP_OPTION("ia_pd", DHCPV6_OPT_IA_PD, "ia_pd")
+
#define DHCPV6_OPT_DNS_SERVER \
DHCP_OPTION("dns_server", DHCPV6_OPT_DNS_SERVER_CODE, "ipv6")
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index c4c71ca81..e5ea2fdf7 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -284,6 +284,7 @@ static struct gen_opts_map supported_dhcp_opts[] = {
static struct gen_opts_map supported_dhcpv6_opts[] = {
DHCPV6_OPT_IA_ADDR,
+ DHCPV6_OPT_IA_PD_PREFIX,
DHCPV6_OPT_SERVER_ID,
DHCPV6_OPT_DOMAIN_SEARCH,
DHCPV6_OPT_DNS_SERVER,
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 15fb1d7e8..8622267e1 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -5918,6 +5918,39 @@ or
</p>
</column>
</group>
+
+ <group title="Prefix Delegation DHCPv6 options">
+ <p>
+ The option below makes OVN delegate an IPv6 prefix to a client that
+ requests one. OVN does not manage a prefix pool or any lease state;
+ the CMS supplies the prefix to delegate, and OVN emits it only when
+ the client solicits a prefix.
+ </p>
+
+ <column name="options" key="ia_pd">
+ <p>
+ The DHCPv6 option code for this option is 25, ``Identity
+ Association for Prefix Delegation'' (IA_PD), which carries an IA
+ Prefix (option 26). The value is the delegated prefix in CIDR
+ notation, e.g. <code>aef0:1::/64</code>. When the client requests
+ an IA_PD, OVN replies with this prefix and echoes the client's
+ IA_PD IAID. Please refer to RFC 8415 for more details.
+ </p>
+
+ <p>
+ A single prefix is delegated: only the first IA_PD a client
+ requests is answered. If this option is not set, no IA_PD is
+ returned even when the client requests one, the same way a missing
+ <code>ia_addr</code> is handled.
+ </p>
+
+ <p>
+ The logical port must also receive a DHCPv6-managed IPv6 address
+ (see <code>ia_addr</code>), which is what makes OVN intercept the
+ port's DHCPv6 traffic.
+ </p>
+ </column>
+ </group>
</group>
<group title="Common Columns">
diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
index 3d79a64b4..40bdf9b32 100644
--- a/ovn-sb.ovsschema
+++ b/ovn-sb.ovsschema
@@ -1,7 +1,7 @@
{
"name": "OVN_Southbound",
- "version": "21.10.0",
- "cksum": "2298996353 36780",
+ "version": "21.11.0",
+ "cksum": "2650041963 36830",
"tables": {
"SB_Global": {
"columns": {
@@ -324,7 +324,8 @@
"type": {
"type": {"key": {
"type": "string",
- "enum": ["set", ["ipv6", "str", "mac", "domain"]]}}}},
+ "enum": ["set", ["ipv6", "str", "mac", "domain",
+ "ia_pd"]]}}}},
"isRoot": true},
"Connection": {
"columns": {
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 0d3ec6c2c..4069acfc0 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -4440,6 +4440,24 @@ tcp.flags = RST;
put_dhcpv6_opts(..., server_id = 01:02:03:04L05:06,...)
</p>
</dd>
+
+ <dt><code>value: ia_pd</code></dt>
+ <dd>
+ <p>
+ This indicates that the value of the DHCPv6 option is a single IPv6
+ prefix to be delegated to the client via Prefix Delegation
+ (IA_PD/IA_PREFIX). The value must be a masked IPv6 address; the
+ mask supplies the delegated prefix length.
+ </p>
+
+ <p>
+ Example. "name=ia_pd", "code=25", "type=ia_pd".
+ </p>
+
+ <p>
+ put_dhcpv6_opts(..., ia_pd = aef0:1::/64,...)
+ </p>
+ </dd>
</dl>
</column>
</table>
diff --git a/tests/ovn.at b/tests/ovn.at
index 5dda50044..9ae023577 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1625,6 +1625,10 @@ put_nd(inport, nd.target, nd.sll);
# put_dhcpv6_opts
reg1[[0]] = put_dhcpv6_opts(ia_addr = ae70::4, server_id = 00:00:00:00:10:02);
encodes as
controller(userdata=00.00.00.05.00.00.00.00.00.01.de.10.00.00.00.40.00.05.00.10.ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.04.00.02.00.06.00.00.00.00.10.02,pause)
+reg1[[0]] = put_dhcpv6_opts(ia_pd = aef0:1::/64);
+ encodes as
controller(userdata=00.00.00.05.00.00.00.00.00.01.de.10.00.00.00.40.00.19.00.11.40.ae.f0.00.01.00.00.00.00.00.00.00.00.00.00.00.00,pause)
+reg1[[0]] = put_dhcpv6_opts(ia_addr = ae70::4, ia_pd = aef0:1::/64, server_id
= 00:00:00:00:10:02);
+ encodes as
controller(userdata=00.00.00.05.00.00.00.00.00.01.de.10.00.00.00.40.00.05.00.10.ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.04.00.19.00.11.40.ae.f0.00.01.00.00.00.00.00.00.00.00.00.00.00.00.00.02.00.06.00.00.00.00.10.02,pause)
reg1[[0]] = put_dhcpv6_opts();
encodes as
controller(userdata=00.00.00.05.00.00.00.00.00.01.de.10.00.00.00.40,pause)
reg1[[0]] = put_dhcpv6_opts(dns_server={ae70::1,ae70::2});
@@ -1645,6 +1649,14 @@ reg1[[0]] = put_dhcpv6_opts(ia_addr="ae70::4");
DHCPv6 option ia_addr requires numeric value.
reg1[[0]] = put_dhcpv6_opts(ia_addr=ae70::4, domain_search=ae70::1);
DHCPv6 option domain_search requires string value.
+reg1[[0]] = put_dhcpv6_opts(ia_pd = "aef0:1::/64");
+ DHCPv6 option ia_pd requires numeric value.
+reg1[[0]] = put_dhcpv6_opts(ia_pd = aef0:1::);
+ DHCPv6 option ia_pd requires a single IPv6 prefix value, e.g. aef0:1::/64.
+reg1[[0]] = put_dhcpv6_opts(ia_pd = {aef0:1::/64, aef0:2::/64});
+ DHCPv6 option ia_pd requires a single IPv6 prefix value, e.g. aef0:1::/64.
+reg1[[0]] = put_dhcpv6_opts(ia_pd = 1.2.3.4/24);
+ DHCPv6 option ia_pd requires a single IPv6 prefix value, e.g. aef0:1::/64.
reg1[[0]] = put_dhcpv6_opts(bootfile_name="https://127.0.0.1/boot.ipxe");
formats as reg1[[0]] = put_dhcpv6_opts(bootfile_name =
"https://127.0.0.1/boot.ipxe");
encodes as
controller(userdata=00.00.00.05.00.00.00.00.00.01.de.10.00.00.00.40.00.3b.00.1b.68.74.74.70.73.3a.2f.2f.31.32.37.2e.30.2e.30.2e.31.2f.62.6f.6f.74.2e.69.70.78.65,pause)
@@ -7623,7 +7635,7 @@ check ovn-nbctl lsp-add ls1 ls1-lp3 \
check ovn-nbctl lsp-set-port-security ls1-lp3 "f0:00:00:00:00:22 ae70::22"
d1="$(ovn-nbctl create DHCP_Options cidr="ae70\:\:/64" \
-options="\"server_id\"=\"00:00:00:10:00:01\"")"
+options="\"server_id\"=\"00:00:00:10:00:01\" \"ia_pd\"=\"aef0:1::/64\"")"
check ovn-nbctl lsp-set-dhcpv6-options ls1-lp1 ${d1}
check ovn-nbctl lsp-set-dhcpv6-options ls1-lp2 ${d1}
@@ -7783,6 +7795,51 @@ test_dhcpv6_release() {
n_resume=$((n_resume + 1))
}
+# This shell function sends a DHCPv6 request that carries an IA_PD option and
+# checks that the reply delegates a prefix in an IA_PD -> IA_PREFIX. The
+# request's IA_PD uses a distinct IAID so the reply must echo the IA_PD IAID,
+# not the IA_NA one.
+# test_dhcpv6_pd INPORT SRC_MAC SRC_LLA DHCPv6_MSG_TYPE OFFER_IP PD_PREFIX
PD_PLEN
+test_dhcpv6_pd() {
+ local inport=$1 src_mac=$2 src_lla=$3 msg_code=$4 offer_ip=$5
+ local pd_prefix=$6 pd_plen=$7
+
+ local req_scapy="Ether(dst='33:33:00:01:00:02', src='${src_mac}')/ \
+ IPv6(dst='ff02::1:2', src='${src_lla}')/ \
+ UDP(sport=546, dport=547)/ \
+ DHCP6(msgtype=${msg_code}, trid=0x010203)/ \
+ DHCP6OptClientId(duid=DUID_LL(lladdr='${src_mac}'))/ \
+ DHCP6OptIA_NA(iaid=0x01020304, T1=3600, T2=5400)/ \
+ DHCP6OptIA_PD(iaid=0x0a0b0c0d, T1=3600, T2=5400)"
+ request=$(fmt_pkt "${req_scapy}")
+
+ local reply_code=7
+ if test $msg_code = 1; then
+ reply_code=2
+ fi
+
+ local ip_scapy="DHCP6OptIAAddress(addr='${offer_ip}', \
+ preflft=0xffffffff, validlft=0xffffffff)"
+ local pd_scapy="DHCP6OptIAPrefix(prefix='${pd_prefix}', plen=${pd_plen}, \
+ preflft=0xffffffff, validlft=0xffffffff)"
+ local rep_scapy="Ether(dst='${src_mac}', src='00:00:00:10:00:01')/ \
+ IPv6(dst='${src_lla}', src='fe80::0200:00ff:fe10:0001')/ \
+ UDP(sport=547, dport=546)/ \
+ DHCP6(msgtype=${reply_code}, trid=0x010203)/ \
+ DHCP6OptClientId(duid=DUID_LL(lladdr='${src_mac}'))/ \
+ DHCP6OptIA_NA(iaid=0x01020304, T1=0xffffffff, \
+ T2=0xffffffff, ianaopts=[[${ip_scapy}]])/ \
+ DHCP6OptIA_PD(iaid=0x0a0b0c0d, T1=0xffffffff, \
+ T2=0xffffffff, iapdopt=[[${pd_scapy}]])/ \
+
DHCP6OptServerId(duid=DUID_LL(lladdr='00:00:00:10:00:01'))"
+ reply=$(fmt_pkt "${rep_scapy}")
+ echo $reply | trim_zeros >> $inport.expected
+
+ as hv1 ovs-appctl netdev-dummy/receive hv1-vif$inport $request
+ OVS_WAIT_UNTIL([test $n_resume = `cat ofctl_monitor*.log | grep -c
NXT_RESUME`])
+ n_resume=$((n_resume + 1))
+}
+
check_packets() {
local port=$1
@@ -7845,6 +7902,15 @@ reset_pcap_file hv1-vif1 hv1/vif1
test_dhcpv6_release 1 $src_mac $src_lla $offer_ip
check_packets 1
+# Request a delegated prefix on ls1-lp2. d1 carries an ia_pd option, so the
+# reply must include an IA_PD -> IA_PREFIX with the delegated prefix and the
+# client's IA_PD IAID. The IA_NA-only request on ls1-lp1 above must NOT get an
+# IA_PD, which exercises the request-aware path.
+reset_pcap_file hv1-vif2 hv1/vif2
+test_dhcpv6_pd 2 f0:00:00:00:00:02 fe80::f200:00ff:fe00:0002 1 ae70::5
aef0:1:: 64
+check_packets 2
+reset_pcap_file hv1-vif2 hv1/vif2
+
# Send invalid packet on ls1-lp2. ovn-controller should resume the packet
# without any modifications and the packet should be received by ls1-lp1.
# ls1-lp1 will receive the packet twice, one from the ovn-controller after the
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index 51471750b..460035b7f 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -203,6 +203,7 @@ create_gen_opts(struct hmap *dhcp_opts, struct hmap
*dhcpv6_opts,
hmap_init(dhcpv6_opts);
dhcp_opt_add(dhcpv6_opts, "server_id", 2, "mac");
dhcp_opt_add(dhcpv6_opts, "ia_addr", 5, "ipv6");
+ dhcp_opt_add(dhcpv6_opts, "ia_pd", 25, "ia_pd");
dhcp_opt_add(dhcpv6_opts, "dns_server", 23, "ipv6");
dhcp_opt_add(dhcpv6_opts, "domain_search", 24, "str");
dhcp_opt_add(dhcpv6_opts, "bootfile_name", 59, "str");
--
2.50.1 (Apple Git-155)
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev