summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/service_dhcp-server.py24
-rwxr-xr-xsrc/conf_mode/service_dhcpv6-server.py9
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py2
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py2
-rwxr-xr-xsrc/migration-scripts/dhcp-server/8-to-975
-rwxr-xr-xsrc/migration-scripts/dhcpv6-server/3-to-455
-rwxr-xr-xsrc/migration-scripts/ipoe-server/1-to-22
-rwxr-xr-xsrc/migration-scripts/l2tp/4-to-544
-rwxr-xr-xsrc/migration-scripts/pppoe-server/6-to-745
-rwxr-xr-xsrc/migration-scripts/pptp/2-to-319
-rwxr-xr-xsrc/migration-scripts/sstp/4-to-517
-rwxr-xr-xsrc/op_mode/image_manager.py25
-rw-r--r--src/op_mode/zone.py215
-rwxr-xr-xsrc/system/on-dhcp-event.sh65
-rwxr-xr-xsrc/validators/ipv4-range-mask36
15 files changed, 521 insertions, 114 deletions
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
index 7ebc560ba..2418c8faa 100755
--- a/src/conf_mode/service_dhcp-server.py
+++ b/src/conf_mode/service_dhcp-server.py
@@ -31,6 +31,7 @@ from vyos.utils.file import chmod_775
from vyos.utils.file import makedir
from vyos.utils.file import write_file
from vyos.utils.process import call
+from vyos.utils.network import interface_exists
from vyos.utils.network import is_subnet_connected
from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
@@ -164,6 +165,7 @@ def verify(dhcp):
shared_networks = len(dhcp['shared_network_name'])
disabled_shared_networks = 0
+ subnet_ids = []
# A shared-network requires a subnet definition
for network, network_config in dhcp['shared_network_name'].items():
@@ -175,6 +177,14 @@ def verify(dhcp):
'lease subnet must be configured.')
for subnet, subnet_config in network_config['subnet'].items():
+ if 'subnet_id' not in subnet_config:
+ raise ConfigError(f'Unique subnet ID not specified for subnet "{subnet}"')
+
+ if subnet_config['subnet_id'] in subnet_ids:
+ raise ConfigError(f'Subnet ID for subnet "{subnet}" is not unique')
+
+ subnet_ids.append(subnet_config['subnet_id'])
+
# All delivered static routes require a next-hop to be set
if 'static_route' in subnet_config:
for route, route_option in subnet_config['static_route'].items():
@@ -222,6 +232,7 @@ def verify(dhcp):
if 'static_mapping' in subnet_config:
# Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
+ used_ips = []
for mapping, mapping_config in subnet_config['static_mapping'].items():
if 'ip_address' in mapping_config:
if ip_address(mapping_config['ip_address']) not in ip_network(subnet):
@@ -233,6 +244,11 @@ def verify(dhcp):
raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
+ if mapping_config['ip_address'] in used_ips:
+ raise ConfigError(f'Configured IP address for static mapping "{mapping}" exists on another static mapping')
+
+ used_ips.append(mapping_config['ip_address'])
+
# There must be one subnet connected to a listen interface.
# This only counts if the network itself is not disabled!
if 'disable' not in network_config:
@@ -294,12 +310,18 @@ def verify(dhcp):
else:
raise ConfigError(f'listen-address "{address}" not configured on any interface')
-
if not listen_ok:
raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n'
'broadcast interface configured, nor was there an explicit listen-address\n'
'configured for serving DHCP relay packets!')
+ if 'listen_address' in dhcp and 'listen_interface' in dhcp:
+ raise ConfigError(f'Cannot define listen-address and listen-interface at the same time')
+
+ for interface in (dict_search('listen_interface', dhcp) or []):
+ if not interface_exists(interface):
+ raise ConfigError(f'listen-interface "{interface}" does not exist')
+
return None
def generate(dhcp):
diff --git a/src/conf_mode/service_dhcpv6-server.py b/src/conf_mode/service_dhcpv6-server.py
index 9cc57dbcf..7cd801cdd 100755
--- a/src/conf_mode/service_dhcpv6-server.py
+++ b/src/conf_mode/service_dhcpv6-server.py
@@ -63,6 +63,7 @@ def verify(dhcpv6):
# Inspect shared-network/subnet
subnets = []
+ subnet_ids = []
listen_ok = False
for network, network_config in dhcpv6['shared_network_name'].items():
# A shared-network requires a subnet definition
@@ -72,6 +73,14 @@ def verify(dhcpv6):
'each shared network!')
for subnet, subnet_config in network_config['subnet'].items():
+ if 'subnet_id' not in subnet_config:
+ raise ConfigError(f'Unique subnet ID not specified for subnet "{subnet}"')
+
+ if subnet_config['subnet_id'] in subnet_ids:
+ raise ConfigError(f'Subnet ID for subnet "{subnet}" is not unique')
+
+ subnet_ids.append(subnet_config['subnet_id'])
+
if 'address_range' in subnet_config:
if 'start' in subnet_config['address_range']:
range6_start = []
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 5bdcf2fa1..adbac0405 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -159,7 +159,7 @@ def verify(ipsec):
if 'id' not in psk_config or 'secret' not in psk_config:
raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"')
- if 'interfaces' in ipsec :
+ if 'interface' in ipsec:
for ifname in ipsec['interface']:
verify_interface_exists(ifname)
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index 03a27d3cd..1a91951b4 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -71,7 +71,7 @@ def verify(l2tp):
raise ConfigError('DA/CoE server key required!')
if dict_search('authentication.mode', l2tp) in ['local', 'noauth']:
- if not l2tp['client_ip_pool'] and not l2tp['client_ipv6_pool']:
+ if not dict_search('client_ip_pool', l2tp) and not dict_search('client_ipv6_pool', l2tp):
raise ConfigError(
"L2TP local auth mode requires local client-ip-pool or client-ipv6-pool to be configured!")
if dict_search('client_ip_pool', l2tp) and not dict_search('default_pool', l2tp):
diff --git a/src/migration-scripts/dhcp-server/8-to-9 b/src/migration-scripts/dhcp-server/8-to-9
new file mode 100755
index 000000000..810e403a6
--- /dev/null
+++ b/src/migration-scripts/dhcp-server/8-to-9
@@ -0,0 +1,75 @@
+#!/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/>.
+
+# T3316:
+# - Migrate dhcp options under new option node
+# - Add subnet IDs to existing subnets
+
+import sys
+import re
+from vyos.configtree import ConfigTree
+
+if len(sys.argv) < 2:
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['service', 'dhcp-server', 'shared-network-name']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+option_nodes = ['bootfile-name', 'bootfile-server', 'bootfile-size', 'captive-portal',
+ 'client-prefix-length', 'default-router', 'domain-name', 'domain-search',
+ 'name-server', 'ip-forwarding', 'ipv6-only-preferred', 'ntp-server',
+ 'pop-server', 'server-identifier', 'smtp-server', 'static-route',
+ 'tftp-server-name', 'time-offset', 'time-server', 'time-zone',
+ 'vendor-option', 'wins-server', 'wpad-url']
+
+subnet_id = 1
+
+for network in config.list_nodes(base):
+ for option in option_nodes:
+ if config.exists(base + [network, option]):
+ config.set(base + [network, 'option'])
+ config.copy(base + [network, option], base + [network, 'option', option])
+ config.delete(base + [network, option])
+
+ if config.exists(base + [network, 'subnet']):
+ for subnet in config.list_nodes(base + [network, 'subnet']):
+ base_subnet = base + [network, 'subnet', subnet]
+
+ for option in option_nodes:
+ if config.exists(base_subnet + [option]):
+ config.set(base_subnet + ['option'])
+ config.copy(base_subnet + [option], base_subnet + ['option', option])
+ config.delete(base_subnet + [option])
+
+ config.set(base_subnet + ['subnet-id'], value=subnet_id)
+ subnet_id += 1
+
+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)
diff --git a/src/migration-scripts/dhcpv6-server/3-to-4 b/src/migration-scripts/dhcpv6-server/3-to-4
new file mode 100755
index 000000000..c065e3d43
--- /dev/null
+++ b/src/migration-scripts/dhcpv6-server/3-to-4
@@ -0,0 +1,55 @@
+#!/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/>.
+
+# T3316:
+# - Add subnet IDs to existing subnets
+
+import sys
+import re
+from vyos.configtree import ConfigTree
+
+if len(sys.argv) < 2:
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['service', 'dhcpv6-server', 'shared-network-name']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+subnet_id = 1
+
+for network in config.list_nodes(base):
+ if config.exists(base + [network, 'subnet']):
+ for subnet in config.list_nodes(base + [network, 'subnet']):
+ base_subnet = base + [network, 'subnet', subnet]
+
+ config.set(base_subnet + ['subnet-id'], value=subnet_id)
+ subnet_id += 1
+
+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)
diff --git a/src/migration-scripts/ipoe-server/1-to-2 b/src/migration-scripts/ipoe-server/1-to-2
index c8cec6835..11d7911e9 100755
--- a/src/migration-scripts/ipoe-server/1-to-2
+++ b/src/migration-scripts/ipoe-server/1-to-2
@@ -57,7 +57,7 @@ for pool_name in config.list_nodes(namedpools_base):
pool_path = namedpools_base + [pool_name]
if config.exists(pool_path + ['subnet']):
subnet = config.return_value(pool_path + ['subnet'])
- config.set(pool_base + [pool_name, 'range'], value=subnet)
+ config.set(pool_base + [pool_name, 'range'], value=subnet, replace=False)
# Get netmask from subnet
mask = subnet.split("/")[1]
if config.exists(pool_path + ['next-pool']):
diff --git a/src/migration-scripts/l2tp/4-to-5 b/src/migration-scripts/l2tp/4-to-5
index 496dc83d6..3176f895a 100755
--- a/src/migration-scripts/l2tp/4-to-5
+++ b/src/migration-scripts/l2tp/4-to-5
@@ -24,7 +24,7 @@ import os
from sys import argv
from sys import exit
from vyos.configtree import ConfigTree
-
+from vyos.base import Warning
if len(argv) < 2:
print("Must specify file name!")
@@ -45,33 +45,33 @@ if not config.exists(pool_base):
exit(0)
default_pool = ''
range_pool_name = 'default-range-pool'
-subnet_base_name = 'default-subnet-pool'
-number = 1
-subnet_pool_name = f'{subnet_base_name}-{number}'
-prev_subnet_pool = subnet_pool_name
-if config.exists(pool_base + ['subnet']):
- default_pool = subnet_pool_name
- for subnet in config.return_values(pool_base + ['subnet']):
- config.set(pool_base + [subnet_pool_name, 'range'], value=subnet)
- if prev_subnet_pool != subnet_pool_name:
- config.set(pool_base + [prev_subnet_pool, 'next-pool'],
- value=subnet_pool_name)
- prev_subnet_pool = subnet_pool_name
- number += 1
- subnet_pool_name = f'{subnet_base_name}-{number}'
-
- config.delete(pool_base + ['subnet'])
if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']):
+ def is_legalrange(ip1: str, ip2: str, mask: str):
+ from ipaddress import IPv4Interface
+ interface1 = IPv4Interface(f'{ip1}/{mask}')
+
+ interface2 = IPv4Interface(f'{ip2}/{mask}')
+ return interface1.network.network_address == interface2.network.network_address and interface2.ip > interface1.ip
+
start_ip = config.return_value(pool_base + ['start'])
stop_ip = config.return_value(pool_base + ['stop'])
- ip_range = f'{start_ip}-{stop_ip}'
+ if is_legalrange(start_ip, stop_ip,'24'):
+ ip_range = f'{start_ip}-{stop_ip}'
+ config.set(pool_base + [range_pool_name, 'range'], value=ip_range, replace=False)
+ default_pool = range_pool_name
+ else:
+ Warning(
+ f'L2TP client-ip-pool range start-ip:{start_ip} and stop-ip:{stop_ip} can not be migrated.')
+
config.delete(pool_base + ['start'])
config.delete(pool_base + ['stop'])
- config.set(pool_base + [range_pool_name, 'range'], value=ip_range)
- if default_pool:
- config.set(pool_base + [range_pool_name, 'next-pool'],
- value=default_pool)
+
+if config.exists(pool_base + ['subnet']):
+ for subnet in config.return_values(pool_base + ['subnet']):
+ config.set(pool_base + [range_pool_name, 'range'], value=subnet, replace=False)
+
+ config.delete(pool_base + ['subnet'])
default_pool = range_pool_name
if default_pool:
diff --git a/src/migration-scripts/pppoe-server/6-to-7 b/src/migration-scripts/pppoe-server/6-to-7
index d856c1f34..b94ce57f9 100755
--- a/src/migration-scripts/pppoe-server/6-to-7
+++ b/src/migration-scripts/pppoe-server/6-to-7
@@ -29,7 +29,7 @@ import os
from sys import argv
from sys import exit
from vyos.configtree import ConfigTree
-
+from vyos.base import Warning
if len(argv) < 2:
print("Must specify file name!")
@@ -48,38 +48,35 @@ if not config.exists(base):
if not config.exists(pool_base):
exit(0)
+
default_pool = ''
range_pool_name = 'default-range-pool'
-subnet_base_name = 'default-subnet-pool'
-number = 1
-subnet_pool_name = f'{subnet_base_name}-{number}'
-prev_subnet_pool = subnet_pool_name
#Default nameless pools migrations
-if config.exists(pool_base + ['subnet']):
- default_pool = subnet_pool_name
- for subnet in config.return_values(pool_base + ['subnet']):
- config.set(pool_base + [subnet_pool_name, 'range'], value=subnet)
- if prev_subnet_pool != subnet_pool_name:
- config.set(pool_base + [prev_subnet_pool, 'next-pool'],
- value=subnet_pool_name)
- prev_subnet_pool = subnet_pool_name
- number += 1
- subnet_pool_name = f'{subnet_base_name}-{number}'
-
- config.delete(pool_base + ['subnet'])
-
if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']):
+ def is_legalrange(ip1: str, ip2: str, mask: str):
+ from ipaddress import IPv4Interface
+ interface1 = IPv4Interface(f'{ip1}/{mask}')
+ interface2 = IPv4Interface(f'{ip2}/{mask}')
+ return interface1.network.network_address == interface2.network.network_address and interface2.ip > interface1.ip
+
start_ip = config.return_value(pool_base + ['start'])
stop_ip = config.return_value(pool_base + ['stop'])
- ip_range = f'{start_ip}-{stop_ip}'
+ if is_legalrange(start_ip, stop_ip, '24'):
+ ip_range = f'{start_ip}-{stop_ip}'
+ config.set(pool_base + [range_pool_name, 'range'], value=ip_range, replace=False)
+ default_pool = range_pool_name
+ else:
+ Warning(
+ f'PPPoE client-ip-pool range start-ip:{start_ip} and stop-ip:{stop_ip} can not be migrated.')
config.delete(pool_base + ['start'])
config.delete(pool_base + ['stop'])
- config.set(pool_base + [range_pool_name, 'range'], value=ip_range)
- if default_pool:
- config.set(pool_base + [range_pool_name, 'next-pool'],
- value=default_pool)
+
+if config.exists(pool_base + ['subnet']):
default_pool = range_pool_name
+ for subnet in config.return_values(pool_base + ['subnet']):
+ config.set(pool_base + [range_pool_name, 'range'], value=subnet, replace=False)
+ config.delete(pool_base + ['subnet'])
gateway = ''
if config.exists(base + ['gateway-address']):
@@ -97,7 +94,7 @@ if config.exists(namedpools_base):
pool_path = namedpools_base + [pool_name]
if config.exists(pool_path + ['subnet']):
subnet = config.return_value(pool_path + ['subnet'])
- config.set(pool_base + [pool_name, 'range'], value=subnet)
+ config.set(pool_base + [pool_name, 'range'], value=subnet, replace=False)
if config.exists(pool_path + ['next-pool']):
next_pool = config.return_value(pool_path + ['next-pool'])
config.set(pool_base + [pool_name, 'next-pool'], value=next_pool)
diff --git a/src/migration-scripts/pptp/2-to-3 b/src/migration-scripts/pptp/2-to-3
index 98dc5c2a6..091cb68ec 100755
--- a/src/migration-scripts/pptp/2-to-3
+++ b/src/migration-scripts/pptp/2-to-3
@@ -23,7 +23,7 @@ import os
from sys import argv
from sys import exit
from vyos.configtree import ConfigTree
-
+from vyos.base import Warning
if len(argv) < 2:
print("Must specify file name!")
@@ -46,13 +46,24 @@ if not config.exists(pool_base):
range_pool_name = 'default-range-pool'
if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']):
+ def is_legalrange(ip1: str, ip2: str, mask: str):
+ from ipaddress import IPv4Interface
+ interface1 = IPv4Interface(f'{ip1}/{mask}')
+ interface2 = IPv4Interface(f'{ip2}/{mask}')
+ return interface1.network.network_address == interface2.network.network_address and interface2.ip > interface1.ip
+
start_ip = config.return_value(pool_base + ['start'])
stop_ip = config.return_value(pool_base + ['stop'])
- ip_range = f'{start_ip}-{stop_ip}'
+ if is_legalrange(start_ip, stop_ip, '24'):
+ ip_range = f'{start_ip}-{stop_ip}'
+ config.set(pool_base + [range_pool_name, 'range'], value=ip_range, replace=False)
+ config.set(base + ['default-pool'], value=range_pool_name)
+ else:
+ Warning(
+ f'PPTP client-ip-pool range start-ip:{start_ip} and stop-ip:{stop_ip} can not be migrated.')
+
config.delete(pool_base + ['start'])
config.delete(pool_base + ['stop'])
- config.set(pool_base + [range_pool_name, 'range'], value=ip_range)
- config.set(base + ['default-pool'], value=range_pool_name)
# format as tag node
config.set_tag(pool_base)
diff --git a/src/migration-scripts/sstp/4-to-5 b/src/migration-scripts/sstp/4-to-5
index 3a86c79ec..95e482713 100755
--- a/src/migration-scripts/sstp/4-to-5
+++ b/src/migration-scripts/sstp/4-to-5
@@ -43,21 +43,12 @@ if not config.exists(base):
if not config.exists(pool_base):
exit(0)
-subnet_base_name = 'default-subnet-pool'
-number = 1
-subnet_pool_name = f'{subnet_base_name}-{number}'
-prev_subnet_pool = subnet_pool_name
+range_pool_name = 'default-range-pool'
+
if config.exists(pool_base + ['subnet']):
- default_pool = subnet_pool_name
+ default_pool = range_pool_name
for subnet in config.return_values(pool_base + ['subnet']):
- config.set(pool_base + [subnet_pool_name, 'range'], value=subnet)
- if prev_subnet_pool != subnet_pool_name:
- config.set(pool_base + [prev_subnet_pool, 'next-pool'],
- value=subnet_pool_name)
- prev_subnet_pool = subnet_pool_name
- number += 1
- subnet_pool_name = f'{subnet_base_name}-{number}'
-
+ config.set(pool_base + [range_pool_name, 'range'], value=subnet, replace=False)
config.delete(pool_base + ['subnet'])
config.set(base + ['default-pool'], value=default_pool)
# format as tag node
diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py
index e75485f9f..e64a85b95 100755
--- a/src/op_mode/image_manager.py
+++ b/src/op_mode/image_manager.py
@@ -33,6 +33,27 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:'
MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first'
MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first'
+def annotated_list(images_list: list[str]) -> list[str]:
+ """Annotate list of images with additional info
+
+ Args:
+ images_list (list[str]): a list of image names
+
+ Returns:
+ list[str]: a list of image names with additional info
+ """
+ index_running: int = None
+ index_default: int = None
+ try:
+ index_running = images_list.index(image.get_running_image())
+ index_default = images_list.index(image.get_default_image())
+ except ValueError:
+ pass
+ if index_running is not None:
+ images_list[index_running] += ' (running)'
+ if index_default is not None:
+ images_list[index_default] += ' (default boot)'
+ return images_list
@compat.grub_cfg_update
def delete_image(image_name: Optional[str] = None,
@@ -42,7 +63,7 @@ def delete_image(image_name: Optional[str] = None,
Args:
image_name (str): a name of image to delete
"""
- available_images: list[str] = grub.version_list()
+ available_images: list[str] = annotated_list(grub.version_list())
if image_name is None:
if no_prompt:
exit('An image name is required for delete action')
@@ -83,7 +104,7 @@ def set_image(image_name: Optional[str] = None,
Args:
image_name (str): an image name
"""
- available_images: list[str] = grub.version_list()
+ available_images: list[str] = annotated_list(grub.version_list())
if image_name is None:
if not prompt:
exit('An image name is required for set action')
diff --git a/src/op_mode/zone.py b/src/op_mode/zone.py
new file mode 100644
index 000000000..d24b1065b
--- /dev/null
+++ b/src/op_mode/zone.py
@@ -0,0 +1,215 @@
+#!/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 typing
+import sys
+import vyos.opmode
+
+import tabulate
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.dict import dict_search_args
+from vyos.utils.dict import dict_search
+
+
+def get_config_zone(conf, name=None):
+ config_path = ['firewall', 'zone']
+ if name:
+ config_path += [name]
+
+ zone_policy = conf.get_config_dict(config_path, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+ return zone_policy
+
+
+def _convert_one_zone_data(zone: str, zone_config: dict) -> dict:
+ """
+ Convert config dictionary of one zone to API dictionary
+ :param zone: Zone name
+ :type zone: str
+ :param zone_config: config dictionary
+ :type zone_config: dict
+ :return: AP dictionary
+ :rtype: dict
+ """
+ list_of_rules = []
+ intrazone_dict = {}
+ if dict_search('from', zone_config):
+ for from_zone, from_zone_config in zone_config['from'].items():
+ from_zone_dict = {'name': from_zone}
+ if dict_search('firewall.name', from_zone_config):
+ from_zone_dict['firewall'] = dict_search('firewall.name',
+ from_zone_config)
+ if dict_search('firewall.ipv6_name', from_zone_config):
+ from_zone_dict['firewall_v6'] = dict_search(
+ 'firewall.ipv6_name', from_zone_config)
+ list_of_rules.append(from_zone_dict)
+
+ zone_dict = {
+ 'name': zone,
+ 'interface': dict_search('interface', zone_config),
+ 'type': 'LOCAL' if dict_search('local_zone',
+ zone_config) is not None else None,
+ }
+ if list_of_rules:
+ zone_dict['from'] = list_of_rules
+ if dict_search('intra_zone_filtering.firewall.name', zone_config):
+ intrazone_dict['firewall'] = dict_search(
+ 'intra_zone_filtering.firewall.name', zone_config)
+ if dict_search('intra_zone_filtering.firewall.ipv6_name', zone_config):
+ intrazone_dict['firewall_v6'] = dict_search(
+ 'intra_zone_filtering.firewall.ipv6_name', zone_config)
+ if intrazone_dict:
+ zone_dict['intrazone'] = intrazone_dict
+ return zone_dict
+
+
+def _convert_zones_data(zone_policies: dict) -> list:
+ """
+ Convert all config dictionary to API list of zone dictionaries
+ :param zone_policies: config dictionary
+ :type zone_policies: dict
+ :return: API list
+ :rtype: list
+ """
+ zone_list = []
+ for zone, zone_config in zone_policies.items():
+ zone_list.append(_convert_one_zone_data(zone, zone_config))
+ return zone_list
+
+
+def _convert_config(zones_config: dict, zone: str = None) -> list:
+ """
+ convert config to API list
+ :param zones_config: zones config
+ :type zones_config:
+ :param zone: zone name
+ :type zone: str
+ :return: API list
+ :rtype: list
+ """
+ if zone:
+ if zones_config:
+ output = [_convert_one_zone_data(zone, zones_config)]
+ else:
+ raise vyos.opmode.DataUnavailable(f'Zone {zone} not found')
+ else:
+ if zones_config:
+ output = _convert_zones_data(zones_config)
+ else:
+ raise vyos.opmode.UnconfiguredSubsystem(
+ 'Zone entries are not configured')
+ return output
+
+
+def output_zone_list(zone_conf: dict) -> list:
+ """
+ Format one zone row
+ :param zone_conf: zone config
+ :type zone_conf: dict
+ :return: formatted list of zones
+ :rtype: list
+ """
+ zone_info = [zone_conf['name']]
+ if zone_conf['type'] == 'LOCAL':
+ zone_info.append('LOCAL')
+ else:
+ zone_info.append("\n".join(zone_conf['interface']))
+
+ from_zone = []
+ firewall = []
+ firewall_v6 = []
+ if 'intrazone' in zone_conf:
+ from_zone.append(zone_conf['name'])
+
+ v4_name = dict_search_args(zone_conf['intrazone'], 'firewall')
+ v6_name = dict_search_args(zone_conf['intrazone'], 'firewall_v6')
+ if v4_name:
+ firewall.append(v4_name)
+ else:
+ firewall.append('')
+ if v6_name:
+ firewall_v6.append(v6_name)
+ else:
+ firewall_v6.append('')
+
+ if 'from' in zone_conf:
+ for from_conf in zone_conf['from']:
+ from_zone.append(from_conf['name'])
+
+ v4_name = dict_search_args(from_conf, 'firewall')
+ v6_name = dict_search_args(from_conf, 'firewall_v6')
+ if v4_name:
+ firewall.append(v4_name)
+ else:
+ firewall.append('')
+ if v6_name:
+ firewall_v6.append(v6_name)
+ else:
+ firewall_v6.append('')
+
+ zone_info.append("\n".join(from_zone))
+ zone_info.append("\n".join(firewall))
+ zone_info.append("\n".join(firewall_v6))
+ return zone_info
+
+
+def get_formatted_output(zone_policy: list) -> str:
+ """
+ Formatted output of all zones
+ :param zone_policy: list of zones
+ :type zone_policy: list
+ :return: formatted table with zones
+ :rtype: str
+ """
+ headers = ["Zone",
+ "Interfaces",
+ "From Zone",
+ "Firewall IPv4",
+ "Firewall IPv6"
+ ]
+ formatted_list = []
+ for zone_conf in zone_policy:
+ formatted_list.append(output_zone_list(zone_conf))
+ tabulate.PRESERVE_WHITESPACE = True
+ output = tabulate.tabulate(formatted_list, headers, numalign="left")
+ return output
+
+
+def show(raw: bool, zone: typing.Optional[str]):
+ """
+ Show zone-policy command
+ :param raw: if API
+ :type raw: bool
+ :param zone: zone name
+ :type zone: str
+ """
+ conf: ConfigTreeQuery = ConfigTreeQuery()
+ zones_config: dict = get_config_zone(conf, zone)
+ zone_policy_api: list = _convert_config(zones_config, zone)
+ if raw:
+ return zone_policy_api
+ else:
+ return get_formatted_output(zone_policy_api)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1) \ No newline at end of file
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
index 03574bdc3..e1a9f1884 100755
--- a/src/system/on-dhcp-event.sh
+++ b/src/system/on-dhcp-event.sh
@@ -15,28 +15,71 @@ if [ $# -lt 1 ]; then
fi
action=$1
-client_name=$LEASE4_HOSTNAME
-client_ip=$LEASE4_ADDRESS
-client_mac=$LEASE4_HWADDR
hostsd_client="/usr/bin/vyos-hostsd-client"
-case "$action" in
- lease4_renew|lease4_recover) # add mapping for new/recovered lease address
- if [ -z "$client_name" ]; then
- logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
- client_name=$(echo "host-$client_mac" | tr : -)
- fi
+get_subnet_domain_name () {
+ python3 <<EOF
+from vyos.kea import kea_get_active_config
+from vyos.utils.dict import dict_search_args
+
+config = kea_get_active_config('4')
+shared_networks = dict_search_args(config, 'arguments', f'Dhcp4', 'shared-networks')
+
+found = False
- $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply
+if shared_networks:
+ for network in shared_networks:
+ for subnet in network[f'subnet4']:
+ if subnet['id'] == $1:
+ for option in subnet['option-data']:
+ if option['name'] == 'domain-name':
+ print(option['data'])
+ found = True
+
+ if not found:
+ for option in network['option-data']:
+ if option['name'] == 'domain-name':
+ print(option['data'])
+EOF
+}
+
+case "$action" in
+ lease4_renew|lease4_recover)
exit 0
;;
lease4_release|lease4_expire|lease4_decline) # delete mapping for released/declined address
+ client_ip=$LEASE4_ADDRESS
$hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply
exit 0
;;
- leases4_committed) # nothing to do
+ leases4_committed) # process committed leases (added/renewed/recovered)
+ for ((i = 0; i < $LEASES4_SIZE; i++)); do
+ client_ip_var="LEASES4_AT${i}_ADDRESS"
+ client_mac_var="LEASES4_AT${i}_HWADDR"
+ client_name_var="LEASES4_AT${i}_HOSTNAME"
+ client_subnet_id_var="LEASES4_AT${i}_SUBNET_ID"
+
+ client_ip=${!client_ip_var}
+ client_mac=${!client_mac_var}
+ client_name=${!client_name_var}
+ client_subnet_id=${!client_subnet_id_var}
+
+ if [ -z "$client_name" ]; then
+ logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
+ client_name=$(echo "host-$client_mac" | tr : -)
+ fi
+
+ client_domain=$(get_subnet_domain_name $client_subnet_id)
+
+ if [ -n "$client_domain" ]; then
+ client_name="$client_name.$client_domain"
+ fi
+
+ $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply
+ done
+
exit 0
;;
diff --git a/src/validators/ipv4-range-mask b/src/validators/ipv4-range-mask
index 7bb4539af..9373328ff 100755
--- a/src/validators/ipv4-range-mask
+++ b/src/validators/ipv4-range-mask
@@ -1,12 +1,5 @@
#!/bin/bash
-# snippet from https://stackoverflow.com/questions/10768160/ip-address-converter
-ip2dec () {
- local a b c d ip=$@
- IFS=. read -r a b c d <<< "$ip"
- printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))"
-}
-
error_exit() {
echo "Error: $1 is not a valid IPv4 address range or these IPs are not under /$2"
exit 1
@@ -22,37 +15,12 @@ do
r) range=${OPTARG}
esac
done
-if [[ "${range}" =~ "-" ]]&&[[ ! -z ${mask} ]]; then
- # This only works with real bash (<<<) - split IP addresses into array with
- # hyphen as delimiter
- readarray -d - -t strarr <<< ${range}
-
- ipaddrcheck --is-ipv4-single ${strarr[0]}
- if [ $? -gt 0 ]; then
- error_exit ${range} ${mask}
- fi
- ipaddrcheck --is-ipv4-single ${strarr[1]}
+if [[ "${range}" =~ "-" ]]&&[[ ! -z ${mask} ]]; then
+ ipaddrcheck --range-prefix-length ${mask} --is-ipv4-range ${range}
if [ $? -gt 0 ]; then
error_exit ${range} ${mask}
fi
-
- ${vyos_validators_dir}/numeric --range 0-32 ${mask} > /dev/null
- if [ $? -ne 0 ]; then
- error_exit ${range} ${mask}
- fi
-
- is_in_24=$( grepcidr ${strarr[0]}"/"${mask} <(echo ${strarr[1]}) )
- if [ -z $is_in_24 ]; then
- error_exit ${range} ${mask}
- fi
-
- start=$(ip2dec ${strarr[0]})
- stop=$(ip2dec ${strarr[1]})
- if [ $start -ge $stop ]; then
- error_exit ${range} ${mask}
- fi
-
exit 0
fi