diff options
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | data/templates/dns-forwarding/recursor.conf.lua.j2 | 28 | ||||
-rw-r--r-- | data/templates/firewall/nftables-nat66.j2 | 24 | ||||
-rw-r--r-- | data/templates/wifi/hostapd.conf.j2 | 12 | ||||
-rw-r--r-- | interface-definitions/container.xml.in | 6 | ||||
-rw-r--r-- | interface-definitions/interfaces_wireless.xml.in | 68 | ||||
-rw-r--r-- | interface-definitions/nat66.xml.in | 1 | ||||
-rw-r--r-- | interface-definitions/service_dns_forwarding.xml.in | 173 | ||||
-rw-r--r-- | op-mode-definitions/execute-port-scan.xml.in | 34 | ||||
-rw-r--r-- | python/vyos/nat.py | 10 | ||||
-rw-r--r-- | python/vyos/utils/convert.py | 62 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_container.py | 16 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_wireless.py | 89 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_nat66.py | 30 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_dns_forwarding.py | 39 | ||||
-rwxr-xr-x | src/conf_mode/container.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/nat66.py | 28 | ||||
-rwxr-xr-x | src/conf_mode/service_dns_forwarding.py | 20 | ||||
-rw-r--r-- | src/op_mode/execute_port-scan.py | 155 |
19 files changed, 752 insertions, 48 deletions
@@ -64,6 +64,7 @@ op_mode_definitions: $(op_xml_obj) ln -s ../node.tag $(OP_TMPL_DIR)/mtr/node.tag/node.tag/ ln -s ../node.tag $(OP_TMPL_DIR)/monitor/traceroute/node.tag/node.tag/ ln -s ../node.tag $(OP_TMPL_DIR)/monitor/traffic/interface/node.tag/node.tag/ + ln -s ../node.tag $(OP_TMPL_DIR)/execute/port-scan/host/node.tag/node.tag/ # XXX: test if there are empty node.def files - this is not allowed as these # could mask help strings or mandatory priority statements diff --git a/data/templates/dns-forwarding/recursor.conf.lua.j2 b/data/templates/dns-forwarding/recursor.conf.lua.j2 index 8026442c7..622283ad8 100644 --- a/data/templates/dns-forwarding/recursor.conf.lua.j2 +++ b/data/templates/dns-forwarding/recursor.conf.lua.j2 @@ -6,3 +6,31 @@ dofile("/usr/share/pdns-recursor/lua-config/rootkeys.lua") -- Load lua from vyos-hostsd -- dofile("{{ config_dir }}/recursor.vyos-hostsd.conf.lua") + +-- ZoneToCache -- +{% if zone_cache is vyos_defined %} +{% set option_mapping = { + 'refresh': 'refreshPeriod', + 'retry_interval': 'retryOnErrorPeriod', + 'max_zone_size': 'maxReceivedMBytes' +} %} +{% for name, conf in zone_cache.items() %} +{% set source = conf.source.items() | first %} +{% set settings = [] %} +{% for key, val in conf.options.items() %} +{% set mapped_key = option_mapping.get(key, key) %} +{% if key == 'refresh' %} +{% set val = val['interval'] %} +{% endif %} +{% if key in ['dnssec', 'zonemd'] %} +{% set _ = settings.append(mapped_key ~ ' = "' ~ val ~ '"') %} +{% else %} +{% set _ = settings.append(mapped_key ~ ' = ' ~ val) %} +{% endif %} +{% endfor %} + +zoneToCache("{{ name }}", "{{ source[0] }}", "{{ source[1] }}", { {{ settings | join(', ') }} }) + +{% endfor %} + +{% endif %} diff --git a/data/templates/firewall/nftables-nat66.j2 b/data/templates/firewall/nftables-nat66.j2 index 67eb2c109..09b5b6ac2 100644 --- a/data/templates/firewall/nftables-nat66.j2 +++ b/data/templates/firewall/nftables-nat66.j2 @@ -1,8 +1,11 @@ #!/usr/sbin/nft -f +{% import 'firewall/nftables-defines.j2' as group_tmpl %} + {% if first_install is not vyos_defined %} delete table ip6 vyos_nat {% endif %} +{% if deleted is not vyos_defined %} table ip6 vyos_nat { # # Destination NAT66 rules build up here @@ -10,11 +13,11 @@ table ip6 vyos_nat { chain PREROUTING { type nat hook prerouting priority -100; policy accept; counter jump VYOS_DNPT_HOOK -{% if destination.rule is vyos_defined %} -{% for rule, config in destination.rule.items() if config.disable is not vyos_defined %} - {{ config | nat_rule(rule, 'destination', ipv6=True) }} -{% endfor %} -{% endif %} +{% if destination.rule is vyos_defined %} +{% for rule, config in destination.rule.items() if config.disable is not vyos_defined %} + {{ config | nat_rule(rule, 'destination', ipv6=True) }} +{% endfor %} +{% endif %} } # @@ -23,11 +26,11 @@ table ip6 vyos_nat { chain POSTROUTING { type nat hook postrouting priority 100; policy accept; counter jump VYOS_SNPT_HOOK -{% if source.rule is vyos_defined %} -{% for rule, config in source.rule.items() if config.disable is not vyos_defined %} +{% if source.rule is vyos_defined %} +{% for rule, config in source.rule.items() if config.disable is not vyos_defined %} {{ config | nat_rule(rule, 'source', ipv6=True) }} -{% endfor %} -{% endif %} +{% endfor %} +{% endif %} } chain VYOS_DNPT_HOOK { @@ -37,4 +40,7 @@ table ip6 vyos_nat { chain VYOS_SNPT_HOOK { return } + +{{ group_tmpl.groups(firewall_group, True, True) }} } +{% endif %} diff --git a/data/templates/wifi/hostapd.conf.j2 b/data/templates/wifi/hostapd.conf.j2 index 0459fbc69..5f3757216 100644 --- a/data/templates/wifi/hostapd.conf.j2 +++ b/data/templates/wifi/hostapd.conf.j2 @@ -46,7 +46,14 @@ hw_mode=a ieee80211h=1 ieee80211ac=1 {% elif mode is vyos_defined('ax') %} +{#{% if capabilities.ht is vyos_defined and capabilities.vht not vyos_defined %}#} +{% if capabilities.he.channel_set_width is vyos_defined('81') or capabilities.he.channel_set_width is vyos_defined('83') or capabilities.he.channel_set_width is vyos_defined('84') %} +{# This is almost certainly a 2.4GHz network #} +hw_mode=g +{% else %} +{# This is likely a 5GHz or 6GHz network #} hw_mode=a +{% endif %} ieee80211h=1 ieee80211ax=1 {% else %} @@ -202,7 +209,7 @@ require_he=1 {% else %} ieee80211n={{ '1' if 'n' in mode or 'ac' in mode or 'ax' in mode else '0' }} {% endif %} -{# HE (802.11ax 6GHz) #} +{# HE (802.11ax) #} {% if capabilities.he is vyos_defined and mode in 'ax' %} {# For now, hard-code power levels for indoor-only AP #} he_6ghz_reg_pwr_type=0 @@ -220,6 +227,9 @@ op_class={{ capabilities.he.channel_set_width }} {% if capabilities.he.bss_color is vyos_defined %} he_bss_color={{ capabilities.he.bss_color }} {% endif %} +{% if capabilities.he.coding_scheme is vyos_defined %} +he_basic_mcs_nss_set={{ capabilities.he.coding_scheme }} +{% endif %} he_6ghz_rx_ant_pat={{ '1' if capabilities.he.antenna_pattern_fixed is vyos_defined else '0' }} he_su_beamformer={{ '1' if capabilities.he.beamform.single_user_beamformer is vyos_defined else '0' }} he_su_beamformee={{ '1' if capabilities.he.beamform.single_user_beamformee is vyos_defined else '0' }} diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index 6ea44a6d4..3dd1b3249 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -519,6 +519,12 @@ <multi/> </properties> </leafNode> + <leafNode name="no-name-server"> + <properties> + <help>Disable Domain Name System (DNS) plugin for this network</help> + <valueless/> + </properties> + </leafNode> #include <include/interface/vrf.xml.i> </children> </tagNode> diff --git a/interface-definitions/interfaces_wireless.xml.in b/interface-definitions/interfaces_wireless.xml.in index fdcb79b19..474953500 100644 --- a/interface-definitions/interfaces_wireless.xml.in +++ b/interface-definitions/interfaces_wireless.xml.in @@ -248,26 +248,26 @@ <properties> <help>VHT operating channel center frequency - center freq 1 (for use with 80, 80+80 and 160 modes)</help> <valueHelp> - <format>u32:34-173</format> + <format>u32:34-177</format> <description>5Ghz (802.11 a/h/j/n/ac) center channel index (use 42 for primary 80MHz channel 36)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 34-173"/> + <validator name="numeric" argument="--range 34-177"/> </constraint> - <constraintErrorMessage>Channel center value must be between 34 and 173</constraintErrorMessage> + <constraintErrorMessage>Channel center value must be between 34 and 177</constraintErrorMessage> </properties> </leafNode> <leafNode name="freq-2"> <properties> <help>VHT operating channel center frequency - center freq 2 (for use with the 80+80 mode)</help> <valueHelp> - <format>u32:34-173</format> + <format>u32:34-177</format> <description>5Ghz (802.11 ac) center channel index (use 58 for secondary 80MHz channel 52)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 34-173"/> + <validator name="numeric" argument="--range 34-177"/> </constraint> - <constraintErrorMessage>Channel center value must be between 34 and 173</constraintErrorMessage> + <constraintErrorMessage>Channel center value must be between 34 and 177</constraintErrorMessage> </properties> </leafNode> </children> @@ -436,30 +436,42 @@ https://w1.fi/cgit/hostap/tree/src/common/ieee802_11_common.c?id=195cc3d919503fb0d699d9a56a58a72602b25f51#n1525 802.11ax (WiFi-6e - HE) can use up to 160MHz bandwidth channels --> - <list>131 132 133 134 135</list> + <list>81 83 84 131 132 133 134 135</list> </completionHelp> <valueHelp> + <format>81</format> + <description>2.4GHz, 20 MHz channel width</description> + </valueHelp> + <valueHelp> + <format>83</format> + <description>2.4GHz, 40 MHz channel width, secondary 20MHz channel above primary channel</description> + </valueHelp> + <valueHelp> + <format>84</format> + <description>2.4GHz, 40 MHz channel width, secondary 20MHz channel below primary channel</description> + </valueHelp> + <valueHelp> <format>131</format> - <description>20 MHz channel width</description> + <description>6GHz, 20 MHz channel width</description> </valueHelp> <valueHelp> <format>132</format> - <description>40 MHz channel width</description> + <description>6GHz, 40 MHz channel width</description> </valueHelp> <valueHelp> <format>133</format> - <description>80 MHz channel width</description> + <description>6GHz, 80 MHz channel width</description> </valueHelp> <valueHelp> <format>134</format> - <description>160 MHz channel width</description> + <description>6GHz, 160 MHz channel width</description> </valueHelp> <valueHelp> <format>135</format> - <description>80+80 MHz channel width</description> + <description>6GHz, 80+80 MHz channel width</description> </valueHelp> <constraint> - <regex>(131|132|133|134|135)</regex> + <regex>(81|83|84|131|132|133|134|135)</regex> </constraint> </properties> </leafNode> @@ -535,6 +547,30 @@ </constraint> </properties> </leafNode> + <leafNode name="coding-scheme"> + <properties> + <help>Spacial Stream and Modulation Coding Scheme settings</help> + <valueHelp> + <format>u32:0</format> + <description>HE-MCS 0-7</description> + </valueHelp> + <valueHelp> + <format>u32:1</format> + <description>HE-MCS 0-9</description> + </valueHelp> + <valueHelp> + <format>u32:2</format> + <description>HE-MCS 0-11</description> + </valueHelp> + <valueHelp> + <format>u32:3</format> + <description>HE-MCS is not supported</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-3"/> + </constraint> + </properties> + </leafNode> </children> </node> <leafNode name="require-he"> @@ -554,10 +590,10 @@ </valueHelp> <valueHelp> <format>u32:1-14</format> - <description>2.4Ghz (802.11 b/g/n) Channel</description> + <description>2.4Ghz (802.11 b/g/n/ax) Channel</description> </valueHelp> <valueHelp> - <format>u32:34-173</format> + <format>u32:34-177</format> <description>5Ghz (802.11 a/h/j/n/ac) Channel</description> </valueHelp> <valueHelp> @@ -565,7 +601,7 @@ <description>6Ghz (802.11 ax) Channel</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-0 --range 1-14 --range 34-173 --range 1-233"/> + <validator name="numeric" argument="--range 0-0 --range 1-14 --range 34-177 --range 1-233"/> </constraint> </properties> <defaultValue>0</defaultValue> diff --git a/interface-definitions/nat66.xml.in b/interface-definitions/nat66.xml.in index 32d501cce..c59725c53 100644 --- a/interface-definitions/nat66.xml.in +++ b/interface-definitions/nat66.xml.in @@ -179,6 +179,7 @@ </properties> </leafNode> #include <include/nat-port.xml.i> + #include <include/firewall/source-destination-group-ipv6.xml.i> </children> </node> <node name="source"> diff --git a/interface-definitions/service_dns_forwarding.xml.in b/interface-definitions/service_dns_forwarding.xml.in index 5667028b7..d0bc2e6c8 100644 --- a/interface-definitions/service_dns_forwarding.xml.in +++ b/interface-definitions/service_dns_forwarding.xml.in @@ -793,6 +793,179 @@ </leafNode> </children> </node> + <tagNode name="zone-cache"> + <properties> + <help>Load a zone into the recursor cache</help> + <valueHelp> + <format>txt</format> + <description>Domain name</description> + </valueHelp> + <constraint> + <validator name="fqdn"/> + </constraint> + </properties> + <children> + <node name="source"> + <properties> + <help>Zone source</help> + </properties> + <children> + <leafNode name="axfr"> + <properties> + <help>DNS server address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="url"> + <properties> + <help>Source URL</help> + <valueHelp> + <format>url</format> + <description>Zone file URL</description> + </valueHelp> + <constraint> + <validator name="url" argument="--scheme http --scheme https"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="options"> + <properties> + <help>Zone caching options</help> + </properties> + <children> + <leafNode name="timeout"> + <properties> + <help>Zone retrieval timeout</help> + <valueHelp> + <format>u32:1-3600</format> + <description>Request timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> + <defaultValue>20</defaultValue> + </leafNode> + <node name="refresh"> + <properties> + <help>Zone caching options</help> + </properties> + <children> + <leafNode name="on-reload"> + <properties> + <help>Retrieval zone only at startup and on reload</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="interval"> + <properties> + <help>Periodic zone retrieval interval</help> + <valueHelp> + <format>u32:0-31536000</format> + <description>Retrieval interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-31536000"/> + </constraint> + </properties> + <defaultValue>86400</defaultValue> + </leafNode> + </children> + </node> + <leafNode name="retry-interval"> + <properties> + <help>Retry interval after zone retrieval errors</help> + <valueHelp> + <format>u32:1-86400</format> + <description>Retry period in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-86400"/> + </constraint> + </properties> + <defaultValue>60</defaultValue> + </leafNode> + <leafNode name="max-zone-size"> + <properties> + <help>Maximum zone size in megabytes</help> + <valueHelp> + <format>u32:0</format> + <description>No restriction</description> + </valueHelp> + <valueHelp> + <format>u32:1-1024</format> + <description>Size in megabytes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-1024"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="zonemd"> + <properties> + <help>Message Digest for DNS Zones (RFC 8976)</help> + <completionHelp> + <list>ignore validate require</list> + </completionHelp> + <valueHelp> + <format>ignore</format> + <description>Ignore ZONEMD records</description> + </valueHelp> + <valueHelp> + <format>validate</format> + <description>Validate ZONEMD if present</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>Require valid ZONEMD record to be present</description> + </valueHelp> + <constraint> + <regex>(ignore|validate|require)</regex> + </constraint> + </properties> + <defaultValue>validate</defaultValue> + </leafNode> + <leafNode name="dnssec"> + <properties> + <help>DNSSEC mode</help> + <completionHelp> + <list>ignore validate require</list> + </completionHelp> + <valueHelp> + <format>ignore</format> + <description>Do not do DNSSEC validation</description> + </valueHelp> + <valueHelp> + <format>validate</format> + <description>Reject zones with incorrect signatures but accept unsigned zones</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>Require DNSSEC validation</description> + </valueHelp> + <constraint> + <regex>(ignore|validate|require)</regex> + </constraint> + </properties> + <defaultValue>validate</defaultValue> + </leafNode> + </children> + </node> + </children> + </tagNode> </children> </node> </children> diff --git a/op-mode-definitions/execute-port-scan.xml.in b/op-mode-definitions/execute-port-scan.xml.in new file mode 100644 index 000000000..52cdab5f0 --- /dev/null +++ b/op-mode-definitions/execute-port-scan.xml.in @@ -0,0 +1,34 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="execute"> + <children> + <node name="port-scan"> + <properties> + <help>Scan network for open ports</help> + </properties> + <children> + <tagNode name="host"> + <properties> + <help>IP address or domain name of the host to scan (scan all ports 1-65535)</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>nmap -p- -T4 --max-retries=1 --host-timeout=30s "$4"</command> + <children> + <leafNode name="node.tag"> + <properties> + <help>Port scan options</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/execute_port-scan.py --get-options-nested "${COMP_WORDS[@]}"</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/execute_port-scan.py "${@:4}"</command> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/nat.py b/python/vyos/nat.py index e54548788..5fab3c2a1 100644 --- a/python/vyos/nat.py +++ b/python/vyos/nat.py @@ -199,7 +199,10 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): if group_name[0] == '!': operator = '!=' group_name = group_name[1:] - output.append(f'{ip_prefix} {prefix}addr {operator} @A_{group_name}') + if ipv6: + output.append(f'{ip_prefix} {prefix}addr {operator} @A6_{group_name}') + else: + output.append(f'{ip_prefix} {prefix}addr {operator} @A_{group_name}') # Generate firewall group domain-group elif 'domain_group' in group and not (ignore_type_addr and target == nat_type): group_name = group['domain_group'] @@ -214,7 +217,10 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): if group_name[0] == '!': operator = '!=' group_name = group_name[1:] - output.append(f'{ip_prefix} {prefix}addr {operator} @N_{group_name}') + if ipv6: + output.append(f'{ip_prefix} {prefix}addr {operator} @N6_{group_name}') + else: + output.append(f'{ip_prefix} {prefix}addr {operator} @N_{group_name}') if 'mac_group' in group: group_name = group['mac_group'] operator = '' diff --git a/python/vyos/utils/convert.py b/python/vyos/utils/convert.py index 41e65081f..dd4266f57 100644 --- a/python/vyos/utils/convert.py +++ b/python/vyos/utils/convert.py @@ -12,41 +12,72 @@ # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. +import re + +# Define the number of seconds in each time unit +time_units = { + 'y': 60 * 60 * 24 * 365.25, # year + 'w': 60 * 60 * 24 * 7, # week + 'd': 60 * 60 * 24, # day + 'h': 60 * 60, # hour + 'm': 60, # minute + 's': 1 # second +} + + +def human_to_seconds(time_str): + """ Converts a human-readable interval such as 1w4d18h35m59s + to number of seconds + """ + + time_patterns = { + 'y': r'(\d+)\s*y', + 'w': r'(\d+)\s*w', + 'd': r'(\d+)\s*d', + 'h': r'(\d+)\s*h', + 'm': r'(\d+)\s*m', + 's': r'(\d+)\s*s' + } + + total_seconds = 0 + + for unit, pattern in time_patterns.items(): + match = re.search(pattern, time_str) + if match: + value = int(match.group(1)) + total_seconds += value * time_units[unit] + + return int(total_seconds) + def seconds_to_human(s, separator=""): """ Converts number of seconds passed to a human-readable interval such as 1w4d18h35m59s """ s = int(s) - - year = 60 * 60 * 24 * 365.25 - week = 60 * 60 * 24 * 7 - day = 60 * 60 * 24 - hour = 60 * 60 - result = [] - years = s // year + years = s // time_units['y'] if years > 0: result.append(f'{int(years)}y') - s = int(s % year) + s = int(s % time_units['y']) - weeks = s // week + weeks = s // time_units['w'] if weeks > 0: result.append(f'{weeks}w') - s = s % week + s = s % time_units['w'] - days = s // day + days = s // time_units['d'] if days > 0: result.append(f'{days}d') - s = s % day + s = s % time_units['d'] - hours = s // hour + hours = s // time_units['h'] if hours > 0: result.append(f'{hours}h') - s = s % hour + s = s % time_units['h'] - minutes = s // 60 + minutes = s // time_units['m'] if minutes > 0: result.append(f'{minutes}m') s = s % 60 @@ -57,6 +88,7 @@ def seconds_to_human(s, separator=""): return separator.join(result) + def bytes_to_human(bytes, initial_exponent=0, precision=2, int_below_exponent=0): """ Converts a value in bytes to a human-readable size string like 640 KB diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py index 5e33eba40..c03b9eb44 100755 --- a/smoketest/scripts/cli/test_container.py +++ b/smoketest/scripts/cli/test_container.py @@ -208,6 +208,22 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): self.assertEqual(c['NetworkSettings']['Networks'][net_name]['Gateway'] , str(ip_interface(prefix4).ip + 1)) self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPAddress'] , str(ip_interface(prefix4).ip + ii)) + def test_no_name_server(self): + prefix = '192.0.2.0/24' + base_name = 'ipv4' + net_name = 'NET01' + + self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) + self.cli_set(base_path + ['network', net_name, 'no-name-server']) + + name = f'{base_name}-2' + self.cli_set(base_path + ['name', name, 'image', cont_image]) + self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)]) + self.cli_commit() + + n = cmd_to_json(f'sudo podman network inspect {net_name}') + self.assertEqual(n['dns_enabled'], False) + def test_uid_gid(self): cont_name = 'uid-test' gid = '100' diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index 7bfe0d221..b8b18f30f 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -300,7 +300,89 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): for key, value in vht_opt.items(): self.assertIn(value, tmp) - def test_wireless_hostapd_he_config(self): + def test_wireless_hostapd_he_2ghz_config(self): + # Only set the hostapd (access-point) options - HE mode for 802.11ax at 2.4GHz + interface = self._interfaces[1] # wlan1 + ssid = 'ssid' + channel = '1' + sae_pw = 'VyOSVyOSVyOS' + bss_color = '13' + channel_set_width = '81' + + self.cli_set(self._base_path + [interface, 'ssid', ssid]) + self.cli_set(self._base_path + [interface, 'type', 'access-point']) + self.cli_set(self._base_path + [interface, 'channel', channel]) + self.cli_set(self._base_path + [interface, 'mode', 'ax']) + self.cli_set(self._base_path + [interface, 'security', 'wpa', 'mode', 'wpa2']) + self.cli_set(self._base_path + [interface, 'security', 'wpa', 'passphrase', sae_pw]) + self.cli_set(self._base_path + [interface, 'security', 'wpa', 'cipher', 'CCMP']) + self.cli_set(self._base_path + [interface, 'security', 'wpa', 'cipher', 'GCMP']) + self.cli_set(self._base_path + [interface, 'capabilities', 'ht', '40mhz-incapable']) + self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'channel-set-width', 'ht20']) + self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'channel-set-width', 'ht40+']) + self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'channel-set-width', 'ht40-']) + self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'short-gi', '20']) + self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'short-gi', '40']) + self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'bss-color', bss_color]) + self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'channel-set-width', channel_set_width]) + self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'beamform', 'multi-user-beamformer']) + self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'beamform', 'single-user-beamformer']) + self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'beamform', 'single-user-beamformee']) + + self.cli_commit() + + # + # Validate Config + # + tmp = get_config_value(interface, 'interface') + self.assertEqual(interface, tmp) + + # ssid + tmp = get_config_value(interface, 'ssid') + self.assertEqual(ssid, tmp) + + # mode of operation resulting from [interface, 'mode', 'ax'] + tmp = get_config_value(interface, 'hw_mode') + self.assertEqual('g', tmp) + tmp = get_config_value(interface, 'ieee80211h') + self.assertEqual('1', tmp) + tmp = get_config_value(interface, 'ieee80211ax') + self.assertEqual('1', tmp) + + # channel and channel width + tmp = get_config_value(interface, 'channel') + self.assertEqual(channel, tmp) + tmp = get_config_value(interface, 'op_class') + self.assertEqual(channel_set_width, tmp) + + # BSS coloring + tmp = get_config_value(interface, 'he_bss_color') + self.assertEqual(bss_color, tmp) + + # sae_password + tmp = get_config_value(interface, 'wpa_passphrase') + self.assertEqual(sae_pw, tmp) + + # WPA3 and dependencies + tmp = get_config_value(interface, 'wpa') + self.assertEqual('2', tmp) + tmp = get_config_value(interface, 'rsn_pairwise') + self.assertEqual('CCMP GCMP', tmp) + tmp = get_config_value(interface, 'wpa_key_mgmt') + self.assertEqual('WPA-PSK WPA-PSK-SHA256', tmp) + + # beamforming + tmp = get_config_value(interface, 'he_mu_beamformer') + self.assertEqual('1', tmp) + tmp = get_config_value(interface, 'he_su_beamformee') + self.assertEqual('1', tmp) + tmp = get_config_value(interface, 'he_mu_beamformer') + self.assertEqual('1', tmp) + + # Check for running process + self.assertTrue(process_named_running('hostapd')) + + def test_wireless_hostapd_he_6ghz_config(self): # Only set the hostapd (access-point) options - HE mode for 802.11ax at 6GHz interface = self._interfaces[1] # wlan1 ssid = 'ssid' @@ -323,6 +405,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'bss-color', bss_color]) self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'channel-set-width', channel_set_width]) self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'center-channel-freq', 'freq-1', center_channel_freq_1]) + self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'antenna-pattern-fixed']) self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'beamform', 'multi-user-beamformer']) self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'beamform', 'single-user-beamformer']) @@ -370,6 +453,10 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): tmp = get_config_value(interface, 'wpa_key_mgmt') self.assertEqual('SAE', tmp) + # antenna pattern + tmp = get_config_value(interface, 'he_6ghz_rx_ant_pat') + self.assertEqual('1', tmp) + # beamforming tmp = get_config_value(interface, 'he_mu_beamformer') self.assertEqual('1', tmp) diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index e8eeae26f..52ad8e3ef 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -141,6 +141,36 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_nat') + def test_destination_nat66_network_group(self): + address_group = 'smoketest_addr' + address_group_member = 'fc00::1' + network_group = 'smoketest_net' + network_group_member = 'fc00::/64' + translation_prefix = 'fc01::/64' + + self.cli_set(['firewall', 'group', 'ipv6-address-group', address_group, 'address', address_group_member]) + self.cli_set(['firewall', 'group', 'ipv6-network-group', network_group, 'network', network_group_member]) + + self.cli_set(dst_path + ['rule', '1', 'destination', 'group', 'address-group', address_group]) + self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_prefix]) + + self.cli_set(dst_path + ['rule', '2', 'destination', 'group', 'network-group', network_group]) + self.cli_set(dst_path + ['rule', '2', 'translation', 'address', translation_prefix]) + + self.cli_commit() + + nftables_search = [ + [f'set A6_{address_group}'], + [f'elements = {{ {address_group_member} }}'], + [f'set N6_{network_group}'], + [f'elements = {{ {network_group_member} }}'], + ['ip6 daddr', f'@A6_{address_group}', 'dnat prefix to fc01::/64'], + ['ip6 daddr', f'@N6_{network_group}', 'dnat prefix to fc01::/64'] + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_nat') + + def test_destination_nat66_without_translation_address(self): self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1']) self.cli_set(dst_path + ['rule', '1', 'destination', 'port', '443']) diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py index 4db1d7495..9a3f4933e 100755 --- a/smoketest/scripts/cli/test_service_dns_forwarding.py +++ b/smoketest/scripts/cli/test_service_dns_forwarding.py @@ -26,6 +26,7 @@ from vyos.utils.process import process_named_running PDNS_REC_RUN_DIR = '/run/pdns-recursor' CONFIG_FILE = f'{PDNS_REC_RUN_DIR}/recursor.conf' +PDNS_REC_LUA_CONF_FILE = f'{PDNS_REC_RUN_DIR}/recursor.conf.lua' FORWARD_FILE = f'{PDNS_REC_RUN_DIR}/recursor.forward-zones.conf' HOSTSD_FILE = f'{PDNS_REC_RUN_DIR}/recursor.vyos-hostsd.conf.lua' PROCESS_NAME= 'pdns_recursor' @@ -300,6 +301,44 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): self.assertRegex(zone_config, fr'test\s+\d+\s+NS\s+ns1\.{test_zone}\.') self.assertRegex(zone_config, fr'test\s+\d+\s+NS\s+ns2\.{test_zone}\.') + def test_zone_cache_url(self): + self.cli_set(base_path + ['zone-cache', 'smoketest', 'source', 'url', 'https://www.internic.net/domain/root.zone']) + self.cli_commit() + + lua_config = read_file(PDNS_REC_LUA_CONF_FILE) + self.assertIn('zoneToCache("smoketest", "url", "https://www.internic.net/domain/root.zone", { dnssec = "validate", zonemd = "validate", maxReceivedMBytes = 0, retryOnErrorPeriod = 60, refreshPeriod = 86400, timeout = 20 })', lua_config) + + def test_zone_cache_axfr(self): + + self.cli_set(base_path + ['zone-cache', 'smoketest', 'source', 'axfr', '127.0.0.1']) + self.cli_commit() + + lua_config = read_file(PDNS_REC_LUA_CONF_FILE) + self.assertIn('zoneToCache("smoketest", "axfr", "127.0.0.1", { dnssec = "validate", zonemd = "validate", maxReceivedMBytes = 0, retryOnErrorPeriod = 60, refreshPeriod = 86400, timeout = 20 })', lua_config) + + def test_zone_cache_options(self): + self.cli_set(base_path + ['zone-cache', 'smoketest', 'source', 'url', 'https://www.internic.net/domain/root.zone']) + self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'dnssec', 'ignore']) + self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'max-zone-size', '100']) + self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'refresh', 'interval', '10']) + self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'retry-interval', '90']) + self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'timeout', '50']) + self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'zonemd', 'require']) + self.cli_commit() + + lua_config = read_file(PDNS_REC_LUA_CONF_FILE) + self.assertIn('zoneToCache("smoketest", "url", "https://www.internic.net/domain/root.zone", { dnssec = "ignore", maxReceivedMBytes = 100, refreshPeriod = 10, retryOnErrorPeriod = 90, timeout = 50, zonemd = "require" })', lua_config) + + def test_zone_cache_wrong_source(self): + self.cli_set(base_path + ['zone-cache', 'smoketest', 'source', 'url', 'https://www.internic.net/domain/root.zone']) + self.cli_set(base_path + ['zone-cache', 'smoketest', 'source', 'axfr', '127.0.0.1']) + + with self.assertRaises(ConfigSessionError): + self.cli_commit() + # correct config to correct finish the test + self.cli_delete(base_path + ['zone-cache', 'smoketest', 'source', 'axfr']) + self.cli_commit() + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index ded370a7a..14387cbbf 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -421,6 +421,10 @@ def generate(container): 'driver': 'host-local' } } + + if 'no_name_server' in network_config: + tmp['dns_enabled'] = False + for prefix in network_config['prefix']: net = {'subnet': prefix, 'gateway': inc_ip(prefix, 1)} tmp['subnets'].append(net) diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index c44320f36..95dfae3a5 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -26,6 +26,7 @@ from vyos.utils.dict import dict_search from vyos.utils.kernel import check_kmod from vyos.utils.network import interface_exists from vyos.utils.process import cmd +from vyos.utils.process import run from vyos.template import is_ipv6 from vyos import ConfigError from vyos import airbag @@ -48,6 +49,14 @@ def get_config(config=None): if not conf.exists(base): nat['deleted'] = '' + return nat + + nat['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + # Remove dynamic firewall groups if present: + if 'dynamic_group' in nat['firewall_group']: + del nat['firewall_group']['dynamic_group'] return nat @@ -99,22 +108,33 @@ def verify(nat): if not interface_exists(interface_name): Warning(f'Interface "{interface_name}" for destination NAT66 rule "{rule}" does not exist!') + if 'destination' in config and 'group' in config['destination']: + if len({'address_group', 'network_group', 'domain_group'} & set(config['destination']['group'])) > 1: + raise ConfigError('Only one address-group, network-group or domain-group can be specified') + return None def generate(nat): if not os.path.exists(nftables_nat66_config): nat['first_install'] = True - render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat, permission=0o755) + render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat) + + # dry-run newly generated configuration + tmp = run(f'nft --check --file {nftables_nat66_config}') + if tmp > 0: + raise ConfigError('Configuration file errors encountered!') + return None def apply(nat): - if not nat: - return None - check_kmod(k_mod) cmd(f'nft --file {nftables_nat66_config}') + + if not nat or 'deleted' in nat: + os.unlink(nftables_nat66_config) + call_dependents() return None diff --git a/src/conf_mode/service_dns_forwarding.py b/src/conf_mode/service_dns_forwarding.py index 70686534f..e3bdbc9f8 100755 --- a/src/conf_mode/service_dns_forwarding.py +++ b/src/conf_mode/service_dns_forwarding.py @@ -224,6 +224,18 @@ def get_config(config=None): dns['authoritative_zones'].append(zone) + if 'zone_cache' in dns: + # convert refresh interval to sec: + for _, zone_conf in dns['zone_cache'].items(): + if 'options' in zone_conf \ + and 'refresh' in zone_conf['options']: + + if 'on_reload' in zone_conf['options']['refresh']: + interval = 0 + else: + interval = zone_conf['options']['refresh']['interval'] + zone_conf['options']['refresh']['interval'] = interval + return dns def verify(dns): @@ -259,8 +271,16 @@ def verify(dns): if not 'system_name_server' in dns: print('Warning: No "system name-server" configured') + if 'zone_cache' in dns: + for name, conf in dns['zone_cache'].items(): + if ('source' not in conf) \ + or ('url' in conf['source'] and 'axfr' in conf['source']): + raise ConfigError(f'Invalid configuration for zone "{name}": ' + f'Please select one source type "url" or "axfr".') + return None + def generate(dns): # bail out early - looks like removal from running config if not dns: diff --git a/src/op_mode/execute_port-scan.py b/src/op_mode/execute_port-scan.py new file mode 100644 index 000000000..bf17d0379 --- /dev/null +++ b/src/op_mode/execute_port-scan.py @@ -0,0 +1,155 @@ +#! /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 sys + +from vyos.utils.process import call + + +options = { + 'port': { + 'cmd': '{command} -p {value}', + 'type': '<1-65535> <list>', + 'help': 'Scan specified ports.' + }, + 'tcp': { + 'cmd': '{command} -sT', + 'type': 'noarg', + 'help': 'Use TCP scan.' + }, + 'udp': { + 'cmd': '{command} -sU', + 'type': 'noarg', + 'help': 'Use UDP scan.' + }, + 'skip-ping': { + 'cmd': '{command} -Pn', + 'type': 'noarg', + 'help': 'Skip the Nmap discovery stage altogether.' + }, + 'ipv6': { + 'cmd': '{command} -6', + 'type': 'noarg', + 'help': 'Enable IPv6 scanning.' + }, +} + +nmap = 'sudo /usr/bin/nmap' + + +class List(list): + def first(self): + return self.pop(0) if self else '' + + def last(self): + return self.pop() if self else '' + + def prepend(self, value): + self.insert(0, value) + + +def completion_failure(option: str) -> None: + """ + Shows failure message after TAB when option is wrong + :param option: failure option + :type str: + """ + sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option)) + sys.stdout.write('<nocomps>') + sys.exit(1) + + +def expansion_failure(option, completions): + reason = 'Ambiguous' if completions else 'Invalid' + sys.stderr.write( + '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv), + option)) + if completions: + sys.stderr.write(' Possible completions:\n ') + sys.stderr.write('\n '.join(completions)) + sys.stderr.write('\n') + sys.stdout.write('<nocomps>') + sys.exit(1) + + +def complete(prefix): + return [o for o in options if o.startswith(prefix)] + + +def convert(command, args): + while args: + shortname = args.first() + longnames = complete(shortname) + if len(longnames) != 1: + expansion_failure(shortname, longnames) + longname = longnames[0] + if options[longname]['type'] == 'noarg': + command = options[longname]['cmd'].format( + command=command, value='') + elif not args: + sys.exit(f'port-scan: missing argument for {longname} option') + else: + command = options[longname]['cmd'].format( + command=command, value=args.first()) + return command + + +if __name__ == '__main__': + args = List(sys.argv[1:]) + host = args.first() + + if host == '--get-options-nested': + args.first() # pop execute + args.first() # pop port-scan + args.first() # pop host + args.first() # pop <host> + usedoptionslist = [] + while args: + option = args.first() # pop option + matched = complete(option) # get option parameters + usedoptionslist.append(option) # list of used options + # Select options + if not args: + # remove from Possible completions used options + for o in usedoptionslist: + if o in matched: + matched.remove(o) + if not matched: + sys.stdout.write('<nocomps>') + else: + sys.stdout.write(' '.join(matched)) + sys.exit(0) + + if len(matched) > 1: + sys.stdout.write(' '.join(matched)) + sys.exit(0) + # If option doesn't have value + if matched: + if options[matched[0]]['type'] == 'noarg': + continue + else: + # Unexpected option + completion_failure(option) + + value = args.first() # pop option's value + if not args: + matched = complete(option) + helplines = options[matched[0]]['type'] + sys.stdout.write(helplines) + sys.exit(0) + + command = convert(nmap, args) + call(f'{command} -T4 {host}') |