Skip to content

Commit a1a1f5a

Browse files
committed
Support Inband Flow Analyzer (IFA)
1.support IFA over ipv4/ipv6 + udp/tcp/vxlan/gre/geneve 2.add IFA unit tests
1 parent 1346522 commit a1a1f5a

File tree

4 files changed

+383
-8
lines changed

4 files changed

+383
-8
lines changed

scapy/contrib/ifa.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# scapy.contrib.description = Inband Flow Analyzer Protocol (IFA)
2+
# scapy.contrib.status = loads
3+
4+
'''
5+
Inband Flow Analyzer Protocol (IFA)
6+
7+
References:
8+
https://datatracker.ietf.org/doc/html/draft-kumar-ippm-ifa-08
9+
'''
10+
11+
import struct
12+
import socket
13+
from scapy.data import IP_PROTOS
14+
from scapy.layers.l2 import Ether, GRE
15+
from scapy.layers.inet import IP, TCP, UDP
16+
from scapy.layers.inet6 import IPv6
17+
from scapy.layers.vxlan import VXLAN
18+
from scapy.contrib.geneve import GENEVE
19+
from scapy.packet import Packet, bind_layers
20+
from scapy.fields import BitField, BitEnumField, FlagsField, \
21+
ByteField, ByteEnumField, ShortField, IntField, PacketListField
22+
23+
IPPROTO_IFA = 131
24+
IP_PROTOS[IPPROTO_IFA] = 'IFA'
25+
26+
_ifa_flags = [
27+
'C', # Checksum
28+
'TA', # Turn Around
29+
'I', # Inband
30+
'TS', # Tail Stamp
31+
'MF', # Metadata Fragment
32+
'R', # Reserved
33+
'R', # Reserved
34+
'R', # Reserved
35+
]
36+
37+
_ifa_action = [
38+
'R', # Reserved
39+
'R', # Reserved
40+
'R', # Reserved
41+
'R', # Reserved
42+
'R', # Reserved
43+
'R', # Reserved
44+
'C', # Color bit to mark the packet
45+
'L', # Loss bit to measure packet loss
46+
]
47+
48+
_ifa_speed = {
49+
0: '10Gbps',
50+
1: '25Gbps',
51+
2: '40Gbps',
52+
3: '50Gbps',
53+
4: '100Gbps',
54+
5: '200Gbps',
55+
6: '400Gbps',
56+
}
57+
58+
59+
class IFA(Packet):
60+
name = 'IFA'
61+
fields_desc = [
62+
BitField('ver', 3, 4),
63+
BitField('gns', 0, 4),
64+
ByteEnumField("nexthdr", 0, IP_PROTOS),
65+
FlagsField("flags", 0, 8, _ifa_flags),
66+
ByteField('maxlen', 255),
67+
]
68+
69+
70+
class IFAMd(Packet):
71+
name = 'IFAMd'
72+
fields_desc = [
73+
BitField('lns', 0, 4),
74+
BitField('device_id', 0, 20),
75+
ByteField('ttl', 0),
76+
BitEnumField('speed', 0, 4, _ifa_speed),
77+
BitField('ecn', 0, 2),
78+
BitField('qid', 0, 6),
79+
BitField('rx_sec', 0, 20),
80+
ShortField('dport', 0),
81+
ShortField('sport', 0),
82+
IntField('rx_nsec', 0),
83+
IntField('latency', 0),
84+
IntField('qbytes', 0),
85+
ShortField('rsvd0', 0),
86+
ShortField('qcells', 0),
87+
IntField('rsvd1', 0),
88+
]
89+
90+
def extract_padding(self, s):
91+
return "", s
92+
93+
94+
class IFAMdHdr(Packet):
95+
name = 'IFAMdHdr'
96+
fields_desc = [
97+
ByteField('request', 0),
98+
FlagsField("action", 0, 8, _ifa_action),
99+
ByteField('hoplmt', 128),
100+
ByteField('curlen', 0),
101+
PacketListField("mdstack", None, IFAMd,
102+
length_from=lambda pkt: pkt.curlen * 4)
103+
]
104+
105+
def post_build(self, p, pay):
106+
mdlen = (len(p) - 4) // 4
107+
if self.curlen != mdlen:
108+
p = p[:3] + struct.pack("!B", mdlen) + p[4:]
109+
return p + pay
110+
111+
def guess_payload_class(self, payload):
112+
if isinstance(self.underlayer, UDP):
113+
if self.underlayer.dport in [4789, 4790]:
114+
return VXLAN
115+
elif self.underlayer.dport == 6081:
116+
return GENEVE
117+
elif isinstance(self.underlayer, GRE):
118+
if self.underlayer.proto == 0x6558:
119+
return Ether
120+
if self.underlayer.proto == 0x0800:
121+
return IP
122+
if self.underlayer.proto == 0x86dd:
123+
return IPv6
124+
return Packet.guess_payload_class(self, payload)
125+
126+
127+
bind_layers(IP, IFA, proto=IPPROTO_IFA)
128+
bind_layers(IPv6, IFA, nh=IPPROTO_IFA)
129+
bind_layers(IFA, TCP, nexthdr=socket.IPPROTO_TCP)
130+
bind_layers(IFA, UDP, nexthdr=socket.IPPROTO_UDP)
131+
bind_layers(IFA, GRE, nexthdr=socket.IPPROTO_GRE)

scapy/layers/inet.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -757,11 +757,15 @@ def post_build(self, p, pay):
757757
dataofs = (dataofs << 4) | orb(p[12]) & 0x0f
758758
p = p[:12] + chb(dataofs & 0xff) + p[13:]
759759
if self.chksum is None:
760-
if isinstance(self.underlayer, IP):
761-
ck = in4_chksum(socket.IPPROTO_TCP, self.underlayer, p)
760+
_underlayer = self.underlayer
761+
from scapy.contrib.ifa import IFA
762+
if isinstance(self.underlayer, IFA):
763+
_underlayer = self.underlayer.underlayer
764+
if isinstance(_underlayer, IP):
765+
ck = in4_chksum(socket.IPPROTO_TCP, _underlayer, p)
762766
p = p[:16] + struct.pack("!H", ck) + p[18:]
763-
elif conf.ipv6_enabled and isinstance(self.underlayer, scapy.layers.inet6.IPv6) or isinstance(self.underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501
764-
ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_TCP, self.underlayer, p) # noqa: E501
767+
elif conf.ipv6_enabled and isinstance(_underlayer, scapy.layers.inet6.IPv6) or isinstance(_underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501
768+
ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_TCP, _underlayer, p) # noqa: E501
765769
p = p[:16] + struct.pack("!H", ck) + p[18:]
766770
else:
767771
log_runtime.info(
@@ -814,6 +818,12 @@ def mysummary(self):
814818
else:
815819
return self.sprintf("TCP %TCP.sport% > %TCP.dport% %TCP.flags%")
816820

821+
def guess_payload_class(self, payload):
822+
from scapy.contrib.ifa import IFA, IFAMdHdr
823+
if isinstance(self.underlayer, IFA):
824+
return IFAMdHdr
825+
return Packet.guess_payload_class(self, payload)
826+
817827

818828
class UDP(Packet):
819829
name = "UDP"
@@ -829,14 +839,18 @@ def post_build(self, p, pay):
829839
tmp_len = len(p)
830840
p = p[:4] + struct.pack("!H", tmp_len) + p[6:]
831841
if self.chksum is None:
832-
if isinstance(self.underlayer, IP):
833-
ck = in4_chksum(socket.IPPROTO_UDP, self.underlayer, p)
842+
_underlayer = self.underlayer
843+
from scapy.contrib.ifa import IFA
844+
if isinstance(self.underlayer, IFA):
845+
_underlayer = self.underlayer.underlayer
846+
if isinstance(_underlayer, IP):
847+
ck = in4_chksum(socket.IPPROTO_UDP, _underlayer, p)
834848
# According to RFC768 if the result checksum is 0, it should be set to 0xFFFF # noqa: E501
835849
if ck == 0:
836850
ck = 0xFFFF
837851
p = p[:6] + struct.pack("!H", ck) + p[8:]
838-
elif isinstance(self.underlayer, scapy.layers.inet6.IPv6) or isinstance(self.underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501
839-
ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_UDP, self.underlayer, p) # noqa: E501
852+
elif isinstance(_underlayer, scapy.layers.inet6.IPv6) or isinstance(_underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501
853+
ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_UDP, _underlayer, p) # noqa: E501
840854
# According to RFC2460 if the result checksum is 0, it should be set to 0xFFFF # noqa: E501
841855
if ck == 0:
842856
ck = 0xFFFF
@@ -870,6 +884,12 @@ def mysummary(self):
870884
else:
871885
return self.sprintf("UDP %UDP.sport% > %UDP.dport%")
872886

887+
def guess_payload_class(self, payload):
888+
from scapy.contrib.ifa import IFA, IFAMdHdr
889+
if isinstance(self.underlayer, IFA):
890+
return IFAMdHdr
891+
return Packet.guess_payload_class(self, payload)
892+
873893

874894
# RFC 4884 ICMP extensions
875895
_ICMP_classnums = {

scapy/layers/l2.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,12 @@ def post_build(self, p, pay):
668668
p = p[:4] + chb((c >> 8) & 0xff) + chb(c & 0xff) + p[6:]
669669
return p
670670

671+
def guess_payload_class(self, payload: bytes) -> Type[Packet]:
672+
from scapy.contrib.ifa import IFA, IFAMdHdr
673+
if isinstance(self.underlayer, IFA):
674+
return IFAMdHdr
675+
return Packet.guess_payload_class(self, payload)
676+
671677

672678
class GRE_PPTP(GRE):
673679

0 commit comments

Comments
 (0)