ifstate 1.13.6__py3-none-any.whl → 2.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  from libifstate.util import logger, IfStateLogging
2
2
  from libifstate.exception import RouteDuplicate, netlinkerror_classes
3
- from ipaddress import ip_address, ip_network, IPv6Network
3
+ from ipaddress import ip_address, ip_interface, ip_network, IPv6Network
4
4
  from pyroute2.netlink.rtnl.fibmsg import FR_ACT_VALUES
5
5
  from pyroute2.netlink.rtnl import rt_type
6
6
  import collections.abc
@@ -15,6 +15,147 @@ from socket import AF_INET, AF_INET6
15
15
  def route_matches(r1, r2, fields=('dst', 'priority', 'proto'), indent=None):
16
16
  return _matches(r1, r2, fields, indent)
17
17
 
18
+ def parse_route(route, implicit_defaults=True):
19
+ rt = {}
20
+
21
+ if implicit_defaults:
22
+ rt['type'] = route.get('type', 'unicast')
23
+
24
+ if 'to' in route:
25
+ dst = ip_network(route['to'])
26
+ rt['dst'] = dst.with_prefixlen
27
+ has_to = True
28
+ else:
29
+ has_to = False
30
+
31
+ for key, lookup in RT_LOOKUPS_DICT.items():
32
+ try:
33
+ if implicit_defaults:
34
+ rt[key] = route.get(key, RT_LOOKUPS_DEFAULTS[key])
35
+ rt[key] = lookup.lookup_id(rt[key])
36
+ else:
37
+ if key in route:
38
+ rt[key] = route[key]
39
+ rt[key] = lookup.lookup_id(rt[key])
40
+ except KeyError as err:
41
+ # mapping not available - catch exception and skip it
42
+ logger.warning('ignoring unknown %s "%s"', key, rt[key],
43
+ extra={'iface': rt['dst']})
44
+ rt[key] = RT_LOOKUPS_DEFAULTS[key]
45
+
46
+ if 'type' in rt and type(rt['type']) == str:
47
+ rt['type'] = rt_type[rt['type']]
48
+
49
+ if 'dev' in route:
50
+ rt['oif'] = route['dev']
51
+
52
+ if 'via' in route:
53
+ via = ip_address(route['via'])
54
+
55
+ if not has_to or via.version == dst.version:
56
+ rt['gateway'] = str(via)
57
+ else:
58
+ if via.version == 4:
59
+ rt['via'] = {
60
+ 'family': int(AF_INET),
61
+ 'addr': str(via),
62
+ }
63
+ else:
64
+ rt['via'] = {
65
+ 'family': int(AF_INET6),
66
+ 'addr': str(via),
67
+ }
68
+
69
+ if 'src' in route:
70
+ rt['prefsrc'] = route['src']
71
+
72
+ if 'preference' in route:
73
+ rt['priority'] = route['preference']
74
+ elif has_to and implicit_defaults:
75
+ if isinstance(dst, IPv6Network):
76
+ rt['priority'] = 1024
77
+ else:
78
+ rt['priority'] = 0
79
+
80
+ if 'vrrp' in route:
81
+ rt['_vrrp'] = route['vrrp']
82
+
83
+ return rt
84
+
85
+ def parse_routes(routes, implicit_defaults=True):
86
+ return [parse_route(route, implicit_defaults) for route in routes]
87
+
88
+ def parse_rule(rule, implicit_defaults=True):
89
+ ru = {}
90
+
91
+ if implicit_defaults:
92
+ ru['table'] = RTLookups.tables.lookup_id(rule.get('table', 254))
93
+ ru['protocol'] = RTLookups.protos.lookup_id(rule.get('proto', 0))
94
+ ru['tos'] = rule.get('tos', 0)
95
+ else:
96
+ if 'table' in rule:
97
+ ru['table'] = RTLookups.tables.lookup_id(rule['table'])
98
+ if 'proto' in rule:
99
+ ru['protocol'] = RTLookups.protos.lookup_id(rule['proto'])
100
+ if 'tos' in rule:
101
+ ru['tos'] = rule['tos']
102
+
103
+ if 'action' in rule and type(rule['action']) == str:
104
+ ru['action'] = {
105
+ "to_tbl": "FR_ACT_TO_TBL",
106
+ "unicast": "FR_ACT_UNICAST",
107
+ "blackhole": "FR_ACT_BLACKHOLE",
108
+ "unreachable": "FR_ACT_UNREACHABLE",
109
+ "prohibit": "FR_ACT_PROHIBIT",
110
+ "nat": "FR_ACT_NAT",
111
+ }.get(rule['action'], rule['action'])
112
+ elif implicit_defaults:
113
+ ru['action'] = "FR_ACT_TO_TBL"
114
+
115
+ if 'fwmark' in rule:
116
+ ru['fwmark'] = rule['fwmark']
117
+
118
+ if 'ipproto' in rule:
119
+ if type(rule['ipproto']) == str:
120
+ ru['ip_proto'] = socket.getprotobyname(rule['ipproto'])
121
+ else:
122
+ ru['ip_proto'] = rule['ipproto']
123
+
124
+ if 'to' in rule:
125
+ ru['dst'] = str(ip_network(rule['to']).network_address)
126
+ ru['dst_len'] = ip_network(rule['to']).prefixlen
127
+
128
+ if 'from' in rule:
129
+ if rule['from'] != "all":
130
+ ru['src'] = str(ip_interface(rule['from']).ip)
131
+ ru['src_len'] = ip_network(ip_interface(rule['from']).network).prefixlen
132
+ else:
133
+ ru['src'] = "0.0.0.0"
134
+ ru['src_len'] = 0
135
+
136
+ if 'iif' in rule:
137
+ ru['iifname'] = rule['iif']
138
+
139
+ if 'oif' in rule:
140
+ ru['oifname'] = rule['oif']
141
+
142
+ if 'priority' in rule:
143
+ ru['priority'] = rule['priority']
144
+
145
+ if 'vrrp' in rule:
146
+ ru['_vrrp'] = rule['vrrp']
147
+
148
+ if 'family' in rule:
149
+ assert rule['family'] in ("AF_INET", "inet", 2, "AF_INET6", "inet6", 10), "rule['family'] doesn't have a valid value. This should have already been rejected by the schema validation."
150
+ if rule['family'] in ("AF_INET", "inet", 2):
151
+ ru['family'] = AF_INET
152
+ elif rule['family'] in ("AF_INET6", "inet6", 10):
153
+ ru['family'] = AF_INET6
154
+
155
+ return ru
156
+
157
+ def parse_rules(rules, implicit_defaults=True):
158
+ return [parse_rule(rule, implicit_defaults) for rule in rules]
18
159
 
19
160
  def rule_matches(r1, r2, fields=('priority', 'iif', 'oif', 'dst', 'metric', 'protocol'), indent=None):
20
161
  return _matches(r1, r2, fields, indent)
@@ -133,64 +274,36 @@ class Tables(collections.abc.Mapping):
133
274
  def __len__(self):
134
275
  return len(self.tables)
135
276
 
136
- def add(self, route):
137
- dst = ip_network(route['to'])
138
- rt = {
139
- 'type': route.get('type', 'unicast'),
140
- 'dst': dst.with_prefixlen,
141
- }
142
-
143
- for key, lookup in RT_LOOKUPS_DICT.items():
144
- try:
145
- rt[key] = route.get(key, RT_LOOKUPS_DEFAULTS[key])
146
- rt[key] = lookup.lookup_id(rt[key])
147
- except KeyError as err:
148
- # mapping not available - catch exception and skip it
149
- logger.warning('ignoring unknown %s "%s"', key, rt[key],
150
- extra={'iface': rt['dst'], 'netns': self.netns})
151
- rt[key] = RT_LOOKUPS_DEFAULTS[key]
152
-
153
- if type(rt['type']) == str:
154
- rt['type'] = rt_type[rt['type']]
155
-
156
- if 'dev' in route:
157
- rt['oif'] = route['dev']
158
-
159
- if 'via' in route:
160
- via = ip_address(route['via'])
161
-
162
- if via.version == dst.version:
163
- rt['gateway'] = str(via)
164
- else:
165
- if via.version == 4:
166
- rt['via'] = {
167
- 'family': int(AF_INET),
168
- 'addr': str(via),
169
- }
170
- else:
171
- rt['via'] = {
172
- 'family': int(AF_INET6),
173
- 'addr': str(via),
174
- }
175
-
176
- if 'src' in route:
177
- rt['prefsrc'] = route['src']
277
+ def parse_ignores(self, ignores):
278
+ parsed = []
279
+ for ignore in ignores:
280
+ ignore = parse_route(ignore, False)
281
+
282
+ for attr in ['dev', 'oif', 'iif']:
283
+ if attr in ignore and type(ignore[attr]) == str:
284
+ idx = next(
285
+ iter(self.netns.ipr.link_lookup(ifname=ignore[attr])), None)
286
+ if idx is None:
287
+ logger.debug("{} '{}' is unknown, skipping ignore".format(attr, ignore[attr]))
288
+ continue
289
+ else:
290
+ ignore[attr] = idx
291
+ parsed.append(ignore)
178
292
 
179
- if 'preference' in route:
180
- rt['priority'] = route['preference']
181
- elif isinstance(dst, IPv6Network):
182
- rt['priority'] = 1024
183
- else:
184
- rt['priority'] = 0
293
+ return parsed
185
294
 
186
- if 'vrrp' in route:
187
- rt['_vrrp'] = route['vrrp']
295
+ def add(self, route):
296
+ # convert config dict into IFLA keys and values
297
+ rt = parse_route(route)
188
298
 
299
+ # track route in the corresponding table
189
300
  if not rt['table'] in self.tables:
190
301
  self.tables[rt['table']] = []
191
302
  self.tables[rt['table']].append(rt)
192
303
 
193
304
  def show_routes(self, ignores):
305
+ ignores = self.parse_ignores(ignores)
306
+
194
307
  routes = []
195
308
  for route in self.netns.ipr.get_routes(family=AF_INET) + self.netns.ipr.get_routes(family=AF_INET6):
196
309
  # skip routes from local table
@@ -208,7 +321,7 @@ class Tables(collections.abc.Mapping):
208
321
  continue
209
322
 
210
323
  if route['dst_len'] > 0:
211
- dst = ip_network(
324
+ dst = ip_interface(
212
325
  '{}/{}'.format(route.get_attr('RTA_DST'), route['dst_len'])).with_prefixlen
213
326
  elif route['family'] == AF_INET:
214
327
  dst = ip_network('0.0.0.0/0').with_prefixlen
@@ -322,6 +435,8 @@ class Tables(collections.abc.Mapping):
322
435
  return routes
323
436
 
324
437
  def apply(self, ignores, do_apply, by_vrrp, vrrp_type, vrrp_name, vrrp_state):
438
+ ignores = self.parse_ignores(ignores)
439
+
325
440
  for table, croutes in self.tables.items():
326
441
  log_str = RTLookups.tables.lookup_str(table)
327
442
  if self.netns.netns != None:
@@ -413,53 +528,10 @@ class Rules():
413
528
  self.rules = []
414
529
 
415
530
  def add(self, rule):
416
- ru = {
417
- 'table': RTLookups.tables.lookup_id(rule.get('table', 254)),
418
- 'protocol': RTLookups.protos.lookup_id(rule.get('proto', 0)),
419
- 'tos': rule.get('tos', 0),
420
- }
421
-
422
- if 'action' in rule and type(rule['action']) == str:
423
- ru['action'] = {
424
- "to_tbl": "FR_ACT_TO_TBL",
425
- "unicast": "FR_ACT_UNICAST",
426
- "blackhole": "FR_ACT_BLACKHOLE",
427
- "unreachable": "FR_ACT_UNREACHABLE",
428
- "prohibit": "FR_ACT_PROHIBIT",
429
- "nat": "FR_ACT_NAT",
430
- }.get(rule['action'], rule['action'])
431
- else:
432
- ru['action'] = rule.get('action'.lower(), "FR_ACT_TO_TBL")
433
-
434
- if 'fwmark' in rule:
435
- ru['fwmark'] = rule['fwmark']
436
-
437
- if 'ipproto' in rule:
438
- if type(rule['ipproto']) == str:
439
- ru['ip_proto'] = socket.getprotobyname(rule['ipproto'])
440
- else:
441
- ru['ip_proto'] = rule['ipproto']
442
-
443
- if 'to' in rule:
444
- ru['dst'] = str(ip_network(rule['to']).network_address)
445
- ru['dst_len'] = ip_network(rule['to']).prefixlen
446
-
447
- if 'from' in rule:
448
- ru['src'] = str(ip_network(rule['from']).network_address)
449
- ru['src_len'] = ip_network(rule['from']).prefixlen
450
-
451
- if 'iif' in rule:
452
- ru['iifname'] = rule['iif']
453
-
454
- if 'oif' in rule:
455
- ru['oifname'] = rule['oif']
456
-
457
- if 'priority' in rule:
458
- ru['priority'] = rule['priority']
459
-
460
- if 'vrrp' in rule:
461
- ru['_vrrp'] = rule['vrrp']
531
+ # convert config dict into IFLA keys and values
532
+ ru = parse_rule(rule)
462
533
 
534
+ # track rule
463
535
  self.rules.append(ru)
464
536
 
465
537
  def kernel_rules(self):
@@ -474,13 +546,18 @@ class Rules():
474
546
  'tos': rule['tos'],
475
547
  }
476
548
 
477
- if rule['dst_len'] > 0:
478
- ru['dst'] = rule.get_attr('FRA_DST')
479
- ru['dst_len'] = rule['dst_len']
549
+ # add source and destination ip spaces even if they are not given by pyroute2
550
+ # as the user might specify something like "from: ::/0" explicitly which would not
551
+ # be present on a krule (because default values are ommited by pyroute2) and therefore
552
+ # would not match, even though it is the same rule
553
+ # if the user doesn't specify 'from' or 'to', these fields are just ignored because
554
+ # rule_matches is instructed to only check fields that are present on the config rule,
555
+ # meaning that these just get ignored
556
+ ru['dst'] = rule.get_attr('FRA_DST', default=("::" if rule['family'] == AF_INET6 else "0.0.0.0"))
557
+ ru['dst_len'] = rule['dst_len']
480
558
 
481
- if rule['src_len'] > 0:
482
- ru['src'] = rule.get_attr('FRA_SRC')
483
- ru['src_len'] = rule['src_len']
559
+ ru['src'] = rule.get_attr('FRA_SRC', default=("::" if rule['family'] == AF_INET6 else "0.0.0.0"))
560
+ ru['src_len'] = rule['src_len']
484
561
 
485
562
  for field in ['iifname', 'oifname', 'fwmark', 'ip_proto']:
486
563
  value = rule.get_attr('FRA_{}'.format(field.upper()))
@@ -491,14 +568,13 @@ class Rules():
491
568
  return rules
492
569
 
493
570
  def show_rules(self, ignores):
571
+ ignores = parse_rules(ignores, implicit_defaults=False)
572
+
494
573
  rules = []
495
574
  for rule in self.kernel_rules():
496
575
  # skip ignored routes
497
576
  ignore = False
498
577
  for irule in ignores:
499
- if 'proto' in irule:
500
- irule['protocol'] = irule['proto']
501
- del irule['proto']
502
578
  if rule_matches(rule, irule, irule.keys()):
503
579
  ignore = True
504
580
  break
@@ -522,7 +598,7 @@ class Rules():
522
598
  del rule['protocol']
523
599
 
524
600
  if 'src_len' in rule and rule['src_len'] > 0:
525
- rule['from'] = ip_network(
601
+ rule['from'] = ip_interface(
526
602
  '{}/{}'.format(rule['src'], rule['src_len'])).with_prefixlen
527
603
 
528
604
  for key in ['src', 'src_len', 'tos', 'protocol']:
@@ -534,33 +610,55 @@ class Rules():
534
610
  return rules
535
611
 
536
612
  def apply(self, ignores, do_apply, by_vrrp, vrrp_type, vrrp_name, vrrp_state):
613
+ ignores = parse_rules(ignores, implicit_defaults=False)
614
+
615
+ # get rules that are currently in the kernels list
537
616
  krules = self.kernel_rules()
617
+
618
+ # loop over all rules that are wanted by the config
538
619
  for rule in self.rules:
539
620
  log_str = '#{}'.format(rule['priority'])
621
+
622
+ if rule.get('family', None) == AF_INET6:
623
+ log_str += " (inet6)"
624
+ elif rule.get('family', None) == AF_INET:
625
+ log_str += " (inet)"
626
+
540
627
  if self.netns.netns != None:
541
628
  log_str += "[netns={}]".format(self.netns.netns)
542
629
 
630
+ vrrp_matched = vrrp_match(rule, by_vrrp, vrrp_type, vrrp_name, vrrp_state)
631
+ # 'found' indicates whether the rule wanted by the config was found
632
+ # in the kernels rules. if not, it is going to be added below
543
633
  found = False
544
- matched = vrrp_match(rule, by_vrrp, vrrp_type, vrrp_name, vrrp_state)
545
634
  for i, krule in enumerate(krules):
546
635
  if rule_matches(
547
636
  rule,
548
637
  krule,
549
638
  # skip helper attrs like _vrrp
639
+ # note that rule_matches it told to only check fields that are
640
+ # present on the config rule, so additional ones from the krule
641
+ # just get ignored
550
642
  [key for key in rule.keys() if key[0] != '_']
551
643
  ):
552
644
  found = True
553
645
 
554
- # remove kernel routes due to vrrp_match
555
- if matched != VRRP_MATCH_DISABLE:
646
+ # we've just confirmed that this krule matches a rule
647
+ # that is wanted by the config so we delete it from the
648
+ # krules because any that remain in krule are unwanted
649
+ # and get deleted below
650
+ # EXCEPT if vrrp tells us to leave it be
651
+ if vrrp_matched != VRRP_MATCH_DISABLE:
556
652
  del krules[i]
557
653
 
558
654
  break
559
655
 
560
- if matched not in [VRRP_MATCH_IGNORE, VRRP_MATCH_DISABLE]:
656
+ if vrrp_matched not in [VRRP_MATCH_IGNORE, VRRP_MATCH_DISABLE]:
561
657
  if found:
658
+ # rule already exists
562
659
  logger.log_ok(log_str)
563
660
  else:
661
+ # rule doesn't exist, add it
564
662
  logger.log_add(log_str)
565
663
 
566
664
  logger.debug("ip rule add: {}".format(
@@ -573,7 +671,12 @@ class Rules():
573
671
  raise
574
672
  logger.warning('rule setup failed: {}'.format(err.args[1]))
575
673
 
674
+ # at this point all wanted rules have been deleted from the krules list,
675
+ # leaving the rest to be deleted here (see 'del krules[i]' above)
576
676
  for rule in krules:
677
+
678
+ # if this rule falls into the ignored ones (like the ones set by the kernel itself by default)
679
+ # we will ignore it and continue
577
680
  ignore = False
578
681
  for irule in ignores:
579
682
  if 'proto' in irule:
@@ -585,9 +688,26 @@ class Rules():
585
688
  if ignore:
586
689
  continue
587
690
 
588
- logger.log_del('#{}'.format(rule['priority']))
691
+ log_str = '#{}'.format(rule['priority'])
692
+
693
+ if rule.get('family', None) == AF_INET6:
694
+ log_str += " (inet6)"
695
+ elif rule.get('family', None) == AF_INET:
696
+ log_str += " (inet)"
697
+
698
+ logger.log_del(log_str)
699
+
589
700
  try:
590
701
  if do_apply:
702
+ # for some reason pyroute2 raises an exception when
703
+ # we pass an address like "0.0.0.0" or "::" with an
704
+ # 'src_len' or 'dst_len' of 0.
705
+ # using this as a workaround:
706
+ if rule['src_len'] == 0:
707
+ del rule['src']
708
+ if rule['dst_len'] == 0:
709
+ del rule['dst']
710
+
591
711
  self.netns.ipr.rule('del', **rule)
592
712
  except Exception as err:
593
713
  if not isinstance(err, netlinkerror_classes):