-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
Copy pathcaclmgrd
executable file
·864 lines (699 loc) · 45.4 KB
/
caclmgrd
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
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
#!/usr/bin/env python3
#
# caclmgrd
#
# Control plane ACL manager daemon for SONiC
#
# Upon starting, this daemon reads control plane ACL tables and rules from
# Config DB, converts the rules into iptables rules and installs the iptables
# rules. The daemon then indefintely listens for notifications from Config DB
# and updates iptables rules if control plane ACL configuration has changed.
#
try:
import ipaddress
import os
import subprocess
import sys
import threading
import time
from sonic_py_common import daemon_base, device_info, multi_asic
from swsscommon import swsscommon
except ImportError as err:
raise ImportError("%s - required module not found" % str(err))
VERSION = "1.0"
SYSLOG_IDENTIFIER = "caclmgrd"
DEFAULT_NAMESPACE = ''
# ========================== Helper Functions =========================
def _ip_prefix_in_key(key):
"""
Function to check if IP prefix is present in a Redis database key.
If it is present, then the key will be a tuple. Otherwise, the
key will be a string.
"""
return (isinstance(key, tuple))
# ============================== Classes ==============================
class ControlPlaneAclManager(daemon_base.DaemonBase):
"""
Class which reads control plane ACL tables and rules from Config DB,
translates them into equivalent iptables commands and runs those
commands in order to apply the control plane ACLs.
Attributes:
config_db: Handle to Config Redis database via SwSS SDK
"""
ACL_TABLE = "ACL_TABLE"
ACL_RULE = "ACL_RULE"
DEVICE_METADATA_TABLE = "DEVICE_METADATA"
MUX_CABLE_TABLE = "MUX_CABLE_TABLE"
ACL_TABLE_TYPE_CTRLPLANE = "CTRLPLANE"
# To specify a port range instead of a single port, use iptables format:
# separate start and end ports with a colon, e.g., "1000:2000"
ACL_SERVICES = {
"NTP": {
"ip_protocols": ["udp"],
"dst_ports": ["123"],
"multi_asic_ns_to_host_fwd":False
},
"SNMP": {
"ip_protocols": ["tcp", "udp"],
"dst_ports": ["161"],
"multi_asic_ns_to_host_fwd":True
},
"SSH": {
"ip_protocols": ["tcp"],
"dst_ports": ["22"],
"multi_asic_ns_to_host_fwd":True
},
"ANY": {
"ip_protocols": ["any"],
"dst_ports": ["0"],
"multi_asic_ns_to_host_fwd":False
}
}
UPDATE_DELAY_SECS = 0.5
DualToR = False
def __init__(self, log_identifier):
super(ControlPlaneAclManager, self).__init__(log_identifier)
# Update-thread-specific data per namespace
self.update_thread = {}
self.lock = {}
self.num_changes = {}
# Initialize update-thread-specific data for default namespace
self.update_thread[DEFAULT_NAMESPACE] = None
self.lock[DEFAULT_NAMESPACE] = threading.Lock()
self.num_changes[DEFAULT_NAMESPACE] = 0
if device_info.is_multi_npu():
swsscommon.SonicDBConfig.load_sonic_global_db_config()
self.config_db_map = {}
self.iptables_cmd_ns_prefix = {}
self.config_db_map[DEFAULT_NAMESPACE] = swsscommon.ConfigDBConnector(use_unix_socket_path=True, namespace=DEFAULT_NAMESPACE)
self.config_db_map[DEFAULT_NAMESPACE].connect()
self.iptables_cmd_ns_prefix[DEFAULT_NAMESPACE] = ""
self.namespace_mgmt_ip = self.get_namespace_mgmt_ip(self.iptables_cmd_ns_prefix[DEFAULT_NAMESPACE], DEFAULT_NAMESPACE)
self.namespace_mgmt_ipv6 = self.get_namespace_mgmt_ipv6(self.iptables_cmd_ns_prefix[DEFAULT_NAMESPACE], DEFAULT_NAMESPACE)
self.namespace_docker_mgmt_ip = {}
self.namespace_docker_mgmt_ipv6 = {}
metadata = self.config_db_map[DEFAULT_NAMESPACE].get_table(self.DEVICE_METADATA_TABLE)
if 'subtype' in metadata['localhost'] and metadata['localhost']['subtype'] == 'DualToR':
self.DualToR = True
namespaces = multi_asic.get_all_namespaces()
for front_asic_namespace in namespaces['front_ns']:
self.update_thread[front_asic_namespace] = None
self.lock[front_asic_namespace] = threading.Lock()
self.num_changes[front_asic_namespace] = 0
self.config_db_map[front_asic_namespace] = swsscommon.ConfigDBConnector(use_unix_socket_path=True, namespace=front_asic_namespace)
self.config_db_map[front_asic_namespace].connect()
self.iptables_cmd_ns_prefix[front_asic_namespace] = "ip netns exec " + front_asic_namespace + " "
self.namespace_docker_mgmt_ip[front_asic_namespace] = self.get_namespace_mgmt_ip(self.iptables_cmd_ns_prefix[front_asic_namespace],
front_asic_namespace)
self.namespace_docker_mgmt_ipv6[front_asic_namespace] = self.get_namespace_mgmt_ipv6(self.iptables_cmd_ns_prefix[front_asic_namespace],
front_asic_namespace)
for back_asic_namespace in namespaces['back_ns']:
self.update_thread[back_asic_namespace] = None
self.lock[back_asic_namespace] = threading.Lock()
self.num_changes[back_asic_namespace] = 0
self.iptables_cmd_ns_prefix[back_asic_namespace] = "ip netns exec " + back_asic_namespace + " "
self.namespace_docker_mgmt_ip[back_asic_namespace] = self.get_namespace_mgmt_ip(self.iptables_cmd_ns_prefix[back_asic_namespace],
back_asic_namespace)
self.namespace_docker_mgmt_ipv6[back_asic_namespace] = self.get_namespace_mgmt_ipv6(self.iptables_cmd_ns_prefix[back_asic_namespace],
back_asic_namespace)
def get_namespace_mgmt_ip(self, iptable_ns_cmd_prefix, namespace):
ip_address_get_command = iptable_ns_cmd_prefix + "ip -4 -o addr show " + ("eth0" if namespace else "docker0") +\
" | awk '{print $4}' | cut -d'/' -f1 | head -1"
return self.run_commands([ip_address_get_command])
def get_namespace_mgmt_ipv6(self, iptable_ns_cmd_prefix, namespace):
ipv6_address_get_command = iptable_ns_cmd_prefix + "ip -6 -o addr show scope global " + ("eth0" if namespace else "docker0") +\
" | awk '{print $4}' | cut -d'/' -f1 | head -1"
return self.run_commands([ipv6_address_get_command])
def run_commands(self, commands):
"""
Given a list of shell commands, run them in order
Args:
commands: List of strings, each string is a shell command
"""
for cmd in commands:
proc = subprocess.Popen(cmd, shell=True, universal_newlines=True, stdout=subprocess.PIPE)
(stdout, stderr) = proc.communicate()
if proc.returncode != 0:
self.log_error("Error running command '{}'".format(cmd))
elif stdout:
return stdout.rstrip('\n')
def parse_int_to_tcp_flags(self, hex_value):
tcp_flags_str = ""
if hex_value & 0x01:
tcp_flags_str += "FIN,"
if hex_value & 0x02:
tcp_flags_str += "SYN,"
if hex_value & 0x04:
tcp_flags_str += "RST,"
if hex_value & 0x08:
tcp_flags_str += "PSH,"
if hex_value & 0x10:
tcp_flags_str += "ACK,"
if hex_value & 0x20:
tcp_flags_str += "URG,"
# iptables doesn't handle the flags below now. It has some special keys for it:
# --ecn-tcp-cwr This matches if the TCP ECN CWR (Congestion Window Received) bit is set.
# --ecn-tcp-ece This matches if the TCP ECN ECE (ECN Echo) bit is set.
# if hex_value & 0x40:
# tcp_flags_str += "ECE,"
# if hex_value & 0x80:
# tcp_flags_str += "CWR,"
# Delete the trailing comma
tcp_flags_str = tcp_flags_str[:-1]
return tcp_flags_str
def generate_block_ip2me_traffic_iptables_commands(self, namespace):
INTERFACE_TABLE_NAME_LIST = [
"LOOPBACK_INTERFACE",
"MGMT_INTERFACE",
"VLAN_INTERFACE",
"PORTCHANNEL_INTERFACE",
"INTERFACE"
]
block_ip2me_cmds = []
# Add iptables rules to drop all packets destined for peer-to-peer interface IP addresses
for iface_table_name in INTERFACE_TABLE_NAME_LIST:
iface_table = self.config_db_map[namespace].get_table(iface_table_name)
if iface_table:
for key, _ in iface_table.items():
if not _ip_prefix_in_key(key):
continue
iface_name, iface_cidr = key
ip_ntwrk = ipaddress.ip_network(iface_cidr, strict=False)
# For VLAN interfaces, the IP address we want to block is the default gateway (i.e.,
# the first available host IP address of the VLAN subnet)
ip_addr = next(ip_ntwrk.hosts()) if iface_table_name == "VLAN_INTERFACE" else ip_ntwrk.network_address
if isinstance(ip_ntwrk, ipaddress.IPv4Network):
block_ip2me_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -d {}/{} -j DROP".format(ip_addr, ip_ntwrk.max_prefixlen))
elif isinstance(ip_ntwrk, ipaddress.IPv6Network):
block_ip2me_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -d {}/{} -j DROP".format(ip_addr, ip_ntwrk.max_prefixlen))
else:
self.log_warning("Unrecognized IP address type on interface '{}': {}".format(iface_name, ip_ntwrk))
return block_ip2me_cmds
def generate_allow_internal_docker_ip_traffic_commands(self, namespace):
allow_internal_docker_ip_cmds = []
if namespace:
# For namespace docker allow local communication on docker management ip for all proto
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -s {} -d {} -j ACCEPT".format
(self.namespace_docker_mgmt_ip[namespace], self.namespace_docker_mgmt_ip[namespace]))
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -s {} -d {} -j ACCEPT".format
(self.namespace_docker_mgmt_ipv6[namespace], self.namespace_docker_mgmt_ipv6[namespace]))
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -s {} -d {} -j ACCEPT".format
(self.namespace_mgmt_ip, self.namespace_docker_mgmt_ip[namespace]))
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -s {} -d {} -j ACCEPT".format
(self.namespace_mgmt_ipv6, self.namespace_docker_mgmt_ipv6[namespace]))
else:
# Also host namespace communication on docker bridge on multi-asic.
if self.namespace_docker_mgmt_ip:
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -s {} -d {} -j ACCEPT".format
(self.namespace_mgmt_ip, self.namespace_mgmt_ip))
if self.namespace_docker_mgmt_ipv6:
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -s {} -d {} -j ACCEPT".format
(self.namespace_mgmt_ipv6, self.namespace_mgmt_ipv6))
# In host allow all tcp/udp traffic from namespace docker eth0 management ip to host docker bridge
for docker_mgmt_ip in list(self.namespace_docker_mgmt_ip.values()):
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -s {} -d {} -j ACCEPT".format
(docker_mgmt_ip, self.namespace_mgmt_ip))
for docker_mgmt_ipv6 in list(self.namespace_docker_mgmt_ipv6.values()):
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -s {} -d {} -j ACCEPT".format
(docker_mgmt_ipv6, self.namespace_mgmt_ipv6))
return allow_internal_docker_ip_cmds
def generate_fwd_traffic_from_namespace_to_host_commands(self, namespace, acl_source_ip_map):
"""
The below SNAT and DNAT rules are added in asic namespace in multi-ASIC platforms. It helps to forward request coming
in through the front panel interfaces created/present in the asic namespace for the servie running in linux host network namespace.
The external IP addresses are NATed to the internal docker IP addresses for the Host service to respond.
"""
if not namespace:
return []
fwd_traffic_from_namespace_to_host_cmds = []
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -t nat -X")
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -t nat -F")
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -t nat -X")
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -t nat -F")
for acl_service in self.ACL_SERVICES:
if self.ACL_SERVICES[acl_service]["multi_asic_ns_to_host_fwd"]:
# Get the Source IP Set if exists else use default source ip prefix
nat_source_ipv4_set = acl_source_ip_map[acl_service]["ipv4"] if acl_source_ip_map and acl_source_ip_map[acl_service]["ipv4"] else { "0.0.0.0/0" }
nat_source_ipv6_set = acl_source_ip_map[acl_service]["ipv6"] if acl_source_ip_map and acl_source_ip_map[acl_service]["ipv6"] else { "::/0" }
for ip_protocol in self.ACL_SERVICES[acl_service]["ip_protocols"]:
for dst_port in self.ACL_SERVICES[acl_service]["dst_ports"]:
for ipv4_src_ip in nat_source_ipv4_set:
# IPv4 rules
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] +
"iptables -t nat -A PREROUTING -p {} -s {} --dport {} -j DNAT --to-destination {}".format
(ip_protocol, ipv4_src_ip, dst_port,
self.namespace_mgmt_ip))
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] +
"iptables -t nat -A POSTROUTING -p {} -s {} --dport {} -j SNAT --to-source {}".format
(ip_protocol, ipv4_src_ip, dst_port,
self.namespace_docker_mgmt_ip[namespace]))
for ipv6_src_ip in nat_source_ipv6_set:
# IPv6 rules
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] +
"ip6tables -t nat -A PREROUTING -p {} -s {} --dport {} -j DNAT --to-destination {}".format
(ip_protocol, ipv6_src_ip, dst_port,
self.namespace_mgmt_ipv6))
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] +
"ip6tables -t nat -A POSTROUTING -p {} -s {} --dport {} -j SNAT --to-source {}".format
(ip_protocol,ipv6_src_ip, dst_port,
self.namespace_docker_mgmt_ipv6[namespace]))
return fwd_traffic_from_namespace_to_host_cmds
def is_rule_ipv4(self, rule_props):
if (("SRC_IP" in rule_props and rule_props["SRC_IP"]) or
("DST_IP" in rule_props and rule_props["DST_IP"])):
return True
else:
return False
def is_rule_ipv6(self, rule_props):
if (("SRC_IPV6" in rule_props and rule_props["SRC_IPV6"]) or
("DST_IPV6" in rule_props and rule_props["DST_IPV6"])):
return True
else:
return False
def setup_dhcp_chain(self, namespace):
all_chains = self.get_chain_list(self.iptables_cmd_ns_prefix[namespace], [""])
dhcp_chain_exist = "DHCP" in all_chains
iptables_cmds = []
if dhcp_chain_exist:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -F DHCP")
self.log_info("DHCP chain exists, flush")
else:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -N DHCP")
self.log_info("DHCP chain does not exist, create")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A DHCP -j RETURN")
self.log_info("Issuing the following iptables commands for DHCP chain:")
for cmd in iptables_cmds:
self.log_info(" " + cmd)
self.run_commands(iptables_cmds)
def get_chain_list(self, iptable_ns_cmd_prefix, exclude_list):
command = iptable_ns_cmd_prefix + "iptables -L -v -n | grep Chain | awk '{print $2}'"
chain_list = self.run_commands([command]).splitlines()
for chain in exclude_list:
if chain in chain_list:
chain_list.remove(chain)
return chain_list
def dhcp_acl_rule(self, iptable_ns_cmd_prefix, op, intf, mark):
'''
sample: iptables --insert/delete/check DHCP -m physdev --physdev-in Ethernet4 -j DROP
sample: iptables --insert/delete/check DHCP -m mark --mark 0x67004 -j DROP
'''
if mark is None:
return iptable_ns_cmd_prefix + 'iptables --{} DHCP -m physdev --physdev-in {} -j DROP'.format(op, intf)
else:
return iptable_ns_cmd_prefix + 'iptables --{} DHCP -m mark --mark {} -j DROP'.format(op, mark)
def update_dhcp_chain(self, op, intf, mark):
for namespace in list(self.config_db_map.keys()):
check_cmd = self.dhcp_acl_rule(self.iptables_cmd_ns_prefix[namespace], "check", intf, mark)
update_cmd = self.dhcp_acl_rule(self.iptables_cmd_ns_prefix[namespace], op, intf, mark)
execute = 0
ret = subprocess.call(check_cmd, shell=True) # ret==0 indicates the rule exists
if op == "insert" and ret == 1:
execute = 1
if op == "delete" and ret == 0:
execute = 1
if execute == 1:
subprocess.call(update_cmd, shell=True)
self.log_info("Update DHCP chain: {}".format(update_cmd))
def update_dhcp_acl(self, key, op, data, mark):
if "state" not in data:
self.log_warning("Unexpected update in MUX_CABLE_TABLE")
return
intf = key
state = data["state"]
if state == "active":
self.update_dhcp_chain("delete", intf, mark)
elif state == "standby":
self.update_dhcp_chain("insert", intf, mark)
elif state == "unknown":
self.update_dhcp_chain("delete", intf, mark)
elif state == "error":
self.log_warning("Cable state shows error")
else:
self.log_warning("Unexpected cable state")
def update_dhcp_acl_for_mark_change(self, key, pre_mark, cur_mark):
for namespace in list(self.config_db_map.keys()):
check_cmd = self.dhcp_acl_rule(self.iptables_cmd_ns_prefix[namespace], "check", key, pre_mark)
ret = subprocess.call(check_cmd, shell=True) # ret==0 indicates the rule exists
'''update only when the rule with pre_mark exists'''
if ret == 0:
delete_cmd = self.dhcp_acl_rule(self.iptables_cmd_ns_prefix[namespace], "delete", key, pre_mark)
insert_cmd = self.dhcp_acl_rule(self.iptables_cmd_ns_prefix[namespace], "insert", key, cur_mark)
subprocess.call(delete_cmd, shell=True)
self.log_info("Update DHCP chain: {}".format(delete_cmd))
subprocess.call(insert_cmd, shell=True)
self.log_info("Update DHCP chain: {}".format(insert_cmd))
def get_acl_rules_and_translate_to_iptables_commands(self, namespace):
"""
Retrieves current ACL tables and rules from Config DB, translates
control plane ACLs into a list of iptables commands that can be run
in order to install ACL rules.
Returns:
A list of strings, each string is an iptables shell command
"""
iptables_cmds = []
service_to_source_ip_map = {}
# First, add iptables commands to set default policies to accept all
# traffic. In case we are connected remotely, the connection will not
# drop when we flush the current rules
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -P INPUT ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -P FORWARD ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -P OUTPUT ACCEPT")
# Add iptables command to flush the current rules and delete all non-default chains
chain_list = self.get_chain_list(self.iptables_cmd_ns_prefix[namespace], ["DHCP"] if self.DualToR else [""])
for chain in chain_list:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -F " + chain)
if chain not in ["INPUT", "FORWARD", "OUTPUT"]:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -X " + chain)
# Add same set of commands for ip6tables
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -P INPUT ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -P FORWARD ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -P OUTPUT ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -F")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -X")
# Add iptables/ip6tables commands to allow all traffic from localhost
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -s 127.0.0.1 -i lo -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -s ::1 -i lo -j ACCEPT")
# Add iptables commands to allow internal docker traffic
iptables_cmds += self.generate_allow_internal_docker_ip_traffic_commands(namespace)
# Add iptables/ip6tables commands to allow all incoming packets from established
# connections or new connections which are related to established connections
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT")
# Add iptables/ip6tables commands to allow bidirectional ICMPv4 ping and traceroute
# TODO: Support processing ICMPv4 service ACL rules, and remove this blanket acceptance
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT")
# Add iptables/ip6tables commands to allow bidirectional ICMPv6 ping and traceroute
# TODO: Support processing ICMPv6 service ACL rules, and remove this blanket acceptance
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type echo-request -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type echo-reply -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type destination-unreachable -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type time-exceeded -j ACCEPT")
# Add iptables/ip6tables commands to allow all incoming Neighbor Discovery Protocol (NDP) NS/NA/RS/RA messages
# TODO: Support processing NDP service ACL rules, and remove this blanket acceptance
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type router-solicitation -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type router-advertisement -j ACCEPT")
# Add iptables commands to link the DCHP chain to block dhcp packets based on ingress interfaces
if self.DualToR:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p udp --dport 67 -j DHCP")
# Add iptables/ip6tables commands to allow all incoming IPv4 DHCP packets
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p udp --dport 67:68 -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p udp --dport 67:68 -j ACCEPT")
# Add iptables/ip6tables commands to allow all incoming IPv6 DHCP packets
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p udp --dport 546:547 -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p udp --dport 546:547 -j ACCEPT")
# Add iptables/ip6tables commands to allow all incoming BGP traffic
# TODO: Determine BGP ACLs based on configured device sessions, and remove this blanket acceptance
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p tcp --dport 179 -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p tcp --dport 179 -j ACCEPT")
# Get current ACL tables and rules from Config DB
self._tables_db_info = self.config_db_map[namespace].get_table(self.ACL_TABLE)
self._rules_db_info = self.config_db_map[namespace].get_table(self.ACL_RULE)
num_ctrl_plane_acl_rules = 0
# Walk the ACL tables
for (table_name, table_data) in self._tables_db_info.items():
table_ip_version = None
# Ignore non-control-plane ACL tables
if table_data["type"] != self.ACL_TABLE_TYPE_CTRLPLANE:
continue
acl_services = table_data["services"]
for acl_service in acl_services:
if acl_service not in self.ACL_SERVICES:
self.log_warning("Ignoring control plane ACL '{}' with unrecognized service '{}'"
.format(table_name, acl_service))
continue
self.log_info("Translating ACL rules for control plane ACL '{}' (service: '{}')"
.format(table_name, acl_service))
# Obtain default IP protocol(s) and destination port(s) for this service
ip_protocols = self.ACL_SERVICES[acl_service]["ip_protocols"]
dst_ports = self.ACL_SERVICES[acl_service]["dst_ports"]
acl_rules = {}
for ((rule_table_name, rule_id), rule_props) in self._rules_db_info.items():
rule_props = {k.upper(): v for k,v in rule_props.items()}
if rule_table_name == table_name:
if not rule_props:
self.log_warning("rule_props for rule_id {} empty or null!".format(rule_id))
continue
try:
acl_rules[rule_props["PRIORITY"]] = rule_props
except KeyError:
self.log_error("rule_props for rule_id {} does not have key 'PRIORITY'!".format(rule_id))
continue
# If we haven't determined the IP version for this ACL table yet,
# try to do it now. We attempt to determine heuristically based on
# whether the src or dst IP of this rule is an IPv4 or IPv6 address.
if not table_ip_version:
if self.is_rule_ipv6(rule_props):
table_ip_version = 6
elif self.is_rule_ipv4(rule_props):
table_ip_version = 4
if (self.is_rule_ipv6(rule_props) and (table_ip_version == 4)):
self.log_error("CtrlPlane ACL table {} is a IPv4 based table and rule {} is a IPV6 rule! Ignoring rule."
.format(table_name, rule_id))
acl_rules.pop(rule_props["PRIORITY"])
elif (self.is_rule_ipv4(rule_props) and (table_ip_version == 6)):
self.log_error("CtrlPlane ACL table {} is a IPv6 based table and rule {} is a IPV4 rule! Ignroing rule."
.format(table_name, rule_id))
acl_rules.pop(rule_props["PRIORITY"])
# If we were unable to determine whether this ACL table contains
# IPv4 or IPv6 rules, log a message and skip processing this table.
if not table_ip_version:
self.log_warning("Unable to determine if ACL table '{}' contains IPv4 or IPv6 rules. Skipping table..."
.format(table_name))
continue
ipv4_src_ip_set = set()
ipv6_src_ip_set = set()
# For each ACL rule in this table (in descending order of priority)
for priority in sorted(iter(acl_rules.keys()), reverse=True):
rule_props = acl_rules[priority]
if "PACKET_ACTION" not in rule_props:
self.log_error("ACL rule does not contain PACKET_ACTION property")
continue
# Apply the rule to the default protocol(s) for this ACL service
for ip_protocol in ip_protocols:
for dst_port in dst_ports:
rule_cmd = "ip6tables" if table_ip_version == 6 else "iptables"
rule_cmd += " -A INPUT"
if ip_protocol != "any":
rule_cmd += " -p {}".format(ip_protocol)
if "SRC_IPV6" in rule_props and rule_props["SRC_IPV6"]:
rule_cmd += " -s {}".format(rule_props["SRC_IPV6"])
if rule_props["PACKET_ACTION"] == "ACCEPT":
ipv6_src_ip_set.add(rule_props["SRC_IPV6"])
elif "SRC_IP" in rule_props and rule_props["SRC_IP"]:
rule_cmd += " -s {}".format(rule_props["SRC_IP"])
if rule_props["PACKET_ACTION"] == "ACCEPT":
ipv4_src_ip_set.add(rule_props["SRC_IP"])
# Destination port 0 is reserved/unused port, so, using it to apply the rule to all ports.
if dst_port != "0":
rule_cmd += " --dport {}".format(dst_port)
# If there are TCP flags present and ip protocol is TCP, append them
if ip_protocol == "tcp" and "TCP_FLAGS" in rule_props and rule_props["TCP_FLAGS"]:
tcp_flags, tcp_flags_mask = rule_props["TCP_FLAGS"].split("/")
tcp_flags = int(tcp_flags, 16)
tcp_flags_mask = int(tcp_flags_mask, 16)
if tcp_flags_mask > 0:
rule_cmd += " --tcp-flags {mask} {flags}".format(mask=self.parse_int_to_tcp_flags(tcp_flags_mask), flags=self.parse_int_to_tcp_flags(tcp_flags))
# Append the packet action as the jump target
rule_cmd += " -j {}".format(rule_props["PACKET_ACTION"])
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + rule_cmd)
num_ctrl_plane_acl_rules += 1
service_to_source_ip_map.update({ acl_service:{ "ipv4":ipv4_src_ip_set, "ipv6":ipv6_src_ip_set } })
# Add iptables commands to block ip2me traffic
iptables_cmds += self.generate_block_ip2me_traffic_iptables_commands(namespace)
# Add iptables/ip6tables commands to allow all incoming packets with TTL of 0 or 1
# This allows the device to respond to tools like tcptraceroute
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -m ttl --ttl-lt 2 -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p tcp -m hl --hl-lt 2 -j ACCEPT")
# Finally, if the device has control plane ACLs configured,
# add iptables/ip6tables commands to drop all other incoming packets
if num_ctrl_plane_acl_rules > 0:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -j DROP")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -j DROP")
return iptables_cmds, service_to_source_ip_map
def update_control_plane_acls(self, namespace):
"""
Convenience wrapper which retrieves current ACL tables and rules from
Config DB, translates control plane ACLs into a list of iptables
commands and runs them.
"""
iptables_cmds, service_to_source_ip_map = self.get_acl_rules_and_translate_to_iptables_commands(namespace)
self.log_info("Issuing the following iptables commands:")
for cmd in iptables_cmds:
self.log_info(" " + cmd)
self.run_commands(iptables_cmds)
self.update_control_plane_nat_acls(namespace, service_to_source_ip_map)
def update_control_plane_nat_acls(self, namespace, service_to_source_ip_map):
"""
Convenience wrapper for multi-asic platforms
which programs the NAT rules for redirecting the
traffic coming on the front panel interface map to namespace
to the host.
"""
# Add iptables commands to allow front panel traffic
iptables_cmds = self.generate_fwd_traffic_from_namespace_to_host_commands(namespace, service_to_source_ip_map)
self.log_info("Issuing the following iptables commands:")
for cmd in iptables_cmds:
self.log_info(" " + cmd)
self.run_commands(iptables_cmds)
def check_and_update_control_plane_acls(self, namespace, num_changes):
"""
This function is intended to be spawned in a separate thread.
Its purpose is to prevent unnecessary iptables updates if we receive
multiple rapid ACL table update notifications. It sleeps for UPDATE_DELAY_SECS
then checks if any more ACL table updates were received in that window. If new
updates were received, it will sleep again and repeat the process until no
updates were received during the delay window, at which point it will update
iptables using the current ACL rules.
"""
while True:
# Sleep for our delay interval
time.sleep(self.UPDATE_DELAY_SECS)
with self.lock[namespace]:
if self.num_changes[namespace] > num_changes:
# More ACL table changes occurred since this thread was spawned
# spawn a new thread with the current number of changes
new_changes = self.num_changes[namespace] - num_changes
self.log_info("ACL config not stable for namespace '{}': {} changes detected in the past {} seconds. Skipping update ..."
.format(namespace, new_changes, self.UPDATE_DELAY_SECS))
num_changes = self.num_changes[namespace]
else:
if num_changes == self.num_changes[namespace] and num_changes > 0:
self.log_info("ACL config for namespace '{}' has not changed for {} seconds. Applying updates ..."
.format(namespace, self.UPDATE_DELAY_SECS))
self.update_control_plane_acls(namespace)
else:
self.log_error("Error updating ACLs for namespace '{}'".format(namespace))
# Re-initialize
self.num_changes[namespace] = 0
self.update_thread[namespace] = None
return
def run(self):
# Set select timeout to 1 second
SELECT_TIMEOUT_MS = 1000
self.log_info("Starting up ...")
if not os.geteuid() == 0:
self.log_error("Must be root to run this daemon")
print("Error: Must be root to run this daemon")
sys.exit(1)
# Initlaize Global config that loads all database*.json
if device_info.is_multi_npu():
swsscommon.SonicDBConfig.initializeGlobalConfig()
# Create the Select object
sel = swsscommon.Select()
# Set up STATE_DB connector to monitor the change in MUX_CABLE_TABLE
state_db_connector = None
subscribe_mux_cable = None
subscribe_dhcp_packet_mark = None
state_db_id = swsscommon.SonicDBConfig.getDbId("STATE_DB")
dhcp_packet_mark_tbl = {}
if self.DualToR:
self.log_info("Dual ToR mode")
# set up state_db connector
state_db_connector = swsscommon.DBConnector("STATE_DB", 0)
subscribe_mux_cable = swsscommon.SubscriberStateTable(state_db_connector, self.MUX_CABLE_TABLE)
sel.addSelectable(subscribe_mux_cable)
subscribe_dhcp_packet_mark = swsscommon.SubscriberStateTable(state_db_connector, "DHCP_PACKET_MARK")
sel.addSelectable(subscribe_dhcp_packet_mark)
# create DHCP chain
for namespace in list(self.config_db_map.keys()):
self.setup_dhcp_chain(namespace)
# Map of Namespace <--> susbcriber table's object
config_db_subscriber_table_map = {}
# Loop through all asic namespaces (if present) and host namespace (DEFAULT_NAMESPACE)
for namespace in list(self.config_db_map.keys()):
# Unconditionally update control plane ACLs once at start on given namespace
self.update_control_plane_acls(namespace)
# Connect to Config DB of given namespace
acl_db_connector = swsscommon.DBConnector("CONFIG_DB", 0, False, namespace)
# Subscribe to notifications when ACL tables changes
subscribe_acl_table = swsscommon.SubscriberStateTable(acl_db_connector, swsscommon.CFG_ACL_TABLE_TABLE_NAME)
# Subscribe to notifications when ACL rule tables changes
subscribe_acl_rule_table = swsscommon.SubscriberStateTable(acl_db_connector, swsscommon.CFG_ACL_RULE_TABLE_NAME)
# Add both tables to the selectable object
sel.addSelectable(subscribe_acl_table)
sel.addSelectable(subscribe_acl_rule_table)
# Update the map
config_db_subscriber_table_map[namespace] = []
config_db_subscriber_table_map[namespace].append(subscribe_acl_table)
config_db_subscriber_table_map[namespace].append(subscribe_acl_rule_table)
# Get the ACL rule table seprator
acl_rule_table_seprator = subscribe_acl_rule_table.getTableNameSeparator()
# Loop on select to see if any event happen on state db or config db of any namespace
while True:
(state, selectableObj) = sel.select(SELECT_TIMEOUT_MS)
# Continue if select is timeout or selectable object is not return
if state != swsscommon.Select.OBJECT:
continue
# Get the redisselect object from selectable object
redisSelectObj = swsscommon.CastSelectableToRedisSelectObj(selectableObj)
# Get the corresponding namespace and db_id from redisselect
namespace = redisSelectObj.getDbConnector().getNamespace()
db_id = redisSelectObj.getDbConnector().getDbId()
if db_id == state_db_id:
if self.DualToR:
'''dhcp packet mark update'''
while True:
key, op, fvs = subscribe_dhcp_packet_mark.pop()
if not key:
break
self.log_info("dhcp packet mark update : '%s'" % str((key, op, fvs)))
'''initial value is None'''
pre_mark = None if key not in dhcp_packet_mark_tbl else dhcp_packet_mark_tbl[key]
cur_mark = None if op == 'DEL' else dict(fvs)['mark']
dhcp_packet_mark_tbl[key] = cur_mark
self.update_dhcp_acl_for_mark_change(key, pre_mark, cur_mark)
'''mux cable update'''
while True:
key, op, fvs = subscribe_mux_cable.pop()
if not key:
break
self.log_info("mux cable update : '%s'" % str((key, op, fvs)))
mark = None if key not in dhcp_packet_mark_tbl else dhcp_packet_mark_tbl[key]
self.update_dhcp_acl(key, op, dict(fvs), mark)
continue
ctrl_plane_acl_notification = set()
# Pop data of both Subscriber Table object of namespace that got config db acl table event
for table in config_db_subscriber_table_map[namespace]:
while True:
(key, op, fvp) = table.pop()
# Pop of table that does not have data so break
if key == '':
break
# ACL Table notification. We will take Control Plane ACTION for any ACL Table Event
# This can be optimize further but we should not have many acl table set/del events in normal
# scenario
if acl_rule_table_seprator not in key:
ctrl_plane_acl_notification.add(namespace)
# Check ACL Rule notification and make sure Rule point to ACL Table which is Controlplane
else:
acl_table = key.split(acl_rule_table_seprator)[0]
if self.config_db_map[namespace].get_table(self.ACL_TABLE)[acl_table]["type"] == self.ACL_TABLE_TYPE_CTRLPLANE:
ctrl_plane_acl_notification.add(namespace)
# Update the Control Plane ACL of the namespace that got config db acl table event
for namespace in ctrl_plane_acl_notification:
with self.lock[namespace]:
if self.num_changes[namespace] == 0:
self.log_info("ACL change detected for namespace '{}'".format(namespace))
# Increment the number of change events we've received for this namespace
self.num_changes[namespace] += 1
# If an update thread is not already spawned for the namespace which we received
# the ACL table update event, spawn one now
if not self.update_thread[namespace]:
self.log_info("Spawning ACL update thread for namepsace '{}' ...".format(namespace))
self.update_thread[namespace] = threading.Thread(target=self.check_and_update_control_plane_acls,
args=(namespace, self.num_changes[namespace]))
self.update_thread[namespace].start()
# ============================= Functions =============================
def main():
# Instantiate a ControlPlaneAclManager object
caclmgr = ControlPlaneAclManager(SYSLOG_IDENTIFIER)
# Log all messages from INFO level and higher
caclmgr.set_min_log_priority_info()
caclmgr.run()
if __name__ == "__main__":
main()