diff mbox

[ovs-dev,v3,1/2] ovn-controller: Add 'dns_lookup' action

Message ID 20170327141258.5501-1-nusiddiq@redhat.com
State Changes Requested
Headers show

Commit Message

Numan Siddique March 27, 2017, 2:12 p.m. UTC
From: Numan Siddique <nusiddiq@redhat.com>

This patch adds a new OVN action 'dns_lookup' to support native DNS.
ovn-controller parses this action and adds a NXT_PACKET_IN2
OF flow with 'pause' flag set.

A new table 'DNS' is added in the SB DB to look up and resolve
the DNS queries. When a valid DNS packet is received by
ovn-controller, it looks up the DNS name in the 'DNS' table
and if successful, it frames a DNS reply, resumes the packet
and stores 1 in the 1-bit subfield. If the packet is invalid
or cannot be resolved, it resumes the packet without any
modifications and stores 0 in the 1-bit subfield.

reg0[4] = dns_lookup(); next;

An upcoming patch will use this action and adds logical flows.

Signed-off-by: Numan Siddique <nusiddiq@redhat.com>
---
 include/ovn/actions.h     |  17 +++-
 ovn/controller/pinctrl.c  | 248 +++++++++++++++++++++++++++++++++++++++++++++-
 ovn/lib/actions.c         |  52 ++++++++++
 ovn/lib/ovn-util.h        |  19 ++++
 ovn/ovn-sb.ovsschema      |  18 +++-
 ovn/ovn-sb.xml            |  68 +++++++++++++
 ovn/utilities/ovn-sbctl.c |   3 +
 ovn/utilities/ovn-trace.c |   5 +
 tests/ovn.at              |   7 ++
 9 files changed, 429 insertions(+), 8 deletions(-)
diff mbox

Patch

diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index d2510fd..0e66ee8 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -70,7 +70,8 @@  struct simap;
     OVNACT(PUT_ND,        ovnact_put_mac_bind)      \
     OVNACT(PUT_DHCPV4_OPTS, ovnact_put_dhcp_opts)   \
     OVNACT(PUT_DHCPV6_OPTS, ovnact_put_dhcp_opts)   \
-    OVNACT(SET_QUEUE,       ovnact_set_queue)
+    OVNACT(SET_QUEUE,       ovnact_set_queue)       \
+    OVNACT(DNS_LOOKUP,      ovnact_dns_lookup)
 
 /* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
 enum OVS_PACKED_ENUM ovnact_type {
@@ -258,6 +259,12 @@  struct ovnact_set_queue {
     uint16_t queue_id;
 };
 
+/* OVNACT_DNS_LOOKUP. */
+struct ovnact_dns_lookup {
+    struct ovnact ovnact;
+    struct expr_field dst;      /* 1-bit destination field. */
+};
+
 /* Internal use by the helpers below. */
 void ovnact_init(struct ovnact *, enum ovnact_type, size_t len);
 void *ovnact_put(struct ofpbuf *, enum ovnact_type, size_t len);
@@ -385,6 +392,14 @@  enum action_opcode {
      *   - Any number of DHCPv6 options.
      */
     ACTION_OPCODE_PUT_DHCPV6_OPTS,
+
+    /* "result = dns_lookup()".
+     * Arguments follow the action_header, in this format:
+     *   - A 32-bit or 64-bit OXM header designating the result field.
+     *   - A 32-bit integer specifying a bit offset within the result field.
+     *
+     */
+    ACTION_OPCODE_DNS_LOOKUP,
 };
 
 /* Header. */
diff --git a/ovn/controller/pinctrl.c b/ovn/controller/pinctrl.c
index b342189..ec7fbd5 100644
--- a/ovn/controller/pinctrl.c
+++ b/ovn/controller/pinctrl.c
@@ -661,7 +661,242 @@  exit:
 }
 
 static void
-process_packet_in(const struct ofp_header *msg)
+pinctrl_handle_dns_lookup(
+    struct dp_packet *pkt_in, struct ofputil_packet_in *pin,
+    struct ofpbuf *userdata, struct ofpbuf *continuation,
+    struct controller_ctx *ctx)
+{
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+    enum ofp_version version = rconn_get_version(swconn);
+    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+    struct dp_packet *pkt_out_ptr = NULL;
+    uint32_t success = 0;
+
+    /* Parse result field. */
+    const struct mf_field *f;
+    enum ofperr ofperr = nx_pull_header(userdata, NULL, &f, NULL);
+    if (ofperr) {
+       VLOG_WARN_RL(&rl, "bad result OXM (%s)", ofperr_to_string(ofperr));
+       goto exit;
+    }
+
+    /* Parse result offset. */
+    ovs_be32 *ofsp = ofpbuf_try_pull(userdata, sizeof *ofsp);
+    if (!ofsp) {
+        VLOG_WARN_RL(&rl, "offset not present in the userdata");
+        goto exit;
+    }
+
+    /* Check that the result is valid and writable. */
+    struct mf_subfield dst = { .field = f, .ofs = ntohl(*ofsp), .n_bits = 1 };
+    ofperr = mf_check_dst(&dst, NULL);
+    if (ofperr) {
+        VLOG_WARN_RL(&rl, "bad result bit (%s)", ofperr_to_string(ofperr));
+        goto exit;
+    }
+
+    /* Extract the DNS header */
+    struct dns_header const *in_dns_header = dp_packet_get_udp_payload(pkt_in);
+
+    /* Check if it is DNS request or not */
+    if (in_dns_header->lo_flag & 0x80) {
+        /* It's a DNS response packet which we are not interested in */
+        goto exit;
+    }
+
+    /* Check if at least one query request is present */
+    if (!in_dns_header->qdcount) {
+        goto exit;
+    }
+
+    struct udp_header *in_udp = dp_packet_l4(pkt_in);
+    size_t udp_len = ntohs(in_udp->udp_len);
+    size_t l4_len = dp_packet_l4_size(pkt_in);
+    uint8_t *end = (uint8_t *)in_udp + MIN(udp_len, l4_len);
+    uint8_t *in_dns_data = (uint8_t *)(in_dns_header + 1);
+    uint8_t *in_hostname = in_dns_data;
+    uint8_t idx = 0;
+    struct ds hostname;
+    ds_init(&hostname);
+    /* Extract the hostname. If the hostname is - 'www.ovn.org' it would be
+     * encoded as (in hex) - 03 77 77 77 03 6f 76 63 03 6f 72 67 00.
+     */
+    bool hostname_present = true;
+    while ((in_dns_data + idx) < end && in_dns_data[idx]) {
+        uint8_t label_len = in_dns_data[idx++];
+        if (in_dns_data + idx + label_len > end) {
+            hostname_present = false;
+            break;
+        }
+        for (uint8_t i = 0; i < label_len; i++) {
+            ds_put_char(&hostname, in_dns_data[idx++]);
+        }
+        ds_put_char(&hostname, '.');
+    }
+
+    if (!hostname_present) {
+        ds_destroy(&hostname);
+        goto exit;
+    }
+
+    idx++;
+    ds_chomp(&hostname, '.');
+    ds_put_char(&hostname, 0);
+    in_dns_data += idx;
+
+    /* Query should have TYPE and CLASS fields */
+    if (in_dns_data + (2 * sizeof(ovs_be16)) > end) {
+        ds_destroy(&hostname);
+        goto exit;
+    }
+
+    uint16_t query_type = ntohs(*ALIGNED_CAST(const ovs_be16 *, in_dns_data));
+    /* Supported query types - A, AAAA and ANY */
+    if (!(query_type == DNS_QUERY_TYPE_A || query_type == DNS_QUERY_TYPE_AAAA
+          || query_type == DNS_QUERY_TYPE_ANY)) {
+        ds_destroy(&hostname);
+        goto exit;
+    }
+
+    uint64_t dp_key = ntohll(pin->flow_metadata.flow.metadata);
+    const struct sbrec_dns *sbrec_dns;
+    SBREC_DNS_FOR_EACH(sbrec_dns, ctx->ovnsb_idl) {
+        if (sbrec_dns->datapath->tunnel_key == dp_key
+            && !strcmp(sbrec_dns->hostname, ds_cstr(&hostname))) {
+            break;
+        }
+    }
+
+    ds_destroy(&hostname);
+    if (!sbrec_dns) {
+        goto exit;
+    }
+
+    uint16_t ancount = 0;
+    uint64_t dns_ans_stub[128 / 8];
+    struct ofpbuf dns_answer = OFPBUF_STUB_INITIALIZER(dns_ans_stub);
+
+    for (size_t i = 0; i < sbrec_dns->n_ip_addresses; i++) {
+        ovs_be32 ip4;
+        if (ip_parse(sbrec_dns->ip_addresses[i], &ip4)) {
+            if (query_type == DNS_QUERY_TYPE_A ||
+                query_type == DNS_QUERY_TYPE_ANY) {
+                /* Copy the answer section */
+                /* Format of the answer section is
+                 *  - NAME     -> The domain name
+                 *  - TYPE     -> 2 octets containing one of the RR type codes
+                 *  - CLASS    -> 2 octets which specify the class of the data
+                 *                in the RDATA field.
+                 *  - TTL      -> 32 bit unsigned int specifying the time
+                 *                interval (in secs) that the resource record
+                 *                 may be cached before it should be discarded.
+                 *  - RDLENGTH -> 16 bit integer specifying the length of the
+                 *                RDATA field.
+                 *  - RDATA    -> a variable length string of octets that
+                 *                describes the resource. In our case it will
+                 *                be IP address of the domain name.
+                 */
+                ofpbuf_put(&dns_answer, in_hostname, idx);
+                ovs_be16 v = htons(DNS_QUERY_TYPE_A);
+                ofpbuf_put(&dns_answer, &v, sizeof(ovs_be16));
+                v = htons(DNS_CLASS_IN);
+                ofpbuf_put(&dns_answer, &v, sizeof(ovs_be16));
+                ovs_be32 ttl = htonl(DNS_DEFAULT_RR_TTL);
+                ofpbuf_put(&dns_answer, &ttl, sizeof(ovs_be32));
+                v = htons(sizeof(ip4)); /* Length of the RDATA field */
+                ofpbuf_put(&dns_answer, &v, sizeof(ovs_be16));
+                ofpbuf_put(&dns_answer, &ip4, sizeof(ovs_be32));
+                ancount++;
+            }
+            continue;
+        }
+
+        struct in6_addr ip6;
+        if (ipv6_parse(sbrec_dns->ip_addresses[i], &ip6) &&
+            (query_type == DNS_QUERY_TYPE_AAAA ||
+             query_type == DNS_QUERY_TYPE_ANY)) {
+            ofpbuf_put(&dns_answer, in_hostname, idx);
+            ovs_be16 v = htons(DNS_QUERY_TYPE_AAAA);
+            ofpbuf_put(&dns_answer, &v, sizeof(ovs_be16));
+            v = htons(DNS_CLASS_IN);
+            ofpbuf_put(&dns_answer, &v, sizeof(ovs_be16));
+            ovs_be32 ttl = htonl(DNS_DEFAULT_RR_TTL);
+            ofpbuf_put(&dns_answer, &ttl, sizeof(ovs_be32));
+            v = htons(sizeof(ip6.s6_addr)); /* Length of the RDATA field */
+            ofpbuf_put(&dns_answer, &v, sizeof(ovs_be16));
+            ofpbuf_put(&dns_answer, &ip6.s6_addr, sizeof(ip6.s6_addr));
+            ancount++;
+        }
+    }
+
+    if (!ancount) {
+        ofpbuf_uninit(&dns_answer);
+        goto exit;
+    }
+
+    uint16_t new_l4_size = ntohs(in_udp->udp_len) +  dns_answer.size;
+    size_t new_packet_size = pkt_in->l4_ofs + new_l4_size;
+    struct dp_packet pkt_out;
+    dp_packet_init(&pkt_out, new_packet_size);
+    dp_packet_clear(&pkt_out);
+    dp_packet_prealloc_tailroom(&pkt_out, new_packet_size);
+    pkt_out_ptr = &pkt_out;
+
+    /* Copy the L2 and L3 headers from the pkt_in as they would remain same.*/
+    dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, pkt_in->l4_ofs), pkt_in->l4_ofs);
+
+    pkt_out.l2_5_ofs = pkt_in->l2_5_ofs;
+    pkt_out.l2_pad_size = pkt_in->l2_pad_size;
+    pkt_out.l3_ofs = pkt_in->l3_ofs;
+    pkt_out.l4_ofs = pkt_in->l4_ofs;
+
+    struct udp_header *out_udp = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, UDP_HEADER_LEN), UDP_HEADER_LEN);
+
+    /* Copy the DNS header. */
+    struct dns_header *out_dns_header = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, sizeof *out_dns_header),
+        sizeof *out_dns_header);
+
+    /* Set the response bit to 1 in the flags. */
+    out_dns_header->lo_flag |= 0x80;
+
+    /* Set the answer RR. */
+    out_dns_header->ancount = htons(ancount);
+
+    /* Copy the Query section. */
+    dp_packet_put(&pkt_out, dp_packet_data(pkt_in), dp_packet_size(pkt_in));
+
+    /* Copy the answer sections. */
+    dp_packet_put(&pkt_out, dns_answer.data, dns_answer.size);
+    ofpbuf_uninit(&dns_answer);
+
+    out_udp->udp_len = htons(new_l4_size);
+    out_udp->udp_csum = 0;
+
+    struct ip_header *out_ip = dp_packet_l3(&pkt_out);
+    out_ip->ip_tot_len = htons(pkt_out.l4_ofs - pkt_out.l3_ofs + new_l4_size);
+    /* Checksum needs to be initialized to zero. */
+    out_ip->ip_csum = 0;
+    out_ip->ip_csum = csum(out_ip, sizeof *out_ip);
+
+    pin->packet = dp_packet_data(&pkt_out);
+    pin->packet_len = dp_packet_size(&pkt_out);
+
+    success = 1;
+exit:
+    if (!ofperr) {
+        union mf_subvalue sv;
+        sv.u8_val = success;
+        mf_write_subfield(&dst, &sv, &pin->flow_metadata);
+    }
+    queue_msg(ofputil_encode_resume(pin, continuation, proto));
+    dp_packet_uninit(pkt_out_ptr);
+}
+
+static void
+process_packet_in(const struct ofp_header *msg, struct controller_ctx *ctx)
 {
     static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
 
@@ -720,6 +955,10 @@  process_packet_in(const struct ofp_header *msg)
                                        &continuation);
         break;
 
+    case ACTION_OPCODE_DNS_LOOKUP:
+        pinctrl_handle_dns_lookup(&packet, &pin, &userdata, &continuation, ctx);
+        break;
+
     default:
         VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
                      ntohl(ah->opcode));
@@ -728,7 +967,8 @@  process_packet_in(const struct ofp_header *msg)
 }
 
 static void
-pinctrl_recv(const struct ofp_header *oh, enum ofptype type)
+pinctrl_recv(const struct ofp_header *oh, enum ofptype type,
+             struct controller_ctx *ctx)
 {
     if (type == OFPTYPE_ECHO_REQUEST) {
         queue_msg(make_echo_reply(oh));
@@ -740,7 +980,7 @@  pinctrl_recv(const struct ofp_header *oh, enum ofptype type)
         config.miss_send_len = UINT16_MAX;
         set_switch_config(swconn, &config);
     } else if (type == OFPTYPE_PACKET_IN) {
-        process_packet_in(oh);
+        process_packet_in(oh, ctx);
     } else if (type != OFPTYPE_ECHO_REPLY && type != OFPTYPE_BARRIER_REPLY) {
         if (VLOG_IS_DBG_ENABLED()) {
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(30, 300);
@@ -786,7 +1026,7 @@  pinctrl_run(struct controller_ctx *ctx, const struct lport_index *lports,
             enum ofptype type;
 
             ofptype_decode(&type, oh);
-            pinctrl_recv(oh, type);
+            pinctrl_recv(oh, type, ctx);
             ofpbuf_delete(msg);
         }
     }
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index fff838b..71f49c1 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -1708,6 +1708,55 @@  static void
 ovnact_set_queue_free(struct ovnact_set_queue *a OVS_UNUSED)
 {
 }
+
+static void
+parse_dns_lookup(struct action_context *ctx, const struct expr_field *dst,
+                 struct ovnact_dns_lookup *dl)
+{
+    lexer_get(ctx->lexer); /* Skip dns_lookup. */
+    lexer_get(ctx->lexer); /* Skip '('. */
+    if (!lexer_match(ctx->lexer, LEX_T_RPAREN)) {
+        lexer_error(ctx->lexer, "dns_lookup doesn't take any parameters");
+        return;
+    }
+    /* Validate that the destination is a 1-bit, modifiable field. */
+    char *error = expr_type_check(dst, 1, true);
+    if (error) {
+        lexer_error(ctx->lexer, "%s", error);
+        free(error);
+        return;
+    }
+    dl->dst = *dst;
+    add_prerequisite(ctx, "udp");
+}
+
+static void
+format_DNS_LOOKUP(const struct ovnact_dns_lookup *dl, struct ds *s)
+{
+    expr_field_format(&dl->dst, s);
+    ds_put_cstr(s, " = dns_lookup();");
+}
+
+static void
+encode_DNS_LOOKUP(const struct ovnact_dns_lookup *dl,
+                  const struct ovnact_encode_params *ep OVS_UNUSED,
+                  struct ofpbuf *ofpacts)
+{
+    struct mf_subfield dst = expr_resolve_field(&dl->dst);
+
+    size_t oc_offset = encode_start_controller_op(ACTION_OPCODE_DNS_LOOKUP,
+                                                  true, ofpacts);
+    nx_put_header(ofpacts, dst.field->id, OFP13_VERSION, false);
+    ovs_be32 ofs = htonl(dst.ofs);
+    ofpbuf_put(ofpacts, &ofs, sizeof ofs);
+    encode_finish_controller_op(oc_offset, ofpacts);
+}
+
+
+static void
+ovnact_dns_lookup_free(struct ovnact_dns_lookup *dl OVS_UNUSED)
+{
+}
 
 /* Parses an assignment or exchange or put_dhcp_opts action. */
 static void
@@ -1731,6 +1780,9 @@  parse_set_action(struct action_context *ctx)
                    && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
             parse_put_dhcp_opts(ctx, &lhs, ovnact_put_PUT_DHCPV6_OPTS(
                                     ctx->ovnacts));
+        } else if (!strcmp(ctx->lexer->token.s, "dns_lookup")
+                   && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
+            parse_dns_lookup(ctx, &lhs, ovnact_put_DNS_LOOKUP(ctx->ovnacts));
         } else {
             parse_assignment_action(ctx, false, &lhs);
         }
diff --git a/ovn/lib/ovn-util.h b/ovn/lib/ovn-util.h
index 30b27b1..fb5f5c0 100644
--- a/ovn/lib/ovn-util.h
+++ b/ovn/lib/ovn-util.h
@@ -64,4 +64,23 @@  char *alloc_nat_zone_key(const struct uuid *key, const char *type);
 const char *default_nb_db(void);
 const char *default_sb_db(void);
 
+#define DNS_HEADER_LEN 12
+struct dns_header {
+    ovs_be16 id;
+    uint8_t lo_flag; /* QR (1), OPCODE (4), AA (1), TC (1) and RD (1) */
+    uint8_t hi_flag; /* RA (1), Z (3) and RCODE (4) */
+    ovs_be16 qdcount; /* Num of entries in the question section. */
+    ovs_be16 ancount; /* Num of resource records in the answer section. */
+    ovs_be16 nscount; /* Num of name server records in the authority record section. */
+    ovs_be16 arcount; /* Num of resource records in the additional records section. */
+};
+
+BUILD_ASSERT_DECL(DNS_HEADER_LEN == sizeof(struct dns_header));
+
+#define DNS_QUERY_TYPE_A        0x01
+#define DNS_QUERY_TYPE_AAAA     0x1c
+#define DNS_QUERY_TYPE_ANY      0xff
+
+#define DNS_CLASS_IN            0x1
+#define DNS_DEFAULT_RR_TTL      3600
 #endif
diff --git a/ovn/ovn-sb.ovsschema b/ovn/ovn-sb.ovsschema
index 0212a5e..15dd961 100644
--- a/ovn/ovn-sb.ovsschema
+++ b/ovn/ovn-sb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Southbound",
-    "version": "1.9.0",
-    "cksum": "2240045372 9719",
+    "version": "1.10.0",
+    "cksum": "2103396660 10290",
     "tables": {
         "SB_Global": {
             "columns": {
@@ -198,4 +198,16 @@ 
                                           "value": "string",
                                           "min": 0,
                                           "max": "unlimited"}}},
-            "maxRows": 1}}}
+            "maxRows": 1},
+        "DNS": {
+            "columns": {
+                "hostname": {"type": "string"},
+                "ip_addresses": {"type": {"key": "string",
+                                 "min": 0,
+                                 "max": "unlimited"}},
+                "datapath": {"type": {"key": {"type": "uuid",
+                                               "refTable": "Datapath_Binding"},
+                                       "min": 1,
+                                       "max": 1}}},
+            "indexes": [["hostname", "datapath"]],
+            "isRoot": true}}}
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 4e95c80..e0e4f67 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1463,6 +1463,49 @@ 
             packet in that connection.
           </p>
         </dd>
+
+        <dt>
+          <code><var>R</var> = dns_lookup();</code>
+        </dt>
+
+        <dd>
+          <p>
+            <b>Parameters</b>: No parameters.
+          </p>
+
+          <p>
+            <b>Result</b>: stored to a 1-bit subfield <var>R</var>.
+          </p>
+
+          <p>
+            Valid only in the ingress pipeline.
+          </p>
+
+          <p>
+            When this action is applied to a valid DNS (UDP packet with
+            udp.src 53) request packet, it changes the packet into a DNS reply
+            if it is able to resolve the query and stores 1 in <var>R</var>.
+            It leaves the packet unchanged if it is not able to resolve and
+            stores 0 in <var>R</var>.
+          </p>
+
+          <p>
+            When this action is applied to a non-DNS or an invalid
+            DNS request packet , it leaves the packet unchanged and stores
+            0 in <var>R</var>.
+          </p>
+
+          <p>
+            The contents of the <ref table="DNS"/> table control the DNS lookup.
+          </p>
+
+          <p>
+            <b>Example:</b>
+            <code>
+              reg0[3] = dns_lookup();
+            </code>
+          </p>
+        </dd>
       </dl>
 
       <p>
@@ -2653,4 +2696,29 @@  tcp.flags = RST;
       <column name="external_ids"/>
     </group>
   </table>
+  <table name="DNS" title="Native DNS resolution">
+    <p>
+      Each row in this table stores the internal DNS resolution data.
+      <code>OVN</code> supports a simple native internal DNS
+      resolution. To resolve the DNS names, it looks for the
+      <ref column="hostname"/>  in this table and if found, resolves it to the
+      IPv4/IPv6 addresses defined based on the query type (A/AAAA/ANY).
+      If no match is found, the DNS request is expected to be handled by an
+      external DNS resolver.
+    </p>
+
+    <column name="datapath">
+      The logical data path to which the <ref column="hostname"/> and the IP
+      addresses belong to.
+    </column>
+
+    <column name="hostname">
+      The host name to be searched.
+    </column>
+
+    <column name="ip_addresses">
+      The IP addresses to include in the DNS answer fields if the
+      <ref column="hostname"/> matches in the DNS query.
+    </column>
+  </table>
 </database>
diff --git a/ovn/utilities/ovn-sbctl.c b/ovn/utilities/ovn-sbctl.c
index 4e3cbad..21b88d6 100644
--- a/ovn/utilities/ovn-sbctl.c
+++ b/ovn/utilities/ovn-sbctl.c
@@ -1055,6 +1055,9 @@  static const struct ctl_table_class tables[SBREC_N_TABLES] = {
 
     [SBREC_TABLE_SSL].row_ids[0] =
     {&sbrec_table_sb_global, NULL, &sbrec_sb_global_col_ssl},
+
+    [SBREC_TABLE_DNS].row_ids[0] =
+    {&sbrec_table_dns, NULL, &sbrec_dns_col_hostname},
 };
 
 
diff --git a/ovn/utilities/ovn-trace.c b/ovn/utilities/ovn-trace.c
index 66844b1..2975e20 100644
--- a/ovn/utilities/ovn-trace.c
+++ b/ovn/utilities/ovn-trace.c
@@ -1542,6 +1542,11 @@  trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
              * though, it would be easy enough to track the queue information
              * by adjusting uflow->skb_priority. */
             break;
+
+        case OVNACT_DNS_LOOKUP:
+            ovntrace_node_append(super, OVNTRACE_NODE_ERROR,
+                                 "*** dns_lookup action not implemented");
+            break;
         }
 
     }
diff --git a/tests/ovn.at b/tests/ovn.at
index bbbec90..4b4beb0 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1027,6 +1027,13 @@  set_queue(61440);
 set_queue(65535);
     Queue ID 65535 for set_queue is not in valid range 0 to 61440.
 
+# dns_lookup
+reg1[0] = dns_lookup();
+    encodes as controller(userdata=00.00.00.06.00.00.00.00.00.01.de.10.00.00.00.40,pause)
+    has prereqs udp
+reg1[0] = dns_lookup("hostname");
+    dns_lookup doesn't take any parameters
+
 # Contradictionary prerequisites (allowed but not useful):
 ip4.src = ip6.src[0..31];
     encodes as move:NXM_NX_IPV6_SRC[0..31]->NXM_OF_IP_SRC[]