summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/frrender.py80
-rw-r--r--python/vyos/utils/network.py21
2 files changed, 101 insertions, 0 deletions
diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py
index 824342773..275316e4a 100644
--- a/python/vyos/frrender.py
+++ b/python/vyos/frrender.py
@@ -47,6 +47,75 @@ def debug(message):
return
print(message)
+# options of a static route in staticd.frr.j2 which are followed by a value
+static_route_options = ['tag', 'table', 'nexthop-vrf', 'segments', 'source', 'profile']
+
+def drop_conflicting_discard_routes(output: str) -> tuple:
+ """
+ FRR staticd refuses a blackhole or reject route next to another route of
+ the same prefix and distance, and frr-reload.py then loses every static
+ route of that reload. protocols_static.py refuses this for static routes,
+ but the default route of a DHCP or PPPoE interface changes without a
+ commit of the static routes. Drop the blackhole and reject routes of such
+ a prefix from the rendered configuration.
+
+ Returns the configuration and a message for every dropped route.
+ """
+ from vyos.utils.network import normalize_prefix
+
+ lines = output.splitlines()
+ routes = {}
+ vrf = None
+ for index, line in enumerate(lines):
+ tokens = line.split()
+ if len(tokens) == 2 and tokens[0] == 'vrf':
+ vrf = tokens[1]
+ continue
+ if tokens == ['exit-vrf']:
+ vrf = None
+ continue
+ # <afi> route <prefix> <next-hop> [options]
+ if len(tokens) < 4 or tokens[0] not in ['ip', 'ipv6'] or tokens[1] != 'route':
+ continue
+
+ table = None
+ distance = None
+ options = iter(tokens[4:])
+ for token in options:
+ if token in static_route_options:
+ value = next(options, None)
+ if token == 'table':
+ table = value
+ elif token.isdigit() and distance is None:
+ distance = int(token)
+ # FRR uses distance 1 if none is given
+ key = (vrf, tokens[0], table, normalize_prefix(tokens[2]), distance or 1)
+ routes.setdefault(key, []).append((index, tokens[3] in ['blackhole', 'reject']))
+
+ drop = []
+ messages = []
+ for (vrf, _, table, prefix, distance), entries in routes.items():
+ discards = [index for index, discard in entries if discard]
+ if not discards or len(discards) == len(entries):
+ continue
+ context = ''
+ if vrf:
+ context += f' in VRF "{vrf}"'
+ if table:
+ context += f' in table {table}'
+ for index in discards:
+ drop.append(index)
+ messages.append(f'Static route "{lines[index].strip()}"{context} not '
+ f'applied, FRR refuses a blackhole or reject route '
+ f'next to another route of prefix {prefix} with the '
+ f'same distance {distance}')
+
+ if not drop:
+ return output, messages
+ drop = set(drop)
+ output = os.linesep.join(line for index, line in enumerate(lines) if index not in drop)
+ return output, messages
+
frr_config_file: str = '/run/frr/config/vyos.frr.conf'
# Configuration of the last successful reload, for consumers which have no
# cached configuration of their own
@@ -953,6 +1022,17 @@ class FRRender:
# remove any accidentally added empty newline to not confuse FRR
output = os.linesep.join([s for s in output.splitlines() if s])
+ # T9482: the default route of a DHCP or PPPoE interface can collide with
+ # a blackhole or reject route, FRR would refuse the whole reload
+ output, errors = drop_conflicting_discard_routes(output)
+ if errors:
+ from systemd import journal
+ for error in errors:
+ debug(error)
+ # journal fields must be upper case, else they are dropped
+ journal.send(error, PRIORITY=journal.LOG_ERR,
+ SYSLOG_IDENTIFIER='vyos-frrender')
+
if '!!' in output:
raise ConfigError('FRR configuration contains "!!" which is not allowed')
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index 42fec00f0..4a4414f56 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -19,6 +19,8 @@ from json import loads
from socket import AF_INET
from socket import AF_INET6
from socket import AF_UNSPEC
+from socket import inet_ntop
+from socket import inet_pton
from vyos.utils.process import cmdl
def _are_same_ip(one, two):
@@ -994,3 +996,22 @@ def fqdn_resolve(
addresses = [item[4][0] for item in res]
return frozenset(addresses) if addresses else None
+
+def normalize_prefix(prefix: str) -> str:
+ """
+ Return the prefix as FRR stores it: host bits cleared and the address in
+ its canonical notation. A prefix that cannot be parsed is returned as is.
+ inet_pton() is what FRR parses a prefix with, and is several times faster
+ than the ipaddress module.
+ """
+ address, _, length = prefix.partition('/')
+ family, bits = (AF_INET6, 128) if ':' in address else (AF_INET, 32)
+ try:
+ length = int(length)
+ value = int.from_bytes(inet_pton(family, address), 'big')
+ except (OSError, ValueError):
+ return prefix
+ if not 0 <= length <= bits:
+ return prefix
+ value &= ~((1 << (bits - length)) - 1)
+ return f'{inet_ntop(family, value.to_bytes(bits // 8, "big"))}/{length}'