Skip to content

Support Inband Flow Analyzer (IFA) #4235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions scapy/contrib/ifa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# This file is part of Scapy
# See https://scapy.net/ for more information
# Copyright (C) 2024 Jingfei Hu <[email protected]>

# 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)
36 changes: 28 additions & 8 deletions scapy/layers/inet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down
6 changes: 6 additions & 0 deletions scapy/layers/l2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: bytes) -> Type[Packet]:
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):

Expand Down
Loading
Loading