Skip to content

Commit 833bda8

Browse files
committed
connections: Add an option to manipulate packets before forwarding them
1 parent 0788957 commit 833bda8

4 files changed

Lines changed: 75 additions & 10 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ as Python objects.
77

88
Moreover, linux-switch let you manipulate packets before the network switch forwards
99
them (see example below). Thus, External binaries/applications that performs
10-
any logic on packets (for example, NAT) can be tested using linux-switch.
10+
any logic on packets (for example - VLAN Hopping, NAT, etc) can be tested using linux-switch.
1111

1212

1313
## Description
@@ -26,6 +26,7 @@ When using access - they'll send untagged packets.
2626

2727
## Example
2828

29+
### Basic
2930
```python
3031

3132
from switch import Switch
@@ -83,3 +84,7 @@ switch.disconnect_device(dev2)
8384

8485
switch.term()
8586
```
87+
88+
### Manipulations
89+
90+
For VLAN-Hopping example, see tests/test_manipulation.py (test_vlan_hopping)

src/connections.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from scapy.all import Ether
55
from collections import namedtuple
66
from contextlib import suppress
7+
import concurrent.futures
78

89
from src.util import PortType, add_vlan_tag, fix_checksums
10+
from src.manipulation import ManipulateArgs, default_manipulation_cb
911

1012

1113
DeviceEntry = namedtuple('DeviceEntry', [
@@ -18,6 +20,8 @@
1820

1921
IP_MAX_SIZE = 65535
2022

23+
MAX_WORKERS = 5
24+
2125

2226
class Connections(object):
2327

@@ -26,25 +30,34 @@ def __init__(self):
2630
self._event_loop = None
2731
self._devices = list()
2832
self._message_queue = None
33+
self._executers = None
34+
self.manipulate_cb = default_manipulation_cb
2935

3036
async def _read_message_queue(self):
3137
while True:
32-
packet = await self._message_queue.get()
33-
await self._process_packet(packet)
38+
packet, dev_entry = await self._message_queue.get()
39+
await self._process_packet(packet, dev_entry)
40+
41+
async def _process_packet(self, packet, src_device_entry):
42+
# Should we deal with tagging. We left an option for the manipulator
43+
# to tag the packet.
44+
should_inject_raw = False
3445

35-
async def _process_packet(self, packet):
36-
dst_mac = Ether(packet).dst
37-
src_mac = Ether(packet).src
46+
if self.manipulate_cb is not None:
47+
packet, should_inject_raw = await self._event_loop.run_in_executor(
48+
self._executers,
49+
self.manipulate_cb,
50+
ManipulateArgs(packet, src_device_entry.port_type, src_device_entry.vlan))
3851

39-
src_vlan = self._get_vlan_by_mac(src_mac)
52+
src_vlan = self._get_vlan_by_mac(Ether(packet).src)
4053
if src_vlan is None:
4154
return None
4255

4356
# Looking for the destination device
4457
for dst_device in self._devices:
45-
if dst_device.dev.get_mac == dst_mac and dst_device.vlan == src_vlan:
58+
if dst_device.dev.get_mac == Ether(packet).dst and dst_device.vlan == src_vlan:
4659

47-
if dst_device.port_type == PortType.TRUNK:
60+
if dst_device.port_type == PortType.TRUNK and not should_inject_raw:
4861
packet = add_vlan_tag(packet, dst_device.vlan)
4962

5063
packet = fix_checksums(packet)
@@ -53,7 +66,7 @@ async def _process_packet(self, packet):
5366
def _read_raw_packet(self, device_entry):
5467
try:
5568
packet = device_entry.sock.recv(IP_MAX_SIZE)
56-
self._message_queue.put_nowait(packet)
69+
self._message_queue.put_nowait((packet, device_entry))
5770
except OSError:
5871
# TODO: Currently the interface is closed successfully, but the cb
5972
# raises an exception.
@@ -63,6 +76,8 @@ def _start_event_loop(self):
6376
self._event_loop = asyncio.new_event_loop()
6477
asyncio.set_event_loop(self._event_loop)
6578

79+
self._executers = concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS)
80+
6681
self._message_queue = asyncio.Queue()
6782
asyncio.ensure_future(self._read_message_queue())
6883

src/manipulation.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import inspect
2+
from collections import namedtuple
3+
4+
ManipulateArgs = namedtuple('ManipulateInfo', [
5+
'packet',
6+
# can be used if the manipulator wants to deal with tagging.
7+
'is_trunk_port',
8+
'src_vlan',
9+
])
10+
11+
ManipulateRet = namedtuple('ManipulateRet', [
12+
'packet',
13+
# In case you want to deal with tagging (add dot1q layer),
14+
# this should be set to True.
15+
'should_inject_raw',
16+
])
17+
18+
19+
def default_manipulation_cb(manipulate_args: ManipulateArgs) -> ManipulateRet:
20+
'''
21+
Can be used as an example of a valid signature of manipulation callback.
22+
This default callback just returns the packet.
23+
'''
24+
return ManipulateRet(manipulate_args.packet, False)
25+
26+
27+
def validate_manipulation_cb(cb):
28+
# TODO: Currently annotations are required in order to check
29+
# function's signature
30+
sig = inspect.signature(cb)
31+
32+
if len(sig.parameters) != 1:
33+
return False
34+
35+
param = list(sig.parameters.values())[0]
36+
37+
return param.annotation == ManipulateArgs and sig.return_annotation == ManipulateRet

src/switch.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from src.connections import Connections
22
from src.util import shell_run_and_check, PortType
3+
from src.manipulation import validate_manipulation_cb
34
from src.exception import (NamespaceCreationException,
45
BridgeInterfaceCreationException, NamespaceConnectionException)
56

@@ -88,5 +89,12 @@ def disconnect_device(self, dev):
8889

8990
dev.term()
9091

92+
def set_manipulation(self, cb):
93+
if validate_manipulation_cb(cb):
94+
self._connections.manipulate_cb = cb
95+
return True
96+
97+
return False
98+
9199
def term(self):
92100
self._connections.stop_connections_thread()

0 commit comments

Comments
 (0)