diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index a90ee73d6..b15c9e530 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -269,6 +269,19 @@ static void pinctrl_ip_mcast_handle_igmp(
     const struct match *md,
     struct ofpbuf *userdata);
 
+static void init_ipv6_prefixd(void);
+static void destroy_ipv6_prefixd(void);
+static void ipv6_prefixd_wait(long long int timeout);
+static void
+prepare_ipv6_prefix_req(struct ovsdb_idl_index *sbrec_port_binding_by_datapath,
+                        struct ovsdb_idl_index *sbrec_port_binding_by_name,
+                        const struct hmap *local_datapaths,
+                        const struct sbrec_chassis *chassis,
+                        const struct sset *active_tunnels)
+    OVS_REQUIRES(pinctrl_mutex);
+static void
+send_ipv6_prefix_msg(struct rconn *swconn, long long int *send_prefixd_time)
+    OVS_REQUIRES(pinctrl_mutex);
 static bool may_inject_pkts(void);
 
 static void init_put_vport_bindings(void);
@@ -440,6 +453,7 @@ pinctrl_init(void)
     init_put_mac_bindings();
     init_send_garps_rarps();
     init_ipv6_ras();
+    init_ipv6_prefixd();
     init_buffered_packets_map();
     init_event_table();
     ip_mcast_snoop_init();
@@ -526,6 +540,55 @@ set_actions_and_enqueue_msg(struct rconn *swconn,
     ofpbuf_uninit(&ofpacts);
 }
 
+static struct shash ipv6_prefixd;
+
+enum {
+    PREFIX_SOLICIT,
+    PREFIX_PENDING,
+    PREFIX_DONE,
+};
+
+struct ipv6_prefixd_state {
+    long long int next_announce;
+    struct in6_addr ipv6_addr;
+    struct eth_addr ea;
+    int64_t port_key;
+    int64_t metadata;
+    struct in6_addr prefix;
+    unsigned plife_time;
+    unsigned vlife_time;
+    unsigned t1;
+    unsigned t2;
+    int8_t plen;
+    int state;
+};
+
+static void
+init_ipv6_prefixd(void)
+{
+    shash_init(&ipv6_prefixd);
+}
+
+static void
+ipv6_prefixd_delete(struct ipv6_prefixd_state *pfd)
+{
+    if (pfd) {
+        free(pfd);
+    }
+}
+
+static void
+destroy_ipv6_prefixd(void)
+{
+    struct shash_node *iter, *next;
+    SHASH_FOR_EACH_SAFE (iter, next, &ipv6_prefixd) {
+        struct ipv6_prefixd_state *pfd = iter->data;
+        ipv6_prefixd_delete(pfd);
+        shash_delete(&ipv6_prefixd, iter);
+    }
+    shash_destroy(&ipv6_prefixd);
+}
+
 struct buffer_info {
     struct ofpbuf ofpacts;
     struct dp_packet *p;
@@ -949,6 +1012,251 @@ pinctrl_handle_tcp_reset(struct rconn *swconn, const struct flow *ip_flow,
     dp_packet_uninit(&packet);
 }
 
+static void
+pinctrl_parse_dhcv6_advt(struct rconn *swconn, const struct flow *ip_flow,
+                         struct dp_packet *pkt_in, const struct match *md,
+                         struct ofpbuf *userdata)
+{
+    struct udp_header *udp_in = dp_packet_l4(pkt_in);
+    unsigned char *in_dhcpv6_data = (unsigned char *)(udp_in + 1);
+    size_t dlen = MIN(ntohs(udp_in->udp_len), dp_packet_l4_size(pkt_in));
+    uint8_t *data, *end = (uint8_t *)udp_in + dlen;
+    int len = 0;
+
+    data = xmalloc(dlen);
+    if (!data) {
+        return;
+    }
+
+    in_dhcpv6_data += 4;
+
+    while (in_dhcpv6_data < end) {
+        struct dhcpv6_opt_header *in_opt =
+             (struct dhcpv6_opt_header *)in_dhcpv6_data;
+        int opt_len = sizeof *in_opt + ntohs(in_opt->len);
+
+        if (dlen < opt_len + len) {
+            goto out;
+        }
+
+        switch (ntohs(in_opt->code)) {
+        case DHCPV6_OPT_IA_PD: {
+            int orig_len = len, hdr_len = 0, size = sizeof *in_opt + 12;
+
+            memcpy(&data[len], in_opt, size);
+            in_opt = (struct dhcpv6_opt_header *)(in_dhcpv6_data + size);
+            len += size;
+
+            while (size < opt_len) {
+                int flen = sizeof *in_opt + ntohs(in_opt->len);
+
+                if (dlen < flen + len) {
+                    goto out;
+                }
+
+                if (ntohs(in_opt->code) == DHCPV6_OPT_IA_PREFIX) {
+                    memcpy(&data[len], in_opt, flen);
+                    hdr_len += flen;
+                    len += flen;
+                }
+                if (ntohs(in_opt->code) == DHCPV6_OPT_STATUS_CODE) {
+                   struct dhcpv6_opt_status *status;
+
+                   status = (struct dhcpv6_opt_status *)in_opt;
+                   if (ntohs(status->status_code)) {
+                       goto out;
+                   }
+                }
+                size += flen;
+                in_opt = (struct dhcpv6_opt_header *)(in_dhcpv6_data + size);
+            }
+            in_opt = (struct dhcpv6_opt_header *)&data[orig_len];
+            in_opt->len = htons(hdr_len + 12);
+            break;
+        }
+        case DHCPV6_OPT_SERVER_ID_CODE:
+        case DHCPV6_OPT_CLIENT_ID_CODE:
+            memcpy(&data[len], in_opt, opt_len);
+            len += opt_len;
+            break;
+        default:
+            break;
+        }
+        in_dhcpv6_data += opt_len;
+    }
+
+    uint64_t packet_stub[256 / 8];
+    struct dp_packet packet;
+
+    dp_packet_use_stub(&packet, packet_stub, sizeof packet_stub);
+    eth_compose(&packet, ip_flow->dl_dst, ip_flow->dl_src, ETH_TYPE_IPV6,
+                IPV6_HEADER_LEN);
+
+    struct udp_header *udp_h = compose_ipv6(&packet, IPPROTO_UDP,
+                                            &ip_flow->ipv6_src,
+                                            &ip_flow->ipv6_dst, 0, 0, 255,
+                                            len + UDP_HEADER_LEN + 4);
+    udp_h->udp_len = htons(len + UDP_HEADER_LEN + 4);
+    udp_h->udp_csum = 0;
+    packet_set_udp_port(&packet, htons(546), htons(547));
+
+    unsigned char *dhcp_hdr = (unsigned char *)(udp_h + 1);
+    *dhcp_hdr = DHCPV6_MSG_TYPE_REQUEST;
+    memcpy(dhcp_hdr + 4, data, len);
+
+    uint32_t csum = packet_csum_pseudoheader6(dp_packet_l3(&packet));
+    csum = csum_continue(csum, udp_h, dp_packet_size(&packet) -
+                         ((const unsigned char *)udp_h -
+                          (const unsigned char *)dp_packet_eth(&packet)));
+    udp_h->udp_csum = csum_finish(csum);
+    if (!udp_h->udp_csum) {
+        udp_h->udp_csum = htons(0xffff);
+    }
+
+    if (ip_flow->vlans[0].tci & htons(VLAN_CFI)) {
+        eth_push_vlan(&packet, htons(ETH_TYPE_VLAN_8021Q),
+                      ip_flow->vlans[0].tci);
+    }
+
+    set_actions_and_enqueue_msg(swconn, &packet, md, userdata);
+    dp_packet_uninit(&packet);
+
+out:
+    free(data);
+}
+
+static struct ipv6_prefixd_state *
+pinctrl_find_prefixd_state(const struct flow *ip_flow)
+{
+    struct shash_node *iter;
+
+    SHASH_FOR_EACH (iter, &ipv6_prefixd) {
+        struct ipv6_prefixd_state *pfd = iter->data;
+        if (IN6_ARE_ADDR_EQUAL(&pfd->ipv6_addr, &ip_flow->ipv6_dst) &&
+            eth_addr_equals(pfd->ea, ip_flow->dl_dst)) {
+            return pfd;
+        }
+    }
+    return NULL;
+}
+
+static void
+pinctrl_prefixd_state_handler(const struct flow *ip_flow,
+                              struct in6_addr addr, char prefix_len,
+                              unsigned t1, unsigned t2,
+                              unsigned plife_time, unsigned vlife_time)
+{
+    struct ipv6_prefixd_state *pfd;
+
+    pfd = pinctrl_find_prefixd_state(ip_flow);
+    if (pfd) {
+        pfd->state = PREFIX_PENDING;
+        pfd->plife_time = plife_time;
+        pfd->vlife_time = vlife_time;
+        pfd->plen = prefix_len;
+        pfd->prefix = addr;
+        pfd->t1 = t1;
+        pfd->t2 = t2;
+    }
+}
+
+static void
+pinctrl_parse_dhcv6_reply(struct dp_packet *pkt_in,
+                          const struct flow *ip_flow)
+    OVS_REQUIRES(pinctrl_mutex)
+{
+    struct udp_header *udp_in = dp_packet_l4(pkt_in);
+    unsigned char *in_dhcpv6_data = (unsigned char *)(udp_in + 1);
+    size_t dlen = MIN(ntohs(udp_in->udp_len), dp_packet_l4_size(pkt_in));
+    unsigned t1 = 0, t2 = 0, vlife_time = 0, plife_time = 0;
+    uint8_t *end = (uint8_t *)udp_in + dlen;
+    uint8_t prefix_len = 0;
+    struct in6_addr ipv6;
+    bool status = false;
+
+    memset(&ipv6, 0, sizeof (struct in6_addr));
+    in_dhcpv6_data += 4;
+    while (in_dhcpv6_data < end) {
+        struct dhcpv6_opt_header *in_opt =
+             (struct dhcpv6_opt_header *)in_dhcpv6_data;
+        int opt_len = sizeof *in_opt + ntohs(in_opt->len);
+
+        if (in_dhcpv6_data + opt_len > end) {
+            break;
+        }
+
+        switch (ntohs(in_opt->code)) {
+        case DHCPV6_OPT_IA_PD: {
+            int size = sizeof *in_opt + 12;
+            in_opt = (struct dhcpv6_opt_header *)(in_dhcpv6_data + size);
+            struct dhcpv6_opt_ia_na *ia_na =
+                (struct dhcpv6_opt_ia_na *)in_dhcpv6_data;
+
+            t1 = ntohl(ia_na->t1);
+            t2 = ntohl(ia_na->t2);
+            if (t1 > t2 && t2 > 0) {
+                break;
+            }
+
+            while (size < opt_len) {
+                if (ntohs(in_opt->code) == DHCPV6_OPT_IA_PREFIX) {
+                    struct dhcpv6_opt_ia_prefix *ia_hdr =
+                        (struct dhcpv6_opt_ia_prefix *)(in_dhcpv6_data + size);
+
+                    prefix_len = ia_hdr->plen;
+                    plife_time = ntohl(ia_hdr->plife_time);
+                    vlife_time = ntohl(ia_hdr->vlife_time);
+                    memcpy(&ipv6, &ia_hdr->ipv6, sizeof (struct in6_addr));
+                }
+                if (ntohs(in_opt->code) == DHCPV6_OPT_STATUS_CODE) {
+                   struct dhcpv6_opt_status *status_hdr;
+
+                   status_hdr = (struct dhcpv6_opt_status *)in_opt;
+                   status = ntohs(status_hdr->status_code) == 0;
+                }
+                size += sizeof *in_opt + ntohs(in_opt->len);
+                in_opt = (struct dhcpv6_opt_header *)(in_dhcpv6_data + size);
+            }
+            break;
+        }
+        default:
+            break;
+        }
+        in_dhcpv6_data += opt_len;
+    }
+    if (status) {
+        pinctrl_prefixd_state_handler(ip_flow, ipv6, prefix_len,
+                                      t1, t2, plife_time, vlife_time);
+    }
+}
+
+static void
+pinctrl_handle_dhcp6_server(struct rconn *swconn, const struct flow *ip_flow,
+                            struct dp_packet *pkt_in, const struct match *md,
+                            struct ofpbuf *userdata)
+{
+    if (ip_flow->dl_type != htons(ETH_TYPE_IPV6) ||
+        ip_flow->nw_proto != IPPROTO_UDP) {
+        return;
+    }
+
+    struct udp_header *udp_in = dp_packet_l4(pkt_in);
+    unsigned char *dhcp_hdr = (unsigned char *)(udp_in + 1);
+
+    switch (*dhcp_hdr) {
+    case DHCPV6_MSG_TYPE_ADVT:
+        pinctrl_parse_dhcv6_advt(swconn, ip_flow, pkt_in, md, userdata);
+        break;
+    case DHCPV6_MSG_TYPE_REPLY:
+        ovs_mutex_lock(&pinctrl_mutex);
+        pinctrl_parse_dhcv6_reply(pkt_in, ip_flow);
+        ovs_mutex_unlock(&pinctrl_mutex);
+        break;
+    default:
+        break;
+    }
+}
+
 /* Called with in the pinctrl_handler thread context. */
 static void
 pinctrl_handle_put_dhcp_opts(
@@ -1980,6 +2288,10 @@ process_packet_in(struct rconn *swconn, const struct ofp_header *msg)
         pinctrl_handle_bind_vport(&pin.flow_metadata.flow, &userdata);
         ovs_mutex_unlock(&pinctrl_mutex);
         break;
+    case ACTION_OPCODE_DHCP6_SERVER:
+        pinctrl_handle_dhcp6_server(swconn, &headers, &packet,
+                                    &pin.flow_metadata, &userdata);
+        break;
 
     default:
         VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
@@ -2050,6 +2362,7 @@ pinctrl_handler(void *arg_)
     static long long int send_garp_rarp_time = LLONG_MAX;
     /* Next multicast query (IGMP) in ms. */
     static long long int send_mcast_query_time = LLONG_MAX;
+    static long long int send_prefixd_time = LLONG_MAX;
 
     swconn = rconn_create(5, 0, DSCP_DEFAULT, 1 << OFP13_VERSION);
 
@@ -2103,6 +2416,7 @@ pinctrl_handler(void *arg_)
                 ovs_mutex_lock(&pinctrl_mutex);
                 send_garp_rarp_run(swconn, &send_garp_rarp_time);
                 send_ipv6_ras(swconn, &send_ipv6_ra_time);
+                send_ipv6_prefix_msg(swconn, &send_prefixd_time);
                 send_mac_binding_buffered_pkts(swconn);
                 ovs_mutex_unlock(&pinctrl_mutex);
 
@@ -2115,6 +2429,7 @@ pinctrl_handler(void *arg_)
         send_garp_rarp_wait(send_garp_rarp_time);
         ipv6_ra_wait(send_ipv6_ra_time);
         ip_mcast_querier_wait(send_mcast_query_time);
+        ipv6_prefixd_wait(send_prefixd_time);
 
         new_seq = seq_read(pinctrl_handler_seq);
         seq_wait(pinctrl_handler_seq, new_seq);
@@ -2165,6 +2480,9 @@ pinctrl_run(struct ovsdb_idl_txn *ovnsb_idl_txn,
                            sbrec_port_binding_by_name, br_int, chassis,
                            local_datapaths, active_tunnels);
     prepare_ipv6_ras(local_datapaths);
+    prepare_ipv6_prefix_req(sbrec_port_binding_by_datapath,
+                            sbrec_port_binding_by_name, local_datapaths,
+                            chassis, active_tunnels);
     sync_dns_cache(dns_table);
     controller_event_run(ovnsb_idl_txn, ce_table, chassis);
     ip_mcast_sync(ovnsb_idl_txn, chassis, local_datapaths,
@@ -2507,6 +2825,149 @@ send_ipv6_ras(struct rconn *swconn, long long int *send_ipv6_ra_time)
     }
 }
 
+static void
+compose_prefixd_solicit(struct dp_packet *b,
+                        const struct eth_addr eth_src,
+                        const struct eth_addr eth_dst,
+                        const struct in6_addr *ipv6_src,
+                        const struct in6_addr *ipv6_dst)
+{
+    eth_compose(b, eth_dst, eth_src, ETH_TYPE_IPV6, IPV6_HEADER_LEN);
+
+    int len = UDP_HEADER_LEN + 4 + sizeof(struct dhcpv6_opt_server_id) +
+              sizeof(struct dhcpv6_opt_ia_na);
+    struct udp_header *udp_h = compose_ipv6(b, IPPROTO_UDP, ipv6_src,
+                                            ipv6_dst, 0, 0, 255, len);
+    udp_h->udp_len = htons(len);
+    udp_h->udp_csum = 0;
+    packet_set_udp_port(b, htons(546), htons(547));
+
+    unsigned char *dhcp_hdr = (unsigned char *)(udp_h + 1);
+    *dhcp_hdr = DHCPV6_MSG_TYPE_SOLICIT;
+
+    struct dhcpv6_opt_server_id *opt_client_id =
+        (struct dhcpv6_opt_server_id *)(dhcp_hdr + 4);
+    opt_client_id->opt.code = htons(DHCPV6_OPT_CLIENT_ID_CODE);
+    opt_client_id->opt.len = htons(sizeof(struct dhcpv6_opt_server_id) -
+                                   sizeof(struct dhcpv6_opt_header));
+    opt_client_id->duid_type = htons(DHCPV6_DUID_LL);
+    opt_client_id->hw_type = htons(DHCPV6_HW_TYPE_ETH);
+    opt_client_id->mac = eth_src;
+
+    struct dhcpv6_opt_ia_na *ia_pd =
+            (struct dhcpv6_opt_ia_na *)(opt_client_id + 1);
+    ia_pd->opt.code = htons(DHCPV6_OPT_IA_PD);
+    ia_pd->opt.len = htons(sizeof(struct dhcpv6_opt_ia_na) -
+                           sizeof(struct dhcpv6_opt_header));
+    ia_pd->iaid = htonl(1);
+    ia_pd->t1 = htonl(0xffffffff);
+    ia_pd->t2 = htonl(0xffffffff);
+
+    uint32_t csum = packet_csum_pseudoheader6(dp_packet_l3(b));
+    csum = csum_continue(csum, udp_h, dp_packet_size(b) -
+                         ((const unsigned char *)udp_h -
+                          (const unsigned char *)dp_packet_eth(b)));
+    udp_h->udp_csum = csum_finish(csum);
+    if (!udp_h->udp_csum) {
+        udp_h->udp_csum = htons(0xffff);
+    }
+}
+
+#define IPV6_PREFIXD_TIMEOUT    3000LL
+static long long int
+ipv6_prefixd_send(struct rconn *swconn, struct ipv6_prefixd_state *pfd)
+{
+    long long int cur_time = time_msec();
+    if (cur_time < pfd->next_announce) {
+        return pfd->next_announce;
+    }
+
+    uint64_t packet_stub[256 / 8];
+    struct dp_packet packet;
+
+    struct eth_addr eth_dst;
+    eth_dst = (struct eth_addr) ETH_ADDR_C(33,33,00,01,00,02);
+    struct in6_addr ipv6_dst;
+    ipv6_parse("ff02::1:2", &ipv6_dst);
+
+    dp_packet_use_stub(&packet, packet_stub, sizeof packet_stub);
+    compose_prefixd_solicit(&packet, pfd->ea, eth_dst, &pfd->ipv6_addr,
+                            &ipv6_dst);
+
+    uint64_t ofpacts_stub[4096 / 8];
+    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
+
+    /* Set MFF_LOG_DATAPATH and MFF_LOG_INPORT. */
+    uint32_t dp_key = pfd->metadata;
+    uint32_t port_key = pfd->port_key;
+    put_load(dp_key, MFF_LOG_DATAPATH, 0, 64, &ofpacts);
+    put_load(port_key, MFF_LOG_INPORT, 0, 32, &ofpacts);
+    put_load(1, MFF_LOG_FLAGS, MLF_LOCAL_ONLY_BIT, 1, &ofpacts);
+    struct ofpact_resubmit *resubmit = ofpact_put_RESUBMIT(&ofpacts);
+    resubmit->in_port = OFPP_CONTROLLER;
+    resubmit->table_id = OFTABLE_LOG_INGRESS_PIPELINE;
+
+    struct ofputil_packet_out po = {
+        .packet = dp_packet_data(&packet),
+        .packet_len = dp_packet_size(&packet),
+        .buffer_id = UINT32_MAX,
+        .ofpacts = ofpacts.data,
+        .ofpacts_len = ofpacts.size,
+    };
+
+    match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER);
+    enum ofp_version version = rconn_get_version(swconn);
+    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+    queue_msg(swconn, ofputil_encode_packet_out(&po, proto));
+    dp_packet_uninit(&packet);
+    ofpbuf_uninit(&ofpacts);
+    pfd->next_announce = cur_time + IPV6_PREFIXD_TIMEOUT;
+
+    return pfd->next_announce;
+}
+
+static bool ipv6_prefixd_should_inject(void)
+{
+    struct shash_node *iter;
+
+    SHASH_FOR_EACH (iter, &ipv6_prefixd) {
+        struct ipv6_prefixd_state *pfd = iter->data;
+        if (pfd->state == PREFIX_SOLICIT) {
+            return true;
+        }
+        if (pfd->state == PREFIX_DONE &&
+            pfd->next_announce < time_msec()) {
+            pfd->state = PREFIX_SOLICIT;
+            return true;
+        }
+    }
+    return false;
+}
+
+static void
+ipv6_prefixd_wait(long long int timeout)
+{
+    if (ipv6_prefixd_should_inject()) {
+        poll_timer_wait_until(timeout);
+    }
+}
+
+static void
+send_ipv6_prefix_msg(struct rconn *swconn, long long int *send_prefixd_time)
+    OVS_REQUIRES(pinctrl_mutex)
+{
+    struct shash_node *iter;
+
+    *send_prefixd_time = LLONG_MAX;
+    SHASH_FOR_EACH (iter, &ipv6_prefixd) {
+        struct ipv6_prefixd_state *pfd = iter->data;
+        long long int next_msg = ipv6_prefixd_send(swconn, pfd);
+        if (*send_prefixd_time > next_msg) {
+            *send_prefixd_time = next_msg;
+        }
+    }
+}
+
 /* Called by pinctrl_run(). Runs with in the main ovn-controller
  * thread context. */
 static void
@@ -2584,6 +3045,96 @@ prepare_ipv6_ras(const struct hmap *local_datapaths)
 
 }
 
+static void
+prepare_ipv6_prefix_req(struct ovsdb_idl_index *sbrec_port_binding_by_datapath,
+                        struct ovsdb_idl_index *sbrec_port_binding_by_name,
+                        const struct hmap *local_datapaths,
+                        const struct sbrec_chassis *chassis,
+                        const struct sset *active_tunnels)
+    OVS_REQUIRES(pinctrl_mutex)
+{
+    const struct local_datapath *ld;
+    struct ipv6_prefixd_state *pfd;
+    bool changed = false;
+    int i;
+
+    HMAP_FOR_EACH (ld, hmap_node, local_datapaths) {
+        struct sbrec_port_binding *target = sbrec_port_binding_index_init_row(
+            sbrec_port_binding_by_datapath);
+        sbrec_port_binding_index_set_datapath(target, ld->datapath);
+
+        struct sbrec_port_binding *pb;
+        SBREC_PORT_BINDING_FOR_EACH_EQUAL (pb, target,
+                                           sbrec_port_binding_by_datapath) {
+            if (!smap_get_bool(&pb->options, "ipv6_prefix_delegation",
+                               false)) {
+                continue;
+            }
+
+            const char *peer_s = smap_get(&pb->options, "peer");
+            if (!peer_s) {
+                continue;
+            }
+
+            const struct sbrec_port_binding *peer
+                = lport_lookup_by_name(sbrec_port_binding_by_name, peer_s);
+            if (!peer) {
+                continue;
+            }
+
+            char *redirect_name = xasprintf("cr-%s", pb->logical_port);
+            bool resident = lport_is_chassis_resident(
+                    sbrec_port_binding_by_name, chassis, active_tunnels,
+                    redirect_name);
+            free(redirect_name);
+            if (!resident || strcmp(pb->type, "l3gateway")) {
+                continue;
+            }
+
+            struct lport_addresses laddrs;
+            for (i = 0; i < pb->n_mac; i++) {
+                if (extract_lsp_addresses(pb->mac[i], &laddrs) &&
+                    laddrs.n_ipv6_addrs > 0 &&
+                    !in6_is_lla(&laddrs.ipv6_addrs[0].addr)) {
+                        break;
+                    }
+            }
+            if (i == pb->n_mac) {
+                continue;
+            }
+
+            pfd = shash_find_data(&ipv6_prefixd, pb->logical_port);
+            if (!pfd) {
+                pfd = xzalloc(sizeof *pfd);
+                pfd->ipv6_addr = laddrs.ipv6_addrs[0].addr;
+                pfd->ea = laddrs.ea;
+                pfd->metadata = peer->datapath->tunnel_key;
+                pfd->port_key = peer->tunnel_key;
+                shash_add(&ipv6_prefixd, pb->logical_port, pfd);
+                pfd->next_announce = time_msec() + IPV6_PREFIXD_TIMEOUT;
+                changed = true;
+            } else if (pfd->state == PREFIX_PENDING) {
+                char prefix_str[INET6_ADDRSTRLEN + 1] = {};
+                struct smap options;
+
+                pfd->state = PREFIX_DONE;
+                pfd->next_announce = time_msec() + pfd->t1 * 1000;
+                ipv6_string_mapped(prefix_str, &pfd->prefix);
+                smap_clone(&options, &pb->options);
+                smap_add_format(&options, "ipv6_ra_pd_list", "%s/%d",
+                                prefix_str, pfd->plen);
+                sbrec_port_binding_set_options(pb, &options);
+                smap_destroy(&options);
+            }
+        }
+        sbrec_port_binding_index_destroy_row(target);
+    }
+
+    if (changed) {
+        notify_pinctrl_handler();
+    }
+}
+
 /* Called by pinctrl_run(). Runs with in the main ovn-controller
  * thread context. */
 void
@@ -2606,6 +3157,7 @@ pinctrl_destroy(void)
     free(pinctrl.br_int_name);
     destroy_send_garps_rarps();
     destroy_ipv6_ras();
+    destroy_ipv6_prefixd();
     destroy_buffered_packets_map();
     event_table_destroy();
     destroy_put_mac_bindings();
@@ -4093,6 +4645,7 @@ may_inject_pkts(void)
 {
     return (!shash_is_empty(&ipv6_ras) ||
             !shash_is_empty(&send_garp_rarp_data) ||
+            ipv6_prefixd_should_inject() ||
             !ovs_list_is_empty(&mcast_query_list) ||
             !ovs_list_is_empty(&buffered_mac_bindings));
 }
diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index f4997e9c9..578292a16 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -88,7 +88,8 @@ struct ovn_extend_table;
     OVNACT(OVNFIELD_LOAD,     ovnact_load)            \
     OVNACT(CHECK_PKT_LARGER,  ovnact_check_pkt_larger) \
     OVNACT(TRIGGER_EVENT,     ovnact_controller_event) \
-    OVNACT(BIND_VPORT,        ovnact_bind_vport)
+    OVNACT(BIND_VPORT,        ovnact_bind_vport)      \
+    OVNACT(DHCP6_SERVER_PKT,  ovnact_nest)
 
 /* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
 enum OVS_PACKED_ENUM ovnact_type {
@@ -537,6 +538,12 @@ enum action_opcode {
      *    MFF_LOG_INPORT.
      */
     ACTION_OPCODE_BIND_VPORT,
+
+    /* dhcp6_server_pkt { ...actions ...}."
+     *
+     *  The actions, in OpenFlow 1.3 format, follow the action_header.
+     */
+    ACTION_OPCODE_DHCP6_SERVER,
 };
 
 /* Header. */
diff --git a/lib/actions.c b/lib/actions.c
index a999a4fda..31fce0685 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -2172,6 +2172,26 @@ ovnact_put_opts_free(struct ovnact_put_opts *pdo)
     free_gen_options(pdo->options, pdo->n_options);
 }
 
+static void
+parse_DHCP6_SERVER_PKT(struct action_context *ctx)
+{
+    parse_nested_action(ctx, OVNACT_DHCP6_SERVER_PKT, "ip6");
+}
+
+static void
+format_DHCP6_SERVER_PKT(const struct ovnact_nest *nest, struct ds *s)
+{
+    format_nested_action(nest, "dhcp6_server_pkt", s);
+}
+
+static void
+encode_DHCP6_SERVER_PKT(const struct ovnact_nest *on,
+                        const struct ovnact_encode_params *ep,
+                        struct ofpbuf *ofpacts)
+{
+    encode_nested_actions(on, ep, ACTION_OPCODE_DHCP6_SERVER, ofpacts);
+}
+
 static void
 parse_SET_QUEUE(struct action_context *ctx)
 {
@@ -2931,6 +2951,8 @@ parse_action(struct action_context *ctx)
         parse_trigger_event(ctx, ovnact_put_TRIGGER_EVENT(ctx->ovnacts));
     } else if (lexer_match_id(ctx->lexer, "bind_vport")) {
         parse_bind_vport(ctx);
+    } else if (lexer_match_id(ctx->lexer, "dhcp6_server_pkt")) {
+        parse_DHCP6_SERVER_PKT(ctx);
     } else {
         lexer_syntax_error(ctx->lexer, "expecting action");
     }
diff --git a/lib/ovn-l7.h b/lib/ovn-l7.h
index 5fc370bf5..3178f475c 100644
--- a/lib/ovn-l7.h
+++ b/lib/ovn-l7.h
@@ -174,8 +174,11 @@ struct dhcp_opt6_header {
 #define DHCPV6_OPT_SERVER_ID_CODE        2
 #define DHCPV6_OPT_IA_NA_CODE            3
 #define DHCPV6_OPT_IA_ADDR_CODE          5
+#define DHCPV6_OPT_STATUS_CODE           13
 #define DHCPV6_OPT_DNS_SERVER_CODE       23
 #define DHCPV6_OPT_DOMAIN_SEARCH_CODE    24
+#define DHCPV6_OPT_IA_PD                 25
+#define DHCPV6_OPT_IA_PREFIX             26
 
 #define DHCPV6_OPT_SERVER_ID \
     DHCP_OPTION("server_id", DHCPV6_OPT_SERVER_ID_CODE, "mac")
@@ -242,6 +245,22 @@ struct ovs_nd_dnssl {
 };
 BUILD_ASSERT_DECL(ND_DNSSL_OPT_LEN == sizeof(struct ovs_nd_dnssl));
 
+OVS_PACKED(
+struct dhcpv6_opt_ia_prefix {
+    struct dhcpv6_opt_header opt;
+    ovs_be32 plife_time;
+    ovs_be32 vlife_time;
+    uint8_t plen;
+    struct in6_addr ipv6;
+});
+
+OVS_PACKED(
+struct dhcpv6_opt_status {
+    struct dhcpv6_opt_header opt;
+    ovs_be16 status_code;
+    uint8_t msg[];
+});
+
 #define DHCPV6_DUID_LL      3
 #define DHCPV6_HW_TYPE_ETH  1
 
diff --git a/tests/ovn.at b/tests/ovn.at
index 410f4b514..1f28af906 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1468,6 +1468,12 @@ bind_vport("xyzzy",;
 bind_vport("xyzzy", inport;
     Syntax error at `;' expecting `)'.
 
+# prefix delegation
+dhcp6_server_pkt{};
+    formats as dhcp6_server_pkt { drop; };
+    encodes as controller(userdata=00.00.00.12.00.00.00.00)
+    has prereqs ip6
+
 # Miscellaneous negative tests.
 ;
     Syntax error at `;'.
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index ea64dc673..1d99651d2 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -2221,6 +2221,8 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
 
         case OVNACT_BIND_VPORT:
             break;
+        case OVNACT_DHCP6_SERVER_PKT:
+            break;
         }
     }
     ds_destroy(&s);
