-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsimple_tcp_auth_wSwitching.py
447 lines (361 loc) · 19.5 KB
/
simple_tcp_auth_wSwitching.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import (
CONFIG_DISPATCHER,
MAIN_DISPATCHER,
set_ev_cls
)
from ryu.ofproto import ofproto_v1_3
from ryu.ofproto import ofproto_v1_3_parser
from ryu.lib.packet import (
in_proto, # ipv4 layer 3 protocols
packet,
ethernet,
ether_types,
ipv4,
ipv6,
arp as ARP,
tcp as TCP
)
from netaddr import IPAddress
from ryu.app.simple_hubswitch_class import SimpleHubSwitch # packet switching logic
from ryu.app.portknock_rest_server import Portknock_Server
from ryu.app.wsgi import WSGIApplication
num_port_bits = 16
def get_seq_len(key_length):
n = 1
while 2**n < key_length:
n+=1
if n > 15: return 0
else: return n
class Port_Knocking(app_manager.RyuApp):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
_CONTEXTS = {"wsgi":WSGIApplication}
def __init__(self, *args, **kwargs):
super(Port_Knocking, self).__init__(*args,**kwargs)
## server location
self.server_known = False # declares a server has been defined, and addresses set
self.server_ipv4_address = IPAddress('10.0.0.2') # IPv4 address that access is restricted to (the 'server')
self.server_ipv6_address = IPAddress('fe80::200:ff:fe00:2') # IPv6 address that access is restricted to (the 'server')
self.server_mac_address = '00:00:00:00:00:02' # MAC address that access is restricted to (the 'server')
# record of server location on each switch
self.datapaths = {} # dpid -> datapath object
self.server_port = {} # dpid -> port number on switch to reach server
## key config
self.auth_port = 1332 # TCP port to initiate authentication key
self.active_keys = {} # Keys available to auth on; key_id -> key sequence (seq of decimal numbers)
self.key_length = 4 # number of packets per key
self.seq_size = get_seq_len( self.key_length ) # number of bits used for the sequence number (1-8 are valid)
## host records
self.authenticated_hosts = {} # Authorised hosts; host_ip -> timeleft (time of expiry? time to remove access)
self.authing_hosts = {} # Hosts currently entering keys; host_ip -> key buffer s.t. key buffer [port0==keyID,port1,port2,port3,..]
self.blocked_hosts = {} # Hosts who entered incorrect key; host_ip -> timeout ## may not implement atm
self.default_time = 1800 # seconds till invalid (3600 == one hour)
# get/register other classes
self.switching = SimpleHubSwitch()
wsgi = kwargs['wsgi']
wsgi.register(Portknock_Server, {'port_knocking' : self})
# testing key
self.add_auth_key([
{"value": 1489, "seq": 0,"port": 1489},
{"value": 15961,"seq": 1,"port": 32345},
{"value": 8637, "seq": 2,"port": 41405},
{"value": 2929, "seq": 3,"port": 52081}])
self.load_keys_from_file('test_keys.txt')
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
"""Runs when switches handshake with controller
installs a default flow to out:CONTROLLER"""
datapath = ev.msg.datapath
self.datapaths[datapath.id] = datapath
self.switching.switch_features_handler(ev)
self.install_server_blocking_flows(datapath)
self.install_auth_init_flow(datapath)
def set_server_address(self, server_mac, server_ipv4, server_ipv6): # currently unused
''' Selects a server on the network '''
self.server_mac_address = server_mac
self.server_ipv4_address = server_ipv4
self.server_ipv6_address = server_ipv6
self.server_known = True
# flush existing flows to for server
return
def load_keys_from_file(self, filename):
""" Loads a list of keys from file
Keys are separated by line, values by commas """
print('(AUTH-key file) loading from file')
def convert_to_key(port_num):
port_num = int(port_num)
i,k = port_to_parts(port_num, self.seq_size)
return {'value': k, 'seq':i, 'port':port_num}
with open(filename, 'r') as infile:
for line in infile:
self.add_auth_key(map(convert_to_key, line.split(',')))
def add_auth_key(self, key_list):
''' Keys are a given as a list of (seq, key) values,
each pair of letters corresponds to a port number
first is used as the key's ID
return true is no key conflict, false if key_id exists '''
if len(key_list) != self.key_length:
print('(AUTH-addkey) invalid key list, too long (%d!=%d)' % (len(key_list),self.key_length))
return True
if len(key_list) > 2**self.seq_size:
print('(AUTH-addkey) invalid key, longer than max sequence (%d > %d)' % (len(key_list),2**self.seq_size))
return True # to avoid infinite loop
idx = 0
for k in key_list:
# check they're valid and in order
n = k['seq']
val = k['value']
if val > 2**(num_port_bits - self.seq_size):
print('(AUTH-addkey) invalid value %d at key[%d] -- too large! (>%d)' % (val,n,2**(num_port_bits - self.seq_size)))
return True
idx+=1
# check key_id doesn't exist already
if key_list[0]['value'] in self.active_keys:
return False
auth_key = []
auth_ports = []
for key in key_list:
# print('%d: %d -> %d' % (key['port'], key['seq'], key['value']))
auth_key.append(key['value'])
auth_ports.append(key['port'])
self.active_keys[auth_key[0]] = {'key': auth_key,'port': auth_ports}
print('(AUTH-addkey) Added key %s' % auth_key)
return True
def install_server_blocking_flows(self, datapath):
''' Blocking IP access to the server and allowing ARP '''
print('(AUTH-install) installing %d\'s server block flows' % datapath.id)
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
action_block = [] # empty == drop
# install block all to server rule (mac, ipv4, ipv6)
match_mac = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IP, eth_dst=self.server_mac_address)
match_ipv4 = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IP, ipv4_dst=self.server_ipv4_address)
match_ipv6 = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IPV6, ipv6_dst=self.server_ipv6_address)
add_flow(datapath, 1, match_mac, action_block)
add_flow(datapath, 1, match_ipv4, action_block)
add_flow(datapath, 1, match_ipv6, action_block)
def install_auth_init_flow(self, datapath):
''' Install rule for matching for the TCP auth init packet '''
print('(AUTH-install) installing %d\'s knock init flows' % datapath.id)
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
action_packet_in = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER)]
# send TCP on port self.auth_port to controller
match_tcp_auth_ipv4 = parser.OFPMatch(
eth_type=ether_types.ETH_TYPE_IP, ip_proto=in_proto.IPPROTO_TCP,
ipv4_dst= IPAddress(self.server_ipv4_address), tcp_dst= self.auth_port)
match_tcp_auth_ipv6 = parser.OFPMatch(
eth_type=ether_types.ETH_TYPE_IP, ip_proto=in_proto.IPPROTO_TCP,
ipv6_dst= IPAddress(self.server_ipv6_address), tcp_dst= self.auth_port)
# add a flow for auth init packet capture
add_flow(datapath, 2, match_tcp_auth_ipv4, action_packet_in)
add_flow(datapath, 2, match_tcp_auth_ipv6, action_packet_in)
def auth_host(self, src_ip, datapath):
''' Allows given host to access the server '''
action_allow_to_server = [ofproto_v1_3_parser.OFPActionOutput(self.server_port[datapath.id])]
# add rules for mac to access server
match_ipv4 = ofproto_v1_3_parser.OFPMatch()
match_ipv4.append_field(ofproto_v1_3.OXM_OF_ETH_TYPE, ether_types.ETH_TYPE_IP)
match_ipv4.append_field(ofproto_v1_3.OXM_OF_IPV4_SRC, int(IPAddress(src_ip)))
match_ipv4.append_field(ofproto_v1_3.OXM_OF_IPV4_DST, int(self.server_ipv4_address))
add_flow(datapath, 3, match_ipv4, action_allow_to_server)
print ('(AUTH-auth authenticated id:%s on dpid:%s' % (src_ip, datapath.id))
def match_key(self, src_ip, dst_port, datapath):
''' Matches the sequence of knocks against buffered key '''
# TODO no penalty on self.auth_port!
print('dst port %d' % dst_port)
idx, key_val = port_to_parts(dst_port, self.seq_size)
if (idx > self.key_length) or (idx < 0):
self.invalid_key('Key sequence number out of bounds')
return
if len(self.authing_hosts[src_ip]) == 0:
# first sequence key
if idx != 0:
print('key not yet defined, seq #%d received' % idx)
return # don't accept anything until key is selected
else: key_id = key_val
else:
key_id = self.authing_hosts[src_ip][0]
if key_id not in self.active_keys:
self.invalid_key('Key id not valid %d' % (key_id))
self.remove_key_from(src_ip)
return
if self.active_keys[key_id]['key'][idx] != key_val: # check they match
self.invalid_key('value %d doesn\'t match key idx %d of key %d (%d)'
% (key_val, idx, key_id, self.active_keys[key_id]['key'][idx]))
return
if idx not in self.authing_hosts[src_ip]:
print('(AUTH-buffered) %s length: %d/%d (%d)' % (src_ip, len(self.authing_hosts[src_ip])+1,self.key_length, key_val))
self.authing_hosts[src_ip][idx] = key_val
else:
print('duplicate %d->%d' % (idx,key_val))
if len(self.authing_hosts[src_ip]) == self.key_length:
# key complete, authorise IP address to access server
print('(AUTH-seq complete) ip:%s' % src_ip)
# add host to authenticated hosts
self.authenticated_hosts[src_ip] = 10000
# tidy up
del self.authing_hosts[src_ip]
del self.active_keys[key_id]
# self.remove_authing_flows(src_ip) # redundant, is replaced with allow flow
# install flows to access server
self.auth_host(src_ip, datapath)
def invalid_key(self, msg=''):
# TODO: block for a few seconds
# TODO: release authing key from host?
print('(AUTH-invalid key) %s' % msg)
def remove_key_from(self, src_ip):
''' expired keys are disassociated from authing host '''
del self.authing_hosts[src_ip]
def initialise_host_auth(self, src_ip, datapath):
print ('(AUTH-auth init) received init from %s' % src_ip)
self.authing_hosts[src_ip] = {} # empty key buffer
# install flow, fwd all tcp to controller
action_fwd_to_controller = [ofproto_v1_3_parser.OFPActionOutput(ofproto_v1_3.OFPP_CONTROLLER)]
match_ipv4 = ofproto_v1_3_parser.OFPMatch()
match_ipv4.append_field(ofproto_v1_3.OXM_OF_ETH_TYPE, ether_types.ETH_TYPE_IP)
match_ipv4.append_field(ofproto_v1_3.OXM_OF_IPV4_SRC, int(IPAddress(src_ip)))
match_ipv4.append_field(ofproto_v1_3.OXM_OF_IPV4_DST, int(self.server_ipv4_address))
add_flow(datapath, 3, match_ipv4, action_fwd_to_controller)
def remove_auth_flows(self,src_ip):
""" removes the flows that capture knock sequence
(identified with src_ip and priority) """
match_ipv4 = ofproto_v1_3_parser.OFPMatch()
match_ipv4.append_field(ofproto_v1_3.OXM_OF_ETH_TYPE, ether_types.ETH_TYPE_IP)
match_ipv4.append_field(ofproto_v1_3.OXM_OF_IPV4_SRC, int(IPAddress(src_ip)))
match_ipv4.append_field(ofproto_v1_3.OXM_OF_IPV4_DST, int(IPAddress(self.server_ipv4_address)))
for id in self.datapaths:
delete_flow(self.datapaths[id], 3, match_ipv4)
def remove_host_access(self,src_ip):
""" Revokes access to server for an authorised host """
if src_ip not in self.authenticated_hosts:
return False
print('removing %s from auth hosts' % src_ip)
del self.authenticated_hosts[src_ip]
self.remove_auth_flows(src_ip)
return True
def set_datapath_svr_port(self, dpid, in_port):
if dpid in self.server_port and self.server_port[dpid] == in_port:
# already set and no change
return
print '(AUTH-packet_in) %d\'s server_port: %d' % (dpid, in_port)
self.server_port[dpid] = in_port
self.server_known = True
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def packet_in_handler(self, ev):
'''Listen for auth packets
and server announcement'''
msg = ev.msg
dp = msg.datapath
ofp = dp.ofproto
parser = dp.ofproto_parser
pkt = packet.Packet(msg.data)
eth = pkt.get_protocols(ethernet.ethernet)[0]
eth_type = eth.ethertype
# ''' register the server (not implemented) '''
# if (broadcast and matches server key):
# set_server_address()
# set server port for this datapath
# capture auth packets
if eth_type == ether_types.ETH_TYPE_IP:
# if TCP and dst is server
# ipv4
ip = pkt.get_protocols(ipv4.ipv4)[0]
if ip.proto == in_proto.IPPROTO_TCP and ip.dst == str(self.server_ipv4_address):
tcp = pkt.get_protocols(TCP.tcp)[0]
if ip.src in self.authenticated_hosts: # likely from another switch, so needs to have flow to server installed
self.auth_host(ip.src, dp)
if len(self.active_keys) <= 0:
print('no keys active')
return
elif ip.src in self.authing_hosts:
self.match_key(ip.src, tcp.dst_port, dp)
elif tcp.dst_port == self.auth_port:
# install key matching flows for host
self.initialise_host_auth(ip.src, dp)
return # avoid installing flow (block TCP traffic to server)
if ip.dst == str(self.server_ipv4_address):
# avoids controller forwarding on other IP packets while ALL TO CONTROLLER is active
return
# ipv6 to server, block from switch
if eth_type == ether_types.ETH_TYPE_IPV6:
ip = pkt.get_protocols(ipv6.ipv6)[0]
if ip.dst == str(self.server_ipv6_address):
return
# if from server, get port_id of server (reply from ARP will trigger this)
if eth.src == self.server_mac_address:
self.set_datapath_svr_port(dp.id, msg.match['in_port'])
# do regular switching
self.switching.packet_in_handler(ev)
def port_to_parts(port_num, seq_size):
idx = port_num >> (num_port_bits - seq_size)
key_val = port_num & (2**(num_port_bits - seq_size)) - 1
return (idx, key_val)
def add_flow(datapath, priority, match, actions, buffer_id=None):
'''Adds this flow to the given datapath'''
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,actions)]
if buffer_id:
mod = parser.OFPFlowMod(datapath=datapath, buffer_id=buffer_id,
priority=priority, match=match, instructions=inst)
else:
mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
match=match, instructions=inst)
# print("(AUTH-add flow): %s %s" % (match, actions))
datapath.send_msg(mod)
def delete_flow(datapath, priority, match):
''' This method is stolen from Jarrod :P '''
print('delete flow %s' % match)
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
command = ofproto.OFPFC_DELETE
mod = parser.OFPFlowMod(datapath=datapath, command=command,
priority=priority, match=match,
out_port=ofproto.OFPP_ANY,
out_group=ofproto.OFPG_ANY)
datapath.send_msg(mod)
def print_object(obj):
''' Prints all the attributes of a object
http://stackoverflow.com/a/5969930 '''
attrs = vars(obj)
print ', '.join("%s: %s" % item for item in attrs.items())
def eth_type_to_str(eth_type):
'''Given an eth_type hex value, return the eth_type name'''
return {
ether_types.ETH_TYPE_IP: 'ETH_TYPE_IP',
ether_types.ETH_TYPE_ARP: 'ETH_TYPE_ARP',
ether_types.ETH_TYPE_8021Q: 'ETH_TYPE_8021Q',
ether_types.ETH_TYPE_IPV6: 'ETH_TYPE_IPV6',
ether_types.ETH_TYPE_SLOW: 'ETH_TYPE_SLOW',
ether_types.ETH_TYPE_MPLS: 'ETH_TYPE_MPLS',
ether_types.ETH_TYPE_8021AD: 'ETH_TYPE_8021AD',
ether_types.ETH_TYPE_LLDP: 'ETH_TYPE_LLDP',
ether_types.ETH_TYPE_8021AH: 'ETH_TYPE_8021AH',
ether_types.ETH_TYPE_IEEE802_3: 'ETH_TYPE_IEEE802_3',
ether_types.ETH_TYPE_CFM: 'ETH_TYPE_CFM'
}.get(eth_type,"Type %x not found" % (eth_type))
def ip_proto_to_str(ip_proto):
''' Given an ip_proto number, returns the protocol name '''
return {
in_proto.IPPROTO_IP: 'IPPROTO_IP',
in_proto.IPPROTO_HOPOPTS: 'IPPROTO_HOPOPTS',
in_proto.IPPROTO_ICMP: 'IPPROTO_ICMP',
in_proto.IPPROTO_IGMP: 'IPPROTO_IGMP',
in_proto.IPPROTO_TCP: 'IPPROTO_TCP',
in_proto.IPPROTO_UDP: 'IPPROTO_UDP',
in_proto.IPPROTO_ROUTING: 'IPPROTO_ROUTING',
in_proto.IPPROTO_FRAGMENT: 'IPPROTO_FRAGMENT',
in_proto.IPPROTO_AH: 'IPPROTO_AH',
in_proto.IPPROTO_ICMPV6: 'IPPROTO_ICMPV6',
in_proto.IPPROTO_NONE: 'IPPROTO_NONE',
in_proto.IPPROTO_DSTOPTS: 'IPPROTO_DSTOPTS',
in_proto.IPPROTO_OSPF: 'IPPROTO_OSPF',
in_proto.IPPROTO_VRRP: 'IPPROTO_VRRP',
in_proto.IPPROTO_SCTP: 'IPPROTO_SCTP'
}.get(ip_proto,"Type %x not found" % (ip_proto))
def convert_key_to_port(key, seq, seq_size):
key_len = 16 - seq_size
return (seq << key_len) + key
# nop