How to implementing ARP in BMv2 Switch

Hello,All.
I would like to implement custom ARP processing using P4 on a BMv2 switch. My goal is to implement the following logic:

ARP Requests: When the switch receives an ARP packet, it should broadcast (flood) it to the network.

ARP Replies: When a reply is received, it should be forwarded to the correct output port.

I noticed that the tutorial topology file contains static ARP commands/entries. I want to remove these static entries and implement the logic manually within the P4 program to make it work dynamically.

Could you please guide me on how to achieve this?

Removing the static ARP commands/entries from the tutorial JSON file is pretty straightforward. Do you have any further questions on how to do that?

Once those have been removed from those files, you have many alternatives available:

  • You could keep the P4 table in the source code that those data objects were causing entries to be added, and instead write controller code that installs entries into those tables.
  • You could modify the P4 tables to have different keys, or actions, or both.
  • You could remove that P4 table and replace it with other P4 code of your choosing. If that new code has one or more P4 tables to do part of the work, you will probably want controller code that can add entries to them at appropriate events of your choosing.

In any case, the most common way to modify the contents of a P4 table is from the controller software. The tutorials JSON data files for the “static” entries are not truly static – there is controller software that reads those JSON files, adds those entries when it starts, and then the rest of the controller code that you may write can do what it wants afterwards to add/delete/modify entries. Most of the tutorials have “no op” controller code that never modifies any table entries, because the focus is on the behavior of the P4 code, not on what the controller software does.

Hi TaikiIto,

I have implemented the full ARP pipeline for bmv2 (v1model) as part of my Master’s thesis in Sweden (LTU). I am still waiting for approval on the draft, and later I will present it online. This version is only a draft, so some parts might change before the final submission and the sharing.

If you want to take a look, send me your email address and I will send the draft to you. There are probably things that are not perfect, but the main goal was to build a complete ARP design for bmv2, including meters and registers.

I am happy to share the thesis work (latex) and the code when the LTU thesis is completed.

I did what @andyfingerhut said, pretty much.

Cheers,

Hi Andy and ederollora2,

Thank you both for your helpful replies.

@AndyAndy I realized from your explanation that my initial question might have been a bit misleading. My current goal is actually simpler than building a full ARP responder or controller-based learning. I simply want to implement basic L2 flooding behavior for ARP requests in P4. I want the switch to broadcast ARP packets to all ports so that the connected hosts can handle ARP resolution normally.

I have written the following P4 code to achieve this. Does this look like the correct approach for simple ARP broadcasting?

This is a copy of the tutorial, so there are still hints and other information written on it, sorry.

//P4//
// SPDX-License-Identifier: Apache-2.0
/* -*- P4_16 -*- */
#include <core.p4>
#include <v1model.p4>

const bit<16> TYPE_IPV4 = 0x800;
const bit<16> TYPE_ARP = 0x0806;

/*************************************************************************
*********************** H E A D E R S  ***********************************
* This program skeleton defines minimal Ethernet and IPv4 headers and    *
* a simple LPM (Longest-Prefix Match) IPv4 forwarding pipeline.          *
* The exercise intentionally leaves TODOs for learners to implement.     *
*************************************************************************/

typedef bit<9>  egressSpec_t;   // Standard BMv2 uses 9 bits for egress_spec
typedef bit<48> macAddr_t;      // Ethernet MAC address
typedef bit<32> ip4Addr_t;      // IPv4 address

header ethernet_t {
    macAddr_t dstAddr;
    macAddr_t 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;
    ip4Addr_t srcAddr;
    ip4Addr_t dstAddr;
}

header ARP_t{
    bit<16> hardware_type;
    bit<16> prtocol_type;
    bit<8> hardware_size;
    bit<8> protocol_size;
    bit<16> opcode;
    macAddr_t src_macAddr;
    ip4Addr_t src_ipAddr;
    macAddr_t dst_macAddr;
    ip4Addr_t dst_ipAddr;
}

struct metadata {
    /* empty */
}

struct headers {
    ethernet_t   ethernet;
    ipv4_t       ipv4;
    ARP_t        ARP;
}

/*************************************************************************
*********************** P A R S E R  *************************************
* New to P4? A typical parser does this:
*   start -> parse_ethernet
*   parse_ethernet:
*       if etherType == TYPE_IPV4 -> parse_ipv4
*       else accept
*   parse_ipv4 -> accept
* This skeleton leaves the actual states as a TODO to implement later.   *
*************************************************************************/

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

    state start {
        transition parse_ethernet;
    }

    state parse_ethernet{
        packet.extract(hdr.ethernet);
        transition select(hdr.ethernet.etherType){
            TYPE_IPV4 : parse_ipv4;
            TYPE_ARP : parse_ARP;
            default : accept;
        }
    }

    state parse_ipv4{
        packet.extract(hdr.ipv4);
        transition accept;
    }

    state parse_ARP{
        packet.extract(hdr.ARP);
        transition  accept;
    }

}


/*************************************************************************
************   C H E C K S U M    V E R I F I C A T I O N   *************
*************************************************************************/

control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
    apply {  }
}


/*************************************************************************
**************  I N G R E S S   P R O C E S S I N G   *******************
* High-level intent:
*   - Do an LPM lookup on IPv4 dstAddr
*   - On hit, call ipv4_forward(next-hop MAC, output port)
*   - Otherwise, drop or NoAction (as configured)                         *
*************************************************************************/

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

    action drop() {
        mark_to_drop(standard_metadata);
    }

    /*********************************************************************
     * NOTE FOR NEW READERS:
     * 'ipv4_forward(dstAddr, port)' is invoked by table 'ipv4_lpm'.
     *
     * The values for 'dstAddr' and 'port' are *action data* supplied by
     * the control plane when it installs entries in 'ipv4_lpm'.
     *
     * They mean:
     *   - dstAddr  => Ethernet destination MAC for the next hop
     *   - port     => output port (ultimately written to standard_metadata.egress_spec)
     *
     * Example (BMv2 simple_switch_CLI):
     *   table_add ipv4_lpm ipv4_forward 10.0.1.1/32 => 00:00:00:00:01:00 1
     * which passes MAC=00:00:00:00:01:00 and PORT=1 as action parameters
     * into ipv4_forward(dstAddr, port).
     *********************************************************************/
    action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
        hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
        hdr.ethernet.dstAddr = dstAddr;
        standard_metadata.egress_spec = port;
        hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
       
    }

    action ARPrequest_forward(){
        standard_metadata.mcast_grp = 1;
    }

    action ARPreply_forward(egressSpec_t port){
        standard_metadata.egress_spec = port;
    }

    /*********************************************************************
     * LPM table for IPv4:
     *   - Matches on hdr.ipv4.dstAddr using longest-prefix match (lpm)
     *   - On hit, calls ipv4_forward with *action data* populated by the
     *     control plane when it installs the table entry.
     *********************************************************************/
    table ipv4_lpm {
        key = {
            hdr.ipv4.dstAddr: lpm;
        }
        actions = {
            ipv4_forward;
            drop;
            NoAction;
        }
        size = 1024;
        default_action = NoAction();
    }

    table ARPreply{
        key = {
            hdr.ethernet.dstAddr : exact;
        }
        actions = {
            ARPreply_forward;
            NoAction;
        }
        size = 1024;
        default_action = NoAction();
    }

    apply {
        if(hdr.ipv4.isValid()){
            ipv4_lpm.apply();
        }
        else if(hdr.ARP.isValid()){
            if(hdr.ARP.opcode==1){
                ARPrequest_forward();
            }
            else if(hdr.ARP.opcode==2){
                ARPreply.apply();
            }
        }
    }
}

/*************************************************************************
****************  E G R E S S   P R O C E S S I N G   *******************
* Often used for queue marks, mirroring, or post-routing edits.          *
*************************************************************************/

control MyEgress(inout headers hdr,
                 inout metadata meta,
                 inout standard_metadata_t standard_metadata) {
    action drop() {
        mark_to_drop(standard_metadata);
    }

    apply {
        if (standard_metadata.egress_port == standard_metadata.ingress_port)
            drop();
    }
}

/*************************************************************************
*************   C H E C K S U M    C O M P U T A T I O N   **************
* This block shows how to compute IPv4 header checksum when needed.      *
*************************************************************************/

control MyComputeChecksum(inout headers hdr, inout metadata meta) {
     apply {
        update_checksum(
            hdr.ipv4.isValid(),
            { 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);
    }
}


/*************************************************************************
***********************  D E P A R S E R  *******************************
* The deparser serializes headers back onto the packet in order.         *
*************************************************************************/

control MyDeparser(packet_out packet, in headers hdr) {
    apply {
        packet.emit(hdr.ethernet);
        packet.emit(hdr.ipv4);
        packet.emit(hdr.ARP);
    }
}

/*************************************************************************
***********************  S W I T C H  ***********************************
*************************************************************************/

V1Switch(
MyParser(),
MyVerifyChecksum(),
MyIngress(),
MyEgress(),
MyComputeChecksum(),
MyDeparser()
) main;

s1-runtime.json

{
  "target": "bmv2",
  "p4info": "build/ARP-INT.p4.p4info.txtpb",
  "bmv2_json": "build/ARP-INT.json",
  "table_entries": [
    {
      "table": "MyIngress.ipv4_lpm",
      "default_action": true,
      "action_name": "MyIngress.drop",
      "action_params": { }
    },
    {
      "table": "MyIngress.ipv4_lpm",
      "match": {
        "hdr.ipv4.dstAddr": ["10.0.0.1", 32]
      },
      "action_name": "MyIngress.ipv4_forward",
      "action_params": {
        "dstAddr": "08:00:00:00:01:11",
        "port": 1
      }
    },
    {
      "table": "MyIngress.ipv4_lpm",
      "match": {
        "hdr.ipv4.dstAddr": ["10.0.0.2", 32]
      },
      "action_name": "MyIngress.ipv4_forward",
      "action_params": {
        "dstAddr": "08:00:00:00:02:22",
        "port": 2
      }
    },

    {
      "table": "MyIngress.ARPreply",
      "match": {
        "hdr.ethernet.dstAddr": "08:00:00:00:01:11"
      },
      "action_name": "MyIngress.ARPreply_forward",
      "action_params": {
        "port": 1
      }
    },
    {
       "table": "MyIngress.ARPreply",
      "match": {
        "hdr.ethernet.dstAddr": "08:00:00:00:02:22"
      },
      "action_name": "MyIngress.ARPreply_forward",
      "action_params": {
        "port": 2
      }
    }
  ],

   "multicast_group_entries": [
    {
      "multicast_group_id": 1,
      "replicas": [
        {
          "egress_port": 1,
          "instance": 1
        },
        {
          "egress_port": 2,
          "instance": 1
        }
      ]
    }
  ]
}

@ ederollora2 Congratulations on your Master’s thesis! That sounds like a fantastic resource. Although my current goal is just basic flooding as mentioned above, I would love to study your full ARP implementation for my future reference. Please send the draft to: sx22013g@st.omu.ac.jp

If you configure multicast group number 1 to include all output ports, then yes, that seems likely to be at least quite close to what you want.

It is common when multicasting/flooding packets to have some mechanism to prevent the packet from going out the same port that it arrived on. If you have two neighboring switches that both flood without dropping the copy back to the port they arrived on, then you get an infinite loop from a single packet sent between the switches, which start sending it back and forth between them forever.

@Andy
Thank you for looking at my code.

This MyEgressProcess covers what you’re worried about, right?

control MyEgress(inout headers hdr,
                 inout metadata meta,
                 inout standard_metadata_t standard_metadata) {
    action drop() {
        mark_to_drop(standard_metadata);
    }

    apply {
        if (standard_metadata.egress_port == standard_metadata.ingress_port)
            drop();
    }
}

That looks good, yes, as long as you never want to forward any type of packets at all, ARP or otherwise, out of a part that it arrived on. I suspect that is what you want for your P4 program.

(A fancier program that implements encapsulating packets into tunnels, or decapsulating packets from tunnel headers, sometimes explicitly wants packets to go out of a physical port on which it arrives, is the only reason I say it conditionally like that. I suspect you have no need of such behavior in your P4 program.)

@andyfingerhut
Yes, exactly. I don’t intend to implement complex features like tunneling for now, so avoiding forwarding packets back to the ingress port is exactly what I want.
Thank you for the clarification!