P4 utils: ECMP(Equal Cost Multi-Path) Test

[Topology]

H1 will send 2Mbytes data to h3. H2 will send 1Mbytes data to h4. In this topology, traffic can be sent via s1-s2-s4 or s1-s3-s4. These two paths are qual cost. So when chosen paths are the same, the flow completion time will be longer. When chosen paths are different, it take less time to finish data transmission.

 

[ip_forward.p4]

#include <core.p4>

#include <v1model.p4>

typedef bit<48> macAddr_t;

typedef bit<9> egressSpec_t;

 

header ethernet_t {

    bit<48> dstAddr;

    bit<48> srcAddr;

    bit<16> etherType;

}

 

header ipv4_t {

    bit<4>  version;

    bit<4>  ihl;

    bit<8>  diffserv;

    bit<16> totalLen;

    bit<16> identification;

    bit<3>  flags;

    bit<13> fragOffset;

    bit<8>  ttl;

    bit<8>  protocol;

    bit<16> hdrChecksum;

    bit<32> srcAddr;

    bit<32> dstAddr;

}

 

struct metadata {

}

 

struct headers {

    @name(".ethernet")

    ethernet_t ethernet;

    @name(".ipv4")

    ipv4_t     ipv4;

}

 

parser ParserImpl(packet_in packet, out headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) {

    @name(".parse_ethernet") state parse_ethernet {

        packet.extract(hdr.ethernet);

        transition select(hdr.ethernet.etherType) {

            16w0x800: parse_ipv4;

            default: accept;

        }

    }

    @name(".parse_ipv4") state parse_ipv4 {

        packet.extract(hdr.ipv4);

        transition accept;

    }

    @name(".start") state start {

        transition parse_ethernet;

    }

}

 

control egress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) {

    apply {

    }

}

 

control ingress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) {

    @name(".set_nhop") action set_nhop(macAddr_t dstAddr, egressSpec_t port) {

        //set the src mac address as the previous dst, this is not correct right?

        hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;

 

        //set the destination mac address that we got from the match in the table

        hdr.ethernet.dstAddr = dstAddr;

 

        //set the output port that we also get from the table

        standard_metadata.egress_spec = port;

 

        //decrease ttl by 1

        hdr.ipv4.ttl = hdr.ipv4.ttl - 1;

    }

    @name("._drop") action _drop() {

        mark_to_drop(standard_metadata);

    }

    @name(".ipv4_lpm") table ipv4_lpm {

        actions = {

            set_nhop;

            _drop;

        }

        key = {

            hdr.ipv4.dstAddr: lpm;

        }

        size = 512;

        const default_action = _drop();

    }

    apply {

       if (hdr.ipv4.isValid()){

         ipv4_lpm.apply();

       }

    }

}

 

control DeparserImpl(packet_out packet, in headers hdr) {

    apply {

        packet.emit(hdr.ethernet);

        packet.emit(hdr.ipv4);

    }

}

 

control verifyChecksum(inout headers hdr, inout metadata meta) {

    apply {

        verify_checksum(true, { hdr.ipv4.version, hdr.ipv4.ihl, hdr.ipv4.diffserv, hdr.ipv4.totalLen, hdr.ipv4.identification, hdr.ipv4.flags, hdr.ipv4.fragOffset, hdr.ipv4.ttl, hdr.ipv4.protocol, hdr.ipv4.srcAddr, hdr.ipv4.dstAddr }, hdr.ipv4.hdrChecksum, HashAlgorithm.csum16);

    }

}

 

control computeChecksum(inout headers hdr, inout metadata meta) {

    apply {

        update_checksum(true, { hdr.ipv4.version, hdr.ipv4.ihl, hdr.ipv4.diffserv, hdr.ipv4.totalLen, hdr.ipv4.identification, hdr.ipv4.flags, hdr.ipv4.fragOffset, hdr.ipv4.ttl, hdr.ipv4.protocol, hdr.ipv4.srcAddr, hdr.ipv4.dstAddr }, hdr.ipv4.hdrChecksum, HashAlgorithm.csum16);

    }

}

 

V1Switch(ParserImpl(), verifyChecksum(), ingress(), egress(), computeChecksum(), DeparserImpl()) main;

 

[p4app.json]

{

  "program": "ip_forward.p4",

  "switch": "simple_switch",

  "compiler": "p4c",

  "options": "--target bmv2 --arch v1model --std p4-16",

  "switch_cli": "simple_switch_CLI",

  "cli": true,

  "pcap_dump": true,

  "enable_log": true,

  "topo_module": {

    "file_path": "",

    "module_name": "p4utils.mininetlib.apptopo",

    "object_name": "AppTopoStrategies"

  },

  "controller_module": null,

  "topodb_module": {

    "file_path": "",

    "module_name": "p4utils.utils.topology",

    "object_name": "Topology"

  },

  "mininet_module": {

    "file_path": "",

    "module_name": "p4utils.mininetlib.p4net",

    "object_name": "P4Mininet"

  },

  "topology": {

    "assignment_strategy": "l3",

    "auto_arp_tables": "true",

    "auto_gw_arp": "true",

    "links": [["h1", "s1", {"bw":10}], ["h2", "s1", {"bw":10}], ["h3", "s4", {"bw":10}], ["h4", "s4", {"bw":10}], ["s1", "s2", {"bw":1}], ["s1", "s3", {"bw":1}], ["s2", "s4", {"bw":1}], ["s3", "s4", {"bw":1}]],

    "hosts": {

      "h1": {

      },

      "h2": {

      },

      "h3": {

      },

      "h4": {

      }          

    },

    "switches": {

      "s1": {

      },

      "s2": {

      },

      "s3": {

      },

      "s4": {

      }

    }

  }

}

 

[routing-controller.py]

from p4utils.utils.topology import Topology

from p4utils.utils.sswitch_API import SimpleSwitchAPI

import random

 

class RoutingController(object):

 

    def __init__(self):

 

        self.topo = Topology(db="topology.db")

        self.controllers = {}

        self.init()

 

    def init(self):

        self.connect_to_switches()

        self.reset_states()

        self.set_table_defaults()

 

    def reset_states(self):

        [controller.reset_state() for controller in self.controllers.values()]

 

    def connect_to_switches(self):

        for p4switch in self.topo.get_p4switches():

            thrift_port = self.topo.get_thrift_port(p4switch)

            self.controllers[p4switch] = SimpleSwitchAPI(thrift_port)

 

    def set_table_defaults(self):

        for controller in self.controllers.values():

            controller.table_set_default("ipv4_lpm", "_drop", [])

 

    def route(self):

        switches = {sw_name:{} for sw_name in self.topo.get_p4switches().keys()}

 

        for sw_name, controller in self.controllers.items():

            for sw_dst in self.topo.get_p4switches():

 

                #if its ourselves we create direct connections

                if sw_name == sw_dst:

                    for host in self.topo.get_hosts_connected_to(sw_name):

                        sw_port = self.topo.node_to_node_port_num(sw_name, host)

                        host_ip = self.topo.get_host_ip(host) + "/32"

                        host_mac = self.topo.get_host_mac(host)

                        print host, "(", host_ip, host_mac, ")", "-->", sw_name, "with port:", sw_port 

 

                        #add rule

                        print "table_add at {}:".format(sw_name)

                        self.controllers[sw_name].table_add("ipv4_lpm", "set_nhop", [str(host_ip)], [str(host_mac), str(sw_port)])

 

                #check if there are directly connected hosts

                else:

                    if self.topo.get_hosts_connected_to(sw_dst):

                        paths = self.topo.get_shortest_paths_between_nodes(sw_name, sw_dst)

                       #print sw_name,"->",sw_dst,":",paths

                        print "routing paths:", paths 

                        for host in self.topo.get_hosts_connected_to(sw_dst):

                            path=random.choice(paths)

                            print "chosen path:", path

                            next_hop = path[1]

                            host_ip = self.topo.get_host_ip(host) + "/24"

                            sw_port = self.topo.node_to_node_port_num(sw_name, next_hop)

                            dst_sw_mac = self.topo.node_to_node_mac(next_hop, sw_name)

 

                            #add rule

                            print "table_add at {}:".format(sw_name)

                            self.controllers[sw_name].table_add("ipv4_lpm", "set_nhop", [str(host_ip)],

                                                                    [str(dst_sw_mac), str(sw_port)])

 

    def main(self):

        self.route()

 

if __name__ == "__main__":

    controller = RoutingController().main()

 

[measure.py]

import subprocess

from datetime import datetime

import os

import time

 

start=datetime.now()

p5=subprocess.Popen("mx h3 iperf -s -i 1 -p 7777 > server7777.txt", shell=True)

p6=subprocess.Popen("mx h4 iperf -s -i 1 -p 8888 > server8888.txt", shell=True)

p7=subprocess.Popen("mx h1 iperf -c 10.4.3.2 -n 2M -p 7777", shell=True)

time.sleep(3)

p8=subprocess.Popen("mx h2 iperf -c 10.4.4.2 -n 1M -p 8888", shell=True)

p7.wait()

p8.wait()

end=datetime.now()

print "execution time:", (end-start).seconds

os.system("pkill mx")

 

Execution

Case 1: when routing paths are different

 

Open anther terminal to run the routing-controller

 

H2-H4: via s1-s3-s4   H1-H3: via s1-s2-s4

 

Run measure.py

Check server7777.txt can server 8888.txt

It takes 17 seconds to finish 2Mbytes transmission.

 

It takes 12.2 seconds to finish 1Mbytes transmission.

 

Case 2: when the routing paths are the same.

Re-run p4app and python routing-controller.py. When you see somethings like the following. It means that h2-h4 and h1-h3 are using the same path, i.e. s1-s3-s4.

 

 

Check server7777.txt can server 8888.txt

It takes 21 seconds to finish 2Mbytes transmission.

 

It takes 19.5 seconds to finish 1Mbytes transmission.

 

Dr. Chih-Heng Ke (smallko@gmail.com)

Department of Computer Science and Information Engineering,

National Quemoy University, Kinmen, Taiwan.