diff mbox series

[ovs-dev,v2] northd, controller: Add support for DHCPv6 FQDN option

Message ID 20230606065102.51899-1-amusil@redhat.com
State Accepted
Headers show
Series [ovs-dev,v2] northd, controller: Add support for DHCPv6 FQDN option | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/github-robot-_ovn-kubernetes fail github build: failed

Commit Message

Ales Musil June 6, 2023, 6:51 a.m. UTC
Add support for FQDN option (39), if specified the server
can overwrite FQDN for the client. It behaves similarly
to DHCP hostname option (12).

Reported-at: https://bugzilla.redhat.com/2211890
Signed-off-by: Ales Musil <amusil@redhat.com>
---
v2: Rebase on top of current main.
    Address comments from Mark:
    - Properly propagate option flags.
    - Extract the fqdn encoding to common function.
---
 NEWS                 |   2 +
 controller/pinctrl.c |  69 +++++++++----------
 lib/actions.c        |   9 ++-
 lib/ovn-l7.h         |  10 +++
 lib/ovn-util.c       |  27 ++++++++
 lib/ovn-util.h       |   5 ++
 northd/ovn-northd.c  |   3 +-
 ovn-nb.xml           |   7 ++
 ovn-sb.ovsschema     |   6 +-
 tests/ovn.at         | 155 ++++++++++++++++++++++++-------------------
 tests/test-ovn.c     |   1 +
 11 files changed, 181 insertions(+), 113 deletions(-)

Comments

Mark Michelson June 9, 2023, 7:15 p.m. UTC | #1
Thanks for the changes Ales.

Acked-by: Mark Michelson <mmichels@redhat.com>

On 6/6/23 02:51, Ales Musil wrote:
> Add support for FQDN option (39), if specified the server
> can overwrite FQDN for the client. It behaves similarly
> to DHCP hostname option (12).
> 
> Reported-at: https://bugzilla.redhat.com/2211890
> Signed-off-by: Ales Musil <amusil@redhat.com>
> ---
> v2: Rebase on top of current main.
>      Address comments from Mark:
>      - Properly propagate option flags.
>      - Extract the fqdn encoding to common function.
> ---
>   NEWS                 |   2 +
>   controller/pinctrl.c |  69 +++++++++----------
>   lib/actions.c        |   9 ++-
>   lib/ovn-l7.h         |  10 +++
>   lib/ovn-util.c       |  27 ++++++++
>   lib/ovn-util.h       |   5 ++
>   northd/ovn-northd.c  |   3 +-
>   ovn-nb.xml           |   7 ++
>   ovn-sb.ovsschema     |   6 +-
>   tests/ovn.at         | 155 ++++++++++++++++++++++++-------------------
>   tests/test-ovn.c     |   1 +
>   11 files changed, 181 insertions(+), 113 deletions(-)
> 
> diff --git a/NEWS b/NEWS
> index 645acea1f..e6c87e9ad 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -1,5 +1,7 @@
>   Post v23.06.0
>   -------------
> +  - Add DHCPv6 "fqdn" (39) option, that works similarly to
> +    DHCPv4 "hostname" (12) option.
>   
>   OVN v23.06.0 - 01 Jun 2023
>   --------------------------
> diff --git a/controller/pinctrl.c b/controller/pinctrl.c
> index c396ad4c2..d7bd20ed6 100644
> --- a/controller/pinctrl.c
> +++ b/controller/pinctrl.c
> @@ -2288,7 +2288,7 @@ exit:
>   static bool
>   compose_out_dhcpv6_opts(struct ofpbuf *userdata,
>                           struct ofpbuf *out_dhcpv6_opts,
> -                        ovs_be32 iaid, bool ipxe_req)
> +                        ovs_be32 iaid, bool ipxe_req, uint8_t fqdn_flags)
>   {
>       while (userdata->size) {
>           struct dhcpv6_opt_header *userdata_opt = ofpbuf_try_pull(
> @@ -2412,6 +2412,24 @@ compose_out_dhcpv6_opts(struct ofpbuf *userdata,
>               break;
>           }
>   
> +        case DHCPV6_OPT_FQDN_CODE: {
> +            if (fqdn_flags != DHCPV6_FQDN_FLAGS_UNDEFINED) {
> +                struct dhcpv6_opt_header *header =
> +                        ofpbuf_put_zeros(out_dhcpv6_opts, sizeof *header);
> +                header->code = htons(DHCPV6_OPT_FQDN_CODE);
> +                header->len = htons(size + 1);
> +                uint8_t *flags = ofpbuf_put_zeros(out_dhcpv6_opts, 1);
> +                /* Always set N to 1, if client requested S inform him that it
> +                 * was overwritten by the server. */
> +                *flags |= DHCPV6_FQDN_FLAGS_N;
> +                if (fqdn_flags & DHCPV6_FQDN_FLAGS_S) {
> +                    *flags |= DHCPV6_FQDN_FLAGS_O;
> +                }
> +                ofpbuf_put(out_dhcpv6_opts, userdata_opt_data, size);
> +            }
> +            break;
> +        }
> +
>           default:
>               return false;
>           }
> @@ -2500,6 +2518,7 @@ pinctrl_handle_put_dhcpv6_opts(
>       size_t l4_len = dp_packet_l4_size(pkt_in);
>       uint8_t *end = (uint8_t *)in_udp + MIN(udp_len, l4_len);
>       bool ipxe_req = false;
> +    uint8_t fqdn_flags = DHCPV6_FQDN_FLAGS_UNDEFINED;
>       while (in_dhcpv6_data < end) {
>           struct dhcpv6_opt_header const *in_opt =
>                (struct dhcpv6_opt_header *)in_dhcpv6_data;
> @@ -2524,6 +2543,10 @@ pinctrl_handle_put_dhcpv6_opts(
>               break;
>           }
>   
> +        case DHCPV6_OPT_FQDN_CODE:
> +            fqdn_flags = *(in_dhcpv6_data + sizeof *in_opt);
> +            break;
> +
>           default:
>               break;
>           }
> @@ -2547,7 +2570,7 @@ pinctrl_handle_put_dhcpv6_opts(
>           OFPBUF_STUB_INITIALIZER(out_ofpacts_dhcpv6_opts_stub);
>   
>       if (!compose_out_dhcpv6_opts(userdata, &out_dhcpv6_opts,
> -                                 iaid, ipxe_req)) {
> +                                 iaid, ipxe_req, fqdn_flags)) {
>           VLOG_WARN_RL(&rl, "Invalid userdata");
>           goto exit;
>       }
> @@ -2762,48 +2785,16 @@ dns_build_ptr_answer(
>       struct ofpbuf *dns_answer, const uint8_t *in_queryname,
>       uint16_t query_length, const char *answer_data)
>   {
> -    char *encoded_answer;
> -    uint16_t encoded_answer_length;
> -
>       dns_build_base_answer(dns_answer, in_queryname, query_length,
>                             DNS_QUERY_TYPE_PTR);
>   
> -    /* Initialize string 2 chars longer than real answer:
> -     * first label length and terminating zero-length label.
> -     * If the answer_data is - vm1tst.ovn.org, it will be encoded as
> -     *  - 0010 (Total length which is 16)
> -     *  - 06766d31747374 (vm1tst)
> -     *  - 036f766e (ovn)
> -     *  - 036f7267 (org
> -     *  - 00 (zero length field) */
> -    encoded_answer_length = strlen(answer_data) + 2;
> -    encoded_answer = (char *)xzalloc(encoded_answer_length);
> -
> -    put_be16(dns_answer, htons(encoded_answer_length));
> -    uint8_t label_len_index = 0;
> -    uint16_t label_len = 0;
> -    char *encoded_answer_ptr = (char *)encoded_answer + 1;
> -    while (*answer_data) {
> -        if (*answer_data == '.') {
> -            /* Label has ended.  Update the length of the label. */
> -            encoded_answer[label_len_index] = label_len;
> -            label_len_index += (label_len + 1);
> -            label_len = 0; /* Init to 0 for the next label. */
> -        } else {
> -            *encoded_answer_ptr =  *answer_data;
> -            label_len++;
> -        }
> -        encoded_answer_ptr++;
> -        answer_data++;
> -    }
> +    size_t encoded_len = 0;
> +    char *encoded = encode_fqdn_string(answer_data, &encoded_len);
>   
> -    /* This is required for the last label if it doesn't end with '.' */
> -    if (label_len) {
> -        encoded_answer[label_len_index] = label_len;
> -    }
> +    put_be16(dns_answer, htons(encoded_len));
> +    ofpbuf_put(dns_answer, encoded, encoded_len);
>   
> -    ofpbuf_put(dns_answer, encoded_answer, encoded_answer_length);
> -    free(encoded_answer);
> +    free(encoded);
>   }
>   
>   /* Called with in the pinctrl_handler thread context. */
> diff --git a/lib/actions.c b/lib/actions.c
> index ec27223f9..037172e60 100644
> --- a/lib/actions.c
> +++ b/lib/actions.c
> @@ -2470,7 +2470,8 @@ parse_gen_opt(struct action_context *ctx, struct ovnact_gen_option *o,
>       }
>   
>       if (!strcmp(o->option->type, "str") ||
> -        !strcmp(o->option->type, "domains")) {
> +        !strcmp(o->option->type, "domains") ||
> +        !strcmp(o->option->type, "domain")) {
>           if (o->value.type != EXPR_C_STRING) {
>               lexer_error(ctx->lexer, "%s option %s requires string value.",
>                           opts_type, o->option->name);
> @@ -2903,6 +2904,12 @@ encode_put_dhcpv6_option(const struct ovnact_gen_option *o,
>           size = strlen(c->string);
>           opt->len = htons(size);
>           ofpbuf_put(ofpacts, c->string, size);
> +    } else if (!strcmp(o->option->type, "domain")) {
> +        char *encoded = encode_fqdn_string(c->string, &size);
> +        opt->len = htons(size);
> +        ofpbuf_put(ofpacts, encoded, size);
> +
> +        free(encoded);
>       }
>   }
>   
> diff --git a/lib/ovn-l7.h b/lib/ovn-l7.h
> index d9b103119..9dc331421 100644
> --- a/lib/ovn-l7.h
> +++ b/lib/ovn-l7.h
> @@ -270,6 +270,7 @@ BUILD_ASSERT_DECL(DHCP_OPT_HEADER_LEN == sizeof(struct dhcp_opt_header));
>   #define DHCPV6_OPT_DOMAIN_SEARCH_CODE    24
>   #define DHCPV6_OPT_IA_PD                 25
>   #define DHCPV6_OPT_IA_PREFIX             26
> +#define DHCPV6_OPT_FQDN_CODE             39
>   #define DHCPV6_OPT_BOOT_FILE_URL         59
>   #define DHCPV6_OPT_BOOT_FILE_URL_ALT    254
>   
> @@ -291,6 +292,15 @@ BUILD_ASSERT_DECL(DHCP_OPT_HEADER_LEN == sizeof(struct dhcp_opt_header));
>   #define DHCPV6_OPT_BOOTFILE_NAME_ALT \
>       DHCP_OPTION("bootfile_name_alt", DHCPV6_OPT_BOOT_FILE_URL_ALT, "str")
>   
> +#define DHCPV6_OPT_FQDN \
> +    DHCP_OPTION("fqdn", DHCPV6_OPT_FQDN_CODE, "domain")
> +
> +/* DHCPv6 FQDN flags. RFC 4704 */
> +#define DHCPV6_FQDN_FLAGS_UNDEFINED 0xff
> +#define DHCPV6_FQDN_FLAGS_S 1 << 0
> +#define DHCPV6_FQDN_FLAGS_O 1 << 1
> +#define DHCPV6_FQDN_FLAGS_N 1 << 2
> +
>   OVS_PACKED(
>   struct dhcpv6_opt_header {
>       ovs_be16 code;
> diff --git a/lib/ovn-util.c b/lib/ovn-util.c
> index bffb521cf..12ca27319 100644
> --- a/lib/ovn-util.c
> +++ b/lib/ovn-util.c
> @@ -1126,3 +1126,30 @@ void flow_collector_ids_clear(struct flow_collector_ids *ids)
>       flow_collector_ids_destroy(ids);
>       flow_collector_ids_init(ids);
>   }
> +
> +char *
> +encode_fqdn_string(const char *fqdn, size_t *len)
> +{
> +
> +    size_t domain_len = strlen(fqdn);
> +    *len = domain_len + 2;
> +    char *encoded = xzalloc(*len);
> +
> +    int8_t label_len = 0;
> +    for (size_t i = 0; i < domain_len; i++) {
> +        if (fqdn[i] == '.') {
> +            encoded[i - label_len] = label_len;
> +            label_len = 0;
> +        } else {
> +            encoded[i + 1] = fqdn[i];
> +            label_len++;
> +        }
> +    }
> +
> +    /* This is required for the last label if it doesn't end with '.' */
> +    if (label_len) {
> +        encoded[domain_len - label_len] = label_len;
> +    }
> +
> +    return encoded;
> +}
> diff --git a/lib/ovn-util.h b/lib/ovn-util.h
> index b17b0e236..0af940918 100644
> --- a/lib/ovn-util.h
> +++ b/lib/ovn-util.h
> @@ -403,4 +403,9 @@ bool flow_collector_ids_lookup(const struct flow_collector_ids *, uint32_t);
>   void flow_collector_ids_destroy(struct flow_collector_ids *);
>   void flow_collector_ids_clear(struct flow_collector_ids *);
>   
> +/* The DNS format is 2 bytes longer than the "domain".
> + * It replaces every '.' with len of the next name.
> + * The returned pointer has to be freed by caller. */
> +char *encode_fqdn_string(const char *fqdn, size_t *len);
> +
>   #endif /* OVN_UTIL_H */
> diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
> index 3515b68a2..2bafbd4af 100644
> --- a/northd/ovn-northd.c
> +++ b/northd/ovn-northd.c
> @@ -273,7 +273,8 @@ static struct gen_opts_map supported_dhcpv6_opts[] = {
>       DHCPV6_OPT_DOMAIN_SEARCH,
>       DHCPV6_OPT_DNS_SERVER,
>       DHCPV6_OPT_BOOTFILE_NAME,
> -    DHCPV6_OPT_BOOTFILE_NAME_ALT
> +    DHCPV6_OPT_BOOTFILE_NAME_ALT,
> +    DHCPV6_OPT_FQDN,
>   };
>   
>   static bool
> diff --git a/ovn-nb.xml b/ovn-nb.xml
> index 9afe3b584..05e155b89 100644
> --- a/ovn-nb.xml
> +++ b/ovn-nb.xml
> @@ -4191,6 +4191,13 @@ or
>               way. Default value for this option is false.
>             </p>
>           </column>
> +
> +        <column name="options" key="fqdn">
> +          <p>
> +            The DHCPv6 option code for this option is 39.
> +            If set, indicates the DHCPv6 option "FQDN".
> +          </p>
> +        </column>
>         </group>
>       </group>
>   
> diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
> index f59af8cc5..06e16b403 100644
> --- a/ovn-sb.ovsschema
> +++ b/ovn-sb.ovsschema
> @@ -1,7 +1,7 @@
>   {
>       "name": "OVN_Southbound",
> -    "version": "20.27.2",
> -    "cksum": "1291808617 30462",
> +    "version": "20.27.3",
> +    "cksum": "3876528905 30472",
>       "tables": {
>           "SB_Global": {
>               "columns": {
> @@ -311,7 +311,7 @@
>                   "type": {
>                       "type": {"key": {
>                           "type": "string",
> -                        "enum": ["set", ["ipv6", "str", "mac"]]}}}},
> +                        "enum": ["set", ["ipv6", "str", "mac", "domain"]]}}}},
>               "isRoot": true},
>           "Connection": {
>               "columns": {
> diff --git a/tests/ovn.at b/tests/ovn.at
> index 5e6a8fefa..2b8ed7571 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -1697,6 +1697,9 @@ reg1[0] = put_dhcpv6_opts(bootfile_name="https://127.0.0.1/boot.ipxe");
>   reg1[0] = put_dhcpv6_opts(bootfile_name_alt="https://127.0.0.1/boot.ipxe");
>       formats as reg1[0] = put_dhcpv6_opts(bootfile_name_alt = "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.fe.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)
> +reg1[0] = put_dhcpv6_opts(fqdn="ovn.org");
> +    formats as reg1[0] = put_dhcpv6_opts(fqdn = "ovn.org");
> +    encodes as controller(userdata=00.00.00.05.00.00.00.00.00.01.de.10.00.00.00.40.00.27.00.09.03.6f.76.6e.03.6f.72.67.00,pause)
>   
>   # lookup_nd
>   reg2[0] = lookup_nd(inport, ip6.dst, eth.src);
> @@ -7083,6 +7086,7 @@ AT_CLEANUP
>   
>   OVN_FOR_EACH_NORTHD([
>   AT_SETUP([dhcpv6 : 1 HV, 2 LS, 5 LSPs])
> +AT_SKIP_IF([test $HAVE_SCAPY = no])
>   ovn_start
>   
>   ovn-nbctl ls-add ls1
> @@ -7171,61 +7175,56 @@ trim_zeros() {
>   # packet should be received twice (one from ovn-controller and the other
>   # from the "ovs-ofctl monitor br-int resume"
>   test_dhcpv6() {
> -    local inport=$1 src_mac=$2 src_lla=$3 msg_code=$4 offer_ip=$5 ipxe=$6
> -    if test $ipxe -eq 2; then
> -        req_len=34
> -    elif test $msg_code != 0b; then
> -        req_len=2a
> -    else
> -        req_len=1a
> -    fi
> -    local request=ffffffffffff${src_mac}86dd6000000000${req_len}1101${src_lla}
> -    # dst ip ff02::1:2
> -    request=${request}ff020000000000000000000000010002
> -    # udp header and dhcpv6 header
> -    request=${request}0222022300${req_len}ffff${msg_code}010203
> -    # Client identifier
> -    request=${request}0001000a00030001${src_mac}
> +    local inport=$1 src_mac=$2 src_lla=$3 msg_code=$4 offer_ip=$5 boot_file=$6
> +    local fqdn=$7
> +
> +    local req_scapy="Ether(dst='ff:ff:ff:ff:ff:ff', 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}'))"
> +
>       # Add IA-NA (Identity Association for Non Temporary Address) if msg_code
>       # is not 11 (information request packet)
> -    if test $msg_code != 0b; then
> -        request=${request}0003000c0102030400000e1000001518
> +    if test $msg_code != 11; then
> +        req_scapy="${req_scapy}/DHCP6OptIA_NA(iaid=0x01020304, T1=3600, T2=5400)"
>       fi
> -    if test $ipxe -eq 2; then
> -        request=${request}000f0006000669505845
> +    if test "$boot_file" = "bootfile_name"; then
> +        local uc_data="USER_CLASS_DATA(data=b'iPXE')"
> +        req_scapy="${req_scapy}/DHCP6OptUserClass(userclassdata=[[${uc_data}]])"
>       fi
> -    shift; shift; shift; shift; shift; shift;
> +    if test -n "$fqdn"; then
> +        req_scapy="${req_scapy}/DHCP6OptClientFQDN(flags=0x01, fqdn=b'test')"
> +    fi
> +    request=$(fmt_pkt "${req_scapy}")
> +    shift; shift; shift; shift; shift; shift; shift;
>       if test $offer_ip != 0; then
> -        local server_mac=000000100001
> -        local server_lla=fe80000000000000020000fffe100001
> -        local reply_code=07
> -        if test $msg_code = 01; then
> -            reply_code=02
> -        fi
> -        local msg_len=54
> -        if test $ipxe -eq 1; then
> -            msg_len=69
> -        elif test $ipxe -eq 2; then
> -            msg_len=65
> -        elif test $offer_ip = 1; then
> -            msg_len=28
> +        local reply_code=7
> +        if test $msg_code = 1; then
> +            reply_code=2
>           fi
> -        local reply=${src_mac}${server_mac}86dd6000000000${msg_len}1101${server_lla}${src_lla}
> -        # udp header and dhcpv6 header
> -        reply=${reply}0223022200${msg_len}ffff${reply_code}010203
> -        # Client identifier
> -        reply=${reply}0001000a00030001${src_mac}
> -        # IA-NA
> +
> +        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}'))"
> +
>           if test $offer_ip != 1; then
> -            reply=${reply}0003002801020304ffffffffffffffff00050018${offer_ip}ffffffffffffffff
> +            local ip_scapy="DHCP6OptIAAddress(addr='${offer_ip}', \
> +                            preflft=0xffffffff, validlft=0xffffffff)"
> +            rep_scapy="${rep_scapy}/ \
> +                       DHCP6OptIA_NA(iaid=0x01020304, T1=0xffffffff, \
> +                       T2=0xffffffff, ianaopts=[[${ip_scapy}]])"
>           fi
> -        if test $ipxe -eq 1; then
> -            reply=${reply}003b0011626f6f7466696c655f6e616d655f616c74
> -        elif test $ipxe -eq 2; then
> -            reply=${reply}003b000d626f6f7466696c655f6e616d65
> +        if test -n "$boot_file"; then
> +            rep_scapy="${rep_scapy}/DHCP6OptBootFileUrl(optdata=b'${boot_file}')"
>           fi
> -        # Server identifier
> -        reply=${reply}0002000a00030001${server_mac}
> +        if test -n "$fqdn"; then
> +            rep_scapy="${rep_scapy}/DHCP6OptClientFQDN(flags=0x06, fqdn=b'${fqdn}')"
> +        fi
> +        rep_scapy="${rep_scapy}/DHCP6OptServerId(duid=DUID_LL(lladdr='00:00:00:10:00:01'))"
> +        reply=$(fmt_pkt "${rep_scapy}")
>           echo $reply | trim_zeros >> $inport.expected
>       else
>           for outport; do
> @@ -7249,16 +7248,16 @@ echo "---------------------"
>   ovn-sbctl list logical_flow
>   echo "---------------------"
>   
> -ovn-sbctl dump-flows > sbflows
> +#ovn-sbctl dump-flows > sbflows
>   AT_CAPTURE_FILE([sbflows])
>   
>   echo "------ hv1 dump ----------"
>   as hv1 ovs-ofctl dump-flows br-int
>   
> -src_mac=f00000000001
> -src_lla=fe80000000000000f20000fffe000001
> -offer_ip=ae700000000000000000000000000004
> -test_dhcpv6 1 $src_mac $src_lla 01 $offer_ip 0
> +src_mac="f0:00:00:00:00:01"
> +src_lla="fe80::f200:00ff:fe00:0001"
> +offer_ip="ae70::4"
> +test_dhcpv6 1 $src_mac $src_lla 1 $offer_ip "" ""
>   
>   # NXT_RESUMEs should be 1.
>   OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
> @@ -7281,12 +7280,12 @@ rm  1.expected
>   reset_pcap_file hv1-vif1 hv1/vif1
>   reset_pcap_file hv1-vif2 hv1/vif2
>   
> -src_mac=f00000000002
> -src_lla=fe80000000000000f20000fffe000002
> -offer_ip=ae700000000000000000000000000005
> +src_mac="f0:00:00:00:00:02"
> +src_lla="fe80::f200:00ff:fe00:0002"
> +offer_ip="ae70::5"
>   # Set invalid msg_type
>   
> -test_dhcpv6 2 $src_mac $src_lla 10 0 0 1 1
> +test_dhcpv6 2 $src_mac $src_lla 16 0 "" "" 1 1
>   
>   # NXT_RESUMEs should be 2.
>   OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
> @@ -7305,9 +7304,9 @@ AT_CHECK([cat 1.packets], [0], [expout])
>   # There should be no DHCPv6 reply from ovn-controller and the request packet
>   # should be received by ls2-lp2.
>   
> -src_mac=f00000000003
> -src_lla=fe80000000000000f20000fffe000003
> -test_dhcpv6 3 $src_mac $src_lla 01 0 0 4
> +src_mac="f0:00:00:00:00:03"
> +src_lla="fe80::f200:00ff:fe00:0003"
> +test_dhcpv6 3 $src_mac $src_lla 1 0 "" "" 4
>   
>   # NXT_RESUMEs should be 2 only.
>   OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
> @@ -7323,10 +7322,10 @@ AT_CHECK([cat 4.packets], [0], [expout])
>   
>   # Send DHCPv6 packet on ls1-lp3. native DHCPv6 works as stateless mode for this port.
>   # The DHCPv6 reply shouldn't contain offer_ip.
> -src_mac=f00000000022
> -src_lla=fe80000000000000f20000fffe000022
> +src_mac="f0:00:00:00:00:22"
> +src_lla="fe80::f200:00ff:fe00:0022"
>   reset_pcap_file hv1-vif5 hv1/vif5
> -test_dhcpv6 5 $src_mac $src_lla 01 1 0 5
> +test_dhcpv6 5 $src_mac $src_lla 1 1 "" "" 5
>   
>   # NXT_RESUMEs should be 3.
>   OVS_WAIT_UNTIL([test 3 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
> @@ -7338,11 +7337,11 @@ AT_CHECK([cat 5.packets | cut -c 1-120,125- ], [0], [expout])
>   
>   # Send DHCPv6 information request (code 11) on ls1-lp3. The DHCPv6 reply
>   # shouldn't contain offer_ip
> -src_mac=f00000000022
> -src_lla=fe80000000000000f20000fffe000022
> +src_mac="f0:00:00:00:00:22"
> +src_lla="fe80::f200:00ff:fe00:0022"
>   reset_pcap_file hv1-vif5 hv1/vif5
>   rm -f 5.expected
> -test_dhcpv6 5 $src_mac $src_lla 0b 1 0 5
> +test_dhcpv6 5 $src_mac $src_lla 11 1 "" "" 5
>   
>   # NXT_RESUMEs should be 4.
>   OVS_WAIT_UNTIL([test 4 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
> @@ -7363,11 +7362,11 @@ ovn-nbctl lsp-set-dhcpv6-options ls1-lp2 ${d1}
>   
>   reset_pcap_file hv1-vif2 hv1/vif2
>   
> -src_mac=f00000000002
> -src_lla=fe80000000000000f20000fffe000002
> -offer_ip=ae700000000000000000000000000005
> +src_mac="f0:00:00:00:00:02"
> +src_lla="fe80::f200:00ff:fe00:0002"
> +offer_ip="ae70::5"
>   
> -test_dhcpv6 2 $src_mac $src_lla 01 $offer_ip 1
> +test_dhcpv6 2 $src_mac $src_lla 1 $offer_ip "bootfile_name_alt" ""
>   # NXT_RESUMEs should be 5.
>   OVS_WAIT_UNTIL([test 5 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
>   
> @@ -7381,7 +7380,7 @@ AT_CHECK([cat 2.packets | cut -c 1-120,125- ], [0], [expout])
>   reset_pcap_file hv1-vif2 hv1/vif2
>   rm 2.packets 2.expected
>   
> -test_dhcpv6 2 $src_mac $src_lla 01 $offer_ip 2
> +test_dhcpv6 2 $src_mac $src_lla 1 $offer_ip "bootfile_name" ""
>   # NXT_RESUMEs should be 6.
>   OVS_WAIT_UNTIL([test 6 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
>   
> @@ -7389,6 +7388,24 @@ $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap | trim_zeros > 2.pa
>   cat 2.expected | cut -c 1-120,125- > expout
>   AT_CHECK([cat 2.packets | cut -c 1-120,125- ], [0], [expout])
>   
> +ovn-nbctl --all destroy dhcp-option
> +d1="$(ovn-nbctl create DHCP_Options cidr="ae70\:\:/64")"
> +ovn-nbctl dhcp-options-set-options $d1 \
> +    server_id=00:00:00:10:00:01 \
> +    fqdn=\"ovn.org\"
> +ovn-nbctl --wait=hv lsp-set-dhcpv6-options ls1-lp2 ${d1}
> +
> +reset_pcap_file hv1-vif2 hv1/vif2
> +rm 2.packets 2.expected
> +
> +test_dhcpv6 2 $src_mac $src_lla 1 $offer_ip "" "ovn.org"
> +# NXT_RESUMEs should be 7.
> +OVS_WAIT_UNTIL([test 7 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
> +
> +$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap | trim_zeros > 2.packets
> +cat 2.expected | cut -c 1-120,125- > expout
> +AT_CHECK([cat 2.packets | cut -c 1-120,125- ], [0], [expout])
> +
>   OVN_CLEANUP([hv1])
>   
>   AT_CLEANUP
> diff --git a/tests/test-ovn.c b/tests/test-ovn.c
> index ce9213c1d..6c7754eac 100644
> --- a/tests/test-ovn.c
> +++ b/tests/test-ovn.c
> @@ -207,6 +207,7 @@ create_gen_opts(struct hmap *dhcp_opts, struct hmap *dhcpv6_opts,
>       dhcp_opt_add(dhcpv6_opts, "domain_search",  24, "str");
>       dhcp_opt_add(dhcpv6_opts, "bootfile_name", 59, "str");
>       dhcp_opt_add(dhcpv6_opts, "bootfile_name_alt", 254, "str");
> +    dhcp_opt_add(dhcpv6_opts, "fqdn", 39, "domain");
>   
>       /* IPv6 ND RA options. */
>       hmap_init(nd_ra_opts);
Mark Michelson June 12, 2023, 5:23 p.m. UTC | #2
Because the FDB aging series went in, there was a small merge conflict 
when trying to apply this patch. I updated the ovn-sb.xml version to 
"20.27.4" instead of "20.27.3".

I applied this patch to main. Thanks Ales!

On 6/9/23 15:15, Mark Michelson wrote:
> Thanks for the changes Ales.
> 
> Acked-by: Mark Michelson <mmichels@redhat.com>
> 
> On 6/6/23 02:51, Ales Musil wrote:
>> Add support for FQDN option (39), if specified the server
>> can overwrite FQDN for the client. It behaves similarly
>> to DHCP hostname option (12).
>>
>> Reported-at: https://bugzilla.redhat.com/2211890
>> Signed-off-by: Ales Musil <amusil@redhat.com>
>> ---
>> v2: Rebase on top of current main.
>>      Address comments from Mark:
>>      - Properly propagate option flags.
>>      - Extract the fqdn encoding to common function.
>> ---
>>   NEWS                 |   2 +
>>   controller/pinctrl.c |  69 +++++++++----------
>>   lib/actions.c        |   9 ++-
>>   lib/ovn-l7.h         |  10 +++
>>   lib/ovn-util.c       |  27 ++++++++
>>   lib/ovn-util.h       |   5 ++
>>   northd/ovn-northd.c  |   3 +-
>>   ovn-nb.xml           |   7 ++
>>   ovn-sb.ovsschema     |   6 +-
>>   tests/ovn.at         | 155 ++++++++++++++++++++++++-------------------
>>   tests/test-ovn.c     |   1 +
>>   11 files changed, 181 insertions(+), 113 deletions(-)
>>
>> diff --git a/NEWS b/NEWS
>> index 645acea1f..e6c87e9ad 100644
>> --- a/NEWS
>> +++ b/NEWS
>> @@ -1,5 +1,7 @@
>>   Post v23.06.0
>>   -------------
>> +  - Add DHCPv6 "fqdn" (39) option, that works similarly to
>> +    DHCPv4 "hostname" (12) option.
>>   OVN v23.06.0 - 01 Jun 2023
>>   --------------------------
>> diff --git a/controller/pinctrl.c b/controller/pinctrl.c
>> index c396ad4c2..d7bd20ed6 100644
>> --- a/controller/pinctrl.c
>> +++ b/controller/pinctrl.c
>> @@ -2288,7 +2288,7 @@ exit:
>>   static bool
>>   compose_out_dhcpv6_opts(struct ofpbuf *userdata,
>>                           struct ofpbuf *out_dhcpv6_opts,
>> -                        ovs_be32 iaid, bool ipxe_req)
>> +                        ovs_be32 iaid, bool ipxe_req, uint8_t 
>> fqdn_flags)
>>   {
>>       while (userdata->size) {
>>           struct dhcpv6_opt_header *userdata_opt = ofpbuf_try_pull(
>> @@ -2412,6 +2412,24 @@ compose_out_dhcpv6_opts(struct ofpbuf *userdata,
>>               break;
>>           }
>> +        case DHCPV6_OPT_FQDN_CODE: {
>> +            if (fqdn_flags != DHCPV6_FQDN_FLAGS_UNDEFINED) {
>> +                struct dhcpv6_opt_header *header =
>> +                        ofpbuf_put_zeros(out_dhcpv6_opts, sizeof 
>> *header);
>> +                header->code = htons(DHCPV6_OPT_FQDN_CODE);
>> +                header->len = htons(size + 1);
>> +                uint8_t *flags = ofpbuf_put_zeros(out_dhcpv6_opts, 1);
>> +                /* Always set N to 1, if client requested S inform 
>> him that it
>> +                 * was overwritten by the server. */
>> +                *flags |= DHCPV6_FQDN_FLAGS_N;
>> +                if (fqdn_flags & DHCPV6_FQDN_FLAGS_S) {
>> +                    *flags |= DHCPV6_FQDN_FLAGS_O;
>> +                }
>> +                ofpbuf_put(out_dhcpv6_opts, userdata_opt_data, size);
>> +            }
>> +            break;
>> +        }
>> +
>>           default:
>>               return false;
>>           }
>> @@ -2500,6 +2518,7 @@ pinctrl_handle_put_dhcpv6_opts(
>>       size_t l4_len = dp_packet_l4_size(pkt_in);
>>       uint8_t *end = (uint8_t *)in_udp + MIN(udp_len, l4_len);
>>       bool ipxe_req = false;
>> +    uint8_t fqdn_flags = DHCPV6_FQDN_FLAGS_UNDEFINED;
>>       while (in_dhcpv6_data < end) {
>>           struct dhcpv6_opt_header const *in_opt =
>>                (struct dhcpv6_opt_header *)in_dhcpv6_data;
>> @@ -2524,6 +2543,10 @@ pinctrl_handle_put_dhcpv6_opts(
>>               break;
>>           }
>> +        case DHCPV6_OPT_FQDN_CODE:
>> +            fqdn_flags = *(in_dhcpv6_data + sizeof *in_opt);
>> +            break;
>> +
>>           default:
>>               break;
>>           }
>> @@ -2547,7 +2570,7 @@ pinctrl_handle_put_dhcpv6_opts(
>>           OFPBUF_STUB_INITIALIZER(out_ofpacts_dhcpv6_opts_stub);
>>       if (!compose_out_dhcpv6_opts(userdata, &out_dhcpv6_opts,
>> -                                 iaid, ipxe_req)) {
>> +                                 iaid, ipxe_req, fqdn_flags)) {
>>           VLOG_WARN_RL(&rl, "Invalid userdata");
>>           goto exit;
>>       }
>> @@ -2762,48 +2785,16 @@ dns_build_ptr_answer(
>>       struct ofpbuf *dns_answer, const uint8_t *in_queryname,
>>       uint16_t query_length, const char *answer_data)
>>   {
>> -    char *encoded_answer;
>> -    uint16_t encoded_answer_length;
>> -
>>       dns_build_base_answer(dns_answer, in_queryname, query_length,
>>                             DNS_QUERY_TYPE_PTR);
>> -    /* Initialize string 2 chars longer than real answer:
>> -     * first label length and terminating zero-length label.
>> -     * If the answer_data is - vm1tst.ovn.org, it will be encoded as
>> -     *  - 0010 (Total length which is 16)
>> -     *  - 06766d31747374 (vm1tst)
>> -     *  - 036f766e (ovn)
>> -     *  - 036f7267 (org
>> -     *  - 00 (zero length field) */
>> -    encoded_answer_length = strlen(answer_data) + 2;
>> -    encoded_answer = (char *)xzalloc(encoded_answer_length);
>> -
>> -    put_be16(dns_answer, htons(encoded_answer_length));
>> -    uint8_t label_len_index = 0;
>> -    uint16_t label_len = 0;
>> -    char *encoded_answer_ptr = (char *)encoded_answer + 1;
>> -    while (*answer_data) {
>> -        if (*answer_data == '.') {
>> -            /* Label has ended.  Update the length of the label. */
>> -            encoded_answer[label_len_index] = label_len;
>> -            label_len_index += (label_len + 1);
>> -            label_len = 0; /* Init to 0 for the next label. */
>> -        } else {
>> -            *encoded_answer_ptr =  *answer_data;
>> -            label_len++;
>> -        }
>> -        encoded_answer_ptr++;
>> -        answer_data++;
>> -    }
>> +    size_t encoded_len = 0;
>> +    char *encoded = encode_fqdn_string(answer_data, &encoded_len);
>> -    /* This is required for the last label if it doesn't end with '.' */
>> -    if (label_len) {
>> -        encoded_answer[label_len_index] = label_len;
>> -    }
>> +    put_be16(dns_answer, htons(encoded_len));
>> +    ofpbuf_put(dns_answer, encoded, encoded_len);
>> -    ofpbuf_put(dns_answer, encoded_answer, encoded_answer_length);
>> -    free(encoded_answer);
>> +    free(encoded);
>>   }
>>   /* Called with in the pinctrl_handler thread context. */
>> diff --git a/lib/actions.c b/lib/actions.c
>> index ec27223f9..037172e60 100644
>> --- a/lib/actions.c
>> +++ b/lib/actions.c
>> @@ -2470,7 +2470,8 @@ parse_gen_opt(struct action_context *ctx, struct 
>> ovnact_gen_option *o,
>>       }
>>       if (!strcmp(o->option->type, "str") ||
>> -        !strcmp(o->option->type, "domains")) {
>> +        !strcmp(o->option->type, "domains") ||
>> +        !strcmp(o->option->type, "domain")) {
>>           if (o->value.type != EXPR_C_STRING) {
>>               lexer_error(ctx->lexer, "%s option %s requires string 
>> value.",
>>                           opts_type, o->option->name);
>> @@ -2903,6 +2904,12 @@ encode_put_dhcpv6_option(const struct 
>> ovnact_gen_option *o,
>>           size = strlen(c->string);
>>           opt->len = htons(size);
>>           ofpbuf_put(ofpacts, c->string, size);
>> +    } else if (!strcmp(o->option->type, "domain")) {
>> +        char *encoded = encode_fqdn_string(c->string, &size);
>> +        opt->len = htons(size);
>> +        ofpbuf_put(ofpacts, encoded, size);
>> +
>> +        free(encoded);
>>       }
>>   }
>> diff --git a/lib/ovn-l7.h b/lib/ovn-l7.h
>> index d9b103119..9dc331421 100644
>> --- a/lib/ovn-l7.h
>> +++ b/lib/ovn-l7.h
>> @@ -270,6 +270,7 @@ BUILD_ASSERT_DECL(DHCP_OPT_HEADER_LEN == 
>> sizeof(struct dhcp_opt_header));
>>   #define DHCPV6_OPT_DOMAIN_SEARCH_CODE    24
>>   #define DHCPV6_OPT_IA_PD                 25
>>   #define DHCPV6_OPT_IA_PREFIX             26
>> +#define DHCPV6_OPT_FQDN_CODE             39
>>   #define DHCPV6_OPT_BOOT_FILE_URL         59
>>   #define DHCPV6_OPT_BOOT_FILE_URL_ALT    254
>> @@ -291,6 +292,15 @@ BUILD_ASSERT_DECL(DHCP_OPT_HEADER_LEN == 
>> sizeof(struct dhcp_opt_header));
>>   #define DHCPV6_OPT_BOOTFILE_NAME_ALT \
>>       DHCP_OPTION("bootfile_name_alt", DHCPV6_OPT_BOOT_FILE_URL_ALT, 
>> "str")
>> +#define DHCPV6_OPT_FQDN \
>> +    DHCP_OPTION("fqdn", DHCPV6_OPT_FQDN_CODE, "domain")
>> +
>> +/* DHCPv6 FQDN flags. RFC 4704 */
>> +#define DHCPV6_FQDN_FLAGS_UNDEFINED 0xff
>> +#define DHCPV6_FQDN_FLAGS_S 1 << 0
>> +#define DHCPV6_FQDN_FLAGS_O 1 << 1
>> +#define DHCPV6_FQDN_FLAGS_N 1 << 2
>> +
>>   OVS_PACKED(
>>   struct dhcpv6_opt_header {
>>       ovs_be16 code;
>> diff --git a/lib/ovn-util.c b/lib/ovn-util.c
>> index bffb521cf..12ca27319 100644
>> --- a/lib/ovn-util.c
>> +++ b/lib/ovn-util.c
>> @@ -1126,3 +1126,30 @@ void flow_collector_ids_clear(struct 
>> flow_collector_ids *ids)
>>       flow_collector_ids_destroy(ids);
>>       flow_collector_ids_init(ids);
>>   }
>> +
>> +char *
>> +encode_fqdn_string(const char *fqdn, size_t *len)
>> +{
>> +
>> +    size_t domain_len = strlen(fqdn);
>> +    *len = domain_len + 2;
>> +    char *encoded = xzalloc(*len);
>> +
>> +    int8_t label_len = 0;
>> +    for (size_t i = 0; i < domain_len; i++) {
>> +        if (fqdn[i] == '.') {
>> +            encoded[i - label_len] = label_len;
>> +            label_len = 0;
>> +        } else {
>> +            encoded[i + 1] = fqdn[i];
>> +            label_len++;
>> +        }
>> +    }
>> +
>> +    /* This is required for the last label if it doesn't end with '.' */
>> +    if (label_len) {
>> +        encoded[domain_len - label_len] = label_len;
>> +    }
>> +
>> +    return encoded;
>> +}
>> diff --git a/lib/ovn-util.h b/lib/ovn-util.h
>> index b17b0e236..0af940918 100644
>> --- a/lib/ovn-util.h
>> +++ b/lib/ovn-util.h
>> @@ -403,4 +403,9 @@ bool flow_collector_ids_lookup(const struct 
>> flow_collector_ids *, uint32_t);
>>   void flow_collector_ids_destroy(struct flow_collector_ids *);
>>   void flow_collector_ids_clear(struct flow_collector_ids *);
>> +/* The DNS format is 2 bytes longer than the "domain".
>> + * It replaces every '.' with len of the next name.
>> + * The returned pointer has to be freed by caller. */
>> +char *encode_fqdn_string(const char *fqdn, size_t *len);
>> +
>>   #endif /* OVN_UTIL_H */
>> diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
>> index 3515b68a2..2bafbd4af 100644
>> --- a/northd/ovn-northd.c
>> +++ b/northd/ovn-northd.c
>> @@ -273,7 +273,8 @@ static struct gen_opts_map supported_dhcpv6_opts[] 
>> = {
>>       DHCPV6_OPT_DOMAIN_SEARCH,
>>       DHCPV6_OPT_DNS_SERVER,
>>       DHCPV6_OPT_BOOTFILE_NAME,
>> -    DHCPV6_OPT_BOOTFILE_NAME_ALT
>> +    DHCPV6_OPT_BOOTFILE_NAME_ALT,
>> +    DHCPV6_OPT_FQDN,
>>   };
>>   static bool
>> diff --git a/ovn-nb.xml b/ovn-nb.xml
>> index 9afe3b584..05e155b89 100644
>> --- a/ovn-nb.xml
>> +++ b/ovn-nb.xml
>> @@ -4191,6 +4191,13 @@ or
>>               way. Default value for this option is false.
>>             </p>
>>           </column>
>> +
>> +        <column name="options" key="fqdn">
>> +          <p>
>> +            The DHCPv6 option code for this option is 39.
>> +            If set, indicates the DHCPv6 option "FQDN".
>> +          </p>
>> +        </column>
>>         </group>
>>       </group>
>> diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
>> index f59af8cc5..06e16b403 100644
>> --- a/ovn-sb.ovsschema
>> +++ b/ovn-sb.ovsschema
>> @@ -1,7 +1,7 @@
>>   {
>>       "name": "OVN_Southbound",
>> -    "version": "20.27.2",
>> -    "cksum": "1291808617 30462",
>> +    "version": "20.27.3",
>> +    "cksum": "3876528905 30472",
>>       "tables": {
>>           "SB_Global": {
>>               "columns": {
>> @@ -311,7 +311,7 @@
>>                   "type": {
>>                       "type": {"key": {
>>                           "type": "string",
>> -                        "enum": ["set", ["ipv6", "str", "mac"]]}}}},
>> +                        "enum": ["set", ["ipv6", "str", "mac", 
>> "domain"]]}}}},
>>               "isRoot": true},
>>           "Connection": {
>>               "columns": {
>> diff --git a/tests/ovn.at b/tests/ovn.at
>> index 5e6a8fefa..2b8ed7571 100644
>> --- a/tests/ovn.at
>> +++ b/tests/ovn.at
>> @@ -1697,6 +1697,9 @@ reg1[0] = 
>> put_dhcpv6_opts(bootfile_name="https://127.0.0.1/boot.ipxe");
>>   reg1[0] = 
>> put_dhcpv6_opts(bootfile_name_alt="https://127.0.0.1/boot.ipxe");
>>       formats as reg1[0] = put_dhcpv6_opts(bootfile_name_alt = 
>> "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.fe.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)
>> +reg1[0] = put_dhcpv6_opts(fqdn="ovn.org");
>> +    formats as reg1[0] = put_dhcpv6_opts(fqdn = "ovn.org");
>> +    encodes as 
>> controller(userdata=00.00.00.05.00.00.00.00.00.01.de.10.00.00.00.40.00.27.00.09.03.6f.76.6e.03.6f.72.67.00,pause)
>>   # lookup_nd
>>   reg2[0] = lookup_nd(inport, ip6.dst, eth.src);
>> @@ -7083,6 +7086,7 @@ AT_CLEANUP
>>   OVN_FOR_EACH_NORTHD([
>>   AT_SETUP([dhcpv6 : 1 HV, 2 LS, 5 LSPs])
>> +AT_SKIP_IF([test $HAVE_SCAPY = no])
>>   ovn_start
>>   ovn-nbctl ls-add ls1
>> @@ -7171,61 +7175,56 @@ trim_zeros() {
>>   # packet should be received twice (one from ovn-controller and the 
>> other
>>   # from the "ovs-ofctl monitor br-int resume"
>>   test_dhcpv6() {
>> -    local inport=$1 src_mac=$2 src_lla=$3 msg_code=$4 offer_ip=$5 
>> ipxe=$6
>> -    if test $ipxe -eq 2; then
>> -        req_len=34
>> -    elif test $msg_code != 0b; then
>> -        req_len=2a
>> -    else
>> -        req_len=1a
>> -    fi
>> -    local 
>> request=ffffffffffff${src_mac}86dd6000000000${req_len}1101${src_lla}
>> -    # dst ip ff02::1:2
>> -    request=${request}ff020000000000000000000000010002
>> -    # udp header and dhcpv6 header
>> -    request=${request}0222022300${req_len}ffff${msg_code}010203
>> -    # Client identifier
>> -    request=${request}0001000a00030001${src_mac}
>> +    local inport=$1 src_mac=$2 src_lla=$3 msg_code=$4 offer_ip=$5 
>> boot_file=$6
>> +    local fqdn=$7
>> +
>> +    local req_scapy="Ether(dst='ff:ff:ff:ff:ff:ff', 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}'))"
>> +
>>       # Add IA-NA (Identity Association for Non Temporary Address) if 
>> msg_code
>>       # is not 11 (information request packet)
>> -    if test $msg_code != 0b; then
>> -        request=${request}0003000c0102030400000e1000001518
>> +    if test $msg_code != 11; then
>> +        req_scapy="${req_scapy}/DHCP6OptIA_NA(iaid=0x01020304, 
>> T1=3600, T2=5400)"
>>       fi
>> -    if test $ipxe -eq 2; then
>> -        request=${request}000f0006000669505845
>> +    if test "$boot_file" = "bootfile_name"; then
>> +        local uc_data="USER_CLASS_DATA(data=b'iPXE')"
>> +        
>> req_scapy="${req_scapy}/DHCP6OptUserClass(userclassdata=[[${uc_data}]])"
>>       fi
>> -    shift; shift; shift; shift; shift; shift;
>> +    if test -n "$fqdn"; then
>> +        req_scapy="${req_scapy}/DHCP6OptClientFQDN(flags=0x01, 
>> fqdn=b'test')"
>> +    fi
>> +    request=$(fmt_pkt "${req_scapy}")
>> +    shift; shift; shift; shift; shift; shift; shift;
>>       if test $offer_ip != 0; then
>> -        local server_mac=000000100001
>> -        local server_lla=fe80000000000000020000fffe100001
>> -        local reply_code=07
>> -        if test $msg_code = 01; then
>> -            reply_code=02
>> -        fi
>> -        local msg_len=54
>> -        if test $ipxe -eq 1; then
>> -            msg_len=69
>> -        elif test $ipxe -eq 2; then
>> -            msg_len=65
>> -        elif test $offer_ip = 1; then
>> -            msg_len=28
>> +        local reply_code=7
>> +        if test $msg_code = 1; then
>> +            reply_code=2
>>           fi
>> -        local 
>> reply=${src_mac}${server_mac}86dd6000000000${msg_len}1101${server_lla}${src_lla}
>> -        # udp header and dhcpv6 header
>> -        reply=${reply}0223022200${msg_len}ffff${reply_code}010203
>> -        # Client identifier
>> -        reply=${reply}0001000a00030001${src_mac}
>> -        # IA-NA
>> +
>> +        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}'))"
>> +
>>           if test $offer_ip != 1; then
>> -            
>> reply=${reply}0003002801020304ffffffffffffffff00050018${offer_ip}ffffffffffffffff
>> +            local ip_scapy="DHCP6OptIAAddress(addr='${offer_ip}', \
>> +                            preflft=0xffffffff, validlft=0xffffffff)"
>> +            rep_scapy="${rep_scapy}/ \
>> +                       DHCP6OptIA_NA(iaid=0x01020304, T1=0xffffffff, \
>> +                       T2=0xffffffff, ianaopts=[[${ip_scapy}]])"
>>           fi
>> -        if test $ipxe -eq 1; then
>> -            reply=${reply}003b0011626f6f7466696c655f6e616d655f616c74
>> -        elif test $ipxe -eq 2; then
>> -            reply=${reply}003b000d626f6f7466696c655f6e616d65
>> +        if test -n "$boot_file"; then
>> +            
>> rep_scapy="${rep_scapy}/DHCP6OptBootFileUrl(optdata=b'${boot_file}')"
>>           fi
>> -        # Server identifier
>> -        reply=${reply}0002000a00030001${server_mac}
>> +        if test -n "$fqdn"; then
>> +            rep_scapy="${rep_scapy}/DHCP6OptClientFQDN(flags=0x06, 
>> fqdn=b'${fqdn}')"
>> +        fi
>> +        
>> rep_scapy="${rep_scapy}/DHCP6OptServerId(duid=DUID_LL(lladdr='00:00:00:10:00:01'))"
>> +        reply=$(fmt_pkt "${rep_scapy}")
>>           echo $reply | trim_zeros >> $inport.expected
>>       else
>>           for outport; do
>> @@ -7249,16 +7248,16 @@ echo "---------------------"
>>   ovn-sbctl list logical_flow
>>   echo "---------------------"
>> -ovn-sbctl dump-flows > sbflows
>> +#ovn-sbctl dump-flows > sbflows
>>   AT_CAPTURE_FILE([sbflows])
>>   echo "------ hv1 dump ----------"
>>   as hv1 ovs-ofctl dump-flows br-int
>> -src_mac=f00000000001
>> -src_lla=fe80000000000000f20000fffe000001
>> -offer_ip=ae700000000000000000000000000004
>> -test_dhcpv6 1 $src_mac $src_lla 01 $offer_ip 0
>> +src_mac="f0:00:00:00:00:01"
>> +src_lla="fe80::f200:00ff:fe00:0001"
>> +offer_ip="ae70::4"
>> +test_dhcpv6 1 $src_mac $src_lla 1 $offer_ip "" ""
>>   # NXT_RESUMEs should be 1.
>>   OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor*.log | grep -c 
>> NXT_RESUME`])
>> @@ -7281,12 +7280,12 @@ rm  1.expected
>>   reset_pcap_file hv1-vif1 hv1/vif1
>>   reset_pcap_file hv1-vif2 hv1/vif2
>> -src_mac=f00000000002
>> -src_lla=fe80000000000000f20000fffe000002
>> -offer_ip=ae700000000000000000000000000005
>> +src_mac="f0:00:00:00:00:02"
>> +src_lla="fe80::f200:00ff:fe00:0002"
>> +offer_ip="ae70::5"
>>   # Set invalid msg_type
>> -test_dhcpv6 2 $src_mac $src_lla 10 0 0 1 1
>> +test_dhcpv6 2 $src_mac $src_lla 16 0 "" "" 1 1
>>   # NXT_RESUMEs should be 2.
>>   OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor*.log | grep -c 
>> NXT_RESUME`])
>> @@ -7305,9 +7304,9 @@ AT_CHECK([cat 1.packets], [0], [expout])
>>   # There should be no DHCPv6 reply from ovn-controller and the 
>> request packet
>>   # should be received by ls2-lp2.
>> -src_mac=f00000000003
>> -src_lla=fe80000000000000f20000fffe000003
>> -test_dhcpv6 3 $src_mac $src_lla 01 0 0 4
>> +src_mac="f0:00:00:00:00:03"
>> +src_lla="fe80::f200:00ff:fe00:0003"
>> +test_dhcpv6 3 $src_mac $src_lla 1 0 "" "" 4
>>   # NXT_RESUMEs should be 2 only.
>>   OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor*.log | grep -c 
>> NXT_RESUME`])
>> @@ -7323,10 +7322,10 @@ AT_CHECK([cat 4.packets], [0], [expout])
>>   # Send DHCPv6 packet on ls1-lp3. native DHCPv6 works as stateless 
>> mode for this port.
>>   # The DHCPv6 reply shouldn't contain offer_ip.
>> -src_mac=f00000000022
>> -src_lla=fe80000000000000f20000fffe000022
>> +src_mac="f0:00:00:00:00:22"
>> +src_lla="fe80::f200:00ff:fe00:0022"
>>   reset_pcap_file hv1-vif5 hv1/vif5
>> -test_dhcpv6 5 $src_mac $src_lla 01 1 0 5
>> +test_dhcpv6 5 $src_mac $src_lla 1 1 "" "" 5
>>   # NXT_RESUMEs should be 3.
>>   OVS_WAIT_UNTIL([test 3 = `cat ofctl_monitor*.log | grep -c 
>> NXT_RESUME`])
>> @@ -7338,11 +7337,11 @@ AT_CHECK([cat 5.packets | cut -c 1-120,125- ], 
>> [0], [expout])
>>   # Send DHCPv6 information request (code 11) on ls1-lp3. The DHCPv6 
>> reply
>>   # shouldn't contain offer_ip
>> -src_mac=f00000000022
>> -src_lla=fe80000000000000f20000fffe000022
>> +src_mac="f0:00:00:00:00:22"
>> +src_lla="fe80::f200:00ff:fe00:0022"
>>   reset_pcap_file hv1-vif5 hv1/vif5
>>   rm -f 5.expected
>> -test_dhcpv6 5 $src_mac $src_lla 0b 1 0 5
>> +test_dhcpv6 5 $src_mac $src_lla 11 1 "" "" 5
>>   # NXT_RESUMEs should be 4.
>>   OVS_WAIT_UNTIL([test 4 = `cat ofctl_monitor*.log | grep -c 
>> NXT_RESUME`])
>> @@ -7363,11 +7362,11 @@ ovn-nbctl lsp-set-dhcpv6-options ls1-lp2 ${d1}
>>   reset_pcap_file hv1-vif2 hv1/vif2
>> -src_mac=f00000000002
>> -src_lla=fe80000000000000f20000fffe000002
>> -offer_ip=ae700000000000000000000000000005
>> +src_mac="f0:00:00:00:00:02"
>> +src_lla="fe80::f200:00ff:fe00:0002"
>> +offer_ip="ae70::5"
>> -test_dhcpv6 2 $src_mac $src_lla 01 $offer_ip 1
>> +test_dhcpv6 2 $src_mac $src_lla 1 $offer_ip "bootfile_name_alt" ""
>>   # NXT_RESUMEs should be 5.
>>   OVS_WAIT_UNTIL([test 5 = `cat ofctl_monitor*.log | grep -c 
>> NXT_RESUME`])
>> @@ -7381,7 +7380,7 @@ AT_CHECK([cat 2.packets | cut -c 1-120,125- ], 
>> [0], [expout])
>>   reset_pcap_file hv1-vif2 hv1/vif2
>>   rm 2.packets 2.expected
>> -test_dhcpv6 2 $src_mac $src_lla 01 $offer_ip 2
>> +test_dhcpv6 2 $src_mac $src_lla 1 $offer_ip "bootfile_name" ""
>>   # NXT_RESUMEs should be 6.
>>   OVS_WAIT_UNTIL([test 6 = `cat ofctl_monitor*.log | grep -c 
>> NXT_RESUME`])
>> @@ -7389,6 +7388,24 @@ $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" 
>> hv1/vif2-tx.pcap | trim_zeros > 2.pa
>>   cat 2.expected | cut -c 1-120,125- > expout
>>   AT_CHECK([cat 2.packets | cut -c 1-120,125- ], [0], [expout])
>> +ovn-nbctl --all destroy dhcp-option
>> +d1="$(ovn-nbctl create DHCP_Options cidr="ae70\:\:/64")"
>> +ovn-nbctl dhcp-options-set-options $d1 \
>> +    server_id=00:00:00:10:00:01 \
>> +    fqdn=\"ovn.org\"
>> +ovn-nbctl --wait=hv lsp-set-dhcpv6-options ls1-lp2 ${d1}
>> +
>> +reset_pcap_file hv1-vif2 hv1/vif2
>> +rm 2.packets 2.expected
>> +
>> +test_dhcpv6 2 $src_mac $src_lla 1 $offer_ip "" "ovn.org"
>> +# NXT_RESUMEs should be 7.
>> +OVS_WAIT_UNTIL([test 7 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
>> +
>> +$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap | 
>> trim_zeros > 2.packets
>> +cat 2.expected | cut -c 1-120,125- > expout
>> +AT_CHECK([cat 2.packets | cut -c 1-120,125- ], [0], [expout])
>> +
>>   OVN_CLEANUP([hv1])
>>   AT_CLEANUP
>> diff --git a/tests/test-ovn.c b/tests/test-ovn.c
>> index ce9213c1d..6c7754eac 100644
>> --- a/tests/test-ovn.c
>> +++ b/tests/test-ovn.c
>> @@ -207,6 +207,7 @@ create_gen_opts(struct hmap *dhcp_opts, struct 
>> hmap *dhcpv6_opts,
>>       dhcp_opt_add(dhcpv6_opts, "domain_search",  24, "str");
>>       dhcp_opt_add(dhcpv6_opts, "bootfile_name", 59, "str");
>>       dhcp_opt_add(dhcpv6_opts, "bootfile_name_alt", 254, "str");
>> +    dhcp_opt_add(dhcpv6_opts, "fqdn", 39, "domain");
>>       /* IPv6 ND RA options. */
>>       hmap_init(nd_ra_opts);
>
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index 645acea1f..e6c87e9ad 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,7 @@ 
 Post v23.06.0
 -------------
+  - Add DHCPv6 "fqdn" (39) option, that works similarly to
+    DHCPv4 "hostname" (12) option.
 
 OVN v23.06.0 - 01 Jun 2023
 --------------------------
diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index c396ad4c2..d7bd20ed6 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -2288,7 +2288,7 @@  exit:
 static bool
 compose_out_dhcpv6_opts(struct ofpbuf *userdata,
                         struct ofpbuf *out_dhcpv6_opts,
-                        ovs_be32 iaid, bool ipxe_req)
+                        ovs_be32 iaid, bool ipxe_req, uint8_t fqdn_flags)
 {
     while (userdata->size) {
         struct dhcpv6_opt_header *userdata_opt = ofpbuf_try_pull(
@@ -2412,6 +2412,24 @@  compose_out_dhcpv6_opts(struct ofpbuf *userdata,
             break;
         }
 
+        case DHCPV6_OPT_FQDN_CODE: {
+            if (fqdn_flags != DHCPV6_FQDN_FLAGS_UNDEFINED) {
+                struct dhcpv6_opt_header *header =
+                        ofpbuf_put_zeros(out_dhcpv6_opts, sizeof *header);
+                header->code = htons(DHCPV6_OPT_FQDN_CODE);
+                header->len = htons(size + 1);
+                uint8_t *flags = ofpbuf_put_zeros(out_dhcpv6_opts, 1);
+                /* Always set N to 1, if client requested S inform him that it
+                 * was overwritten by the server. */
+                *flags |= DHCPV6_FQDN_FLAGS_N;
+                if (fqdn_flags & DHCPV6_FQDN_FLAGS_S) {
+                    *flags |= DHCPV6_FQDN_FLAGS_O;
+                }
+                ofpbuf_put(out_dhcpv6_opts, userdata_opt_data, size);
+            }
+            break;
+        }
+
         default:
             return false;
         }
@@ -2500,6 +2518,7 @@  pinctrl_handle_put_dhcpv6_opts(
     size_t l4_len = dp_packet_l4_size(pkt_in);
     uint8_t *end = (uint8_t *)in_udp + MIN(udp_len, l4_len);
     bool ipxe_req = false;
+    uint8_t fqdn_flags = DHCPV6_FQDN_FLAGS_UNDEFINED;
     while (in_dhcpv6_data < end) {
         struct dhcpv6_opt_header const *in_opt =
              (struct dhcpv6_opt_header *)in_dhcpv6_data;
@@ -2524,6 +2543,10 @@  pinctrl_handle_put_dhcpv6_opts(
             break;
         }
 
+        case DHCPV6_OPT_FQDN_CODE:
+            fqdn_flags = *(in_dhcpv6_data + sizeof *in_opt);
+            break;
+
         default:
             break;
         }
@@ -2547,7 +2570,7 @@  pinctrl_handle_put_dhcpv6_opts(
         OFPBUF_STUB_INITIALIZER(out_ofpacts_dhcpv6_opts_stub);
 
     if (!compose_out_dhcpv6_opts(userdata, &out_dhcpv6_opts,
-                                 iaid, ipxe_req)) {
+                                 iaid, ipxe_req, fqdn_flags)) {
         VLOG_WARN_RL(&rl, "Invalid userdata");
         goto exit;
     }
@@ -2762,48 +2785,16 @@  dns_build_ptr_answer(
     struct ofpbuf *dns_answer, const uint8_t *in_queryname,
     uint16_t query_length, const char *answer_data)
 {
-    char *encoded_answer;
-    uint16_t encoded_answer_length;
-
     dns_build_base_answer(dns_answer, in_queryname, query_length,
                           DNS_QUERY_TYPE_PTR);
 
-    /* Initialize string 2 chars longer than real answer:
-     * first label length and terminating zero-length label.
-     * If the answer_data is - vm1tst.ovn.org, it will be encoded as
-     *  - 0010 (Total length which is 16)
-     *  - 06766d31747374 (vm1tst)
-     *  - 036f766e (ovn)
-     *  - 036f7267 (org
-     *  - 00 (zero length field) */
-    encoded_answer_length = strlen(answer_data) + 2;
-    encoded_answer = (char *)xzalloc(encoded_answer_length);
-
-    put_be16(dns_answer, htons(encoded_answer_length));
-    uint8_t label_len_index = 0;
-    uint16_t label_len = 0;
-    char *encoded_answer_ptr = (char *)encoded_answer + 1;
-    while (*answer_data) {
-        if (*answer_data == '.') {
-            /* Label has ended.  Update the length of the label. */
-            encoded_answer[label_len_index] = label_len;
-            label_len_index += (label_len + 1);
-            label_len = 0; /* Init to 0 for the next label. */
-        } else {
-            *encoded_answer_ptr =  *answer_data;
-            label_len++;
-        }
-        encoded_answer_ptr++;
-        answer_data++;
-    }
+    size_t encoded_len = 0;
+    char *encoded = encode_fqdn_string(answer_data, &encoded_len);
 
-    /* This is required for the last label if it doesn't end with '.' */
-    if (label_len) {
-        encoded_answer[label_len_index] = label_len;
-    }
+    put_be16(dns_answer, htons(encoded_len));
+    ofpbuf_put(dns_answer, encoded, encoded_len);
 
-    ofpbuf_put(dns_answer, encoded_answer, encoded_answer_length);
-    free(encoded_answer);
+    free(encoded);
 }
 
 /* Called with in the pinctrl_handler thread context. */
diff --git a/lib/actions.c b/lib/actions.c
index ec27223f9..037172e60 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -2470,7 +2470,8 @@  parse_gen_opt(struct action_context *ctx, struct ovnact_gen_option *o,
     }
 
     if (!strcmp(o->option->type, "str") ||
-        !strcmp(o->option->type, "domains")) {
+        !strcmp(o->option->type, "domains") ||
+        !strcmp(o->option->type, "domain")) {
         if (o->value.type != EXPR_C_STRING) {
             lexer_error(ctx->lexer, "%s option %s requires string value.",
                         opts_type, o->option->name);
@@ -2903,6 +2904,12 @@  encode_put_dhcpv6_option(const struct ovnact_gen_option *o,
         size = strlen(c->string);
         opt->len = htons(size);
         ofpbuf_put(ofpacts, c->string, size);
+    } else if (!strcmp(o->option->type, "domain")) {
+        char *encoded = encode_fqdn_string(c->string, &size);
+        opt->len = htons(size);
+        ofpbuf_put(ofpacts, encoded, size);
+
+        free(encoded);
     }
 }
 
diff --git a/lib/ovn-l7.h b/lib/ovn-l7.h
index d9b103119..9dc331421 100644
--- a/lib/ovn-l7.h
+++ b/lib/ovn-l7.h
@@ -270,6 +270,7 @@  BUILD_ASSERT_DECL(DHCP_OPT_HEADER_LEN == sizeof(struct dhcp_opt_header));
 #define DHCPV6_OPT_DOMAIN_SEARCH_CODE    24
 #define DHCPV6_OPT_IA_PD                 25
 #define DHCPV6_OPT_IA_PREFIX             26
+#define DHCPV6_OPT_FQDN_CODE             39
 #define DHCPV6_OPT_BOOT_FILE_URL         59
 #define DHCPV6_OPT_BOOT_FILE_URL_ALT    254
 
@@ -291,6 +292,15 @@  BUILD_ASSERT_DECL(DHCP_OPT_HEADER_LEN == sizeof(struct dhcp_opt_header));
 #define DHCPV6_OPT_BOOTFILE_NAME_ALT \
     DHCP_OPTION("bootfile_name_alt", DHCPV6_OPT_BOOT_FILE_URL_ALT, "str")
 
+#define DHCPV6_OPT_FQDN \
+    DHCP_OPTION("fqdn", DHCPV6_OPT_FQDN_CODE, "domain")
+
+/* DHCPv6 FQDN flags. RFC 4704 */
+#define DHCPV6_FQDN_FLAGS_UNDEFINED 0xff
+#define DHCPV6_FQDN_FLAGS_S 1 << 0
+#define DHCPV6_FQDN_FLAGS_O 1 << 1
+#define DHCPV6_FQDN_FLAGS_N 1 << 2
+
 OVS_PACKED(
 struct dhcpv6_opt_header {
     ovs_be16 code;
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index bffb521cf..12ca27319 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -1126,3 +1126,30 @@  void flow_collector_ids_clear(struct flow_collector_ids *ids)
     flow_collector_ids_destroy(ids);
     flow_collector_ids_init(ids);
 }
+
+char *
+encode_fqdn_string(const char *fqdn, size_t *len)
+{
+
+    size_t domain_len = strlen(fqdn);
+    *len = domain_len + 2;
+    char *encoded = xzalloc(*len);
+
+    int8_t label_len = 0;
+    for (size_t i = 0; i < domain_len; i++) {
+        if (fqdn[i] == '.') {
+            encoded[i - label_len] = label_len;
+            label_len = 0;
+        } else {
+            encoded[i + 1] = fqdn[i];
+            label_len++;
+        }
+    }
+
+    /* This is required for the last label if it doesn't end with '.' */
+    if (label_len) {
+        encoded[domain_len - label_len] = label_len;
+    }
+
+    return encoded;
+}
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index b17b0e236..0af940918 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -403,4 +403,9 @@  bool flow_collector_ids_lookup(const struct flow_collector_ids *, uint32_t);
 void flow_collector_ids_destroy(struct flow_collector_ids *);
 void flow_collector_ids_clear(struct flow_collector_ids *);
 
+/* The DNS format is 2 bytes longer than the "domain".
+ * It replaces every '.' with len of the next name.
+ * The returned pointer has to be freed by caller. */
+char *encode_fqdn_string(const char *fqdn, size_t *len);
+
 #endif /* OVN_UTIL_H */
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index 3515b68a2..2bafbd4af 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -273,7 +273,8 @@  static struct gen_opts_map supported_dhcpv6_opts[] = {
     DHCPV6_OPT_DOMAIN_SEARCH,
     DHCPV6_OPT_DNS_SERVER,
     DHCPV6_OPT_BOOTFILE_NAME,
-    DHCPV6_OPT_BOOTFILE_NAME_ALT
+    DHCPV6_OPT_BOOTFILE_NAME_ALT,
+    DHCPV6_OPT_FQDN,
 };
 
 static bool
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 9afe3b584..05e155b89 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -4191,6 +4191,13 @@  or
             way. Default value for this option is false.
           </p>
         </column>
+
+        <column name="options" key="fqdn">
+          <p>
+            The DHCPv6 option code for this option is 39.
+            If set, indicates the DHCPv6 option "FQDN".
+          </p>
+        </column>
       </group>
     </group>
 
diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
index f59af8cc5..06e16b403 100644
--- a/ovn-sb.ovsschema
+++ b/ovn-sb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Southbound",
-    "version": "20.27.2",
-    "cksum": "1291808617 30462",
+    "version": "20.27.3",
+    "cksum": "3876528905 30472",
     "tables": {
         "SB_Global": {
             "columns": {
@@ -311,7 +311,7 @@ 
                 "type": {
                     "type": {"key": {
                         "type": "string",
-                        "enum": ["set", ["ipv6", "str", "mac"]]}}}},
+                        "enum": ["set", ["ipv6", "str", "mac", "domain"]]}}}},
             "isRoot": true},
         "Connection": {
             "columns": {
diff --git a/tests/ovn.at b/tests/ovn.at
index 5e6a8fefa..2b8ed7571 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1697,6 +1697,9 @@  reg1[0] = put_dhcpv6_opts(bootfile_name="https://127.0.0.1/boot.ipxe");
 reg1[0] = put_dhcpv6_opts(bootfile_name_alt="https://127.0.0.1/boot.ipxe");
     formats as reg1[0] = put_dhcpv6_opts(bootfile_name_alt = "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.fe.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)
+reg1[0] = put_dhcpv6_opts(fqdn="ovn.org");
+    formats as reg1[0] = put_dhcpv6_opts(fqdn = "ovn.org");
+    encodes as controller(userdata=00.00.00.05.00.00.00.00.00.01.de.10.00.00.00.40.00.27.00.09.03.6f.76.6e.03.6f.72.67.00,pause)
 
 # lookup_nd
 reg2[0] = lookup_nd(inport, ip6.dst, eth.src);
@@ -7083,6 +7086,7 @@  AT_CLEANUP
 
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([dhcpv6 : 1 HV, 2 LS, 5 LSPs])
+AT_SKIP_IF([test $HAVE_SCAPY = no])
 ovn_start
 
 ovn-nbctl ls-add ls1
@@ -7171,61 +7175,56 @@  trim_zeros() {
 # packet should be received twice (one from ovn-controller and the other
 # from the "ovs-ofctl monitor br-int resume"
 test_dhcpv6() {
-    local inport=$1 src_mac=$2 src_lla=$3 msg_code=$4 offer_ip=$5 ipxe=$6
-    if test $ipxe -eq 2; then
-        req_len=34
-    elif test $msg_code != 0b; then
-        req_len=2a
-    else
-        req_len=1a
-    fi
-    local request=ffffffffffff${src_mac}86dd6000000000${req_len}1101${src_lla}
-    # dst ip ff02::1:2
-    request=${request}ff020000000000000000000000010002
-    # udp header and dhcpv6 header
-    request=${request}0222022300${req_len}ffff${msg_code}010203
-    # Client identifier
-    request=${request}0001000a00030001${src_mac}
+    local inport=$1 src_mac=$2 src_lla=$3 msg_code=$4 offer_ip=$5 boot_file=$6
+    local fqdn=$7
+
+    local req_scapy="Ether(dst='ff:ff:ff:ff:ff:ff', 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}'))"
+
     # Add IA-NA (Identity Association for Non Temporary Address) if msg_code
     # is not 11 (information request packet)
-    if test $msg_code != 0b; then
-        request=${request}0003000c0102030400000e1000001518
+    if test $msg_code != 11; then
+        req_scapy="${req_scapy}/DHCP6OptIA_NA(iaid=0x01020304, T1=3600, T2=5400)"
     fi
-    if test $ipxe -eq 2; then
-        request=${request}000f0006000669505845
+    if test "$boot_file" = "bootfile_name"; then
+        local uc_data="USER_CLASS_DATA(data=b'iPXE')"
+        req_scapy="${req_scapy}/DHCP6OptUserClass(userclassdata=[[${uc_data}]])"
     fi
-    shift; shift; shift; shift; shift; shift;
+    if test -n "$fqdn"; then
+        req_scapy="${req_scapy}/DHCP6OptClientFQDN(flags=0x01, fqdn=b'test')"
+    fi
+    request=$(fmt_pkt "${req_scapy}")
+    shift; shift; shift; shift; shift; shift; shift;
     if test $offer_ip != 0; then
-        local server_mac=000000100001
-        local server_lla=fe80000000000000020000fffe100001
-        local reply_code=07
-        if test $msg_code = 01; then
-            reply_code=02
-        fi
-        local msg_len=54
-        if test $ipxe -eq 1; then
-            msg_len=69
-        elif test $ipxe -eq 2; then
-            msg_len=65
-        elif test $offer_ip = 1; then
-            msg_len=28
+        local reply_code=7
+        if test $msg_code = 1; then
+            reply_code=2
         fi
-        local reply=${src_mac}${server_mac}86dd6000000000${msg_len}1101${server_lla}${src_lla}
-        # udp header and dhcpv6 header
-        reply=${reply}0223022200${msg_len}ffff${reply_code}010203
-        # Client identifier
-        reply=${reply}0001000a00030001${src_mac}
-        # IA-NA
+
+        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}'))"
+
         if test $offer_ip != 1; then
-            reply=${reply}0003002801020304ffffffffffffffff00050018${offer_ip}ffffffffffffffff
+            local ip_scapy="DHCP6OptIAAddress(addr='${offer_ip}', \
+                            preflft=0xffffffff, validlft=0xffffffff)"
+            rep_scapy="${rep_scapy}/ \
+                       DHCP6OptIA_NA(iaid=0x01020304, T1=0xffffffff, \
+                       T2=0xffffffff, ianaopts=[[${ip_scapy}]])"
         fi
-        if test $ipxe -eq 1; then
-            reply=${reply}003b0011626f6f7466696c655f6e616d655f616c74
-        elif test $ipxe -eq 2; then
-            reply=${reply}003b000d626f6f7466696c655f6e616d65
+        if test -n "$boot_file"; then
+            rep_scapy="${rep_scapy}/DHCP6OptBootFileUrl(optdata=b'${boot_file}')"
         fi
-        # Server identifier
-        reply=${reply}0002000a00030001${server_mac}
+        if test -n "$fqdn"; then
+            rep_scapy="${rep_scapy}/DHCP6OptClientFQDN(flags=0x06, fqdn=b'${fqdn}')"
+        fi
+        rep_scapy="${rep_scapy}/DHCP6OptServerId(duid=DUID_LL(lladdr='00:00:00:10:00:01'))"
+        reply=$(fmt_pkt "${rep_scapy}")
         echo $reply | trim_zeros >> $inport.expected
     else
         for outport; do
@@ -7249,16 +7248,16 @@  echo "---------------------"
 ovn-sbctl list logical_flow
 echo "---------------------"
 
-ovn-sbctl dump-flows > sbflows
+#ovn-sbctl dump-flows > sbflows
 AT_CAPTURE_FILE([sbflows])
 
 echo "------ hv1 dump ----------"
 as hv1 ovs-ofctl dump-flows br-int
 
-src_mac=f00000000001
-src_lla=fe80000000000000f20000fffe000001
-offer_ip=ae700000000000000000000000000004
-test_dhcpv6 1 $src_mac $src_lla 01 $offer_ip 0
+src_mac="f0:00:00:00:00:01"
+src_lla="fe80::f200:00ff:fe00:0001"
+offer_ip="ae70::4"
+test_dhcpv6 1 $src_mac $src_lla 1 $offer_ip "" ""
 
 # NXT_RESUMEs should be 1.
 OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
@@ -7281,12 +7280,12 @@  rm  1.expected
 reset_pcap_file hv1-vif1 hv1/vif1
 reset_pcap_file hv1-vif2 hv1/vif2
 
-src_mac=f00000000002
-src_lla=fe80000000000000f20000fffe000002
-offer_ip=ae700000000000000000000000000005
+src_mac="f0:00:00:00:00:02"
+src_lla="fe80::f200:00ff:fe00:0002"
+offer_ip="ae70::5"
 # Set invalid msg_type
 
-test_dhcpv6 2 $src_mac $src_lla 10 0 0 1 1
+test_dhcpv6 2 $src_mac $src_lla 16 0 "" "" 1 1
 
 # NXT_RESUMEs should be 2.
 OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
@@ -7305,9 +7304,9 @@  AT_CHECK([cat 1.packets], [0], [expout])
 # There should be no DHCPv6 reply from ovn-controller and the request packet
 # should be received by ls2-lp2.
 
-src_mac=f00000000003
-src_lla=fe80000000000000f20000fffe000003
-test_dhcpv6 3 $src_mac $src_lla 01 0 0 4
+src_mac="f0:00:00:00:00:03"
+src_lla="fe80::f200:00ff:fe00:0003"
+test_dhcpv6 3 $src_mac $src_lla 1 0 "" "" 4
 
 # NXT_RESUMEs should be 2 only.
 OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
@@ -7323,10 +7322,10 @@  AT_CHECK([cat 4.packets], [0], [expout])
 
 # Send DHCPv6 packet on ls1-lp3. native DHCPv6 works as stateless mode for this port.
 # The DHCPv6 reply shouldn't contain offer_ip.
-src_mac=f00000000022
-src_lla=fe80000000000000f20000fffe000022
+src_mac="f0:00:00:00:00:22"
+src_lla="fe80::f200:00ff:fe00:0022"
 reset_pcap_file hv1-vif5 hv1/vif5
-test_dhcpv6 5 $src_mac $src_lla 01 1 0 5
+test_dhcpv6 5 $src_mac $src_lla 1 1 "" "" 5
 
 # NXT_RESUMEs should be 3.
 OVS_WAIT_UNTIL([test 3 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
@@ -7338,11 +7337,11 @@  AT_CHECK([cat 5.packets | cut -c 1-120,125- ], [0], [expout])
 
 # Send DHCPv6 information request (code 11) on ls1-lp3. The DHCPv6 reply
 # shouldn't contain offer_ip
-src_mac=f00000000022
-src_lla=fe80000000000000f20000fffe000022
+src_mac="f0:00:00:00:00:22"
+src_lla="fe80::f200:00ff:fe00:0022"
 reset_pcap_file hv1-vif5 hv1/vif5
 rm -f 5.expected
-test_dhcpv6 5 $src_mac $src_lla 0b 1 0 5
+test_dhcpv6 5 $src_mac $src_lla 11 1 "" "" 5
 
 # NXT_RESUMEs should be 4.
 OVS_WAIT_UNTIL([test 4 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
@@ -7363,11 +7362,11 @@  ovn-nbctl lsp-set-dhcpv6-options ls1-lp2 ${d1}
 
 reset_pcap_file hv1-vif2 hv1/vif2
 
-src_mac=f00000000002
-src_lla=fe80000000000000f20000fffe000002
-offer_ip=ae700000000000000000000000000005
+src_mac="f0:00:00:00:00:02"
+src_lla="fe80::f200:00ff:fe00:0002"
+offer_ip="ae70::5"
 
-test_dhcpv6 2 $src_mac $src_lla 01 $offer_ip 1
+test_dhcpv6 2 $src_mac $src_lla 1 $offer_ip "bootfile_name_alt" ""
 # NXT_RESUMEs should be 5.
 OVS_WAIT_UNTIL([test 5 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
@@ -7381,7 +7380,7 @@  AT_CHECK([cat 2.packets | cut -c 1-120,125- ], [0], [expout])
 reset_pcap_file hv1-vif2 hv1/vif2
 rm 2.packets 2.expected
 
-test_dhcpv6 2 $src_mac $src_lla 01 $offer_ip 2
+test_dhcpv6 2 $src_mac $src_lla 1 $offer_ip "bootfile_name" ""
 # NXT_RESUMEs should be 6.
 OVS_WAIT_UNTIL([test 6 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
 
@@ -7389,6 +7388,24 @@  $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap | trim_zeros > 2.pa
 cat 2.expected | cut -c 1-120,125- > expout
 AT_CHECK([cat 2.packets | cut -c 1-120,125- ], [0], [expout])
 
+ovn-nbctl --all destroy dhcp-option
+d1="$(ovn-nbctl create DHCP_Options cidr="ae70\:\:/64")"
+ovn-nbctl dhcp-options-set-options $d1 \
+    server_id=00:00:00:10:00:01 \
+    fqdn=\"ovn.org\"
+ovn-nbctl --wait=hv lsp-set-dhcpv6-options ls1-lp2 ${d1}
+
+reset_pcap_file hv1-vif2 hv1/vif2
+rm 2.packets 2.expected
+
+test_dhcpv6 2 $src_mac $src_lla 1 $offer_ip "" "ovn.org"
+# NXT_RESUMEs should be 7.
+OVS_WAIT_UNTIL([test 7 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])
+
+$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap | trim_zeros > 2.packets
+cat 2.expected | cut -c 1-120,125- > expout
+AT_CHECK([cat 2.packets | cut -c 1-120,125- ], [0], [expout])
+
 OVN_CLEANUP([hv1])
 
 AT_CLEANUP
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index ce9213c1d..6c7754eac 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -207,6 +207,7 @@  create_gen_opts(struct hmap *dhcp_opts, struct hmap *dhcpv6_opts,
     dhcp_opt_add(dhcpv6_opts, "domain_search",  24, "str");
     dhcp_opt_add(dhcpv6_opts, "bootfile_name", 59, "str");
     dhcp_opt_add(dhcpv6_opts, "bootfile_name_alt", 254, "str");
+    dhcp_opt_add(dhcpv6_opts, "fqdn", 39, "domain");
 
     /* IPv6 ND RA options. */
     hmap_init(nd_ra_opts);