Skip to content

Commit 618a85a

Browse files
committed
manipulation: Return a callback that queues packets
Can be used by the manipulator when it wants to init connections
1 parent 5ca6a6a commit 618a85a

6 files changed

Lines changed: 93 additions & 113 deletions

File tree

linuxswitch/connections.py

Lines changed: 31 additions & 39 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 linuxswitch.util import PortType, add_vlan_tag, fix_checksums, apply_bpf_filter
10-
from linuxswitch.manipulation import ManipulateArgs, ManipulateActions
9+
from linuxswitch.util import PortType, DeviceType, add_vlan_tag, fix_checksums, apply_bpf_filter
10+
from linuxswitch.manipulation import ManipulateArgs
1111

1212

1313
DeviceEntry = namedtuple('DeviceEntry', [
@@ -43,52 +43,41 @@ async def _read_message_queue(self):
4343
await self._process_packet(packet, dev_entry)
4444

4545
async def _process_packet(self, packet, src_device_entry):
46-
# Should we deal with tagging. We left an option for the manipulator
47-
# to tag the packet.
48-
should_inject_raw = False
49-
50-
packets = [packet]
51-
52-
if self._manipulate_cb is not None:
46+
"""
47+
If `src_device_entry` is None, we're not performing manipulation.
48+
"""
49+
if src_device_entry is not None and self._manipulate_cb is not None:
5350
# We'll manipulate if there's no filter or if there's
5451
# filter and the packet matches the filter
5552
if self._manipulate_filter == NO_FILTER or \
5653
apply_bpf_filter(packet, self._manipulate_filter):
5754

58-
# In case the packet matches the filter, and `duplicate` is not
59-
# set, we remove the packet from the list, since the manipulator
60-
# might drop the packet.
61-
if not self._manipulate_dup:
62-
packets.remove(packet)
63-
64-
packet, action = await self._event_loop.run_in_executor(
55+
await self._event_loop.run_in_executor(
6556
self._executers,
6657
self._manipulate_cb,
6758
ManipulateArgs(packet, src_device_entry.port_type, src_device_entry.vlan))
6859

69-
if action == ManipulateActions.INJECT_RAW:
70-
should_inject_raw = True
71-
packets.append(packet)
72-
elif action == ManipulateActions.HANDLE_ENCAP:
73-
should_inject_raw = False
74-
packets.append(packet)
75-
# else - action is DROP, so we don't add the packet to the list.
76-
77-
for packet in packets:
60+
# In case the packet matches the filter, and `duplicate` is not
61+
# set, then only the manipulator should process the packet.
62+
# Therefore, we abort here.
63+
if not self._manipulate_dup:
64+
return None
7865

79-
src_vlan = self._get_vlan_by_mac(Ether(packet).src)
80-
if src_vlan is None:
81-
return None
66+
src_vlan = self._get_vlan_by_mac(Ether(packet).src)
67+
if src_vlan is None:
68+
return None
8269

83-
# Looking for the destination device
84-
for dst_device in self._devices:
85-
if dst_device.dev.get_mac == Ether(packet).dst and dst_device.vlan == src_vlan:
70+
# Looking for the destination device
71+
for dst_device in self._devices:
72+
if dst_device.dev.get_mac == Ether(packet).dst and dst_device.vlan == src_vlan:
8673

87-
if dst_device.port_type == PortType.TRUNK and not should_inject_raw:
88-
packet = add_vlan_tag(packet, dst_device.vlan)
74+
# TODO: currently we're taggint packets. Maybe the Device
75+
# should state whether we should tag or inject raw.
76+
if dst_device.port_type == PortType.TRUNK:
77+
packet = add_vlan_tag(packet, dst_device.vlan)
8978

90-
packet = fix_checksums(packet)
91-
await self._event_loop.sock_sendall(dst_device.sock, packet)
79+
packet = fix_checksums(packet)
80+
await self._event_loop.sock_sendall(dst_device.sock, packet)
9281

9382
def _read_raw_packet(self, device_entry):
9483
try:
@@ -137,12 +126,12 @@ def _get_vlan_by_mac(self, mac):
137126

138127
return None
139128

140-
def append_device(self, dev, vlan, port_type):
129+
def append_device(self, dev, vlan, port_type, iface_name):
141130
def _append_device_entry(self, new_dev_entry):
142131
self._devices.append(new_dev_entry)
143132

144133
sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(0x0003))
145-
sock.bind(('br-{}'.format(dev.get_name), 0))
134+
sock.bind((iface_name, 0))
146135

147136
new_dev_entry = DeviceEntry(dev, sock, vlan, port_type)
148137

@@ -168,15 +157,18 @@ def _remove_device_entry(self, dev_to_remove):
168157

169158
def set_manipulation(self, cb, bpf_filter, duplicate):
170159
''' Assumes cb is in valid form '''
171-
def __set_manipulation(cb, bpf_filter):
160+
def __set_manipulation(cb, bpf_filter, duplicate):
172161
self._manipulate_cb = cb
173162
self._manipulate_filter = bpf_filter
174163
self._manipulate_dup = duplicate
175164

176165
# set_manipulation is called from the main thread.
177166
# Since we're affecting the event loop thread, we should call_soon_threadsafe,
178167
# so the call will be synchronized.
179-
self._event_loop.call_soon_threadsafe(__set_manipulation, cb, bpf_filter)
168+
self._event_loop.call_soon_threadsafe(__set_manipulation, cb, bpf_filter, duplicate)
169+
170+
def manipulator_queue_packet(self, packet):
171+
self._message_queue.put_nowait((packet, None))
180172

181173
def start_connections_thread(self):
182174
self._connections_thread.start()

linuxswitch/exception.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ class BridgeInterfaceCreationException(Exception):
1212

1313
class NamespaceConnectionException(Exception):
1414
pass
15+
16+
17+
class ManipulationCallbackException(Exception):
18+
pass

linuxswitch/manipulation.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,14 @@
1-
import enum
21
import inspect
32
from collections import namedtuple
43

54

6-
class ManipulateActions(enum.Enum):
7-
DROP = 0
8-
# The manipulator will deal with tagging, if needed.
9-
INJECT_RAW = 1
10-
# The switch should deal with tagging, if needed.
11-
HANDLE_ENCAP = 2
12-
13-
145
ManipulateArgs = namedtuple('ManipulateInfo', [
156
'packet',
167
# can be used if the manipulator wants to deal with tagging.
178
'is_trunk_port',
189
'src_vlan',
1910
])
2011

21-
ManipulateRet = namedtuple('ManipulateRet', [
22-
'packet',
23-
# From ManipulateActions
24-
'action',
25-
])
26-
27-
28-
def default_manipulation_cb(manipulate_args: ManipulateArgs) -> ManipulateRet:
29-
'''
30-
Can be used as an example of a valid signature of manipulation callback.
31-
This default callback just returns the packet.
32-
'''
33-
return ManipulateRet(manipulate_args.packet, ManipulateActions.HANDLE_ENCAP)
34-
3512

3613
def validate_manipulation_cb(cb):
3714
# TODO: Currently annotations are required in order to check
@@ -43,4 +20,4 @@ def validate_manipulation_cb(cb):
4320

4421
param = list(sig.parameters.values())[0]
4522

46-
return param.annotation == ManipulateArgs and sig.return_annotation == ManipulateRet
23+
return param.annotation == ManipulateArgs

linuxswitch/switch.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from linuxswitch.util import shell_run_and_check, PortType
33
from linuxswitch.manipulation import validate_manipulation_cb
44
from linuxswitch.exception import (NamespaceCreationException,
5-
BridgeInterfaceCreationException, NamespaceConnectionException)
5+
BridgeInterfaceCreationException,
6+
NamespaceConnectionException,
7+
ManipulationCallbackException)
68

79

810
class Switch(object):
@@ -56,43 +58,37 @@ def set_manipulation(self, cb, punt_policies_bpf=NO_FILTER, duplicate=False):
5658
Sets the manipulation routine.
5759
5860
Before the switch checks to which device a packet should be forwarded,
59-
it sends the packet the a manipulation routine.
61+
it sends the packet to the manipulation routine.
6062
You can pass a callback that manipulate the packet, for example, change the
6163
destination and source addresses and the vlan tag, in order to perform VLAN-Hopping,
6264
or NAT.
6365
66+
The function returns a callback that can be used by the manipulator in order
67+
to queue packets that should be processed by the `Switch`.
68+
6469
In addition, In case you want to emulate "Punt-Policies" (decide what packets
6570
should be forwarded to the manipulation callback), you can pass a bpf filter.
6671
As mentioned, only packets that are filtered by this bpf filter will be forwarded
6772
to the manipultion routine before they get processed by the switch.
6873
6974
:param cb: A callback the the manipulation routine.
70-
The callback must have arguments and return annotations,
75+
The callback must have arguments annotations,
7176
since this function validates this callback using them.
72-
73-
A valid callback has to match the following form:
74-
1. Recieve one argument. Its type is ManipulateArgs.
75-
2. Return ManipulateRet.
76-
77-
An example of a valid callback (taken from manipulation.py:
78-
`default_manipulation_cb`):
79-
80-
def default_manipulation_cb(manipulate_args: ManipulateArgs) -> ManipulateRet:
81-
return ManipulateRet(manipulate_args.packet, ManipulateActions.HANDLE_ENCAP)
82-
83-
This callback for example, just returns the packet.
77+
A valid callback recieves one argument.
78+
Its type is ManipulateArgs (see manipulation.py).
8479
8580
:param punt_policies_bpf: The "Punt-Policies". Default is no-filter.
8681
:param duplicate: True if the packet should be processed by both switch
8782
and manipulator, False if the packet should be processed
8883
by the manipulator only.
89-
:return: True if the callback is in a valid form, False otherwise.
84+
:return: A callback that can be used by the manipulator in order to queue
85+
packets that should be processed by the `Switch`.
9086
"""
91-
if validate_manipulation_cb(cb):
92-
self._connections.set_manipulation(cb, punt_policies_bpf, duplicate)
93-
return True
87+
if not validate_manipulation_cb(cb):
88+
raise ManipulationCallbackException("Callback has invalid signature")
9489

95-
return False
90+
self._connections.set_manipulation(cb, punt_policies_bpf, duplicate)
91+
return self._connections.manipulator_queue_packet
9692

9793
def connect_device_access(self, dev, vlan):
9894
"""
@@ -115,7 +111,7 @@ def connect_device_access(self, dev, vlan):
115111
raise NamespaceConnectionException("failed to create connection to namespace {}".format(
116112
dev.get_name))
117113

118-
self._connections.append_device(dev, vlan, PortType.ACCESS)
114+
self._connections.append_device(dev, vlan, PortType.ACCESS, 'br-{}'.format(dev.get_name))
119115

120116
def connect_device_trunk(self, dev, vlan):
121117
"""
@@ -138,7 +134,7 @@ def connect_device_trunk(self, dev, vlan):
138134
raise NamespaceConnectionException("failed to create connection to namespace {}".format(
139135
dev.get_name))
140136

141-
self._connections.append_device(dev, vlan, PortType.TRUNK)
137+
self._connections.append_device(dev, vlan, PortType.TRUNK, 'br-{}'.format(dev.get_name))
142138

143139
def disconnect_device(self, dev):
144140
"""

linuxswitch/util.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ class PortType(enum.Enum):
1313
TRUNK = 1
1414

1515

16+
class DeviceType(enum.Enum):
17+
DEVICE = 0
18+
MANIPULATOR = 1
19+
20+
1621
def shell_run_and_check(cmd):
1722
# Pretty ugly but whatever
1823
res = (b'', b'') == run_shell_cmd(cmd)

tests/test_manipulation.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import time
12
import threading
23
from scapy.all import Ether, IP, ICMP, Raw
34

45
from linuxswitch.device import Device
5-
from linuxswitch.manipulation import ManipulateArgs, ManipulateRet, ManipulateActions
6+
from linuxswitch.manipulation import ManipulateArgs
67
from linuxswitch.util import add_vlan_tag
78

89

@@ -14,14 +15,20 @@
1415
GLOBAL_D3_MAC = None
1516
GLOBAL_D4_MAC = None
1617

18+
GLOBAL_QUEUE_PKTS_CB = None
1719

18-
def forward_d1_to_d3(arg: ManipulateArgs) -> ManipulateRet:
20+
GLOBAL_RECIEVED_ECHO_REPLY = False
21+
22+
23+
def forward_d1_to_d3(arg: ManipulateArgs):
1924
'''
2025
Replace addresses of d2 with d4, so the packet will be hopped
2126
to the second vlan.
2227
'''
2328
global GLOBAL_D3_MAC
2429
global GLOBAL_D4_MAC
30+
global GLOBAL_QUEUE_PKTS_CB
31+
2532
packet = Ether(arg.packet)
2633

2734
packet[IP].src = D4_ADDR
@@ -30,19 +37,21 @@ def forward_d1_to_d3(arg: ManipulateArgs) -> ManipulateRet:
3037
packet[Ether].src = GLOBAL_D4_MAC
3138
packet[Ether].dst = GLOBAL_D3_MAC
3239

33-
return ManipulateRet(Raw(packet).load, ManipulateActions.HANDLE_ENCAP)
40+
return GLOBAL_QUEUE_PKTS_CB(Raw(packet).load)
3441

3542

36-
def drop_packet(arg: ManipulateArgs) -> ManipulateRet:
37-
return ManipulateRet(None, ManipulateActions.DROP)
43+
def drop_packet(arg: ManipulateArgs):
44+
# Does nothing
45+
pass
3846

3947

40-
def tag_packet_inject_raw(arg: ManipulateArgs) -> ManipulateRet:
41-
return ManipulateRet(add_vlan_tag(arg.packet, arg.src_vlan), ManipulateActions.INJECT_RAW)
48+
def check_echo_reply(arg: ManipulateArgs):
49+
global GLOBAL_RECIEVED_ECHO_REPLY
4250

51+
ECHO_REPLY = 0
52+
packet = Ether(arg.packet)
4353

44-
def tag_packet_handle_encap(arg: ManipulateArgs) -> ManipulateRet:
45-
return ManipulateRet(add_vlan_tag(arg.packet, arg.src_vlan), ManipulateActions.HANDLE_ENCAP)
54+
GLOBAL_RECIEVED_ECHO_REPLY = ICMP in packet and packet['ICMP'].type == ECHO_REPLY
4655

4756

4857
def test_vlan_hopping_and_punt_policies(switch):
@@ -60,6 +69,7 @@ def test_vlan_hopping_and_punt_policies(switch):
6069
"""
6170
global GLOBAL_D3_MAC
6271
global GLOBAL_D4_MAC
72+
global GLOBAL_QUEUE_PKTS_CB
6373

6474
d1 = Device('a', D1_ADDR, '255.255.255.0')
6575
d2 = Device('b', D2_ADDR, '255.255.255.0')
@@ -77,8 +87,10 @@ def test_vlan_hopping_and_punt_policies(switch):
7787
GLOBAL_D4_MAC = d4.get_mac
7888

7989
# Setting the manipulation routine, and punt-policies
80-
switch.set_manipulation(forward_d1_to_d3, 'src host {} && dst host {}'.format(
81-
D1_ADDR, D2_ADDR), False)
90+
GLOBAL_QUEUE_PKTS_CB = switch.set_manipulation(
91+
forward_d1_to_d3,
92+
'src host {} && dst host {}'.format(D1_ADDR, D2_ADDR),
93+
False)
8294

8395
def _run_listening_nc(dev):
8496
out = dev.run_from_namespace('timeout 7s nc -lu 0.0.0.0 4444')
@@ -131,33 +143,27 @@ def test_manipulator_drop_packets(switch):
131143
switch.disconnect_device(d2)
132144

133145

134-
def test_manipulator_inject_raw(switch):
146+
def test_manipulator_init_connection(switch):
147+
global GLOBAL_RECIEVED_ECHO_REPLY
135148
d1 = Device('a', '192.168.250.1', '255.255.255.0')
136149
d2 = Device('b', '192.168.250.2', '255.255.255.0')
137150

138151
switch.connect_device_trunk(d1, 20)
139152
switch.connect_device_trunk(d2, 20)
140153

141-
# The manipulator will tag all packets, and will set INJECT_RAW
142-
# as the action. The original packets won't get processed by switch,
143-
# So if we get a valid connection, it means that the manipulator
144-
# successfully tagged the packet, and the switch didn't deal with
145-
# with tagging; it just send the packet as is (raw).
146-
switch.set_manipulation(tag_packet_inject_raw, duplicate=False)
154+
cb = switch.set_manipulation(check_echo_reply,
155+
'icmp && src host 192.168.250.1',
156+
duplicate=False)
147157

148-
out = d1.run_from_namespace('ping -c 1 192.168.250.2')
149-
assert '1 packets transmitted, 1 received' in out
158+
pkt = Ether(src=d2.get_mac, dst=d1.get_mac) / \
159+
IP(src='192.168.250.2', dst='192.168.250.1', ttl=20) / ICMP()
150160

151-
out = d2.run_from_namespace('ping -c 1 192.168.250.1')
152-
assert '1 packets transmitted, 1 received' in out
161+
cb(Raw(pkt).load)
153162

154-
switch.set_manipulation(tag_packet_handle_encap, duplicate=False)
163+
# Wait for the packet to get processed by the switch
164+
time.sleep(1.5)
155165

156-
out = d1.run_from_namespace('ping -c 1 192.168.250.2 -W 2')
157-
assert '1 packets transmitted, 0 received' in out
158-
159-
out = d2.run_from_namespace('ping -c 1 192.168.250.1 -W 2')
160-
assert '1 packets transmitted, 0 received' in out
166+
assert GLOBAL_RECIEVED_ECHO_REPLY
161167

162168
switch.disconnect_device(d1)
163169
switch.disconnect_device(d2)

0 commit comments

Comments
 (0)