summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/nat_cgnat.py288
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py27
-rwxr-xr-xsrc/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook6
-rwxr-xr-xsrc/migration-scripts/firewall/6-to-780
-rwxr-xr-xsrc/migration-scripts/nat/5-to-6120
5 files changed, 465 insertions, 56 deletions
diff --git a/src/conf_mode/nat_cgnat.py b/src/conf_mode/nat_cgnat.py
new file mode 100755
index 000000000..f41d66c66
--- /dev/null
+++ b/src/conf_mode/nat_cgnat.py
@@ -0,0 +1,288 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import ipaddress
+import jmespath
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.utils.process import cmd
+from vyos.utils.process import run
+from vyos import ConfigError
+from vyos import airbag
+
+airbag.enable()
+
+
+nftables_cgnat_config = '/run/nftables-cgnat.nft'
+
+
+class IPOperations:
+ def __init__(self, ip_prefix: str):
+ self.ip_prefix = ip_prefix
+ self.ip_network = ipaddress.ip_network(ip_prefix) if '/' in ip_prefix else None
+
+ def get_ips_count(self) -> int:
+ """Returns the number of IPs in a prefix or range.
+
+ Example:
+ % ip = IPOperations('192.0.2.0/30')
+ % ip.get_ips_count()
+ 4
+ % ip = IPOperations('192.0.2.0-192.0.2.2')
+ % ip.get_ips_count()
+ 3
+ """
+ if '-' in self.ip_prefix:
+ start_ip, end_ip = self.ip_prefix.split('-')
+ start_ip = ipaddress.ip_address(start_ip)
+ end_ip = ipaddress.ip_address(end_ip)
+ return int(end_ip) - int(start_ip) + 1
+ elif '/31' in self.ip_prefix:
+ return 2
+ elif '/32' in self.ip_prefix:
+ return 1
+ else:
+ return sum(
+ 1
+ for _ in [self.ip_network.network_address]
+ + list(self.ip_network.hosts())
+ + [self.ip_network.broadcast_address]
+ )
+
+ def convert_prefix_to_list_ips(self) -> list:
+ """Converts a prefix or IP range to a list of IPs including the network and broadcast addresses.
+
+ Example:
+ % ip = IPOperations('192.0.2.0/30')
+ % ip.convert_prefix_to_list_ips()
+ ['192.0.2.0', '192.0.2.1', '192.0.2.2', '192.0.2.3']
+ %
+ % ip = IPOperations('192.0.0.1-192.0.2.5')
+ % ip.convert_prefix_to_list_ips()
+ ['192.0.2.1', '192.0.2.2', '192.0.2.3', '192.0.2.4', '192.0.2.5']
+ """
+ if '-' in self.ip_prefix:
+ start_ip, end_ip = self.ip_prefix.split('-')
+ start_ip = ipaddress.ip_address(start_ip)
+ end_ip = ipaddress.ip_address(end_ip)
+ return [
+ str(ipaddress.ip_address(ip))
+ for ip in range(int(start_ip), int(end_ip) + 1)
+ ]
+ elif '/31' in self.ip_prefix:
+ return [
+ str(ip)
+ for ip in [
+ self.ip_network.network_address,
+ self.ip_network.broadcast_address,
+ ]
+ ]
+ elif '/32' in self.ip_prefix:
+ return [str(self.ip_network.network_address)]
+ else:
+ return [
+ str(ip)
+ for ip in [self.ip_network.network_address]
+ + list(self.ip_network.hosts())
+ + [self.ip_network.broadcast_address]
+ ]
+
+
+def generate_port_rules(
+ external_hosts: list,
+ internal_hosts: list,
+ port_count: int,
+ global_port_range: str = '1024-65535',
+) -> list:
+ """Generates list of nftables rules for the batch file."""
+ rules = []
+ proto_map_elements = []
+ other_map_elements = []
+ start_port, end_port = map(int, global_port_range.split('-'))
+ total_possible_ports = (end_port - start_port) + 1
+
+ # Calculate the required number of ports per host
+ required_ports_per_host = port_count
+
+ # Check if there are enough external addresses for all internal hosts
+ if required_ports_per_host * len(internal_hosts) > total_possible_ports * len(
+ external_hosts
+ ):
+ raise ConfigError("Not enough ports available for the specified parameters!")
+
+ current_port = start_port
+ current_external_index = 0
+
+ for internal_host in internal_hosts:
+ external_host = external_hosts[current_external_index]
+ next_end_port = current_port + required_ports_per_host - 1
+
+ # If the port range exceeds the end_port, move to the next external host
+ while next_end_port > end_port:
+ current_external_index = (current_external_index + 1) % len(external_hosts)
+ external_host = external_hosts[current_external_index]
+ current_port = start_port
+ next_end_port = current_port + required_ports_per_host - 1
+
+ # Ensure the same port is not assigned to the same external host
+ if any(
+ rule.endswith(f'{external_host}:{current_port}-{next_end_port}')
+ for rule in rules
+ ):
+ raise ConfigError("Not enough ports available for the specified parameters")
+
+ proto_map_elements.append(
+ f'{internal_host} : {external_host} . {current_port}-{next_end_port}'
+ )
+ other_map_elements.append(f'{internal_host} : {external_host}')
+
+ current_port = next_end_port + 1
+ if current_port > end_port:
+ current_port = start_port
+ current_external_index += 1 # Move to the next external host
+
+ return [proto_map_elements, other_map_elements]
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['nat', 'cgnat']
+ config = conf.get_config_dict(
+ base,
+ get_first_key=True,
+ key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ )
+
+ return config
+
+
+def verify(config):
+ # bail out early - looks like removal from running config
+ if not config:
+ return None
+
+ if 'pool' not in config:
+ raise ConfigError(f'Pool must be defined!')
+ if 'rule' not in config:
+ raise ConfigError(f'Rule must be defined!')
+
+ # As PoC allow only one rule for CGNAT translations
+ # one internal pool and one external pool
+ if len(config['rule']) > 1:
+ raise ConfigError(f'Only one rule is allowed for translations!')
+
+ for pool in ('external', 'internal'):
+ if pool not in config['pool']:
+ raise ConfigError(f'{pool} pool must be defined!')
+ for pool_name, pool_config in config['pool'][pool].items():
+ if 'range' not in pool_config:
+ raise ConfigError(
+ f'Range for "{pool} pool {pool_name}" must be defined!'
+ )
+
+ for rule, rule_config in config['rule'].items():
+ if 'source' not in rule_config:
+ raise ConfigError(f'Rule "{rule}" source pool must be defined!')
+ if 'pool' not in rule_config['source']:
+ raise ConfigError(f'Rule "{rule}" source pool must be defined!')
+
+ if 'translation' not in rule_config:
+ raise ConfigError(f'Rule "{rule}" translation pool must be defined!')
+
+
+def generate(config):
+ if not config:
+ return None
+ # first external pool as we allow only one as PoC
+ ext_pool_name = jmespath.search("rule.*.translation | [0]", config).get('pool')
+ int_pool_name = jmespath.search("rule.*.source | [0]", config).get('pool')
+ ext_query = f"pool.external.{ext_pool_name}.range | keys(@)"
+ int_query = f"pool.internal.{int_pool_name}.range"
+ external_ranges = jmespath.search(ext_query, config)
+ internal_ranges = [jmespath.search(int_query, config)]
+
+ external_list_count = []
+ external_list_hosts = []
+ internal_list_count = []
+ internal_list_hosts = []
+ for ext_range in external_ranges:
+ # External hosts count
+ e_count = IPOperations(ext_range).get_ips_count()
+ external_list_count.append(e_count)
+ # External hosts list
+ e_hosts = IPOperations(ext_range).convert_prefix_to_list_ips()
+ external_list_hosts.extend(e_hosts)
+ for int_range in internal_ranges:
+ # Internal hosts count
+ i_count = IPOperations(int_range).get_ips_count()
+ internal_list_count.append(i_count)
+ # Internal hosts list
+ i_hosts = IPOperations(int_range).convert_prefix_to_list_ips()
+ internal_list_hosts.extend(i_hosts)
+
+ external_host_count = sum(external_list_count)
+ internal_host_count = sum(internal_list_count)
+ ports_per_user = int(
+ jmespath.search(f'pool.external.{ext_pool_name}.per_user_limit.port', config)
+ )
+ external_port_range: str = jmespath.search(
+ f'pool.external.{ext_pool_name}.external_port_range', config
+ )
+
+ proto_maps, other_maps = generate_port_rules(
+ external_list_hosts, internal_list_hosts, ports_per_user, external_port_range
+ )
+
+ config['proto_map_elements'] = ', '.join(proto_maps)
+ config['other_map_elements'] = ', '.join(other_maps)
+
+ render(nftables_cgnat_config, 'firewall/nftables-cgnat.j2', config)
+
+ # dry-run newly generated configuration
+ tmp = run(f'nft --check --file {nftables_cgnat_config}')
+ if tmp > 0:
+ raise ConfigError('Configuration file errors encountered!')
+
+
+def apply(config):
+ if not config:
+ # Cleanup cgnat
+ cmd('nft delete table ip cgnat')
+ if os.path.isfile(nftables_cgnat_config):
+ os.unlink(nftables_cgnat_config)
+ return None
+ cmd(f'nft --file {nftables_cgnat_config}')
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index c9d1e805f..b9d174933 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -38,6 +38,16 @@ airbag.enable()
pppoe_conf = r'/run/accel-pppd/pppoe.conf'
pppoe_chap_secrets = r'/run/accel-pppd/pppoe.chap-secrets'
+def convert_pado_delay(pado_delay):
+ new_pado_delay = {'delays_without_sessions': [],
+ 'delays_with_sessions': []}
+ for delay, sessions in pado_delay.items():
+ if not sessions:
+ new_pado_delay['delays_without_sessions'].append(delay)
+ else:
+ new_pado_delay['delays_with_sessions'].append((delay, int(sessions['sessions'])))
+ return new_pado_delay
+
def get_config(config=None):
if config:
conf = config
@@ -54,6 +64,10 @@ def get_config(config=None):
# Multiple named pools require ordered values T5099
pppoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', pppoe))
+ if dict_search('pado_delay', pppoe):
+ pado_delay = dict_search('pado_delay', pppoe)
+ pppoe['pado_delay'] = convert_pado_delay(pado_delay)
+
# reload-or-restart does not implemented in accel-ppp
# use this workaround until it will be implemented
# https://phabricator.accel-ppp.org/T3
@@ -65,6 +79,17 @@ def get_config(config=None):
pppoe['server_type'] = 'pppoe'
return pppoe
+def verify_pado_delay(pppoe):
+ if 'pado_delay' in pppoe:
+ pado_delay = pppoe['pado_delay']
+
+ delays_without_sessions = pado_delay['delays_without_sessions']
+ if len(delays_without_sessions) > 1:
+ raise ConfigError(
+ f'Cannot add more then ONE pado-delay without sessions, '
+ f'but {len(delays_without_sessions)} were set'
+ )
+
def verify(pppoe):
if not pppoe:
return None
@@ -73,7 +98,7 @@ def verify(pppoe):
verify_accel_ppp_ip_pool(pppoe)
verify_accel_ppp_name_servers(pppoe)
verify_accel_ppp_wins_servers(pppoe)
-
+ verify_pado_delay(pppoe)
if 'interface' not in pppoe:
raise ConfigError('At least one listen interface must be defined!')
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook
index ebb100e8b..57f803055 100755
--- a/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook
@@ -17,7 +17,7 @@
DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_interfaces"
if ! { [ -f $DHCP_HOOK_IFLIST ] && grep -qw $interface $DHCP_HOOK_IFLIST; }; then
- exit 0
+ return 0
fi
# Re-generate the config on the following events:
@@ -26,10 +26,10 @@ fi
# - REBIND: re-generate if the IP address changed
if [ "$reason" == "RENEW" ] || [ "$reason" == "REBIND" ]; then
if [ "$old_ip_address" == "$new_ip_address" ]; then
- exit 0
+ return 0
fi
elif [ "$reason" != "BOUND" ]; then
- exit 0
+ return 0
fi
# Best effort wait for any active commit to finish
diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7
index 72f07880b..938044c6d 100755
--- a/src/migration-scripts/firewall/6-to-7
+++ b/src/migration-scripts/firewall/6-to-7
@@ -107,6 +107,12 @@ icmpv6_translations = {
'unknown-option': [4, 2]
}
+v4_found = False
+v6_found = False
+v4_groups = ["address-group", "network-group", "port-group"]
+v6_groups = ["ipv6-address-group", "ipv6-network-group", "port-group"]
+translated_dict = {}
+
if config.exists(base + ['group']):
for group_type in config.list_nodes(base + ['group']):
for group_name in config.list_nodes(base + ['group', group_type]):
@@ -114,6 +120,19 @@ if config.exists(base + ['group']):
if config.exists(name_description):
tmp = config.return_value(name_description)
config.set(name_description, value=tmp[:max_len_description])
+ if '+' in group_name:
+ replacement_string = "_"
+ if group_type in v4_groups and not v4_found:
+ v4_found = True
+ if group_type in v6_groups and not v6_found:
+ v6_found = True
+ new_group_name = group_name.replace('+', replacement_string)
+ while config.exists(base + ['group', group_type, new_group_name]):
+ replacement_string = replacement_string + "_"
+ new_group_name = group_name.replace('+', replacement_string)
+ translated_dict[group_name] = new_group_name
+ config.copy(base + ['group', group_type, group_name], base + ['group', group_type, new_group_name])
+ config.delete(base + ['group', group_type, group_name])
if config.exists(base + ['name']):
for name in config.list_nodes(base + ['name']):
@@ -173,11 +192,31 @@ if config.exists(base + ['name']):
config.set(rule_icmp + ['type'], value=translate[0])
config.set(rule_icmp + ['code'], value=translate[1])
- for src_dst in ['destination', 'source']:
- pg_base = base + ['name', name, 'rule', rule, src_dst, 'group', 'port-group']
- proto_base = base + ['name', name, 'rule', rule, 'protocol']
- if config.exists(pg_base) and not config.exists(proto_base):
- config.set(proto_base, value='tcp_udp')
+ for direction in ['destination', 'source']:
+ if config.exists(base + ['name', name, 'rule', rule, direction]):
+ if config.exists(base + ['name', name, 'rule', rule, direction, 'group']) and v4_found:
+ for group_type in config.list_nodes(base + ['name', name, 'rule', rule, direction, 'group']):
+ group_name = config.return_value(base + ['name', name, 'rule', rule, direction, 'group', group_type])
+ if '+' in group_name:
+ if group_name[0] == "!":
+ new_group_name = "!" + translated_dict[group_name[1:]]
+ else:
+ new_group_name = translated_dict[group_name]
+ config.set(base + ['name', name, 'rule', rule, direction, 'group', group_type], value=new_group_name)
+
+ pg_base = base + ['name', name, 'rule', rule, direction, 'group', 'port-group']
+ proto_base = base + ['name', name, 'rule', rule, 'protocol']
+ if config.exists(pg_base) and not config.exists(proto_base):
+ config.set(proto_base, value='tcp_udp')
+
+ if '+' in name:
+ replacement_string = "_"
+ new_name = name.replace('+', replacement_string)
+ while config.exists(base + ['name', new_name]):
+ replacement_string = replacement_string + "_"
+ new_name = name.replace('+', replacement_string)
+ config.copy(base + ['name', name], base + ['name', new_name])
+ config.delete(base + ['name', name])
if config.exists(base + ['ipv6-name']):
for name in config.list_nodes(base + ['ipv6-name']):
@@ -250,12 +289,31 @@ if config.exists(base + ['ipv6-name']):
else:
config.rename(rule_icmp + ['type'], 'type-name')
- for src_dst in ['destination', 'source']:
- pg_base = base + ['ipv6-name', name, 'rule', rule, src_dst, 'group', 'port-group']
- proto_base = base + ['ipv6-name', name, 'rule', rule, 'protocol']
- if config.exists(pg_base) and not config.exists(proto_base):
- config.set(proto_base, value='tcp_udp')
-
+ for direction in ['destination', 'source']:
+ if config.exists(base + ['ipv6-name', name, 'rule', rule, direction]):
+ if config.exists(base + ['ipv6-name', name, 'rule', rule, direction, 'group']) and v6_found:
+ for group_type in config.list_nodes(base + ['ipv6-name', name, 'rule', rule, direction, 'group']):
+ group_name = config.return_value(base + ['ipv6-name', name, 'rule', rule, direction, 'group', group_type])
+ if '+' in group_name:
+ if group_name[0] == "!":
+ new_group_name = "!" + translated_dict[group_name[1:]]
+ else:
+ new_group_name = translated_dict[group_name]
+ config.set(base + ['ipv6-name', name, 'rule', rule, direction, 'group', group_type], value=new_group_name)
+
+ pg_base = base + ['ipv6-name', name, 'rule', rule, direction, 'group', 'port-group']
+ proto_base = base + ['ipv6-name', name, 'rule', rule, 'protocol']
+ if config.exists(pg_base) and not config.exists(proto_base):
+ config.set(proto_base, value='tcp_udp')
+
+ if '+' in name:
+ replacement_string = "_"
+ new_name = name.replace('+', replacement_string)
+ while config.exists(base + ['ipv6-name', new_name]):
+ replacement_string = replacement_string + "_"
+ new_name = name.replace('+', replacement_string)
+ config.copy(base + ['ipv6-name', name], base + ['ipv6-name', new_name])
+ config.delete(base + ['ipv6-name', name])
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
diff --git a/src/migration-scripts/nat/5-to-6 b/src/migration-scripts/nat/5-to-6
index c83b93d84..cfe98ddcf 100755
--- a/src/migration-scripts/nat/5-to-6
+++ b/src/migration-scripts/nat/5-to-6
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright (C) 2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -18,46 +18,84 @@
# to
# 'set nat [source|destination] rule X [inbound-interface|outbound interface] interface-name <iface>'
+# T6100: Migration from 1.3.X to 1.4
+# Change IP/netmask to Network/netmask in
+# 'set nat [source|destination] rule X [source| destination| translation] address <IP/Netmask| !IP/Netmask>'
+
+import ipaddress
from sys import argv,exit
from vyos.configtree import ConfigTree
-if len(argv) < 2:
- print("Must specify file name!")
- exit(1)
-
-file_name = argv[1]
-
-with open(file_name, 'r') as f:
- config_file = f.read()
-
-config = ConfigTree(config_file)
-
-if not config.exists(['nat']):
- # Nothing to do
- exit(0)
-
-for direction in ['source', 'destination']:
- # If a node doesn't exist, we obviously have nothing to do.
- if not config.exists(['nat', direction]):
- continue
-
- # However, we also need to handle the case when a 'source' or 'destination' sub-node does exist,
- # but there are no rules under it.
- if not config.list_nodes(['nat', direction]):
- continue
-
- for rule in config.list_nodes(['nat', direction, 'rule']):
- base = ['nat', direction, 'rule', rule]
- for iface in ['inbound-interface','outbound-interface']:
- if config.exists(base + [iface]):
- tmp = config.return_value(base + [iface])
- if tmp:
- config.delete(base + [iface])
- config.set(base + [iface, 'interface-name'], value=tmp)
-
-try:
- with open(file_name, 'w') as f:
- f.write(config.to_string())
-except OSError as e:
- print("Failed to save the modified config: {}".format(e))
- exit(1)
+
+def _func_T5643(conf, base_path):
+ for iface in ['inbound-interface', 'outbound-interface']:
+ if conf.exists(base_path + [iface]):
+ tmp = conf.return_value(base_path + [iface])
+ if tmp:
+ conf.delete(base_path + [iface])
+ conf.set(base_path + [iface, 'interface-name'], value=tmp)
+ return
+
+
+def _func_T6100(conf, base_path):
+ for addr_type in ['source', 'destination', 'translation']:
+ base_addr_type = base_path + [addr_type]
+ if not conf.exists(base_addr_type) or not conf.exists(
+ base_addr_type + ['address']):
+ continue
+
+ address = conf.return_value(base_addr_type + ['address'])
+
+ if not address or '/' not in address:
+ continue
+
+ negative = ''
+ network = address
+ if '!' in address:
+ negative = '!'
+ network = str(address.split(negative)[1])
+
+ network_ip = ipaddress.ip_network(network, strict=False)
+ if str(network_ip) != network:
+ network = f'{negative}{str(network_ip)}'
+ conf.set(base_addr_type + ['address'], value=network)
+ return
+
+
+if __name__ == '__main__':
+ if len(argv) < 2:
+ print("Must specify file name!")
+ exit(1)
+
+ file_name = argv[1]
+
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+
+ config = ConfigTree(config_file)
+
+ if not config.exists(['nat']):
+ # Nothing to do
+ exit(0)
+
+ for direction in ['source', 'destination']:
+ # If a node doesn't exist, we obviously have nothing to do.
+ if not config.exists(['nat', direction]):
+ continue
+
+ # However, we also need to handle the case when a 'source' or 'destination' sub-node does exist,
+ # but there are no rules under it.
+ if not config.list_nodes(['nat', direction]):
+ continue
+
+ for rule in config.list_nodes(['nat', direction, 'rule']):
+ base = ['nat', direction, 'rule', rule]
+ _func_T5643(config,base)
+ _func_T6100(config,base)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)