SRv6项目实践(二):基本的P4框架

1.数据包头的定义

在实现SRv6之前,有很多的工作需要做,首先先阅读一下p4的代码总体框架,数据包的包头格式一共有如下这些,我们需要把他们的协议逐一完善

struct parsed_headers_t {
    cpu_out_header_t cpu_out;
    cpu_in_header_t cpu_in;
    ethernet_t ethernet;
    ipv4_t ipv4;
    ipv6_t ipv6;
    srv6h_t srv6h;
    srv6_list_t[SRV6_MAX_HOPS] srv6_list;
    tcp_t tcp;
    udp_t udp;
    icmp_t icmp;
    icmpv6_t icmpv6;
    ndp_t ndp;
}

展开来说,这些协议的包头格式如下所示,其中一个很重要的数据包头是packet_in和out

header ethernet_t {
    mac_addr_t  dst_addr;
    mac_addr_t  src_addr;
    bit<16>     ether_type;
}

header ipv4_t {
    bit<4>   version;
    bit<4>   ihl;
    bit<6>   dscp;
    bit<2>   ecn;
    bit<16>  total_len;
    bit<16>  identification;
    bit<3>   flags;
    bit<13>  frag_offset;
    bit<8>   ttl;
    bit<8>   protocol;
    bit<16>  hdr_checksum;
    bit<32>  src_addr;
    bit<32>  dst_addr;
}

header ipv6_t {
    bit<4>    version;
    bit<8>    traffic_class;
    bit<20>   flow_label;
    bit<16>   payload_len;
    bit<8>    next_hdr;
    bit<8>    hop_limit;
    bit<128>  src_addr;
    bit<128>  dst_addr;
}

header srv6h_t {
    bit<8>   next_hdr;
    bit<8>   hdr_ext_len;
    bit<8>   routing_type;
    bit<8>   segment_left;
    bit<8>   last_entry;
    bit<8>   flags;
    bit<16>  tag;
}

header srv6_list_t {
    bit<128>  segment_id;
}

header tcp_t {
    bit<16>  src_port;
    bit<16>  dst_port;
    bit<32>  seq_no;
    bit<32>  ack_no;
    bit<4>   data_offset;
    bit<3>   res;
    bit<3>   ecn;
    bit<6>   ctrl;
    bit<16>  window;
    bit<16>  checksum;
    bit<16>  urgent_ptr;
}

header udp_t {
    bit<16> src_port;
    bit<16> dst_port;
    bit<16> len;
    bit<16> checksum;
}

header icmp_t {
    bit<8>   type;
    bit<8>   icmp_code;
    bit<16>  checksum;
    bit<16>  identifier;
    bit<16>  sequence_number;
    bit<64>  timestamp;
}

header icmpv6_t {
    bit<8>   type;
    bit<8>   code;
    bit<16>  checksum;
}

header ndp_t {
    bit<32>      flags;
    ipv6_addr_t  target_ipv6_addr;
    // NDP option.
    bit<8>       type;
    bit<8>       length;
    bit<48>      target_mac_addr;
}

// Packet-in header. Prepended to packets sent to the CPU_PORT and used by the
// P4Runtime server (Stratum) to populate the PacketIn message metadata fields.
// Here we use it to carry the original ingress port where the packet was
// received.
@controller_header("packet_in")
header cpu_in_header_t {
    port_num_t  ingress_port;
    bit<7>      _pad;
}

// Packet-out header. Prepended to packets received from the CPU_PORT. Fields of
// this header are populated by the P4Runtime server based on the P4Runtime
// PacketOut metadata fields. Here we use it to inform the P4 pipeline on which
// port this packet-out should be transmitted.
@controller_header("packet_out")
header cpu_out_header_t {
    port_num_t  egress_port;
    bit<7>      _pad;
}

2.包的解析

对数据包的解析工作主要是:

  1. 端口检查:判定数据包的进端口是否是CPU_PORT,这里CPU_PORT=255,如果数据包的进端口是从这里进来的,那么它是一个从数据平面下达的packet_out类型的数据包(跳转到2),如果是普通数据包,直接解析以太网地址即可(跳转到3)
  2. packet_out:解析packet_out数据包,观察这个数据包的cpu_out的字段,发现它的字段中包括了一个egress_port,这里使用它来定义接下去packet_out的包要如何转发(跳转到3)
  3. ethernet:正常的以太网地址解析,根据其以太网地址解析IPv4(跳转到4)或者IPv6(跳转到5)的数据包
  4. ipv4:解析ipv4数据包头,并用ip_proto来标记数据包的ip协议的上层协议类型,区分它是TCP(跳转到8)还是UDP(跳转到9)还是ICMP(跳转到10)
  5. ipv6:解析ipv6数据包头,并用ip_proto来标记数据包的ipv6协议的下一个协议类型,区分它是TCP(跳转到8)还是UDP(跳转到9)还是ICMPv6(跳转到11),当然,可能上层协议是自己的拓展头(跳转到6),在这里这个拓展头是SRH,协议号是43
  6. srv6:解析出来它的srv6的拓展头,紧接着去解析它的SID序列(跳转到7)

  7. srv6_list:核心思想是,根据自己的SL一步一步拆开,然后解析,直到最后一个为之,srv6的解析才算结束。

    • bool next_segment = 
      (bit<32>)hdr.srv6h.segment_left - 1 == 
      (bit<32>)hdr.srv6_list.lastIndex;
      首先判断SL的值是否是SID序列中当前对应的索引,如果是了话,说明这个SID是接下来我要去的地方,就把它替换到自己的包头的IPv6目的地址上mark_current_srv6
    • 如果上述判断没有成功,就继续逐个检查下去:check_last_srv6。检查的过程中,如果发现这是最后一个了,那就去解析srv6以外的东西了,也就是上层协议

      bool last_segment = 
      (bit<32>)hdr.srv6h.last_entry == 
      (bit<32>)hdr.srv6_list.lastIndex;
    • 解析srv6以外的东西:根据hdr.srv6h.next_hdr,判断下一个协议是TCP(跳转到8)还是UDP(跳转到9)还是ICMP(跳转到10)。小注释:hdr.srv6_list.next用完以后,srv6_list和lastIndex都会自动加一
  8. TCP:解析tcp协议,把交换机内的local_metadata的传输层源和目的端口都赋值

  9. UDP:解析udp协议,把交换机内的local_metadata的传输层源和目的端口都赋值

  10. ICMP:解析icmp协议,把交换机内local_metadata的icmp类型赋值

  11. ICMPv6:解析icmpv6协议,把交换机内local_metadata的icmp类型赋值,这里icmpv6有3种类型,分别是ICMP6_TYPE_NS和ICMP6_TYPE_NA和其他,他们都被视为NDP解析(跳转到12),无视其他类型

  12. NDP:解析NDP的首部,解析结束

parser ParserImpl (packet_in packet,
                   out parsed_headers_t hdr,
                   inout local_metadata_t local_metadata,
                   inout standard_metadata_t standard_metadata)
{
    state start {
        transition select(standard_metadata.ingress_port) {
            CPU_PORT: parse_packet_out;
            default: parse_ethernet;
        }
    }

    state parse_packet_out {
        packet.extract(hdr.cpu_out);
        transition parse_ethernet;
    }

    state parse_ethernet {
        packet.extract(hdr.ethernet);
        transition select(hdr.ethernet.ether_type){
            ETHERTYPE_IPV4: parse_ipv4;
            ETHERTYPE_IPV6: parse_ipv6;
            default: accept;
        }
    }

    state parse_ipv4 {
        packet.extract(hdr.ipv4);
        local_metadata.ip_proto = hdr.ipv4.protocol;
        transition select(hdr.ipv4.protocol) {
            IP_PROTO_TCP: parse_tcp;
            IP_PROTO_UDP: parse_udp;
            IP_PROTO_ICMP: parse_icmp;
            default: accept;
        }
    }

    state parse_ipv6 {
        packet.extract(hdr.ipv6);
        local_metadata.ip_proto = hdr.ipv6.next_hdr;
        transition select(hdr.ipv6.next_hdr) {
            IP_PROTO_TCP: parse_tcp;
            IP_PROTO_UDP: parse_udp;
            IP_PROTO_ICMPV6: parse_icmpv6;
            IP_PROTO_SRV6: parse_srv6;
            default: accept;
        }
    }

    state parse_tcp {
        packet.extract(hdr.tcp);
        local_metadata.l4_src_port = hdr.tcp.src_port;
        local_metadata.l4_dst_port = hdr.tcp.dst_port;
        transition accept;
    }

    state parse_udp {
        packet.extract(hdr.udp);
        local_metadata.l4_src_port = hdr.udp.src_port;
        local_metadata.l4_dst_port = hdr.udp.dst_port;
        transition accept;
    }

    state parse_icmp {
        packet.extract(hdr.icmp);
        local_metadata.icmp_type = hdr.icmp.type;
        transition accept;
    }

    state parse_icmpv6 {
        packet.extract(hdr.icmpv6);
        local_metadata.icmp_type = hdr.icmpv6.type;
        transition select(hdr.icmpv6.type) {
            ICMP6_TYPE_NS: parse_ndp;
            ICMP6_TYPE_NA: parse_ndp;
            default: accept;
        }
    }

    state parse_ndp {
        packet.extract(hdr.ndp);
        transition accept;
    }

    state parse_srv6 {
        packet.extract(hdr.srv6h);
        transition parse_srv6_list;
    }

    state parse_srv6_list {
        packet.extract(hdr.srv6_list.next);
        bool next_segment = (bit<32>)hdr.srv6h.segment_left - 1 == (bit<32>)hdr.srv6_list.lastIndex;
        transition select(next_segment) {
            true: mark_current_srv6;
            default: check_last_srv6;
        }
    }

    state mark_current_srv6 {
        local_metadata.next_srv6_sid = hdr.srv6_list.last.segment_id;
        transition check_last_srv6;
    }

    state check_last_srv6 {
        // working with bit<8> and int<32> which cannot be cast directly; using
        // bit<32> as common intermediate type for comparision
        bool last_segment = (bit<32>)hdr.srv6h.last_entry == (bit<32>)hdr.srv6_list.lastIndex;
        transition select(last_segment) {
           true: parse_srv6_next_hdr;
           false: parse_srv6_list;
        }
    }

    state parse_srv6_next_hdr {
        transition select(hdr.srv6h.next_hdr) {
            IP_PROTO_TCP: parse_tcp;
            IP_PROTO_UDP: parse_udp;
            IP_PROTO_ICMPV6: parse_icmpv6;
            default: accept;
        }
    }
}

小插曲:在继续讲之前,可能有很多人对这些协议都有个大概的了解,但是对ICMPv6可能比较陌生,所以,如果知道ICMPv6协议的朋友,请直接看后面。

以下是IPV6深入-NDP邻居发现协议 - 知乎 (zhihu.com)的一些截取

在IPv4中,当主机需要和目标主机通信时,必须先通过ARP协议获得目的主机的链路层地址。在IPv6中,同样需要从IP地址解析到链路层地址的功能。邻居发现协议实现了这个功能。

与IPv4的ARP相比,IPv6地址解析技术工作在OSI参考模型的网络层,与链路层协议无关。这一特点的益处如下:

(1)在第三层实现地址解析可以利用三层标准的安全认证机制来防止ARP攻击和ARP欺骗。

(2)IPv6的地址解析利用三层组播寻址限制了报文的传播范围,可节省网络带宽。

IPV6地址解析的具体过程如下:

(1)节点A向节点B发送NS报文,源地址为A的IPV6单播地址(可以是唯一本地地址也可以是链路本地地址),目的地址是B的被请求节点组播地址。源mac地址是节点A mac地址,目的mac地址是节点B的被请求节点组播mac地址。

(2)节点B收到节点A的NS报文后即知道节点A的IPV6单播地址、mac地址及被请求节点组播地址等,此时,节点B会回复NA报文,NA报文中源地址为节点B的IPV6单播地址、 MAC地址,目的地址为节点A的IPV6单播地址、mac地址

(3)节点A收到节点B的NA报文后,即知晓了节点B的IPV6单播地址、mac地址

NUD(Neighbor Unreachable Detection,邻居不可达检测)是节点确定邻居可达性的过程。邻居不可达检测机制通过邻居可达性状态机来描述邻居的可达性。

邻居可达性状态机保存在邻居缓存表中,共有如下6种状态:

(1)INCOMPLETE(未完成状态):表示正在解析地址,但邻居链路层地址尚未确定。

(2)REACHABLE(可达状态):表示地址解析成功,该邻居可达。

(3)STALE(失效状态):表示可达时间耗尽,未确定邻居是否可达。

(4)DELAY(延迟状态):表示未确定邻居是否可达。DELAY状态不是一个稳定的状态,而是一个延时等待状态。

(5)PROBE(探测状态):节点会向处于PROBE状态的邻居持续发送NS报文。

(6)EMPTY(空闲状态):表示节点上没有相关邻接点的邻居缓存表项。

之后的东西大家可以上对应的网站查询,本项目只关注NA和NS

3.进端口控制流

首先,在进端口控制流中,先写几个基本的表(在SRv6项目实践系列中还会不断添加),这里的注解用于在控制面能够得到匹配到该表的计数器,这个是一个L2的根据具体以太网目的地址转发的表,很简单吧,它是一个用户单播的表。

action set_egress_port(port_num_t port_num) {
        standard_metadata.egress_spec = port_num;
    }

    table l2_exact_table {
        key = {
            hdr.ethernet.dst_addr: exact;
        }
        actions = {
            set_egress_port;
            @defaultonly drop;
        }
        const default_action = drop;
        // The @name annotation is used here to provide a name to this table
        // counter, as it will be needed by the compiler to generate the
        // corresponding P4Info entity.
        @name("l2_exact_table_counter")
        counters = direct_counter(CounterType.packets_and_bytes);
    }

 这是一个用于组播的表,它被运用于组播中,比如NS的消息,在以太网中,NS组播的L2地址是以33:33为掩码的,在set_multicast_group中,设定一下它的组播的id就好啦,至于id对应的组播哪些地址,还不用说。

action set_multicast_group(mcast_group_id_t gid) {
        // gid will be used by the Packet Replication Engine (PRE) in the
        // Traffic Manager--located right after the ingress pipeline, to
        // replicate a packet to multiple egress ports, specified by the control
        // plane by means of P4Runtime MulticastGroupEntry messages.
        standard_metadata.mcast_grp = gid;
        local_metadata.is_multicast = true;
    }

    table l2_ternary_table {
        key = {
            hdr.ethernet.dst_addr: ternary;
        }
        actions = {
            set_multicast_group;
            @defaultonly drop;
        }
        const default_action = drop;
        @name("l2_ternary_table_counter")
        counters = direct_counter(CounterType.packets_and_bytes);
    }

这里,有一个及其重要的东西叫 ACLACL(访问控制列表)基础篇-超有趣学网络 - 知乎 (zhihu.com)

ACL,是Access Control List的简写,中文名称叫做“访问控制列表”。它是由一系列条件规则(即描述报文匹配条件的判断语句)组成, 这些条件规则可以是报文的源地址、目的地址、端口号等,是一种应用在网络设备各种软硬接口上的的指令列表。

访问控制列表的使用场景:

根据ACL中的匹配条件对进站和出站的报文进行过滤处理。打个比方,ACL其实是一种报文过滤器,ACL规则就是过滤器的滤芯。安装什么样的滤芯(即根据报文特征配置相应的ACL规则),ACL就能过滤出什么样的报文。

 ACL的主要功能:根据数据包的信息,决定把他们丢掉或者发送到cpu中,或者复制一份发到cpu,然后转发。在这里CPU就是p4runtime

action send_to_cpu() {
        standard_metadata.egress_spec = CPU_PORT;
    }

    action clone_to_cpu() {
        // Cloning is achieved by using a v1model-specific primitive. Here we
        // set the type of clone operation (ingress-to-egress pipeline), the
        // clone session ID (the CPU one), and the metadata fields we want to
        // preserve for the cloned packet replica.
        clone3(CloneType.I2E, CPU_CLONE_SESSION_ID, { standard_metadata.ingress_port });
    }

    table acl_table {
        key = {
            standard_metadata.ingress_port: ternary;
            hdr.ethernet.dst_addr:          ternary;
            hdr.ethernet.src_addr:          ternary;
            hdr.ethernet.ether_type:        ternary;
            local_metadata.ip_proto:        ternary;
            local_metadata.icmp_type:       ternary;
            local_metadata.l4_src_port:     ternary;
            local_metadata.l4_dst_port:     ternary;
        }
        actions = {
            send_to_cpu;
            clone_to_cpu;
            drop;
        }
        @name("acl_table_counter")
        counters = direct_counter(CounterType.packets_and_bytes);
    }

最后,apply这几个表,可以看到首先匹配单播,然后匹配多播,最后都要运用ACL实现访问控制

 apply {


        bool do_l3_l2 = true;

        if (do_l3_l2) {

            // L2 bridging logic. Apply the exact table first...
            if (!l2_exact_table.apply().hit) {
                // ...if an entry is NOT found, apply the ternary one in case
                // this is a multicast/broadcast NDP NS packet.
                l2_ternary_table.apply();
            }
        }
acl_table.apply();
}

4.出端口控制流

出端口的实现,相对简单,本地的组播不可能会组播回入端口。

control EgressPipeImpl (inout parsed_headers_t hdr,
                        inout local_metadata_t local_metadata,
                        inout standard_metadata_t standard_metadata) {
    apply {


        // If this is a multicast packet (flag set by l2_ternary_table), make
        // sure we are not replicating the packet on the same port where it was
        // received. This is useful to avoid broadcasting NDP requests on the
        // ingress port.
        if (local_metadata.is_multicast == true &&
              standard_metadata.ingress_port == standard_metadata.egress_port) {
            mark_to_drop(standard_metadata);
        }
    }
}

最后的校验和和封包就不用看了,从这个基本框架上来看,目前,还没能实现与控制面的交互,接下来的SRv6项目实践(三),我们将手动使用P4runtime控制数据平面。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值