Skip to content

Commit cf13ab8

Browse files
committed
connections: Add "punt-policies"-like mechanisem for the manipulation feature
1 parent 00c06a5 commit cf13ab8

5 files changed

Lines changed: 81 additions & 29 deletions

File tree

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ as Python objects.
88
Moreover, linux-switch let you manipulate packets before the network switch forwards
99
them (see example below). Thus, External binaries/applications that performs
1010
any logic on packets (for example - VLAN Hopping, NAT, etc) can be tested using linux-switch.
11+
Also, linux-switch provide a "Punt-Policies"-like mechanisem, which gives you the option
12+
to filter traffic that the manipulation routine will be recieve.
1113

1214

1315
## Description
@@ -22,6 +24,11 @@ switch device, meaning:
2224
3. When connecting `Device`s to the `Switch`, the connection type must be specified (access or trunk).
2325
When using trunk - the switch and the device will send/recieve tagged packet.
2426
When using access - they'll send untagged packets.
27+
4. The switch have "Punt-Policies", which means only filtered packets will be forwarded
28+
to the manipulation routine. The punt-policies feature introduce a `duplicate` mode. When set,
29+
packets that are filtered (using the punt policies) will be processed by both manipulation routine
30+
and switch. When not set, packets will be processed by the manipultion routine only, so that routine
31+
can, for example, drop packets!
2532

2633

2734
## Example
@@ -85,6 +92,7 @@ switch.disconnect_device(dev2)
8592
switch.term()
8693
```
8794

88-
### Manipulations
95+
### Manipulations(VLAN Hopping) and "Punt-Policies"
8996

90-
For VLAN-Hopping example, see tests/test_manipulation.py (test_vlan_hopping)
97+
A very good example for VLAN-Hopping and Punt-Policies can be seen
98+
in tests/test_manipulation (test_vlan_hopping_and_punt_policies).

src/connections.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from contextlib import suppress
77
import concurrent.futures
88

9-
from src.util import PortType, add_vlan_tag, fix_checksums
10-
from src.manipulation import ManipulateArgs, default_manipulation_cb
9+
from src.util import PortType, add_vlan_tag, fix_checksums, apply_bpf_filter
10+
from src.manipulation import ManipulateArgs
1111

1212

1313
DeviceEntry = namedtuple('DeviceEntry', [
@@ -21,6 +21,7 @@
2121
IP_MAX_SIZE = 65535
2222

2323
MAX_WORKERS = 5
24+
NO_FILTER = None
2425

2526

2627
class Connections(object):
@@ -31,7 +32,9 @@ def __init__(self):
3132
self._devices = list()
3233
self._message_queue = None
3334
self._executers = None
34-
self.manipulate_cb = default_manipulation_cb
35+
36+
self._manipulate_cb = None
37+
self._manipulate_filter = NO_FILTER
3538

3639
async def _read_message_queue(self):
3740
while True:
@@ -43,11 +46,15 @@ async def _process_packet(self, packet, src_device_entry):
4346
# to tag the packet.
4447
should_inject_raw = False
4548

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))
49+
if self._manipulate_cb is not None:
50+
# We'll manipulate if there's no filter or if there's
51+
# filter and the packet matches the filter
52+
if self._manipulate_filter == NO_FILTER or \
53+
apply_bpf_filter(packet, self._manipulate_filter):
54+
packet, should_inject_raw = await self._event_loop.run_in_executor(
55+
self._executers,
56+
self._manipulate_cb,
57+
ManipulateArgs(packet, src_device_entry.port_type, src_device_entry.vlan))
5158

5259
src_vlan = self._get_vlan_by_mac(Ether(packet).src)
5360
if src_vlan is None:
@@ -139,6 +146,17 @@ def _remove_device_entry(self, dev_to_remove):
139146

140147
self._event_loop.call_soon_threadsafe(_remove_device_entry, self, dev_to_remove)
141148

149+
def set_manipulation(self, cb, bpf_filter):
150+
''' Assumes cb is in valid form '''
151+
def __set_manipulation(cb, bpf_filter):
152+
self._manipulate_cb = cb
153+
self._manipulate_filter = bpf_filter
154+
155+
# set_manipulation is called from the main thread.
156+
# Since we're affecting the event loop thread, we should call_soon_threadsafe,
157+
# so the call will be synchronized.
158+
self._event_loop.call_soon_threadsafe(__set_manipulation, cb, bpf_filter)
159+
142160
def start_connections_thread(self):
143161
self._connections_thread.start()
144162

src/switch.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from src.connections import Connections
1+
from src.connections import Connections, NO_FILTER
22
from src.util import shell_run_and_check, PortType
33
from src.manipulation import validate_manipulation_cb
44
from src.exception import (NamespaceCreationException,
@@ -89,9 +89,9 @@ def disconnect_device(self, dev):
8989

9090
dev.term()
9191

92-
def set_manipulation(self, cb):
92+
def set_manipulation(self, cb, bpf_filter=NO_FILTER):
9393
if validate_manipulation_cb(cb):
94-
self._connections.manipulate_cb = cb
94+
self._connections.set_manipulation(cb, bpf_filter)
9595
return True
9696

9797
return False

src/util.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import os
12
import enum
23
import subprocess
34
from netaddr import IPAddress
4-
from scapy.all import NoPayload, Ether, Dot1Q, Raw, IP, TCP, UDP
5+
from scapy.all import NoPayload, Ether, Dot1Q, Raw, IP, TCP, UDP, sniff
56

67

78
DOT1Q_ETH_TYPE = 0x8100
@@ -69,3 +70,18 @@ def fix_checksums(packet):
6970

7071
packet = packet.__class__(bytes(packet))
7172
return Raw(packet).load
73+
74+
75+
def apply_bpf_filter(packet, filter):
76+
# TODO: this is horrible. Unfortunately scapy's `sniff` (which uses tcpdump)
77+
# always outputs annoying stuff to stderr, so we have to redirect it to /dev/null.
78+
# For now we'll settle with this, until this issue will be fixed in scapy.
79+
stderr_backup = os.dup(2)
80+
81+
dev_null = os.open('/dev/null', os.O_WRONLY)
82+
os.dup2(dev_null, 2)
83+
84+
filtered = len(sniff(offline=Ether(packet), filter=filter).res) == 1
85+
86+
os.dup2(stderr_backup, 2)
87+
return filtered

tests/test_manipulation.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import threading
2-
from scapy.all import Ether, IP, Raw
2+
from scapy.all import Ether, IP, ICMP, Raw
33

44
from src.device import Device
55
from src.manipulation import ManipulateArgs, ManipulateRet
@@ -17,15 +17,12 @@
1717
def forward_d1_to_d3(arg: ManipulateArgs) -> ManipulateRet:
1818
'''
1919
Replace addresses of d2 with d4, so the packet will be hopped
20-
to the second vlan
20+
to the second vlan.
2121
'''
2222
global GLOBAL_D3_MAC
2323
global GLOBAL_D4_MAC
2424
packet = Ether(arg.packet)
2525

26-
if not (IP in packet and packet[IP].dst == D2_ADDR and packet[IP].src == D1_ADDR):
27-
return ManipulateRet(arg.packet, False)
28-
2926
packet[IP].src = D4_ADDR
3027
packet[IP].dst = D3_ADDR
3128

@@ -35,16 +32,19 @@ def forward_d1_to_d3(arg: ManipulateArgs) -> ManipulateRet:
3532
return ManipulateRet(Raw(packet).load, False)
3633

3734

38-
def test_vlan_hopping(switch):
39-
'''
35+
def test_vlan_hopping_and_punt_policies(switch):
36+
"""
4037
In this test, d1 is in vlan 20, and it'll try to connect
4138
to d3, which is part of vlan 10. The manipulation callback works follow:
4239
For each packet that its source is d1 and destined to d2,
4340
the callback will change the packet so it'll look like the source is d4,
4441
and it's destined to d3. The switch will think that the packet came from
4542
d4 who's part of vlan 10, so it'll forward the packet to d3 (who's also in vlan 10).
4643
That is an example of a VLAN hopping.
47-
'''
44+
45+
This test also shows the behaviour of the "punt-policies":
46+
Only packets from d1 to d2 will be passed to the manipulation callback.
47+
"""
4848
global GLOBAL_D3_MAC
4949
global GLOBAL_D4_MAC
5050

@@ -54,27 +54,37 @@ def test_vlan_hopping(switch):
5454
d3 = Device('c', D3_ADDR, '255.255.255.0')
5555
d4 = Device('d', D4_ADDR, '255.255.255.0')
5656

57-
assert switch.connect_device_trunk(d1, 20)
58-
assert switch.connect_device_access(d2, 20)
57+
switch.connect_device_trunk(d1, 20)
58+
switch.connect_device_access(d2, 20)
5959

60-
assert switch.connect_device_trunk(d3, 10)
61-
assert switch.connect_device_access(d4, 10)
60+
switch.connect_device_trunk(d3, 10)
61+
switch.connect_device_access(d4, 10)
6262

6363
GLOBAL_D3_MAC = d3.get_mac
6464
GLOBAL_D4_MAC = d4.get_mac
6565

66-
switch.set_manipulation(forward_d1_to_d3)
66+
# Setting the manipulation routine, and punt-policies
67+
switch.set_manipulation(forward_d1_to_d3, 'src host {} && dst host {}'.format(
68+
D1_ADDR, D2_ADDR))
6769

6870
def _run_listening_nc(dev):
69-
out = dev.run_from_namespace('timeout 5s nc -lu 0.0.0.0 4444')
71+
out = dev.run_from_namespace('timeout 7s nc -lu 0.0.0.0 4444')
7072
assert 'hello' == out
7173

74+
# d3 is going to listen
7275
t = threading.Thread(target=_run_listening_nc, args=(d3, ))
7376
t.start()
7477

75-
out = d1.run_from_namespace('bash -c "echo hello | nc -u 192.168.250.2 4444 -w 3"')
78+
# and d1 will connect to it
79+
out = d1.run_from_namespace('bash -c "echo hello | nc -u 192.168.250.2 4444 -w 5"')
7680
assert '' == out
7781

82+
# Note that the manipulation callback modifies every packet that it recieves,
83+
# But it won't recieve this packet (and its reply) since they won't match the
84+
# bpf filter that we set before. That's why we expect an echo-reply.
85+
out = d3.run_from_namespace('ping -c 1 192.168.1.2')
86+
assert '1 packets transmitted, 1 received' in out
87+
7888
switch.disconnect_device(d1)
7989
switch.disconnect_device(d2)
8090
switch.disconnect_device(d3)

0 commit comments

Comments
 (0)