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.包的解析
对数据包的解析工作主要是:
- 端口检查:判定数据包的进端口是否是CPU_PORT,这里CPU_PORT=255,如果数据包的进端口是从这里进来的,那么它是一个从数据平面下达的packet_out类型的数据包(跳转到2),如果是普通数据包,直接解析以太网地址即可(跳转到3)
- packet_out:解析packet_out数据包,观察这个数据包的cpu_out的字段,发现它的字段中包括了一个egress_port,这里使用它来定义接下去packet_out的包要如何转发(跳转到3)
- ethernet:正常的以太网地址解析,根据其以太网地址解析IPv4(跳转到4)或者IPv6(跳转到5)的数据包
- ipv4:解析ipv4数据包头,并用ip_proto来标记数据包的ip协议的上层协议类型,区分它是TCP(跳转到8)还是UDP(跳转到9)还是ICMP(跳转到10)
- ipv6:解析ipv6数据包头,并用ip_proto来标记数据包的ipv6协议的下一个协议类型,区分它是TCP(跳转到8)还是UDP(跳转到9)还是ICMPv6(跳转到11),当然,可能上层协议是自己的拓展头(跳转到6),在这里这个拓展头是SRH,协议号是43
-
srv6:解析出来它的srv6的拓展头,紧接着去解析它的SID序列(跳转到7)
-
srv6_list:核心思想是,根据自己的SL一步一步拆开,然后解析,直到最后一个为之,srv6的解析才算结束。
-
首先判断SL的值是否是SID序列中当前对应的索引,如果是了话,说明这个SID是接下来我要去的地方,就把它替换到自己的包头的IPv6目的地址上mark_current_srv6。bool next_segment = (bit<32>)hdr.srv6h.segment_left - 1 == (bit<32>)hdr.srv6_list.lastIndex;
-
如果上述判断没有成功,就继续逐个检查下去: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都会自动加一
-
-
TCP:解析tcp协议,把交换机内的local_metadata的传输层源和目的端口都赋值
-
UDP:解析udp协议,把交换机内的local_metadata的传输层源和目的端口都赋值
-
ICMP:解析icmp协议,把交换机内local_metadata的icmp类型赋值
-
ICMPv6:解析icmpv6协议,把交换机内local_metadata的icmp类型赋值,这里icmpv6有3种类型,分别是ICMP6_TYPE_NS和ICMP6_TYPE_NA和其他,他们都被视为NDP解析(跳转到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控制数据平面。