From 4687de0cd21622976a1897945d3ee6d435aca5e1 Mon Sep 17 00:00:00 2001 From: hujingfei Date: Mon, 30 Dec 2024 10:31:55 +0800 Subject: [PATCH] Support Inband Flow Analyzer (IFA) 1.support IFA over ipv4/ipv6 + udp/tcp/vxlan/gre/geneve 2.add IFA unit tests --- scapy/contrib/ifa.py | 131 ++++++++++++++++++++++++++ scapy/layers/inet.py | 36 +++++-- scapy/layers/l2.py | 6 ++ test/contrib/ifa.uts | 218 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 383 insertions(+), 8 deletions(-) create mode 100644 scapy/contrib/ifa.py create mode 100644 test/contrib/ifa.uts diff --git a/scapy/contrib/ifa.py b/scapy/contrib/ifa.py new file mode 100644 index 00000000000..b6128141fc9 --- /dev/null +++ b/scapy/contrib/ifa.py @@ -0,0 +1,131 @@ +# scapy.contrib.description = Inband Flow Analyzer Protocol (IFA) +# scapy.contrib.status = loads + +''' +Inband Flow Analyzer Protocol (IFA) + +References: +https://datatracker.ietf.org/doc/html/draft-kumar-ippm-ifa-08 +''' + +import struct +import socket +from scapy.data import IP_PROTOS +from scapy.layers.l2 import Ether, GRE +from scapy.layers.inet import IP, TCP, UDP +from scapy.layers.inet6 import IPv6 +from scapy.layers.vxlan import VXLAN +from scapy.contrib.geneve import GENEVE +from scapy.packet import Packet, bind_layers +from scapy.fields import BitField, BitEnumField, FlagsField, \ + ByteField, ByteEnumField, ShortField, IntField, PacketListField + +IPPROTO_IFA = 131 +IP_PROTOS[IPPROTO_IFA] = 'IFA' + +_ifa_flags = [ + 'C', # Checksum + 'TA', # Turn Around + 'I', # Inband + 'TS', # Tail Stamp + 'MF', # Metadata Fragment + 'R', # Reserved + 'R', # Reserved + 'R', # Reserved +] + +_ifa_action = [ + 'R', # Reserved + 'R', # Reserved + 'R', # Reserved + 'R', # Reserved + 'R', # Reserved + 'R', # Reserved + 'C', # Color bit to mark the packet + 'L', # Loss bit to measure packet loss +] + +_ifa_speed = { + 0: '10Gbps', + 1: '25Gbps', + 2: '40Gbps', + 3: '50Gbps', + 4: '100Gbps', + 5: '200Gbps', + 6: '400Gbps', +} + + +class IFA(Packet): + name = 'IFA' + fields_desc = [ + BitField('ver', 3, 4), + BitField('gns', 0, 4), + ByteEnumField("nexthdr", 0, IP_PROTOS), + FlagsField("flags", 0, 8, _ifa_flags), + ByteField('maxlen', 255), + ] + + +class IFAMd(Packet): + name = 'IFAMd' + fields_desc = [ + BitField('lns', 0, 4), + BitField('device_id', 0, 20), + ByteField('ttl', 0), + BitEnumField('speed', 0, 4, _ifa_speed), + BitField('ecn', 0, 2), + BitField('qid', 0, 6), + BitField('rx_sec', 0, 20), + ShortField('dport', 0), + ShortField('sport', 0), + IntField('rx_nsec', 0), + IntField('latency', 0), + IntField('qbytes', 0), + ShortField('rsvd0', 0), + ShortField('qcells', 0), + IntField('rsvd1', 0), + ] + + def extract_padding(self, s): + return "", s + + +class IFAMdHdr(Packet): + name = 'IFAMdHdr' + fields_desc = [ + ByteField('request', 0), + FlagsField("action", 0, 8, _ifa_action), + ByteField('hoplmt', 128), + ByteField('curlen', 0), + PacketListField("mdstack", None, IFAMd, + length_from=lambda pkt: pkt.curlen * 4) + ] + + def post_build(self, p, pay): + mdlen = (len(p) - 4) // 4 + if self.curlen != mdlen: + p = p[:3] + struct.pack("!B", mdlen) + p[4:] + return p + pay + + def guess_payload_class(self, payload): + if isinstance(self.underlayer, UDP): + if self.underlayer.dport in [4789, 4790]: + return VXLAN + elif self.underlayer.dport == 6081: + return GENEVE + elif isinstance(self.underlayer, GRE): + if self.underlayer.proto == 0x6558: + return Ether + if self.underlayer.proto == 0x0800: + return IP + if self.underlayer.proto == 0x86dd: + return IPv6 + return Packet.guess_payload_class(self, payload) + + +bind_layers(IP, IFA, proto=IPPROTO_IFA) +bind_layers(IPv6, IFA, nh=IPPROTO_IFA) +bind_layers(IFA, TCP, nexthdr=socket.IPPROTO_TCP) +bind_layers(IFA, UDP, nexthdr=socket.IPPROTO_UDP) +bind_layers(IFA, GRE, nexthdr=socket.IPPROTO_GRE) diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py index a361664a681..f676b71f603 100644 --- a/scapy/layers/inet.py +++ b/scapy/layers/inet.py @@ -757,11 +757,15 @@ def post_build(self, p, pay): dataofs = (dataofs << 4) | orb(p[12]) & 0x0f p = p[:12] + chb(dataofs & 0xff) + p[13:] if self.chksum is None: - if isinstance(self.underlayer, IP): - ck = in4_chksum(socket.IPPROTO_TCP, self.underlayer, p) + _underlayer = self.underlayer + from scapy.contrib.ifa import IFA + if isinstance(self.underlayer, IFA): + _underlayer = self.underlayer.underlayer + if isinstance(_underlayer, IP): + ck = in4_chksum(socket.IPPROTO_TCP, _underlayer, p) p = p[:16] + struct.pack("!H", ck) + p[18:] - elif conf.ipv6_enabled and isinstance(self.underlayer, scapy.layers.inet6.IPv6) or isinstance(self.underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501 - ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_TCP, self.underlayer, p) # noqa: E501 + elif conf.ipv6_enabled and isinstance(_underlayer, scapy.layers.inet6.IPv6) or isinstance(_underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501 + ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_TCP, _underlayer, p) # noqa: E501 p = p[:16] + struct.pack("!H", ck) + p[18:] else: log_runtime.info( @@ -814,6 +818,12 @@ def mysummary(self): else: return self.sprintf("TCP %TCP.sport% > %TCP.dport% %TCP.flags%") + def guess_payload_class(self, payload): + from scapy.contrib.ifa import IFA, IFAMdHdr + if isinstance(self.underlayer, IFA): + return IFAMdHdr + return Packet.guess_payload_class(self, payload) + class UDP(Packet): name = "UDP" @@ -829,14 +839,18 @@ def post_build(self, p, pay): tmp_len = len(p) p = p[:4] + struct.pack("!H", tmp_len) + p[6:] if self.chksum is None: - if isinstance(self.underlayer, IP): - ck = in4_chksum(socket.IPPROTO_UDP, self.underlayer, p) + _underlayer = self.underlayer + from scapy.contrib.ifa import IFA + if isinstance(self.underlayer, IFA): + _underlayer = self.underlayer.underlayer + if isinstance(_underlayer, IP): + ck = in4_chksum(socket.IPPROTO_UDP, _underlayer, p) # According to RFC768 if the result checksum is 0, it should be set to 0xFFFF # noqa: E501 if ck == 0: ck = 0xFFFF p = p[:6] + struct.pack("!H", ck) + p[8:] - elif isinstance(self.underlayer, scapy.layers.inet6.IPv6) or isinstance(self.underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501 - ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_UDP, self.underlayer, p) # noqa: E501 + elif isinstance(_underlayer, scapy.layers.inet6.IPv6) or isinstance(_underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501 + ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_UDP, _underlayer, p) # noqa: E501 # According to RFC2460 if the result checksum is 0, it should be set to 0xFFFF # noqa: E501 if ck == 0: ck = 0xFFFF @@ -870,6 +884,12 @@ def mysummary(self): else: return self.sprintf("UDP %UDP.sport% > %UDP.dport%") + def guess_payload_class(self, payload): + from scapy.contrib.ifa import IFA, IFAMdHdr + if isinstance(self.underlayer, IFA): + return IFAMdHdr + return Packet.guess_payload_class(self, payload) + # RFC 4884 ICMP extensions _ICMP_classnums = { diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py index 532cd9a5f21..d091a6f6287 100644 --- a/scapy/layers/l2.py +++ b/scapy/layers/l2.py @@ -668,6 +668,12 @@ def post_build(self, p, pay): p = p[:4] + chb((c >> 8) & 0xff) + chb(c & 0xff) + p[6:] return p + def guess_payload_class(self, payload): + from scapy.contrib.ifa import IFA, IFAMdHdr + if isinstance(self.underlayer, IFA): + return IFAMdHdr + return Packet.guess_payload_class(self, payload) + class GRE_PPTP(GRE): diff --git a/test/contrib/ifa.uts b/test/contrib/ifa.uts new file mode 100644 index 00000000000..30eaf1f67d3 --- /dev/null +++ b/test/contrib/ifa.uts @@ -0,0 +1,218 @@ +% IFA unit test + +# +# execute test: +# > test/run_tests -P "load_contrib('ifa')" -t test/contrib/ifa.uts +# TBD: IPv6 with ext header +# + ++ IFA testsuit + + += Build & Dissect, IFA over TCP/UDP + +oeth = Ether(dst='b6:18:00:33:33:00', src='b6:18:00:22:22:00') +oip4 = IP(src='172.1.1.0', dst='172.1.3.0', ttl=62) +oip6 = IPv6(src='1000::1', dst='1000::3', hlim=62) +ifa = IFA(ver=2, gns=15, flags=4, maxlen=255) +tri_md = IFAMd(lns=1, device_id=2850, ttl=63, speed=4, dport=25, sport=24) +src_md = IFAMd(lns=0, device_id=2577, ttl=63, speed=0, dport=25, sport=24) +ifa_md = IFAMdHdr(request=238, action=192, hoplmt=126, mdstack=[tri_md, src_md]) +vxlan = VXLAN(vni=1000) +ieth = Ether(dst='b6:18:00:33:33:00', src='b6:18:00:11:11:00') +iip4 = IP(src='198.1.1.7', dst='198.1.2.8', ttl=63) +iip6 = IPv6(src='1000::7', dst='1000::8', hlim=63) +iudp = UDP(sport=1100, dport=2200) +itcp = TCP(sport=3300, dport=4400) +payl = Raw([0xff]*64) + +pkts = [ + ieth/iip4/ifa/iudp/ifa_md/payl, + ieth/iip4/ifa/itcp/ifa_md/payl, + ieth/iip6/ifa/iudp/ifa_md/payl, + ieth/iip6/ifa/itcp/ifa_md/payl, +] + +chks = [ + {'IFAMdHdr.curlen':16, 'UDP.chksum':0x7219, 'IP.chksum':0xebc4}, + {'IFAMdHdr.curlen':16, 'TCP.chksum':0xf171, 'IP.chksum':0xebb8}, + {'IFAMdHdr.curlen':16, 'UDP.chksum':0xe11c}, + {'IFAMdHdr.curlen':16, 'TCP.chksum':0x6075}, +] + +for pkt, chk in zip(pkts, chks): + newpkt=Ether(raw(pkt)) + assert newpkt.summary() == pkt.summary() + for key, exp_val in chk.items(): + layer, filed = key.split('.') + value = newpkt[layer].fields[filed] + assert value == exp_val, f'0x{value:x} != 0x{exp_val:x}' + print(f'IFA over TCP/UDP pkts [{pkts.index(pkt)+1}/{len(pkts)}] pass') + + += Build & Dissect, IFA over IPv4.GRE + +pkts = [ + oeth/oip4/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip4/payl, + oeth/oip4/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip4/iudp/payl, + oeth/oip4/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip4/itcp/payl, + oeth/oip4/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip6/payl, + oeth/oip4/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip6/iudp/payl, + oeth/oip4/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip6/itcp/payl, + oeth/oip4/ifa/GRE(proto=0x0800)/ifa_md/iip4/payl, + oeth/oip4/ifa/GRE(proto=0x0800)/ifa_md/iip4/iudp/payl, + oeth/oip4/ifa/GRE(proto=0x0800)/ifa_md/iip4/itcp/payl, + oeth/oip4/ifa/GRE(proto=0x86dd)/ifa_md/iip6/payl, + oeth/oip4/ifa/GRE(proto=0x86dd)/ifa_md/iip6/iudp/payl, + oeth/oip4/ifa/GRE(proto=0x86dd)/ifa_md/iip6/itcp/payl, +] + +chks = [ + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fb6}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fae}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fa2}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fa2}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1f9a}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1f8e}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fc4}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fbc}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fb0}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fb0}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fa8}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1f9c}, +] + +for pkt, chk in zip(pkts, chks): + newpkt=Ether(raw(pkt)) + assert newpkt.summary() == pkt.summary() + for key, exp_val in chk.items(): + layer, filed = key.split('.') + value = newpkt[layer].fields[filed] + assert value == exp_val, f'0x{value:x} != 0x{exp_val:x}' + print(f'IFA over IPv4.GRE pkts [{pkts.index(pkt)+1}/{len(pkts)}] pass') + + += Build & Dissect, IFA over IPv6.GRE + +pkts = [ + oeth/oip6/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip4/payl, + oeth/oip6/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip4/iudp/payl, + oeth/oip6/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip4/itcp/payl, + oeth/oip6/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip6/payl, + oeth/oip6/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip6/iudp/payl, + oeth/oip6/ifa/GRE(proto=0x6558)/ifa_md/ieth/iip6/itcp/payl, + oeth/oip6/ifa/GRE(proto=0x0800)/ifa_md/iip4/payl, + oeth/oip6/ifa/GRE(proto=0x0800)/ifa_md/iip4/iudp/payl, + oeth/oip6/ifa/GRE(proto=0x0800)/ifa_md/iip4/itcp/payl, + oeth/oip6/ifa/GRE(proto=0x86dd)/ifa_md/iip6/payl, + oeth/oip6/ifa/GRE(proto=0x86dd)/ifa_md/iip6/iudp/payl, + oeth/oip6/ifa/GRE(proto=0x86dd)/ifa_md/iip6/itcp/payl, +] + +chks = [ + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, +] + +for pkt, chk in zip(pkts, chks): + newpkt=Ether(raw(pkt)) + assert newpkt.summary() == pkt.summary() + for key, exp_val in chk.items(): + layer, filed = key.split('.') + value = newpkt[layer].fields[filed] + assert value == exp_val, f'0x{value:x} != 0x{exp_val:x}' + print(f'IFA over IPv6.GRE pkts [{pkts.index(pkt)+1}/{len(pkts)}] pass') + + += Build & Dissect, IFA over IPv4.VXLAN + +pkts = [ + oeth/oip4/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip4/payl, + oeth/oip4/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip4/iudp/payl, + oeth/oip4/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip4/itcp/payl, + oeth/oip4/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip6/payl, + oeth/oip4/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip6/iudp/payl, + oeth/oip4/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip6/itcp/payl, + oeth/oip4/ifa/UDP(dport=4790)/ifa_md/vxlan/iip4/payl, + oeth/oip4/ifa/UDP(dport=4790)/ifa_md/vxlan/iip4/iudp/payl, + oeth/oip4/ifa/UDP(dport=4790)/ifa_md/vxlan/iip4/itcp/payl, + oeth/oip4/ifa/UDP(dport=4790)/ifa_md/vxlan/iip6/payl, + oeth/oip4/ifa/UDP(dport=4790)/ifa_md/vxlan/iip6/iudp/payl, + oeth/oip4/ifa/UDP(dport=4790)/ifa_md/vxlan/iip6/itcp/payl, +] + +chks = [ + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1faa}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fa2}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1f96}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1f96}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1f8e}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1f82}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fb8}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fb0}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fa4}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1fa4}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1f9c}, + {'IFAMdHdr.curlen':16, 'IP.chksum':0x1f90}, +] + +for pkt, chk in zip(pkts, chks): + newpkt=Ether(raw(pkt)) + assert newpkt.summary() == pkt.summary() + for key, exp_val in chk.items(): + layer, filed = key.split('.') + value = newpkt[layer].fields[filed] + assert value == exp_val, f'0x{value:x} != 0x{exp_val:x}' + print(f'IFA over IPv4.VXLAN pkts [{pkts.index(pkt)+1}/{len(pkts)}] pass') + + += Build & Dissect, IFA over IPv6.VXLAN + +pkts = [ + oeth/oip6/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip4/payl, + oeth/oip6/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip4/iudp/payl, + oeth/oip6/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip4/itcp/payl, + oeth/oip6/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip6/payl, + oeth/oip6/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip6/iudp/payl, + oeth/oip6/ifa/UDP(dport=4789)/ifa_md/vxlan/ieth/iip6/itcp/payl, + oeth/oip6/ifa/UDP(dport=4790)/ifa_md/vxlan/iip4/payl, + oeth/oip6/ifa/UDP(dport=4790)/ifa_md/vxlan/iip4/iudp/payl, + oeth/oip6/ifa/UDP(dport=4790)/ifa_md/vxlan/iip4/itcp/payl, + oeth/oip6/ifa/UDP(dport=4790)/ifa_md/vxlan/iip6/payl, + oeth/oip6/ifa/UDP(dport=4790)/ifa_md/vxlan/iip6/iudp/payl, + oeth/oip6/ifa/UDP(dport=4790)/ifa_md/vxlan/iip6/itcp/payl, +] + +chks = [ + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, + {'IFAMdHdr.curlen':16}, +] + +for pkt, chk in zip(pkts, chks): + newpkt=Ether(raw(pkt)) + assert newpkt.summary() == pkt.summary() + for key, exp_val in chk.items(): + layer, filed = key.split('.') + value = newpkt[layer].fields[filed] + assert value == exp_val, f'0x{value:x} != 0x{exp_val:x}' + print(f'IFA over IPv6.VXLAN pkts [{pkts.index(pkt)+1}/{len(pkts)}] pass')