From patchwork Thu Apr 28 13:51:41 2016 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Numan Siddique X-Patchwork-Id: 616271 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from archives.nicira.com (archives.nicira.com [96.126.127.54]) by ozlabs.org (Postfix) with ESMTP id 3qwdY11xdJz9t7M for ; Thu, 28 Apr 2016 23:51:49 +1000 (AEST) Received: from archives.nicira.com (localhost [127.0.0.1]) by archives.nicira.com (Postfix) with ESMTP id 9092710566; Thu, 28 Apr 2016 06:51:48 -0700 (PDT) X-Original-To: dev@openvswitch.org Delivered-To: dev@openvswitch.org Received: from mx3v3.cudamail.com (mx3.cudamail.com [64.34.241.5]) by archives.nicira.com (Postfix) with ESMTPS id 9A6E110535 for ; Thu, 28 Apr 2016 06:51:46 -0700 (PDT) Received: from bar6.cudamail.com (localhost [127.0.0.1]) by mx3v3.cudamail.com (Postfix) with ESMTPS id 32EBE162AA5 for ; Thu, 28 Apr 2016 07:51:46 -0600 (MDT) X-ASG-Debug-ID: 1461851505-0b32374eda3dd020001-byXFYA Received: from mx3-pf2.cudamail.com ([192.168.14.1]) by bar6.cudamail.com with ESMTP id sPStS0zcoaPhhULR (version=TLSv1 cipher=DHE-RSA-AES256-SHA bits=256 verify=NO) for ; Thu, 28 Apr 2016 07:51:45 -0600 (MDT) X-Barracuda-Envelope-From: nusiddiq@redhat.com X-Barracuda-RBL-Trusted-Forwarder: 192.168.14.1 Received: from unknown (HELO mx1.redhat.com) (209.132.183.28) by mx3-pf2.cudamail.com with ESMTPS (DHE-RSA-AES256-SHA encrypted); 28 Apr 2016 13:51:45 -0000 Received-SPF: pass (mx3-pf2.cudamail.com: SPF record at _spf1.redhat.com designates 209.132.183.28 as permitted sender) X-Barracuda-Apparent-Source-IP: 209.132.183.28 X-Barracuda-RBL-IP: 209.132.183.28 Received: from int-mx13.intmail.prod.int.phx2.redhat.com (int-mx13.intmail.prod.int.phx2.redhat.com [10.5.11.26]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 2B64A7AE8E for ; Thu, 28 Apr 2016 13:51:44 +0000 (UTC) Received: from nusiddiq.blr.redhat.com (dhcp-0-108.blr.redhat.com [10.70.1.108]) by int-mx13.intmail.prod.int.phx2.redhat.com (8.14.4/8.14.4) with ESMTP id u3SDpg6A022438 for ; Thu, 28 Apr 2016 09:51:42 -0400 X-CudaMail-Envelope-Sender: nusiddiq@redhat.com From: Numan Siddique X-CudaMail-Whitelist-To: dev@openvswitch.org X-CudaMail-MID: CM-V2-427014691 X-CudaMail-DTE: 042816 X-CudaMail-Originating-IP: 209.132.183.28 To: ovs dev X-ASG-Orig-Subj: [##CM-V2-427014691##][PATCH v3 2/2] ovn: Add logical flows to support native DHCP In-Reply-To: <57221528.3060508@redhat.com> Organization: Red Hat Message-ID: <5722156D.7010602@redhat.com> Date: Thu, 28 Apr 2016 19:21:41 +0530 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Thunderbird/38.6.0 MIME-Version: 1.0 X-Scanned-By: MIMEDefang 2.68 on 10.5.11.26 X-Barracuda-Connect: UNKNOWN[192.168.14.1] X-Barracuda-Start-Time: 1461851505 X-Barracuda-Encrypted: DHE-RSA-AES256-SHA X-Barracuda-URL: https://web.cudamail.com:443/cgi-mod/mark.cgi X-ASG-Whitelist: Header =?UTF-8?B?eFwtY3VkYW1haWxcLXdoaXRlbGlzdFwtdG8=?= X-Barracuda-BRTS-Status: 1 X-Virus-Scanned: by bsmtpd at cudamail.com Subject: [ovs-dev] [PATCH v3 2/2] ovn: Add logical flows to support native DHCP X-BeenThere: dev@openvswitch.org X-Mailman-Version: 2.1.16 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@openvswitch.org Sender: "dev" OVN implements a native DHCP support which caters to the common use case of providing an IP address to a booting instance by providing stateless replies to DHCP requests based on statically configured address mappings. To do this it allows a short list of DHCP options to be configured and applied at each compute host running ovn-controller. A new table 'Subnet' is added in OVN NB DB to store the DHCP options. A logical flow is added for each logical port to handle DHCP packets using the 'dhcp_offer' action if the CMS has defined DHCP options in the 'Subnet' column. Signed-Off-by: Numan Siddique Acked-by: Ryan Moats Tested-by: Ryan Moats --- ovn/northd/ovn-northd.8.xml | 79 ++++++++++-- ovn/northd/ovn-northd.c | 282 +++++++++++++++++++++++++++++++++++++++--- ovn/ovn-nb.ovsschema | 19 ++- ovn/ovn-nb.xml | 185 ++++++++++++++++++++++++++- ovn/utilities/ovn-nbctl.8.xml | 29 +++++ ovn/utilities/ovn-nbctl.c | 198 +++++++++++++++++++++++++++++ tests/ovn.at | 187 ++++++++++++++++++++++++++++ tests/test-ovn-dhcp.c | 154 +++++++++++++++++++++++ 8 files changed, 1101 insertions(+), 32 deletions(-) diff --git a/ovn/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml index 1cd8072..d98daf8 100644 --- a/ovn/northd/ovn-northd.8.xml +++ b/ovn/northd/ovn-northd.8.xml @@ -223,17 +223,57 @@ -

Ingress Table 3: from-lport Pre-ACLs

+

Ingress Table 3: DHCP

- Ingress table 3 prepares flows for possible stateful ACL processing + Ingress table 3 implements DHCP responder for the logical ports + configured with IPv4 address(es) and DHCP options. +

    +
  • +

    + A priority-100 logical flow is added for these logical ports + which match dhcp packets (udp.src = 68 and udp.dst = 67) + and replies with the DHCP REQUEST/ACK packet with the configured + DHCP options. +

    + +
    +dhcp_offer(DHCP OPTIONS)
    +eth.dst = eth.src;
    +eth.src = E;
    +ip4.dst = O;
    +ip4.src = S;
    +udp.src = 67;
    +udp.dst = 68;
    +outport = P;
    +inport = ""; /* Allow sending out inport. */
    +output;
    +          
    + +

    + Where E is the server mac address and S is the + server IPv4 address defined in the DHCP options and O is + the IPv4 address defined in the logical port's addresses column. +

    +
  • + +
  • + A priority-0 flow that matches all packets to advances to table 4. +
  • +
+

+ +

Ingress Table 4: from-lport Pre-ACLs

+ +

+ Ingress table 4 prepares flows for possible stateful ACL processing in table 4. It contains a priority-0 flow that simply moves traffic to table 4. If stateful ACLs are used in the logical datapath, a priority-100 flow is added that sends IP packets to the connection tracker before advancing to table 4.

-

Ingress table 4: from-lport ACLs

+

Ingress table 5: from-lport ACLs

Logical flows in this table closely reproduce those in the @@ -249,7 +289,7 @@

- Ingress table 4 also contains a priority 0 flow with action + Ingress table 5 also contains a priority 0 flow with action next;, so that ACLs allow packets by default. If the logical datapath has a statetful ACL, the following flows will also be added: @@ -281,7 +321,7 @@ -

Ingress Table 5: ARP responder

+

Ingress Table 6: ARP responder

This table implements ARP responder for known IPs. It contains these @@ -326,7 +366,7 @@ output; -

Ingress Table 6: Destination Lookup

+

Ingress Table 7: Destination Lookup

This table implements switching behavior. It contains these logical @@ -357,20 +397,35 @@ output; -

Egress Table 0: to-lport Pre-ACLs

+

Egress Table 0: DHCP

+

+

    +
  • + This table adds a priority-100 flow to skip the ACL stages + for the DHCP REQUEST/ACK packets from the Ingress Table 3 DHCP + responder. +
  • + +
  • + A priority-0 flow that matches all packets to advances to table 4. +
  • +
+

+ +

Egress Table 1: to-lport Pre-ACLs

- This is similar to ingress table 3 except for to-lport + This is similar to ingress table 4 except for to-lport traffic.

-

Egress Table 1: to-lport ACLs

+

Egress Table 2: to-lport ACLs

- This is similar to ingress table 4 except for to-lport ACLs. + This is similar to ingress table 5 except for to-lport ACLs.

-

Egress Table 2: Egress Port Security - IP

+

Egress Table 3: Egress Port Security - IP

This is similar to the ingress port security logic in table 1 except @@ -379,7 +434,7 @@ output; eth.src, ip4.src and ip6.src

-

Egress Table 3: Egress Port Security - L2

+

Egress Table 4: Egress Port Security - L2

This is similar to the ingress port security logic in ingress table 0, diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c index d0319d5..39e2077 100644 --- a/ovn/northd/ovn-northd.c +++ b/ovn/northd/ovn-northd.c @@ -26,12 +26,14 @@ #include "hash.h" #include "hmap.h" #include "json.h" +#include "ovn/lib/ovn-dhcp.h" #include "ovn/lib/lex.h" #include "ovn/lib/ovn-nb-idl.h" #include "ovn/lib/ovn-sb-idl.h" #include "packets.h" #include "poll-loop.h" #include "smap.h" +#include "sset.h" #include "stream.h" #include "stream-ssl.h" #include "unixctl.h" @@ -90,16 +92,18 @@ enum ovn_stage { PIPELINE_STAGE(SWITCH, IN, PORT_SEC_L2, 0, "ls_in_port_sec_l2") \ PIPELINE_STAGE(SWITCH, IN, PORT_SEC_IP, 1, "ls_in_port_sec_ip") \ PIPELINE_STAGE(SWITCH, IN, PORT_SEC_ND, 2, "ls_in_port_sec_nd") \ - PIPELINE_STAGE(SWITCH, IN, PRE_ACL, 3, "ls_in_pre_acl") \ - PIPELINE_STAGE(SWITCH, IN, ACL, 4, "ls_in_acl") \ - PIPELINE_STAGE(SWITCH, IN, ARP_RSP, 5, "ls_in_arp_rsp") \ - PIPELINE_STAGE(SWITCH, IN, L2_LKUP, 6, "ls_in_l2_lkup") \ + PIPELINE_STAGE(SWITCH, IN, DHCP, 3, "ls_in_dhcp") \ + PIPELINE_STAGE(SWITCH, IN, PRE_ACL, 4, "ls_in_pre_acl") \ + PIPELINE_STAGE(SWITCH, IN, ACL, 5, "ls_in_acl") \ + PIPELINE_STAGE(SWITCH, IN, ARP_RSP, 6, "ls_in_arp_rsp") \ + PIPELINE_STAGE(SWITCH, IN, L2_LKUP, 7, "ls_in_l2_lkup") \ \ /* Logical switch egress stages. */ \ - PIPELINE_STAGE(SWITCH, OUT, PRE_ACL, 0, "ls_out_pre_acl") \ - PIPELINE_STAGE(SWITCH, OUT, ACL, 1, "ls_out_acl") \ - PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_IP, 2, "ls_out_port_sec_ip") \ - PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_L2, 3, "ls_out_port_sec_l2") \ + PIPELINE_STAGE(SWITCH, OUT, DHCP, 0, "ls_out_dhcp") \ + PIPELINE_STAGE(SWITCH, OUT, PRE_ACL, 1, "ls_out_pre_acl") \ + PIPELINE_STAGE(SWITCH, OUT, ACL, 2, "ls_out_acl") \ + PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_IP, 3, "ls_out_port_sec_ip") \ + PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_L2, 4, "ls_out_port_sec_l2") \ \ /* Logical router ingress stages. */ \ PIPELINE_STAGE(ROUTER, IN, ADMISSION, 0, "lr_in_admission") \ @@ -1367,6 +1371,90 @@ has_stateful_acl(struct ovn_datapath *od) return false; } +static bool +build_dhcp_action(struct ovn_port *op, ovs_be32 offer_ip, struct ds *action) +{ + if(smap_get_bool(&op->nbs->options, "dhcp_disabled", false)) { + /* CMS has disabled native dhcp for this lport */ + return false; + } + + struct nbrec_subnet *subnet = NULL; + ovs_be32 host_ip, mask; + for (size_t i = 0; i < op->od->nbs->n_subnets; i++) { + char *error = ip_parse_masked(op->od->nbs->subnets[i]->cidr, &host_ip, + &mask); + if (!error && !((offer_ip ^ host_ip) & mask)) { + /* offerip belongs to this subnet */ + subnet = op->od->nbs->subnets[i]; + break; + } + free(error); + } + + if (!subnet || !subnet->gateway_ip) { + return false; + } + + struct smap_node *node; + char *server_ip= NULL; + char *server_mac = NULL; + uint8_t options_bmap = 0; + + SMAP_FOR_EACH(node, &subnet->dhcp_options) { + if(!strcmp(node->key, "server_id")) { + options_bmap |= 0x1; + server_ip = node->value; + } else if (!strcmp(node->key, "server_mac")) { + options_bmap |= 0x2; + server_mac = node->value; + } else if (!strcmp(node->key, "lease_time")) { + options_bmap |= 0x4; + } + } + + if ((options_bmap & 0x07) != 0x07) { + /* "server_id", "server_mac" and "lease_time" should be present + * in the dhcp_options. */ + return false; + } + + struct smap dhcp_options = SMAP_INITIALIZER(&dhcp_options); + smap_clone(&dhcp_options, &subnet->dhcp_options); + + /* server_mac is not dhcp option, delete it from the smap */ + smap_remove(&dhcp_options, "server_mac"); + smap_add(&dhcp_options, "router", subnet->gateway_ip); + char *netmask = xasprintf(IP_FMT, IP_ARGS(mask)); + smap_add(&dhcp_options, "netmask", netmask); + free(netmask); + + /* override the dhcp options define in the lport options if any */ + SMAP_FOR_EACH(node, &op->nbs->options) { + if(!strncmp(node->key, "dhcp_opt_", 9)) { + smap_replace(&dhcp_options, &node->key[9], node->value); + } + } + + ds_put_format(action, "dhcp_offer(offerip = "IP_FMT", ", + IP_ARGS(offer_ip)); + SMAP_FOR_EACH(node, &dhcp_options) { + ds_put_format(action, "%s = %s, ", node->key, node->value); + } + + ds_chomp(action, ' '); + ds_chomp(action, ','); + + ds_put_format(action, "); eth.dst = eth.src; eth.src = %s; " + "ip4.dst = "IP_FMT"; ip4.src = %s; udp.src = 67;" + "udp.dst = 68; ", server_mac, IP_ARGS(offer_ip), server_ip); + ds_put_cstr(action, "outport = inport; inport = \"\"; " + "/* Allow sending out inport. */ output;"); + + smap_destroy(&dhcp_options); + return true; +} + static void build_acls(struct ovn_datapath *od, struct hmap *lflows, struct hmap *ports) { @@ -1588,8 +1676,8 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports, } } - /* Ingress table 1 and 2: Port security - IP and ND, by default goto next. - * (priority 0)*/ + /* Ingress table 1 and 2: Port security - IP and ND, tabl3 - DCHP + * by default goto next. (priority 0)*/ HMAP_FOR_EACH (od, key_node, datapaths) { if (!od->nbs) { continue; @@ -1597,9 +1685,54 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports, ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC_ND, 0, "1", "next;"); ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC_IP, 0, "1", "next;"); + ovn_lflow_add(lflows, od, S_SWITCH_IN_DHCP, 0, "1", "next;"); } - /* Ingress table 3: ARP responder, skip requests coming from localnet ports. + /* Logical switch ingress table 3: DHCP priority 50. */ + HMAP_FOR_EACH (op, key_node, ports) { + if (!op->nbs) { + continue; + } + + if (!lport_is_enabled(op->nbs)) { + /* Drop packets from disabled logical ports (since logical flow + * tables are default-drop). */ + continue; + } + + for (size_t i = 0; i < op->nbs->n_addresses; i++) { + struct lport_addresses laddrs; + if (!extract_lport_addresses(op->nbs->addresses[i], &laddrs, + false)) { + continue; + } + + if (!laddrs.n_ipv4_addrs) { + continue; + } + + for (size_t j = 0; j < laddrs.n_ipv4_addrs; j++) { + struct ds action = DS_EMPTY_INITIALIZER; + if (build_dhcp_action(op, laddrs.ipv4_addrs[j].addr, &action)) { + struct ds match = DS_EMPTY_INITIALIZER; + ds_put_format( + &match, "inport == %s && eth.src == "ETH_ADDR_FMT + " && ip4.src == 0.0.0.0 && " + "ip4.dst == 255.255.255.255 && udp.src == 68 && " + "udp.dst == 67", op->json_key, + ETH_ADDR_ARGS(laddrs.ea)); + + ovn_lflow_add(lflows, op->od, S_SWITCH_IN_DHCP, 50, + ds_cstr(&match), ds_cstr(&action)); + ds_destroy(&match); + ds_destroy(&action); + break; + } + } + } + } + + /* Ingress table 6: ARP responder, skip requests coming from localnet ports. * (priority 100). */ HMAP_FOR_EACH (op, key_node, ports) { if (!op->nbs) { @@ -1614,7 +1747,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports, } } - /* Ingress table 5: ARP responder, reply for known IPs. + /* Ingress table 6: ARP responder, reply for known IPs. * (priority 50). */ HMAP_FOR_EACH (op, key_node, ports) { if (!op->nbs) { @@ -1664,7 +1797,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports, } } - /* Ingress table 5: ARP responder, by default goto next. + /* Ingress table 6: ARP responder, by default goto next. * (priority 0)*/ HMAP_FOR_EACH (od, key_node, datapaths) { if (!od->nbs) { @@ -1674,7 +1807,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports, ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_RSP, 0, "1", "next;"); } - /* Ingress table 6: Destination lookup, broadcast and multicast handling + /* Ingress table 7: Destination lookup, broadcast and multicast handling * (priority 100). */ HMAP_FOR_EACH (op, key_node, ports) { if (!op->nbs) { @@ -1694,7 +1827,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports, "outport = \""MC_FLOOD"\"; output;"); } - /* Ingress table 6: Destination lookup, unicast handling (priority 50), */ + /* Ingress table 7: Destination lookup, unicast handling (priority 50), */ HMAP_FOR_EACH (op, key_node, ports) { if (!op->nbs) { continue; @@ -1731,7 +1864,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports, } } - /* Ingress table 6: Destination lookup for unknown MACs (priority 0). */ + /* Ingress table 7: Destination lookup for unknown MACs (priority 0). */ HMAP_FOR_EACH (od, key_node, datapaths) { if (!od->nbs) { continue; @@ -1743,6 +1876,47 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports, } } + /* Egress table 0: dhcp response - Priority 100 flow to skip ACL + * stages for dhcp response from ovn-controller. + * Priority 0 flow to advance to the next stage. + * + * Note: Once the bug reported here [1] is fixed, there is no need + * to skip ACL stages for dhcp response packets. + * [1] - http://openvswitch.org/pipermail/dev/2016-April/069542.html + */ + HMAP_FOR_EACH (od, key_node, datapaths) { + if (!od->nbs) { + continue; + } + + ovn_lflow_add(lflows, od, S_SWITCH_OUT_DHCP, 0, "1", "next;"); + if (!(od->nbs->n_ports && od->nbs->n_subnets)) { + continue; + } + + for (size_t i = 0; i < od->nbs->n_subnets; i++) { + if (!(od->nbs->subnets[i]->enable_dhcp && + od->nbs->subnets[i]->ip_version == 4)) { + continue; + } + + const char *server_id = smap_get( + &od->nbs->subnets[i]->dhcp_options, "server_id"); + const char *server_mac = smap_get( + &od->nbs->subnets[i]->dhcp_options, "server_mac"); + + if (server_id && server_mac) { + struct ds match = DS_EMPTY_INITIALIZER; + ds_put_format(&match, "eth.src == %s && ip4.src == %s &&" + " udp && udp.src == 67 && udp.dst == 68", + server_mac, server_id); + ovn_lflow_add( + lflows, od, S_SWITCH_OUT_DHCP, 100, ds_cstr(&match), + "/* Skip ACL stages for dhcp response */ next(3);"); + } + } + } + /* Egress table 2: Egress port security - IP (priority 0) * port security L2 - multicast/broadcast (priority * 100). */ @@ -2309,6 +2483,74 @@ ovnsb_db_run(struct northd_context *ctx) } +static struct dhcp_opts_map supported_dhcp_opts[] = { + OFFERIP, + DHCP_OPT_NETMASK, + DHCP_OPT_ROUTER, + DHCP_OPT_DNS_SERVER, + DHCP_OPT_LOG_SERVER, + DHCP_OPT_LPR_SERVER, + DHCP_OPT_SWAP_SERVER, + DHCP_OPT_POLICY_FILTER, + DHCP_OPT_ROUTER_SOLICITATION, + DHCP_OPT_NIS_SERVER, + DHCP_OPT_NTP_SERVER, + DHCP_OPT_SERVER_ID, + DHCP_OPT_TFTP_SERVER, + DHCP_OPT_CLASSLESS_STATIC_ROUTE, + DHCP_OPT_IP_FORWARD_ENABLE, + DHCP_OPT_ROUTER_DISCOVERY, + DHCP_OPT_ETHERNET_ENCAP, + DHCP_OPT_DEFAULT_TTL, + DHCP_OPT_TCP_TTL, + DHCP_OPT_MTU, + DHCP_OPT_LEASE_TIME +}; + +static void +check_and_add_supported_dhcp_opts_to_sb_db(struct northd_context *ctx) +{ + static bool nothing_to_add = false; + + if (nothing_to_add) { + return; + } + + struct hmap dhcp_opts_to_add = HMAP_INITIALIZER(&dhcp_opts_to_add); + for (size_t i = 0; (i < sizeof(supported_dhcp_opts) / + sizeof(supported_dhcp_opts[0])); i++) { + hmap_insert(&dhcp_opts_to_add, &supported_dhcp_opts[i].hmap_node, + dhcp_opt_hash(supported_dhcp_opts[i].name)); + } + + const struct sbrec_dhcp_options *opt_row, *opt_row_next; + SBREC_DHCP_OPTIONS_FOR_EACH_SAFE(opt_row, opt_row_next, ctx->ovnsb_idl) { + struct dhcp_opts_map *dhcp_opt = + dhcp_opts_find(&dhcp_opts_to_add, opt_row->name); + if (dhcp_opt) { + hmap_remove(&dhcp_opts_to_add, &dhcp_opt->hmap_node); + } + else { + sbrec_dhcp_options_delete(opt_row); + } + } + + if (!dhcp_opts_to_add.n) { + nothing_to_add = true; + } + + struct dhcp_opts_map *opt; + HMAP_FOR_EACH_POP(opt, hmap_node, &dhcp_opts_to_add) { + struct sbrec_dhcp_options *sbrec_dhcp_option = + sbrec_dhcp_options_insert(ctx->ovnsb_txn); + sbrec_dhcp_options_set_name(sbrec_dhcp_option, opt->name); + sbrec_dhcp_options_set_code(sbrec_dhcp_option, opt->code); + sbrec_dhcp_options_set_type(sbrec_dhcp_option, opt->type); + } + + hmap_destroy(&dhcp_opts_to_add); +} + static char *default_nb_db_; static const char * @@ -2477,6 +2719,10 @@ main(int argc, char *argv[]) add_column_noalert(ovnsb_idl_loop.idl, &sbrec_port_binding_col_options); add_column_noalert(ovnsb_idl_loop.idl, &sbrec_port_binding_col_mac); ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_port_binding_col_chassis); + ovsdb_idl_add_table(ovnsb_idl_loop.idl, &sbrec_table_dhcp_options); + add_column_noalert(ovnsb_idl_loop.idl, &sbrec_dhcp_options_col_code); + add_column_noalert(ovnsb_idl_loop.idl, &sbrec_dhcp_options_col_type); + add_column_noalert(ovnsb_idl_loop.idl, &sbrec_dhcp_options_col_name); /* Main loop. */ exiting = false; @@ -2490,7 +2736,9 @@ main(int argc, char *argv[]) ovnnb_db_run(&ctx); ovnsb_db_run(&ctx); - + if (ctx.ovnsb_txn) { + check_and_add_supported_dhcp_opts_to_sb_db(&ctx); + } unixctl_server_run(unixctl); unixctl_server_wait(unixctl); if (exiting) { diff --git a/ovn/ovn-nb.ovsschema b/ovn/ovn-nb.ovsschema index e3e41e3..693a182 100644 --- a/ovn/ovn-nb.ovsschema +++ b/ovn/ovn-nb.ovsschema @@ -1,7 +1,7 @@ { "name": "OVN_Northbound", - "version": "2.1.0", - "cksum": "2201582413 4513", + "version": "2.2.0", + "cksum": "904993916 5326", "tables": { "Logical_Switch": { "columns": { @@ -16,6 +16,11 @@ "refType": "strong"}, "min": 0, "max": "unlimited"}}, + "subnets": {"type": {"key": {"type": "uuid", + "refTable": "Subnet", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, @@ -63,6 +68,16 @@ "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": false}, + "Subnet": { + "columns": { + "cidr": {"type": "string"}, + "ip_version": {"type": {"key": {"type": "integer", + "enum": ["set", [4, 6]]}}}, + "gateway_ip": {"type": "string"}, + "enable_dhcp": {"type": "boolean"}, + "dhcp_options": {"type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, "Logical_Router": { "columns": { "name": {"type": "string"}, diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml index 843ae4c..3c22e69 100644 --- a/ovn/ovn-nb.xml +++ b/ovn/ovn-nb.xml @@ -73,11 +73,18 @@ Access control rules that apply to packets within the logical switch. + +

+ Subnets configured to the logical switch. +

+ + See External IDs at the beginning of this document. + @@ -212,6 +219,28 @@ interface, in kb. + + +

+ These options apply to logical ports with having + (empty string) +

+ + + Each logical port can override the DHCP options defined in the + of + by defining them in this column with the prefix "dhcp_opt_". + Please see the of + for supported DHCP options. + + Example: key="dhcp_opt_mtu", value="1300" + + + + If this is defined, ovn-northd will disable the native + DHCP responder for the logical port. + +
@@ -605,6 +634,160 @@
+ + +

+ cidr of the subnet. +

+
+ + +

+ IP version of the subnet -4 or 6. +

+
+ + +

+ Gateway ip of the subnet. +

+
+ + +

+ If set to true, native dhcp support will be enabled for all the + logical ports having the IPv4 address from the subnet cidr. +

+
+ + + +

+ OVN implements a native DHCP support which caters to the common + use case of providing an IP address to a booting instance by + providing stateless replies to DHCP requests based on statically + configured address mappings. To do this it allows a short list of + DHCP options to be configured and applied at each compute host + running ovn-controller. +

+ +

+ CMS should define the set of DHCP options as key/value pairs. + The defined DHCP options will be include in the DHCP response to the + DHCP DISCOVER/REQUEST packet from the logical ports having the IPv4 + addresses in the . +

+
+ + +

+ Below are the supported DHCP options whose values are IPv4 address + or addresses. If the value has more than one IPv4 address, then it + should be enclosed within '{}' braces. +

+ + +

+ Example. key="netmask", value="255.255.255.0" +

+
+ + + + + + + + + + + + + + + +

+ This option can contain one or more static routes, each of which + consists of a destination descriptor and the IP address of the + router that should be used to reach that destination. Please see + RFC 3442 for more details. +

+ +

+ Example. + key="classless_static_route" + value="{30.0.0.0/24,10.0.0.10, 0.0.0.0/0,10.0.0.1}" +

+
+ + +

+ eth.src will be set to this value in the DHCP + response packet. +

+
+
+ + + +

+ The value of this DHCP option is of type bool. + + Example. key="ip_forward_enable", value="1" +

+
+ + +

+ The value of this DHCP option is of type bool. +

+
+ + +

+ The value of this DHCP option is of type bool. +

+
+ + +

+ The value of this DHCP option is of type uint8. + + Example. key="default_ttl", value="128". +

+
+ + +

+ The value of this DHCP option is of type uint8. +

+
+ + +

+ The value of this DHCP option is of type uint16. +

+
+ + +

+ The value of this DHCP option is of type uint32. + + Example. key="lease_time", value="42000" +

+
+
+ + +

+ DHCP options "server_id", "server_mac" + and "lease_time" are mandatory options which CMS should + define for OVN to support native DHCP. +

+
+
+
+

Each row represents one L3 logical router. @@ -633,7 +816,7 @@ column is set to false, the router is disabled. A disabled router has all ingress and egress traffic dropped. - + See External IDs at the beginning of this document. diff --git a/ovn/utilities/ovn-nbctl.8.xml b/ovn/utilities/ovn-nbctl.8.xml index 3b337dd..adf8813 100644 --- a/ovn/utilities/ovn-nbctl.8.xml +++ b/ovn/utilities/ovn-nbctl.8.xml @@ -40,6 +40,35 @@ +

Subnet Commands

+ +
+
subnet-add lswitch cidr gateway_ip [enable_dhcp]
+
+ Adds a Subnet to the lswitch +
+ +
subnet-del subnet
+
+ Deletes the subnet referred by subnet UUID. +
+ +
subnet-list lswitch
+
+ Lists all the subnets belonging to the lswitch +
+ +
subnet-set-dhcp-options subnet [key=value]...
+
+ Sets the DHCP options for the subnet +
+ +
subnet-get-dhcp-options subnet
+
+ Lists the DHCP options of the subnet +
+
+

ACL Commands

[--log] acl-add lswitch direction priority match action
diff --git a/ovn/utilities/ovn-nbctl.c b/ovn/utilities/ovn-nbctl.c index bdad368..9726c7d 100644 --- a/ovn/utilities/ovn-nbctl.c +++ b/ovn/utilities/ovn-nbctl.c @@ -330,6 +330,18 @@ Logical port commands:\n\ Set options related to the type of LPORT\n\ lport-get-options LPORT Get the type specific options for LPORT\n\ \n\ +Subnet commands:\n\ + subnet-add LSWITCH CIDR GATEWAYIP [ENABLE_DHCP]\n\ + add a subnet to LSWITCH\n\ + subnet-del SUBNET_UUID\n\ + remove subnet from its attached LSWITCH\n\ + subnet-list LSWITCH\n\ + list subnets attached to LSWITCH\n\ + subnet-set-dhcp-options SUBNET_UUID KEY=VALUE [KEY=VALUE]...\n\ + Set DHCP options for the SUBNET\n\ + subnet-get-dhcp-options SUBNET_UUID\n\ + Get the DHCP options for the SUBNET\n\ +\n\ %s\ \n\ Options:\n\ @@ -478,6 +490,7 @@ nbctl_lswitch_list(struct ctl_context *ctx) smap_destroy(&lswitches); free(nodes); } + static const struct nbrec_logical_port * lport_by_name_or_uuid(struct ctl_context *ctx, const char *id) @@ -878,6 +891,177 @@ nbctl_lport_get_options(struct ctl_context *ctx) } } +static void +nbctl_subnet_add(struct ctl_context *ctx) +{ + const struct nbrec_logical_switch *lswitch; + lswitch = lswitch_by_name_or_uuid(ctx, ctx->argv[1]); + if (!lswitch) { + return; + } + + /* Validate the cidr */ + int64_t ip_version = 4; + ovs_be32 ip, plen; + char *error = ip_parse_cidr(ctx->argv[2], &ip, &plen); + if (error){ + /* check if its IPv6 cidr */ + free(error); + struct in6_addr ipv6; + error = ipv6_parse_cidr(ctx->argv[2], &ipv6, &plen); + if (error) { + free(error); + VLOG_WARN("Invalid cidr format '%s'", ctx->argv[2]); + return; + } + ip_version = 6; + } + + /* Validate the gateway ip format */ + bool valid_gatewap_ip = false; + if (ip_version == 4) { + valid_gatewap_ip = ip_parse(ctx->argv[3], &ip); + } + else { + struct in6_addr ipv6; + valid_gatewap_ip = ipv6_parse(ctx->argv[3], &ipv6); + } + + if (!valid_gatewap_ip) { + VLOG_WARN("Invalid Gateway ip format '%s'", ctx->argv[3]); + return; + } + + bool enable_dhcp = true; + if (ctx->argc == 5) { + if (!strcmp(ctx->argv[4], "false")) { + enable_dhcp = false; + } + } + + struct nbrec_subnet *subnet = nbrec_subnet_insert(ctx->txn); + nbrec_subnet_set_cidr(subnet, ctx->argv[2]); + nbrec_subnet_set_ip_version(subnet, ip_version); + nbrec_subnet_set_gateway_ip(subnet, ctx->argv[3]); + nbrec_subnet_set_enable_dhcp(subnet, enable_dhcp); + + /* Insert the subnet into the logical switch */ + nbrec_logical_switch_verify_subnets(lswitch); + struct nbrec_subnet **new_subnets = xmalloc(sizeof *new_subnets * + (lswitch->n_subnets + 1)); + memcpy(new_subnets, lswitch->subnets, + sizeof *new_subnets * lswitch->n_subnets); + new_subnets[lswitch->n_subnets] = subnet; + nbrec_logical_switch_set_subnets(lswitch, new_subnets, + lswitch->n_subnets + 1); + free(new_subnets); +} + +static void +remove_subnet(const struct nbrec_logical_switch *lswitch, size_t idx) +{ + const struct nbrec_subnet *subnet = lswitch->subnets[idx]; + + struct nbrec_subnet **new_subnets + = xmemdup(lswitch->subnets, sizeof *new_subnets * lswitch->n_subnets); + new_subnets[idx] = new_subnets[lswitch->n_subnets - 1]; + nbrec_logical_switch_verify_subnets(lswitch); + nbrec_logical_switch_set_subnets(lswitch, new_subnets, lswitch->n_subnets - 1); + free(new_subnets); + + nbrec_subnet_delete(subnet); +} + +static const struct nbrec_subnet * +subnet_get(struct ctl_context *ctx, char *id) +{ + struct uuid subnet_uuid; + if (uuid_from_string(&subnet_uuid, id)) { + return nbrec_subnet_get_for_uuid(ctx->idl, &subnet_uuid); + } + + return NULL; +} + +static void +nbctl_subnet_del(struct ctl_context *ctx) +{ + const struct nbrec_subnet *subnet = subnet_get(ctx, ctx->argv[1]); + if (!subnet) { + VLOG_WARN("subnet not found for '%s'", ctx->argv[1]); + return; + } + + /* Find the switch that contains 'subnet', then delete it. */ + const struct nbrec_logical_switch *lswitch; + NBREC_LOGICAL_SWITCH_FOR_EACH (lswitch, ctx->idl) { + for (size_t i = 0; i < lswitch->n_subnets; i++) { + if (lswitch->subnets[i] == subnet) { + remove_subnet(lswitch, i); + return; + } + } + } + + VLOG_WARN("subnet %s is not part of any logical switch", + ctx->argv[1]); +} + +static void +nbctl_subnet_list(struct ctl_context *ctx) +{ + const struct nbrec_logical_switch *lswitch; + lswitch = lswitch_by_name_or_uuid(ctx, ctx->argv[1]); + if (!lswitch) { + return; + } + + for (size_t i = 0; i < lswitch->n_subnets; i++) { + const struct nbrec_subnet *subnet = lswitch->subnets[i]; + ds_put_format(&ctx->output, UUID_FMT " (%s)\n", + UUID_ARGS(&subnet->header_.uuid), subnet->cidr); + } +} + +static void +nbctl_subnet_set_dhcp_options(struct ctl_context *ctx) +{ + const struct nbrec_subnet *subnet = subnet_get(ctx, ctx->argv[1]); + if (!subnet) { + VLOG_WARN("subnet not found for '%s'", ctx->argv[1]); + return; + } + + struct smap dhcp_options = SMAP_INITIALIZER(&dhcp_options); + for (size_t i = 2; i < ctx->argc; i++) { + char *key, *value; + value = xstrdup(ctx->argv[i]); + key = strsep(&value, "="); + if (value) { + smap_add(&dhcp_options, key, value); + } + free(key); + } + + nbrec_subnet_set_dhcp_options(subnet, &dhcp_options); + smap_destroy(&dhcp_options); +} + +static void +nbctl_subnet_get_dhcp_options(struct ctl_context *ctx) +{ + const struct nbrec_subnet *subnet = subnet_get(ctx, ctx->argv[1]); + if (!subnet) { + VLOG_WARN("subnet not found for '%s'", ctx->argv[1]); + return; + } + + struct smap_node *node; + SMAP_FOR_EACH(node, &subnet->dhcp_options) { + ds_put_format(&ctx->output, "%s=%s\n", node->key, node->value); + } +} + enum { DIR_FROM_LPORT, DIR_TO_LPORT @@ -1092,6 +1276,10 @@ static const struct ctl_table_class tables[] = { {{NULL, NULL, NULL}, {NULL, NULL, NULL}}}, + {&nbrec_table_subnet, + {{&nbrec_table_subnet, NULL, NULL}, + {NULL, NULL, NULL}}}, + {&nbrec_table_logical_router, {{&nbrec_table_logical_router, &nbrec_logical_router_col_name, NULL}, {NULL, NULL, NULL}}}, @@ -1351,6 +1539,16 @@ static const struct ctl_command_syntax nbctl_commands[] = { { "lport-get-options", 1, 1, "LPORT", NULL, nbctl_lport_get_options, NULL, "", RO }, + /* subnet commands */ + {"subnet-add", 3, 4, "LSWITCH CIDR GATEWAY_IP ENABLE_DHCP", NULL, + nbctl_subnet_add, NULL, "", RW }, + {"subnet-del", 1, 1, "SUBNET", NULL, nbctl_subnet_del, NULL, "", RW}, + {"subnet-list", 1, 1, "LSWITCH", NULL, nbctl_subnet_list, NULL, "", RO}, + {"subnet-set-dhcp-options", 1, INT_MAX, "SUBNET KEY=VALUE [KEY=VALUE]...", + NULL, nbctl_subnet_set_dhcp_options, NULL, "", RW }, + {"subnet-get-dhcp-options", 1, 1, "SUBNET", NULL, + nbctl_subnet_get_dhcp_options, NULL, "", RO }, + {NULL, 0, 0, NULL, NULL, NULL, NULL, "", RO}, }; diff --git a/tests/ovn.at b/tests/ovn.at index 971c8cd..07b22d2 100644 --- a/tests/ovn.at +++ b/tests/ovn.at @@ -2380,3 +2380,190 @@ AT_CHECK([ovstest test-ovn-dhcp-offer-action $dhcp_options $expected_dhcp_opts], [1], [ignore]) AT_CLEANUP + +AT_SETUP([ovn -- dhcp : 1 HV, 2 LS, 2 lports]) +AT_KEYWORDS([dhcp]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +ovn_start + +ovn-nbctl lswitch-add ls1 +ovn-nbctl lswitch-add ls2 + +ovn-nbctl -- --id=@s1 create Subnet cidr=10.0.0.0/24 gateway_ip=10.0.0.1 \ +ip_version=4 enable_dhcp=true dhcp_options="\"server_id\"=\"10.0.0.1\" \ +\"server_mac\"=\"00:00:00:10:00:01\" \"lease_time\"=\"36\"" \ +-- add Logical_Switch ls1 subnets @s1 + +ovn-nbctl lport-add ls1 ls1-lp1 \ +-- lport-set-addresses ls1-lp1 "f0:00:00:00:00:01 10.0.0.4" + +ovn-nbctl lport-set-port-security ls1-lp1 "f0:00:00:00:00:01 10.0.0.4" + +ovn-nbctl lport-add ls2 ls2-lp1 \ +-- lport-set-addresses ls2-lp1 "f0:00:00:00:00:02 20.0.0.3" + +ovn-nbctl lport-set-port-security ls2-lp1 "f0:00:00:00:00:02 20.0.0.3" + +net_add n1 +sim_add hv1 + +as hv1 +ovs-vsctl add-br br-phys +ovn_attach n1 br-phys 192.168.0.1 +ovs-vsctl -- add-port br-int hv1-vif1 -- \ + set interface hv1-vif1 external-ids:iface-id=ls1-lp1 \ + options:tx_pcap=hv1/vif1-tx.pcap \ + options:rxq_pcap=hv1/vif1-rx.pcap \ + ofport-request=1 +ovs-vsctl -- add-port br-int hv1-vif2 -- \ + set interface hv1-vif2 external-ids:iface-id=ls2-lp1 \ + options:tx_pcap=hv1/vif2-tx.pcap \ + options:rxq_pcap=hv1/vif2-rx.pcap \ + ofport-request=2 + +ovn_populate_arp + +sleep 2 + +send_dhcp_packet() { + local inport=$1 src_mac=$2 dhcp_type=$3 + local request=ffffffffffff${src_mac}080045100110000000008011000000000000ffffffff + # udp header and dhcp header + request+=0044004300FC0000 + request+=010106006359aa760000000000000000000000000000000000000000${src_mac} + # client hardware padding + request+=00000000000000000000 + # server hostname + request+=0000000000000000000000000000000000000000000000000000000000000000 + request+=0000000000000000000000000000000000000000000000000000000000000000 + # boot file name + request+=0000000000000000000000000000000000000000000000000000000000000000 + request+=0000000000000000000000000000000000000000000000000000000000000000 + request+=0000000000000000000000000000000000000000000000000000000000000000 + request+=0000000000000000000000000000000000000000000000000000000000000000 + # dhcp magic cookie + request+=63825363 + # dhcp message type + request+=3501${dhcp_type}ff + shift; shift; shift; shift; shift + echo "inport = $inport" + echo "src mac = $src_mac" + as hv1 ovs-appctl netdev-dummy/receive hv1-vif$inport $request +} + +run_dhcp_test() { + local inport=$1 dhcp_type=$2 + send_dhcp_packet $inport f0000000000$inport $dhcp_type +} + +AT_CAPTURE_FILE([ofctl_monitor0.log]) +as hv1 ovs-ofctl monitor br-int resume --detach --no-chdir \ +--pidfile=ovs-ofctl0.pid 2> ofctl_monitor0.log + +echo "---------NB dump-----" +ovn-nbctl show +echo "---------------------" +echo "---------SB dump-----" +ovn-sbctl list datapath_binding +echo "---------------------" +ovn-sbctl list logical_flow +echo "---------------------" + +echo "---------------------" +ovn-sbctl dump-flows +echo "---------------------" + +echo "------ hv1 dump ----------" +as hv1 ovs-ofctl dump-flows br-int + +#send DHCP DISCOVER +run_dhcp_test 1 01 + +# Wait for the expected number of NXT_RESUMEs to be logged. +echo "waiting for 1 NXT_RESUMEs..." +OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`]) + +# Extract the dhcp options in userdata. It will be in the format +# ' userdata=00.00.00.02.00.00.00.00.0a.00.00.04.01.04.ff......' +# Skip 47 columns - userdata= +dhcp_opts_in_user_data=`cat ofctl_monitor0.log | grep userdata | cut -c 47-` + +AT_CHECK([ovstest test-ovn-dhcp hv1/vif1-tx.pcap 10.0.0.4 10.0.0.1 \ + 00:00:00:10:00:01 1 $dhcp_opts_in_user_data], [0], [ignore]) + +# run dhcp test on ls2-lp1. There are no dhcp options defined. So the dhcp +# packet should not be handled by ovn-controller +cat ofctl_monitor*.log + +echo "###########################################" +run_dhcp_test 2 01 + +# NXT_RESUMEs should be 1. +echo "waiting for 1 NXT_RESUMEs..." +cat ofctl_monitor*.log +echo "###########################################" + +OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`]) + +ovn-nbctl subnet-add ls2 20.0.0.0/24 20.0.0.1 true +subnet_id=`ovn-nbctl subnet-list ls2 | cut -c -37` + +ovn-nbctl subnet-set-dhcp-options $subnet_id "server_id=20.0.0.1" \ +"lease_time=4500" + +sleep 2 + +# there is no "server_mac" field in the dhcp options. So there should be no +# dhcp flow for this port +OVS_WAIT_UNTIL([test 1 = `as hv1 ovs-ofctl dump-flows br-int | \ +grep -c "actions=controller(userdata=00.00.00.02.00.00.00"`]) + +ovn-nbctl subnet-set-dhcp-options $subnet_id "server_id=20.0.0.1" \ +"server_mac=00:00:00:10:00:02" "lease_time=4500" + +sleep 3 +OVS_WAIT_UNTIL([test 2 = `as hv1 ovs-ofctl dump-flows br-int | \ +grep -c "actions=controller(userdata=00.00.00.02.00.00.00"`]) + +echo "###########################################" +run_dhcp_test 2 03 + +# NXT_RESUMEs should be 2. +echo "waiting for 2 NXT_RESUMEs..." +OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`]) + +dhcp_opts_in_user_data=`cat ofctl_monitor0.log | grep userdata | \ +cut -c 47- | sed '1d'` + +AT_CHECK([ovstest test-ovn-dhcp hv1/vif2-tx.pcap 20.0.0.3 20.0.0.1 \ + 00:00:00:10:00:02 3 $dhcp_opts_in_user_data], [0], [ignore]) + + +echo "---------NB dump-----" +ovn-nbctl show +echo "---------SB dump-----" +ovn-sbctl list datapath_binding +echo "---------------------" +ovn-sbctl list logical_flow +ovn-sbctl list port_binding +echo "---------------------" +ovn-sbctl dump-flows +echo "---------------------" + +echo "------ hv1 dump ----------" +as hv1 ovs-ofctl dump-flows br-int + +as ovn-sb +OVS_APP_EXIT_AND_WAIT([ovsdb-server]) + +as ovn-nb +OVS_APP_EXIT_AND_WAIT([ovsdb-server]) + +as northd +OVS_APP_EXIT_AND_WAIT([ovn-northd]) + +as main +OVS_APP_EXIT_AND_WAIT([ovs-vswitchd]) +OVS_APP_EXIT_AND_WAIT([ovsdb-server]) + +AT_CLEANUP diff --git a/tests/test-ovn-dhcp.c b/tests/test-ovn-dhcp.c index 60726d9..107cc06 100644 --- a/tests/test-ovn-dhcp.c +++ b/tests/test-ovn-dhcp.c @@ -16,10 +16,15 @@ #include #include #include "command-line.h" +#include "dp-packet.h" +#include +#include "flow.h" +#include "lib/dhcp.h" #include "openvswitch/ofp-actions.h" #include "ovstest.h" #include "ovn/lib/actions.h" #include "ovn/lib/ovn-dhcp.h" +#include "pcap-file.h" static void @@ -114,4 +119,153 @@ test_dhcp_offer_action(int argc OVS_UNUSED, char *argv[] OVS_UNUSED) exit(0); } +static void +test_ovn_dhcp_main(int argc OVS_UNUSED, char *argv[] OVS_UNUSED) +{ + if (argc != 7) { + printf("Usage: %s pcap-file offer-ip server-ip" + " server-mac dhcp-type userdata\n", argv[0]); + exit(1); + } + + int retval = 1; + FILE *pcap; + + set_program_name(argv[0]); + + pcap = fopen(argv[1], "rb"); + if (!pcap) { + ovs_fatal(errno, "failed to open %s", argv[1]); + } + + retval = ovs_pcap_read_header(pcap); + if (retval) { + ovs_fatal(retval > 0 ? retval : 0, "reading pcap header failed"); + } + + /* verify if the offer-ip is in proper format */ + ovs_be32 expected_offer_ip; + if (!ovs_scan(argv[2], IP_SCAN_FMT, IP_SCAN_ARGS(&expected_offer_ip))) { + ovs_fatal(1, "invalid expected offer ip"); + } + + /* verify if the server-ip is in proper format */ + ovs_be32 server_ip; + if (!ovs_scan(argv[3], IP_SCAN_FMT, IP_SCAN_ARGS(&server_ip))) { + ovs_fatal(1, "invalid expected server ip"); + } + + struct eth_addr server_mac; + if (!eth_addr_from_string(argv[4], &server_mac)) { + ovs_fatal(1, "invalid expected server mac"); + } + + /* + * In the testing its been observed that, the pcap file is + * receiving 2 packets. + * + * 1. The resumed dhcp response packet from the ovn-controller + * 2. The dhcp response packet without the 'pause' with all the other + * actions applied. + */ + + int num_pcap_reads = 0; + int exit_code = 1; + struct dp_packet *packet = NULL; + do { + num_pcap_reads++; + retval = ovs_pcap_read(pcap, &packet, NULL); + if (retval == EOF) { + ovs_fatal(0, "unexpected end of file reading pcap file : [%s]\n", + argv[1]); + } else if (retval) { + ovs_fatal(retval, "error reading pcap file"); + } + + struct flow flow; + flow_extract(packet, &flow); + + struct dhcp_header const *dhcp_data = dp_packet_get_udp_payload(packet); + if (dhcp_data->op != (uint8_t)2) { + if (num_pcap_reads == 1) { + /* This is the incomplete dhcp reply packet without the + * 'pause'. Read the next packet */ + dp_packet_delete(packet); + continue; + } + printf("Invalid dhcp op reply code : %d\n", dhcp_data->op); + break; + } + + if (flow.tp_src != htons(DHCP_SERVER_PORT) && + flow.tp_dst != htons(DHCP_CLIENT_PORT)) { + printf("Error. Not a dhcp response packet \n"); + break; + } + + if (flow.nw_dst != expected_offer_ip) { + printf("Error. Offered ip : "IP_FMT " : Expected ip : %s\n", + IP_ARGS(flow.nw_dst), argv[2]); + break; + } + + if(dhcp_data->yiaddr != expected_offer_ip) { + printf("Error. Offered yiaddr : "IP_FMT " : Expected ip : %s\n", + IP_ARGS(dhcp_data->yiaddr), argv[2]); + break; + } + + /* Verify the dhcp option cookie */ + char const *footer = (char *)dhcp_data + sizeof(*dhcp_data); + uint32_t cookie = *(uint32_t *)footer; + if (cookie != htonl(DHCP_MAGIC_COOKIE)) { + printf("Error. Invalid dhcp magic cookie\n"); + break; + } + + /* Validate userdata. It should be ASCII hex */ + uint64_t dhcp_opts_stub[1024 / 8]; + struct ofpbuf dhcp_opts = OFPBUF_STUB_INITIALIZER(dhcp_opts_stub); + if (atoi(argv[5]) == 1) { + /* DHCP reply type should be OFFER (02) */ + ofpbuf_put_hex(&dhcp_opts, "350102", NULL); + } else { + /* DHCP reply type should be ACK (05) */ + ofpbuf_put_hex(&dhcp_opts, "350105", NULL); + } + + if (ofpbuf_put_hex(&dhcp_opts, argv[6], NULL)[0] != '\0') { + printf("Error. Invalid userdata\n"); + break; + } + + /* 4 bytes padding, 1 byte FF and 4 bytes padding */ + ofpbuf_put_hex(&dhcp_opts, "00000000FF00000000", NULL); + + footer += sizeof(uint32_t); + + size_t dhcp_opts_size = (const char *)dp_packet_tail(packet) - ( + const char *)footer; + if (dhcp_opts_size != dhcp_opts.size) { + printf("Error. dhcp options size mismatch\n"); + break; + } + + if (memcmp(footer, dhcp_opts.data, dhcp_opts.size)) { + printf("Error. Invalid dhcp options present\n"); + break; + } + + exit_code = 0; + break; + }while(num_pcap_reads < 2); + + fclose(pcap); + if (packet) { + dp_packet_delete(packet); + } + exit(exit_code); +} + OVSTEST_REGISTER("test-ovn-dhcp-offer-action", test_dhcp_offer_action); +OVSTEST_REGISTER("test-ovn-dhcp", test_ovn_dhcp_main);