diff options
83 files changed, 2354 insertions, 456 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index d77275d38..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Build -on: - push: - branches: - - current - pull_request: - types: [opened, synchronize, reopened] -jobs: - sonarcloud: - name: SonarCloud - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/data/config-mode-dependencies.json b/data/config-mode-dependencies/vyos-1x.json index 91a757c16..08732bd4c 100644 --- a/data/config-mode-dependencies.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -1,5 +1,5 @@ { - "firewall": {"group_resync": ["nat", "policy-route"]}, + "firewall": {"group_resync": ["conntrack", "nat", "policy-route"]}, "http_api": {"https": ["https"]}, "pki": { "ethernet": ["interfaces-ethernet"], diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index 042c466ab..ded934bff 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -16,6 +16,7 @@ "neighbor.py", "nhrp.py", "openconnect.py", +"otp.py", "openvpn.py", "reset_vpn.py", "reverseproxy.py", diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index add3dc7e4..f59428509 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -36,7 +36,9 @@ verbose=1 {% set shared = 'shared=0,' %} {% endif %} {% set range = 'range=' ~ iface_config.client_subnet ~ ',' if iface_config.client_subnet is vyos_defined else '' %} -{{ tmp }},{{ shared }}mode={{ iface_config.mode | upper }},ifcfg=1,{{ range }}start=dhcpv4,ipv6=1 +{% set relay = ',' ~ 'relay=' ~ iface_config.external_dhcp.dhcp_relay if iface_config.external_dhcp.dhcp_relay is vyos_defined else '' %} +{% set giaddr = ',' ~ 'giaddr=' ~ iface_config.external_dhcp.giaddr if iface_config.external_dhcp.giaddr is vyos_defined else '' %} +{{ tmp }},{{ shared }}mode={{ iface_config.mode | upper }},ifcfg=1,{{ range }}start=dhcpv4,ipv6=1{{ relay }}{{ giaddr }} {% if iface_config.vlan is vyos_defined %} vlan-mon={{ iface }},{{ iface_config.vlan | join(',') }} {% endif %} diff --git a/data/templates/conntrack/nftables-ct.j2 b/data/templates/conntrack/nftables-ct.j2 index 16a03fc6e..970869043 100644 --- a/data/templates/conntrack/nftables-ct.j2 +++ b/data/templates/conntrack/nftables-ct.j2 @@ -1,5 +1,7 @@ #!/usr/sbin/nft -f +{% import 'firewall/nftables-defines.j2' as group_tmpl %} + {% set nft_ct_ignore_name = 'VYOS_CT_IGNORE' %} {% set nft_ct_timeout_name = 'VYOS_CT_TIMEOUT' %} @@ -10,29 +12,35 @@ flush chain raw {{ nft_ct_timeout_name }} table raw { chain {{ nft_ct_ignore_name }} { -{% if ignore.rule is vyos_defined %} -{% for rule, rule_config in ignore.rule.items() %} +{% if ignore.ipv4.rule is vyos_defined %} +{% for rule, rule_config in ignore.ipv4.rule.items() %} + # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} + {{ rule_config | conntrack_ignore_rule(rule, ipv6=False) }} +{% endfor %} +{% endif %} + return + } + chain {{ nft_ct_timeout_name }} { +{% if timeout.custom.rule is vyos_defined %} +{% for rule, rule_config in timeout.custom.rule.items() %} + # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} +{% endfor %} +{% endif %} + return + } + +{{ group_tmpl.groups(firewall_group, False) }} +} + +flush chain ip6 raw {{ nft_ct_ignore_name }} +flush chain ip6 raw {{ nft_ct_timeout_name }} + +table ip6 raw { + chain {{ nft_ct_ignore_name }} { +{% if ignore.ipv6.rule is vyos_defined %} +{% for rule, rule_config in ignore.ipv6.rule.items() %} # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} -{% set nft_command = '' %} -{% if rule_config.inbound_interface is vyos_defined %} -{% set nft_command = nft_command ~ ' iifname ' ~ rule_config.inbound_interface %} -{% endif %} -{% if rule_config.protocol is vyos_defined %} -{% set nft_command = nft_command ~ ' ip protocol ' ~ rule_config.protocol %} -{% endif %} -{% if rule_config.destination.address is vyos_defined %} -{% set nft_command = nft_command ~ ' ip daddr ' ~ rule_config.destination.address %} -{% endif %} -{% if rule_config.destination.port is vyos_defined %} -{% set nft_command = nft_command ~ ' ' ~ rule_config.protocol ~ ' dport { ' ~ rule_config.destination.port ~ ' }' %} -{% endif %} -{% if rule_config.source.address is vyos_defined %} -{% set nft_command = nft_command ~ ' ip saddr ' ~ rule_config.source.address %} -{% endif %} -{% if rule_config.source.port is vyos_defined %} -{% set nft_command = nft_command ~ ' ' ~ rule_config.protocol ~ ' sport { ' ~ rule_config.source.port ~ ' }' %} -{% endif %} - {{ nft_command }} counter notrack comment ignore-{{ rule }} + {{ rule_config | conntrack_ignore_rule(rule, ipv6=True) }} {% endfor %} {% endif %} return @@ -45,4 +53,6 @@ table raw { {% endif %} return } + +{{ group_tmpl.groups(firewall_group, True) }} } diff --git a/src/systemd/isc-dhcp-server.service b/data/templates/dhcp-server/10-override.conf.j2 index a7d86e69c..1504b6808 100644 --- a/src/systemd/isc-dhcp-server.service +++ b/data/templates/dhcp-server/10-override.conf.j2 @@ -1,22 +1,28 @@ +### Autogenerated by dhcp_server.py ### +{% set lease_file = '/config/dhcpd.leases' %} [Unit] Description=ISC DHCP IPv4 server Documentation=man:dhcpd(8) RequiresMountsFor=/run +ConditionPathExists= ConditionPathExists=/run/dhcp-server/dhcpd.conf +After= After=vyos-router.service [Service] Type=forking +WorkingDirectory= WorkingDirectory=/run/dhcp-server RuntimeDirectory=dhcp-server RuntimeDirectoryPreserve=yes -Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhcpd.conf LEASE_FILE=/config/dhcpd.leases +Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhcpd.conf LEASE_FILE={{ lease_file }} PIDFile=/run/dhcp-server/dhcpd.pid ExecStartPre=/bin/sh -ec '\ touch ${LEASE_FILE}; \ chown dhcpd:vyattacfg ${LEASE_FILE}* ; \ chmod 664 ${LEASE_FILE}* ; \ /usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' +ExecStart= ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} Restart=always diff --git a/data/templates/firewall/nftables-bridge.j2 b/data/templates/firewall/nftables-bridge.j2 new file mode 100644 index 000000000..7f94e10d6 --- /dev/null +++ b/data/templates/firewall/nftables-bridge.j2 @@ -0,0 +1,35 @@ +{% macro bridge(bridge) %} +{% set ns = namespace(sets=[]) %} +{% if bridge.forward is vyos_defined %} +{% for prior, conf in bridge.forward.items() %} +{% set def_action = conf.default_action %} + chain VYOS_FORWARD_{{ prior }} { + type filter hook forward priority {{ prior }}; policy {{ def_action }}; +{% if conf.rule is vyos_defined %} +{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} + {{ rule_conf | nft_rule('FWD', prior, rule_id, 'bri') }} +{% if rule_conf.recent is vyos_defined %} +{% set ns.sets = ns.sets + ['FWD_' + prior + '_' + rule_id] %} +{% endif %} +{% endfor %} +{% endif %} + } +{% endfor %} +{% endif %} + +{% if bridge.name is vyos_defined %} +{% for name_text, conf in bridge.name.items() %} + chain NAME_{{ name_text }} { +{% if conf.rule is vyos_defined %} +{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} + {{ rule_conf | nft_rule('NAM', name_text, rule_id, 'bri') }} +{% if rule_conf.recent is vyos_defined %} +{% set ns.sets = ns.sets + ['NAM_' + name_text + '_' + rule_id] %} +{% endif %} +{% endfor %} +{% endif %} + {{ conf | nft_default_rule(name_text) }} + } +{% endfor %} +{% endif %} +{% endmacro %} diff --git a/data/templates/firewall/nftables-defines.j2 b/data/templates/firewall/nftables-defines.j2 index 0a7e79edd..a20c399ae 100644 --- a/data/templates/firewall/nftables-defines.j2 +++ b/data/templates/firewall/nftables-defines.j2 @@ -1,7 +1,7 @@ -{% macro groups(group, is_ipv6) %} +{% macro groups(group, is_ipv6, is_l3) %} {% if group is vyos_defined %} {% set ip_type = 'ipv6_addr' if is_ipv6 else 'ipv4_addr' %} -{% if group.address_group is vyos_defined and not is_ipv6 %} +{% if group.address_group is vyos_defined and not is_ipv6 and is_l3 %} {% for group_name, group_conf in group.address_group.items() %} {% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} set A_{{ group_name }} { @@ -14,7 +14,7 @@ } {% endfor %} {% endif %} -{% if group.ipv6_address_group is vyos_defined and is_ipv6 %} +{% if group.ipv6_address_group is vyos_defined and is_ipv6 and is_l3 %} {% for group_name, group_conf in group.ipv6_address_group.items() %} {% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} set A6_{{ group_name }} { @@ -27,7 +27,7 @@ } {% endfor %} {% endif %} -{% if group.domain_group is vyos_defined %} +{% if group.domain_group is vyos_defined and is_l3 %} {% for name, name_config in group.domain_group.items() %} set D_{{ name }} { type {{ ip_type }} @@ -46,7 +46,7 @@ } {% endfor %} {% endif %} -{% if group.network_group is vyos_defined and not is_ipv6 %} +{% if group.network_group is vyos_defined and not is_ipv6 and is_l3 %} {% for group_name, group_conf in group.network_group.items() %} {% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} set N_{{ group_name }} { @@ -59,7 +59,7 @@ } {% endfor %} {% endif %} -{% if group.ipv6_network_group is vyos_defined and is_ipv6 %} +{% if group.ipv6_network_group is vyos_defined and is_ipv6 and is_l3 %} {% for group_name, group_conf in group.ipv6_network_group.items() %} {% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} set N6_{{ group_name }} { @@ -72,7 +72,7 @@ } {% endfor %} {% endif %} -{% if group.port_group is vyos_defined %} +{% if group.port_group is vyos_defined and is_l3 %} {% for group_name, group_conf in group.port_group.items() %} {% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} set P_{{ group_name }} { diff --git a/data/templates/firewall/nftables-nat.j2 b/data/templates/firewall/nftables-nat.j2 index f0be3cf5d..dcf28da88 100644 --- a/data/templates/firewall/nftables-nat.j2 +++ b/data/templates/firewall/nftables-nat.j2 @@ -62,6 +62,6 @@ table ip vyos_nat { return } -{{ group_tmpl.groups(firewall_group, False) }} +{{ group_tmpl.groups(firewall_group, False, True) }} } {% endif %} diff --git a/data/templates/firewall/nftables-policy.j2 b/data/templates/firewall/nftables-policy.j2 index 699349e2b..d77e3f6e9 100644 --- a/data/templates/firewall/nftables-policy.j2 +++ b/data/templates/firewall/nftables-policy.j2 @@ -32,7 +32,7 @@ table ip vyos_mangle { {% endfor %} {% endif %} -{{ group_tmpl.groups(firewall_group, False) }} +{{ group_tmpl.groups(firewall_group, False, True) }} } table ip6 vyos_mangle { @@ -61,5 +61,5 @@ table ip6 vyos_mangle { {% endfor %} {% endif %} -{{ group_tmpl.groups(firewall_group, True) }} +{{ group_tmpl.groups(firewall_group, True, True) }} } diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index 0fbddfaa9..87630940b 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -1,33 +1,41 @@ #!/usr/sbin/nft -f {% import 'firewall/nftables-defines.j2' as group_tmpl %} +{% import 'firewall/nftables-bridge.j2' as bridge_tmpl %} flush chain raw FW_CONNTRACK flush chain ip6 raw FW_CONNTRACK +flush chain raw vyos_global_rpfilter +flush chain ip6 raw vyos_global_rpfilter + table raw { chain FW_CONNTRACK { {{ ipv4_conntrack_action }} } + + chain vyos_global_rpfilter { +{% if global_options.source_validation is vyos_defined('loose') %} + fib saddr oif 0 counter drop +{% elif global_options.source_validation is vyos_defined('strict') %} + fib saddr . iif oif 0 counter drop +{% endif %} + return + } } table ip6 raw { chain FW_CONNTRACK { {{ ipv6_conntrack_action }} } -} -{% if first_install is not vyos_defined %} -delete table inet vyos_global_rpfilter -{% endif %} -table inet vyos_global_rpfilter { - chain PREROUTING { - type filter hook prerouting priority -300; policy accept; -{% if global_options.source_validation is vyos_defined('loose') %} + chain vyos_global_rpfilter { +{% if global_options.ipv6_source_validation is vyos_defined('loose') %} fib saddr oif 0 counter drop -{% elif global_options.source_validation is vyos_defined('strict') %} +{% elif global_options.ipv6_source_validation is vyos_defined('strict') %} fib saddr . iif oif 0 counter drop {% endif %} + return } } @@ -147,7 +155,7 @@ table ip vyos_filter { {% endfor %} {% endif %} {% endif %} -{{ group_tmpl.groups(group, False) }} +{{ group_tmpl.groups(group, False, True) }} } {% if first_install is not vyos_defined %} @@ -250,5 +258,16 @@ table ip6 vyos_filter { {% endfor %} {% endif %} {% endif %} -{{ group_tmpl.groups(group, True) }} -}
\ No newline at end of file +{{ group_tmpl.groups(group, True, True) }} +} + +## Bridge Firewall +{% if first_install is not vyos_defined %} +delete table bridge vyos_filter +{% endif %} +{% if bridge is vyos_defined %} +table bridge vyos_filter { +{{ bridge_tmpl.bridge(bridge) }} +{{ group_tmpl.groups(group, False, False) }} +} +{% endif %} diff --git a/data/templates/frr/igmp.frr.j2 b/data/templates/frr/igmp.frr.j2 index ce1f8fdda..b75884484 100644 --- a/data/templates/frr/igmp.frr.j2 +++ b/data/templates/frr/igmp.frr.j2 @@ -27,9 +27,9 @@ interface {{ interface }} {% if interface_config.query_max_resp_time %} ip igmp query-max-response-time {{ interface_config.query_max_resp_time }} {% endif %} -{% for group in interface_config.gr_join %} -{% if ifaces[iface].gr_join[group] %} -{% for source in ifaces[iface].gr_join[group] %} +{% for group, sources in interface_config.gr_join.items() %} +{% if sources is vyos_defined %} +{% for source in sources %} ip igmp join {{ group }} {{ source }} {% endfor %} {% else %} diff --git a/src/etc/systemd/system/keepalived.service.d/override.conf b/data/templates/high-availability/10-override.conf.j2 index d91a824b9..d1cb25581 100644 --- a/src/etc/systemd/system/keepalived.service.d/override.conf +++ b/data/templates/high-availability/10-override.conf.j2 @@ -1,3 +1,5 @@ +### Autogenerated by ${vyos_conf_scripts_dir}/high-availability.py ### +{% set snmp = '' if vrrp.disable_snmp is vyos_defined else '--snmp' %} [Unit] After=vyos-router.service # Only start if there is our configuration file - remove Debian default @@ -10,5 +12,5 @@ KillMode=process Type=simple # Read configuration variable file if it is present ExecStart= -ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork --snmp +ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork {{ snmp }} PIDFile=/run/keepalived/keepalived.pid diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2 index f8e1587f8..0a40e1ecf 100644 --- a/data/templates/load-balancing/haproxy.cfg.j2 +++ b/data/templates/load-balancing/haproxy.cfg.j2 @@ -150,13 +150,13 @@ backend {{ back }} {% endfor %} {% endif %} {% if back_config.timeout.check is vyos_defined %} - timeout check {{ back_config.timeout.check }} + timeout check {{ back_config.timeout.check }}s {% endif %} {% if back_config.timeout.connect is vyos_defined %} - timeout connect {{ back_config.timeout.connect }} + timeout connect {{ back_config.timeout.connect }}s {% endif %} {% if back_config.timeout.server is vyos_defined %} - timeout server {{ back_config.timeout.server }} + timeout server {{ back_config.timeout.server }}s {% endif %} {% endfor %} diff --git a/data/templates/wifi/hostapd.conf.j2 b/data/templates/wifi/hostapd.conf.j2 index 613038597..c3f32da72 100644 --- a/data/templates/wifi/hostapd.conf.j2 +++ b/data/templates/wifi/hostapd.conf.j2 @@ -340,6 +340,11 @@ vht_oper_chwidth={{ capabilities.vht.channel_set_width }} {% endif %} {% set output = namespace(value='') %} +{% if capabilities.vht.channel_set_width is vyos_defined('2') %} +{% set output.value = output.value ~ '[VHT160]' %} +{% elif capabilities.vht.channel_set_width is vyos_defined('3') %} +{% set output.value = output.value ~ '[VHT160-80PLUS80]' %} +{% endif %} {% if capabilities.vht.stbc.tx is vyos_defined %} {% set output.value = output.value ~ '[TX-STBC-2BY1]' %} {% endif %} @@ -363,30 +368,21 @@ vht_oper_chwidth={{ capabilities.vht.channel_set_width }} {% endif %} {% if capabilities.vht.max_mpdu_exp is vyos_defined %} {% set output.value = output.value ~ '[MAX-A-MPDU-LEN-EXP-' ~ capabilities.vht.max_mpdu_exp ~ ']' %} -{% if capabilities.vht.max_mpdu_exp is vyos_defined('2') %} -{% set output.value = output.value ~ '[VHT160]' %} -{% endif %} -{% if capabilities.vht.max_mpdu_exp is vyos_defined('3') %} -{% set output.value = output.value ~ '[VHT160-80PLUS80]' %} -{% endif %} {% endif %} {% if capabilities.vht.link_adaptation is vyos_defined('unsolicited') %} {% set output.value = output.value ~ '[VHT-LINK-ADAPT2]' %} {% elif capabilities.vht.link_adaptation is vyos_defined('both') %} {% set output.value = output.value ~ '[VHT-LINK-ADAPT3]' %} {% endif %} - {% for short_gi in capabilities.vht.short_gi if capabilities.vht.short_gi is vyos_defined %} {% set output.value = output.value ~ '[SHORT-GI-' ~ short_gi | upper ~ ']' %} {% endfor %} - {% for beamform in capabilities.vht.beamform if capabilities.vht.beamform is vyos_defined %} {% set output.value = output.value ~ '[SU-BEAMFORMER]' if beamform is vyos_defined('single-user-beamformer') else '' %} {% set output.value = output.value ~ '[SU-BEAMFORMEE]' if beamform is vyos_defined('single-user-beamformee') else '' %} {% set output.value = output.value ~ '[MU-BEAMFORMER]' if beamform is vyos_defined('multi-user-beamformer') else '' %} {% set output.value = output.value ~ '[MU-BEAMFORMEE]' if beamform is vyos_defined('multi-user-beamformee') else '' %} {% endfor %} - {% if capabilities.vht.antenna_count is vyos_defined and capabilities.vht.antenna_count | int > 1 %} {% if capabilities.vht.beamform is vyos_defined %} {% if capabilities.vht.beamform == 'single-user-beamformer' %} diff --git a/data/vyos-firewall-init.conf b/data/vyos-firewall-init.conf index 41e7627f5..7e258e6f1 100644 --- a/data/vyos-firewall-init.conf +++ b/data/vyos-firewall-init.conf @@ -19,6 +19,15 @@ table raw { type filter hook forward priority -300; policy accept; } + chain vyos_global_rpfilter { + return + } + + chain vyos_rpfilter { + type filter hook prerouting priority -300; policy accept; + counter jump vyos_global_rpfilter + } + chain PREROUTING { type filter hook prerouting priority -300; policy accept; counter jump VYOS_CT_IGNORE @@ -82,12 +91,19 @@ table ip6 raw { type filter hook forward priority -300; policy accept; } + chain vyos_global_rpfilter { + return + } + chain vyos_rpfilter { type filter hook prerouting priority -300; policy accept; + counter jump vyos_global_rpfilter } chain PREROUTING { type filter hook prerouting priority -300; policy accept; + counter jump VYOS_CT_IGNORE + counter jump VYOS_CT_TIMEOUT counter jump VYOS_CT_PREROUTING_HOOK counter jump FW_CONNTRACK notrack @@ -95,11 +111,40 @@ table ip6 raw { chain OUTPUT { type filter hook output priority -300; policy accept; + counter jump VYOS_CT_IGNORE + counter jump VYOS_CT_TIMEOUT counter jump VYOS_CT_OUTPUT_HOOK counter jump FW_CONNTRACK notrack } + ct helper rpc_tcp { + type "rpc" protocol tcp; + } + + ct helper rpc_udp { + type "rpc" protocol udp; + } + + ct helper tns_tcp { + type "tns" protocol tcp; + } + + chain VYOS_CT_HELPER { + ct helper set "rpc_tcp" tcp dport {111} return + ct helper set "rpc_udp" udp dport {111} return + ct helper set "tns_tcp" tcp dport {1521,1525,1536} return + return + } + + chain VYOS_CT_IGNORE { + return + } + + chain VYOS_CT_TIMEOUT { + return + } + chain VYOS_CT_PREROUTING_HOOK { return } diff --git a/debian/rules b/debian/rules index e6bbeeafb..9a6ab2996 100755 --- a/debian/rules +++ b/debian/rules @@ -117,6 +117,10 @@ override_dh_auto_install: mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/config/ cp -r smoketest/configs/* $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/config + # Install smoke test config tests + mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/config-tests/ + cp -r smoketest/config-tests/* $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/config-tests + # Install system programs mkdir -p $(DIR)/$(VYOS_BIN_DIR) cp -r smoketest/bin/* $(DIR)/$(VYOS_BIN_DIR) diff --git a/debian/vyos-1x-smoketest.install b/debian/vyos-1x-smoketest.install index 406fef4be..739cb189b 100644 --- a/debian/vyos-1x-smoketest.install +++ b/debian/vyos-1x-smoketest.install @@ -3,3 +3,4 @@ usr/bin/vyos-configtest usr/bin/vyos-configtest-pki usr/libexec/vyos/tests/smoke usr/libexec/vyos/tests/config +usr/libexec/vyos/tests/config-tests diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index 8259e7bdf..b35ba8d1c 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -25,7 +25,7 @@ <properties> <help>Container capabilities/permissions</help> <completionHelp> - <list>net-admin net-bind-service net-raw setpcap sys-admin sys-time</list> + <list>net-admin net-bind-service net-raw setpcap sys-admin sys-module sys-time</list> </completionHelp> <valueHelp> <format>net-admin</format> @@ -48,11 +48,15 @@ <description>Administation operations (quotactl, mount, sethostname, setdomainame)</description> </valueHelp> <valueHelp> + <format>sys-module</format> + <description>Load, unload and delete kernel modules</description> + </valueHelp> + <valueHelp> <format>sys-time</format> <description>Permission to set system clock</description> </valueHelp> <constraint> - <regex>(net-admin|net-bind-service|net-raw|setpcap|sys-admin|sys-time)</regex> + <regex>(net-admin|net-bind-service|net-raw|setpcap|sys-admin|sys-module|sys-time)</regex> </constraint> <multi/> </properties> @@ -110,7 +114,7 @@ <constraint> <regex>[ !#-%&(-~]+</regex> </constraint> - <constraintErrorMessage>Entrypoint must be ascii characters, use &quot; and &apos for double and single quotes respectively</constraintErrorMessage> + <constraintErrorMessage>Entrypoint must be ASCII characters, use &quot; and &apos for double and single quotes respectively</constraintErrorMessage> </properties> </leafNode> <leafNode name="host-name"> @@ -133,7 +137,7 @@ <constraint> <regex>[ !#-%&(-~]+</regex> </constraint> - <constraintErrorMessage>Command must be ascii characters, use &quot; and &apos for double and single quotes respectively</constraintErrorMessage> + <constraintErrorMessage>Command must be ASCII characters, use &quot; and &apos for double and single quotes respectively</constraintErrorMessage> </properties> </leafNode> <leafNode name="arguments"> @@ -142,7 +146,7 @@ <constraint> <regex>[ !#-%&(-~]+</regex> </constraint> - <constraintErrorMessage>The command's arguments must be ascii characters, use &quot; and &apos for double and single quotes respectively</constraintErrorMessage> + <constraintErrorMessage>The command's arguments must be ASCII characters, use &quot; and &apos for double and single quotes respectively</constraintErrorMessage> </properties> </leafNode> <tagNode name="label"> diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index 127f4b7e7..8e462f3eb 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -284,6 +284,15 @@ </tagNode> </children> </node> + <node name="bridge"> + <properties> + <help>Bridge firewall</help> + </properties> + <children> + #include <include/firewall/bridge-hook-forward.xml.i> + #include <include/firewall/bridge-custom-name.xml.i> + </children> + </node> <node name="ipv4"> <properties> <help>IPv4 firewall</help> diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in index 4f55916fa..47a772d04 100644 --- a/interface-definitions/high-availability.xml.in +++ b/interface-definitions/high-availability.xml.in @@ -12,6 +12,12 @@ <help>Virtual Router Redundancy Protocol settings</help> </properties> <children> + <leafNode name="disable-snmp"> + <properties> + <valueless/> + <help>Disable SNMP</help> + </properties> + </leafNode> <node name="global-parameters"> <properties> <help>VRRP global parameters</help> diff --git a/interface-definitions/include/firewall/action-l2.xml.i b/interface-definitions/include/firewall/action-l2.xml.i new file mode 100644 index 000000000..84af576c8 --- /dev/null +++ b/interface-definitions/include/firewall/action-l2.xml.i @@ -0,0 +1,37 @@ +<!-- include start from firewall/action.xml.i --> +<leafNode name="action"> + <properties> + <help>Rule action</help> + <completionHelp> + <list>accept continue jump return drop queue</list> + </completionHelp> + <valueHelp> + <format>accept</format> + <description>Accept matching entries</description> + </valueHelp> + <valueHelp> + <format>continue</format> + <description>Continue parsing next rule</description> + </valueHelp> + <valueHelp> + <format>jump</format> + <description>Jump to another chain</description> + </valueHelp> + <valueHelp> + <format>return</format> + <description>Return from the current chain and continue at the next rule of the last chain</description> + </valueHelp> + <valueHelp> + <format>drop</format> + <description>Drop matching entries</description> + </valueHelp> + <valueHelp> + <format>queue</format> + <description>Enqueue packet to userspace</description> + </valueHelp> + <constraint> + <regex>(accept|continue|jump|return|drop|queue)</regex> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/firewall/action.xml.i b/interface-definitions/include/firewall/action.xml.i index 7c6e33839..9391a7bee 100644 --- a/interface-definitions/include/firewall/action.xml.i +++ b/interface-definitions/include/firewall/action.xml.i @@ -3,13 +3,17 @@ <properties> <help>Rule action</help> <completionHelp> - <list>accept jump reject return drop queue</list> + <list>accept continue jump reject return drop queue</list> </completionHelp> <valueHelp> <format>accept</format> <description>Accept matching entries</description> </valueHelp> <valueHelp> + <format>continue</format> + <description>Continue parsing next rule</description> + </valueHelp> + <valueHelp> <format>jump</format> <description>Jump to another chain</description> </valueHelp> @@ -30,7 +34,7 @@ <description>Enqueue packet to userspace</description> </valueHelp> <constraint> - <regex>(accept|jump|reject|return|drop|queue)</regex> + <regex>(accept|continue|jump|reject|return|drop|queue)</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/firewall/bridge-custom-name.xml.i b/interface-definitions/include/firewall/bridge-custom-name.xml.i new file mode 100644 index 000000000..a85fd5a19 --- /dev/null +++ b/interface-definitions/include/firewall/bridge-custom-name.xml.i @@ -0,0 +1,39 @@ +<!-- include start from firewall/bridge-custom-name.xml.i --> +<tagNode name="name"> + <properties> + <help>Bridge custom firewall</help> + <constraint> + <regex>[a-zA-Z0-9][\w\-\.]*</regex> + </constraint> + </properties> + <children> + #include <include/firewall/default-action.xml.i> + #include <include/firewall/enable-default-log.xml.i> + #include <include/generic-description.xml.i> + <leafNode name="default-jump-target"> + <properties> + <help>Set jump target. Action jump must be defined in default-action to use this setting</help> + <completionHelp> + <path>firewall bridge name</path> + </completionHelp> + </properties> + </leafNode> + <tagNode name="rule"> + <properties> + <help>Bridge Firewall forward filter rule number</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number for this firewall rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage> + </properties> + <children> + #include <include/firewall/common-rule-bridge.xml.i> + </children> + </tagNode> + </children> +</tagNode> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/bridge-hook-forward.xml.i b/interface-definitions/include/firewall/bridge-hook-forward.xml.i new file mode 100644 index 000000000..23d757070 --- /dev/null +++ b/interface-definitions/include/firewall/bridge-hook-forward.xml.i @@ -0,0 +1,34 @@ +<!-- include start from firewall/bridge-hook-forward.xml.i --> +<node name="forward"> + <properties> + <help>Bridge forward firewall</help> + </properties> + <children> + <node name="filter"> + <properties> + <help>Bridge firewall forward filter</help> + </properties> + <children> + #include <include/firewall/default-action-base-chains.xml.i> + #include <include/generic-description.xml.i> + <tagNode name="rule"> + <properties> + <help>Bridge Firewall forward filter rule number</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number for this firewall rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage> + </properties> + <children> + #include <include/firewall/common-rule-bridge.xml.i> + </children> + </tagNode> + </children> + </node> + </children> +</node> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/common-rule-bridge.xml.i b/interface-definitions/include/firewall/common-rule-bridge.xml.i new file mode 100644 index 000000000..381e04b1e --- /dev/null +++ b/interface-definitions/include/firewall/common-rule-bridge.xml.i @@ -0,0 +1,57 @@ +<!-- include start from firewall/common-rule-bridge.xml.i --> +#include <include/firewall/action-l2.xml.i> +#include <include/firewall/nft-queue.xml.i> +<node name="destination"> + <properties> + <help>Destination parameters</help> + </properties> + <children> + #include <include/firewall/mac-address.xml.i> + </children> +</node> +<leafNode name="disable"> + <properties> + <help>Option to disable firewall rule</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="jump-target"> + <properties> + <help>Set jump target. Action jump must be defined to use this setting</help> + <completionHelp> + <path>firewall bridge name</path> + </completionHelp> + </properties> +</leafNode> +<leafNode name="log"> + <properties> + <help>Option to log packets matching rule</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable log</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable log</description> + </valueHelp> + <constraint> + <regex>(enable|disable)</regex> + </constraint> + </properties> +</leafNode> +#include <include/firewall/rule-log-options.xml.i> +<node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/firewall/mac-address.xml.i> + </children> +</node> +#include <include/firewall/inbound-interface.xml.i> +#include <include/firewall/outbound-interface.xml.i> +#include <include/firewall/match-vlan.xml.i> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/default-action-bridge.xml.i b/interface-definitions/include/firewall/default-action-bridge.xml.i new file mode 100644 index 000000000..858c7aeeb --- /dev/null +++ b/interface-definitions/include/firewall/default-action-bridge.xml.i @@ -0,0 +1,34 @@ +<!-- include start from firewall/default-action.xml.i --> +<leafNode name="default-action"> + <properties> + <help>Default-action for rule-set</help> + <completionHelp> + <list>drop jump return accept continue</list> + </completionHelp> + <valueHelp> + <format>drop</format> + <description>Drop if no prior rules are hit</description> + </valueHelp> + <valueHelp> + <format>jump</format> + <description>Jump to another chain if no prior rules are hit</description> + </valueHelp> + <valueHelp> + <format>return</format> + <description>Return from the current chain and continue at the next rule of the last chain</description> + </valueHelp> + <valueHelp> + <format>accept</format> + <description>Accept if no prior rules are hit</description> + </valueHelp> + <valueHelp> + <format>continue</format> + <description>Continue parsing next rule</description> + </valueHelp> + <constraint> + <regex>(drop|jump|return|accept|continue)</regex> + </constraint> + </properties> + <defaultValue>drop</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/firewall/default-action.xml.i b/interface-definitions/include/firewall/default-action.xml.i index 80efaf335..53a161495 100644 --- a/interface-definitions/include/firewall/default-action.xml.i +++ b/interface-definitions/include/firewall/default-action.xml.i @@ -3,7 +3,7 @@ <properties> <help>Default-action for rule-set</help> <completionHelp> - <list>drop jump reject return accept</list> + <list>drop jump reject return accept continue</list> </completionHelp> <valueHelp> <format>drop</format> @@ -25,8 +25,12 @@ <format>accept</format> <description>Accept if no prior rules are hit</description> </valueHelp> + <valueHelp> + <format>continue</format> + <description>Continue parsing next rule</description> + </valueHelp> <constraint> - <regex>(drop|jump|reject|return|accept)</regex> + <regex>(drop|jump|reject|return|accept|continue)</regex> </constraint> </properties> <defaultValue>drop</defaultValue> diff --git a/interface-definitions/include/firewall/global-options.xml.i b/interface-definitions/include/firewall/global-options.xml.i index a63874cb0..e655cd6ac 100644 --- a/interface-definitions/include/firewall/global-options.xml.i +++ b/interface-definitions/include/firewall/global-options.xml.i @@ -145,21 +145,21 @@ </leafNode> <leafNode name="source-validation"> <properties> - <help>Policy for source validation by reversed path, as specified in RFC3704</help> + <help>Policy for IPv4 source validation by reversed path, as specified in RFC3704</help> <completionHelp> <list>strict loose disable</list> </completionHelp> <valueHelp> <format>strict</format> - <description>Enable Strict Reverse Path Forwarding as defined in RFC3704</description> + <description>Enable IPv4 Strict Reverse Path Forwarding as defined in RFC3704</description> </valueHelp> <valueHelp> <format>loose</format> - <description>Enable Loose Reverse Path Forwarding as defined in RFC3704</description> + <description>Enable IPv4 Loose Reverse Path Forwarding as defined in RFC3704</description> </valueHelp> <valueHelp> <format>disable</format> - <description>No source validation</description> + <description>No IPv4 source validation</description> </valueHelp> <constraint> <regex>(strict|loose|disable)</regex> @@ -227,6 +227,30 @@ </properties> <defaultValue>disable</defaultValue> </leafNode> + <leafNode name="ipv6-source-validation"> + <properties> + <help>Policy for IPv6 source validation by reversed path, as specified in RFC3704</help> + <completionHelp> + <list>strict loose disable</list> + </completionHelp> + <valueHelp> + <format>strict</format> + <description>Enable IPv6 Strict Reverse Path Forwarding as defined in RFC3704</description> + </valueHelp> + <valueHelp> + <format>loose</format> + <description>Enable IPv6 Loose Reverse Path Forwarding as defined in RFC3704</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>No IPv6 source validation</description> + </valueHelp> + <constraint> + <regex>(strict|loose|disable)</regex> + </constraint> + </properties> + <defaultValue>disable</defaultValue> + </leafNode> <leafNode name="ipv6-src-route"> <properties> <help>Policy for handling IPv6 packets with routing extension header</help> diff --git a/interface-definitions/include/firewall/match-vlan.xml.i b/interface-definitions/include/firewall/match-vlan.xml.i new file mode 100644 index 000000000..44ad02c99 --- /dev/null +++ b/interface-definitions/include/firewall/match-vlan.xml.i @@ -0,0 +1,41 @@ +<!-- include start from firewall/match-vlan.xml.i --> +<node name="vlan"> + <properties> + <help>VLAN parameters</help> + </properties> + <children> + <leafNode name="id"> + <properties> + <help>Vlan id</help> + <valueHelp> + <format>u32:0-4096</format> + <description>Vlan id</description> + </valueHelp> + <valueHelp> + <format><start-end></format> + <description>Vlan id range to match</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--allow-range --range 0-4095"/> + </constraint> + </properties> + </leafNode> + <leafNode name="priority"> + <properties> + <help>Vlan priority(pcp)</help> + <valueHelp> + <format>u32:0-7</format> + <description>Vlan priority</description> + </valueHelp> + <valueHelp> + <format><start-end></format> + <description>Vlan priority range to match</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--allow-range --range 0-7"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/source-destination-group-ipv4.xml.i b/interface-definitions/include/firewall/source-destination-group-ipv4.xml.i new file mode 100644 index 000000000..8c34fb933 --- /dev/null +++ b/interface-definitions/include/firewall/source-destination-group-ipv4.xml.i @@ -0,0 +1,41 @@ +<!-- include start from firewall/source-destination-group-ipv4.xml.i --> +<node name="group"> + <properties> + <help>Group</help> + </properties> + <children> + <leafNode name="address-group"> + <properties> + <help>Group of addresses</help> + <completionHelp> + <path>firewall group address-group</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="domain-group"> + <properties> + <help>Group of domains</help> + <completionHelp> + <path>firewall group domain-group</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="network-group"> + <properties> + <help>Group of networks</help> + <completionHelp> + <path>firewall group network-group</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="port-group"> + <properties> + <help>Group of ports</help> + <completionHelp> + <path>firewall group port-group</path> + </completionHelp> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/version/conntrack-version.xml.i b/interface-definitions/include/version/conntrack-version.xml.i index 696f76362..c0f632c70 100644 --- a/interface-definitions/include/version/conntrack-version.xml.i +++ b/interface-definitions/include/version/conntrack-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/conntrack-version.xml.i --> -<syntaxVersion component='conntrack' version='3'></syntaxVersion> +<syntaxVersion component='conntrack' version='4'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces-virtual-ethernet.xml.in b/interface-definitions/interfaces-virtual-ethernet.xml.in index 1daa764d4..5f205f354 100644 --- a/interface-definitions/interfaces-virtual-ethernet.xml.in +++ b/interface-definitions/interfaces-virtual-ethernet.xml.in @@ -21,6 +21,7 @@ #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable.xml.i> + #include <include/interface/netns.xml.i> #include <include/interface/vif-s.xml.i> #include <include/interface/vif.xml.i> #include <include/interface/vrf.xml.i> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index fb60c93d0..b246d9a09 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -101,6 +101,22 @@ #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> #include <include/vni.xml.i> + <tagNode name="vlan-to-vni"> + <properties> + <help>Configuring VLAN-to-VNI mappings for EVPN-VXLAN</help> + <valueHelp> + <format>u32:0-4094</format> + <description>Virtual Local Area Network (VLAN) ID</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4094"/> + </constraint> + <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> + </properties> + <children> + #include <include/vni.xml.i> + </children> + </tagNode> </children> </tagNode> </children> diff --git a/interface-definitions/system-conntrack.xml.in b/interface-definitions/system-conntrack.xml.in index 8dad048b8..3abf9bbf0 100644 --- a/interface-definitions/system-conntrack.xml.in +++ b/interface-definitions/system-conntrack.xml.in @@ -40,82 +40,177 @@ <help>Customized rules to ignore selective connection tracking</help> </properties> <children> - <tagNode name="rule"> + <node name="ipv4"> <properties> - <help>Rule number</help> - <valueHelp> - <format>u32:1-999999</format> - <description>Number of conntrack ignore rule</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-999999"/> - </constraint> - <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> + <help>IPv4 rules</help> </properties> <children> - #include <include/generic-description.xml.i> - <node name="destination"> + <tagNode name="rule"> <properties> - <help>Destination parameters</help> + <help>Rule number</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number of conntrack ignore rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> - #include <include/nat-address.xml.i> - #include <include/nat-port.xml.i> + #include <include/generic-description.xml.i> + <node name="destination"> + <properties> + <help>Destination parameters</help> + </properties> + <children> + #include <include/firewall/source-destination-group-ipv4.xml.i> + #include <include/nat-address.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> + <leafNode name="inbound-interface"> + <properties> + <help>Interface to ignore connections tracking on</help> + <completionHelp> + <list>any</list> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + </leafNode> + #include <include/ip-protocol.xml.i> + <leafNode name="protocol"> + <properties> + <help>Protocol to match (protocol name, number, or "all")</help> + <completionHelp> + <script>${vyos_completion_dir}/list_protocols.sh</script> + <list>all tcp_udp</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All IP protocols</description> + </valueHelp> + <valueHelp> + <format>tcp_udp</format> + <description>Both TCP and UDP</description> + </valueHelp> + <valueHelp> + <format>u32:0-255</format> + <description>IP protocol number</description> + </valueHelp> + <valueHelp> + <format><protocol></format> + <description>IP protocol name</description> + </valueHelp> + <valueHelp> + <format>!<protocol></format> + <description>IP protocol name</description> + </valueHelp> + <constraint> + <validator name="ip-protocol"/> + </constraint> + </properties> + </leafNode> + <node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/firewall/source-destination-group-ipv4.xml.i> + #include <include/nat-address.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> </children> - </node> - <leafNode name="inbound-interface"> - <properties> - <help>Interface to ignore connections tracking on</help> - <completionHelp> - <list>any</list> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> - </properties> - </leafNode> - #include <include/ip-protocol.xml.i> - <leafNode name="protocol"> + </tagNode> + </children> + </node> + <node name="ipv6"> + <properties> + <help>IPv6 rules</help> + </properties> + <children> + <tagNode name="rule"> <properties> - <help>Protocol to match (protocol name, number, or "all")</help> - <completionHelp> - <script>${vyos_completion_dir}/list_protocols.sh</script> - <list>all tcp_udp</list> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All IP protocols</description> - </valueHelp> - <valueHelp> - <format>tcp_udp</format> - <description>Both TCP and UDP</description> - </valueHelp> - <valueHelp> - <format>u32:0-255</format> - <description>IP protocol number</description> - </valueHelp> - <valueHelp> - <format><protocol></format> - <description>IP protocol name</description> - </valueHelp> + <help>Rule number</help> <valueHelp> - <format>!<protocol></format> - <description>IP protocol name</description> + <format>u32:1-999999</format> + <description>Number of conntrack ignore rule</description> </valueHelp> <constraint> - <validator name="ip-protocol"/> + <validator name="numeric" argument="--range 1-999999"/> </constraint> - </properties> - </leafNode> - <node name="source"> - <properties> - <help>Source parameters</help> + <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> - #include <include/nat-address.xml.i> - #include <include/nat-port.xml.i> + #include <include/generic-description.xml.i> + <node name="destination"> + <properties> + <help>Destination parameters</help> + </properties> + <children> + #include <include/firewall/address-ipv6.xml.i> + #include <include/firewall/source-destination-group-ipv6.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> + <leafNode name="inbound-interface"> + <properties> + <help>Interface to ignore connections tracking on</help> + <completionHelp> + <list>any</list> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + </leafNode> + #include <include/ip-protocol.xml.i> + <leafNode name="protocol"> + <properties> + <help>Protocol to match (protocol name, number, or "all")</help> + <completionHelp> + <script>${vyos_completion_dir}/list_protocols.sh</script> + <list>all tcp_udp</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All IP protocols</description> + </valueHelp> + <valueHelp> + <format>tcp_udp</format> + <description>Both TCP and UDP</description> + </valueHelp> + <valueHelp> + <format>u32:0-255</format> + <description>IP protocol number</description> + </valueHelp> + <valueHelp> + <format><protocol></format> + <description>IP protocol name</description> + </valueHelp> + <valueHelp> + <format>!<protocol></format> + <description>IP protocol name</description> + </valueHelp> + <constraint> + <validator name="ip-protocol"/> + </constraint> + </properties> + </leafNode> + <node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/firewall/address-ipv6.xml.i> + #include <include/firewall/source-destination-group-ipv6.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> </children> - </node> + </tagNode> </children> - </tagNode> + </node> + </children> </node> <node name="log"> diff --git a/op-mode-definitions/monitor-command.xml.in b/op-mode-definitions/monitor-command.xml.in new file mode 100644 index 000000000..31c68f029 --- /dev/null +++ b/op-mode-definitions/monitor-command.xml.in @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <tagNode name="command"> + <properties> + <help>Monitor operational mode command (refreshes every 2 seconds)</help> + </properties> + <command>watch --no-title ${vyos_op_scripts_dir}/vyos-op-cmd-wrapper.sh ${@:3}</command> + </tagNode> + <node name="command"> + <children> + <node name="diff"> + <properties> + <help>Show differences during each run</help> + </properties> + </node> + <tagNode name="diff"> + <properties> + <help>Monitor operational mode command (refreshes every 2 seconds)</help> + </properties> + <command>watch --no-title --differences ${vyos_op_scripts_dir}/vyos-op-cmd-wrapper.sh ${@:4}</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-hardware.xml.in b/op-mode-definitions/show-hardware.xml.in index ebd806ba5..21079765a 100644 --- a/op-mode-definitions/show-hardware.xml.in +++ b/op-mode-definitions/show-hardware.xml.in @@ -31,7 +31,7 @@ <properties> <help>Show system DMI details</help> </properties> - <command>${vyatta_bindir}/vyatta-show-dmi</command> + <command>sudo dmidecode</command> </node> <node name="mem"> <properties> diff --git a/op-mode-definitions/show-system.xml.in b/op-mode-definitions/show-system.xml.in index 85bfdcdba..116c7460f 100644 --- a/op-mode-definitions/show-system.xml.in +++ b/op-mode-definitions/show-system.xml.in @@ -102,6 +102,55 @@ <help>Show user accounts</help> </properties> <children> + <node name="authentication"> + <properties> + <help>Show user account authentication information</help> + </properties> + <children> + <tagNode name="user"> + <properties> + <help>Show configured user</help> + <completionHelp> + <path>system login user</path> + </completionHelp> + </properties> + <children> + <node name="otp"> + <properties> + <help>Show OTP key information</help> + </properties> + <command>${vyos_op_scripts_dir}/otp.py show_login --username="$6" --info="full"</command> + <children> + <leafNode name="full"> + <properties> + <help>Show full settings, including QR code and commands for VyOS</help> + </properties> + <command>${vyos_op_scripts_dir}/otp.py show_login --username="$6" --info="full"</command> + </leafNode> + <leafNode name="key-b32"> + <properties> + <help>Show OTP authentication secret in Base32 (used in mobile apps)</help> + </properties> + <command>${vyos_op_scripts_dir}/otp.py show_login --username="$6" --info="key-b32"</command> + </leafNode> + <leafNode name="qrcode"> + <properties> + <help>Show OTP authentication QR code</help> + </properties> + <command>${vyos_op_scripts_dir}/otp.py show_login --username="$6" --info="qrcode"</command> + </leafNode> + <leafNode name="uri"> + <properties> + <help>Show OTP authentication otpauth URI</help> + </properties> + <command>${vyos_op_scripts_dir}/otp.py show_login --username="$6" --info="uri"</command> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> <node name="users"> <properties> <help>Show user account information</help> diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in index c7ba780a3..b551af2be 100644 --- a/op-mode-definitions/vpn-ipsec.xml.in +++ b/op-mode-definitions/vpn-ipsec.xml.in @@ -177,7 +177,7 @@ <properties> <help>Show all the pre-shared key secrets</help> </properties> - <command>sudo cat /etc/ipsec.secrets | sed 's/#.*//'</command> + <command>${vyos_op_scripts_dir}/ipsec.py show_psk</command> </node> <node name="status"> <properties> diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index 0fc72e660..dbf17ade4 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -25,12 +25,14 @@ from datetime import datetime from textwrap import dedent from pathlib import Path from tabulate import tabulate +from shutil import copy from vyos.config import Config from vyos.configtree import ConfigTree, ConfigTreeError, show_diff from vyos.defaults import directories from vyos.version import get_full_version_data from vyos.utils.io import ask_yes_no +from vyos.utils.boot import boot_configuration_complete from vyos.utils.process import is_systemd_service_active from vyos.utils.process import rc_cmd @@ -200,9 +202,9 @@ Proceed ?''' raise ConfigMgmtError(out) entry = self._read_tmp_log_entry() - self._add_log_entry(**entry) if self._archive_active_config(): + self._add_log_entry(**entry) self._update_archive() msg = 'Reboot timer stopped' @@ -223,8 +225,6 @@ Proceed ?''' def rollback(self, rev: int, no_prompt: bool=False) -> Tuple[str,int]: """Reboot to config revision 'rev'. """ - from shutil import copy - msg = '' if not self._check_revision_number(rev): @@ -334,10 +334,10 @@ Proceed ?''' user = self._get_user() via = 'init' comment = '' - self._add_log_entry(user, via, comment) # add empty init config before boot-config load for revision # and diff consistency if self._archive_active_config(): + self._add_log_entry(user, via, comment) self._update_archive() os.umask(mask) @@ -352,9 +352,8 @@ Proceed ?''' self._new_log_entry(tmp_file=tmp_log_entry) return - self._add_log_entry() - if self._archive_active_config(): + self._add_log_entry() self._update_archive() def commit_archive(self): @@ -475,22 +474,26 @@ Proceed ?''' conf_file.chmod(0o644) def _archive_active_config(self) -> bool: + save_to_tmp = (boot_configuration_complete() or not + os.path.isfile(archive_config_file)) mask = os.umask(0o113) ext = os.getpid() - tmp_save = f'/tmp/config.boot.{ext}' - save_config(tmp_save) + cmp_saved = f'/tmp/config.boot.{ext}' + if save_to_tmp: + save_config(cmp_saved) + else: + copy(config_file, cmp_saved) try: - if cmp(tmp_save, archive_config_file, shallow=False): - # this will be the case on boot, as well as certain - # re-initialiation instances after delete/set - os.unlink(tmp_save) + if cmp(cmp_saved, archive_config_file, shallow=False): + os.unlink(cmp_saved) + os.umask(mask) return False except FileNotFoundError: pass - rc, out = rc_cmd(f'sudo mv {tmp_save} {archive_config_file}') + rc, out = rc_cmd(f'sudo mv {cmp_saved} {archive_config_file}') os.umask(mask) if rc != 0: @@ -522,9 +525,8 @@ Proceed ?''' return len(l) def _check_revision_number(self, rev: int) -> bool: - # exclude init revision: maxrev = self._get_number_of_revisions() - if not 0 <= rev < maxrev - 1: + if not 0 <= rev < maxrev: return False return True diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py index 7a8559839..05d9a3fa3 100644 --- a/python/vyos/configdep.py +++ b/python/vyos/configdep.py @@ -1,4 +1,4 @@ -# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -17,8 +17,10 @@ import os import json import typing from inspect import stack +from graphlib import TopologicalSorter, CycleError from vyos.utils.system import load_as_module +from vyos.configdict import dict_merge from vyos.defaults import directories from vyos.configsource import VyOSError from vyos import ConfigError @@ -28,6 +30,9 @@ from vyos import ConfigError if typing.TYPE_CHECKING: from vyos.config import Config +dependency_dir = os.path.join(directories['data'], + 'config-mode-dependencies') + dependent_func: dict[str, list[typing.Callable]] = {} def canon_name(name: str) -> str: @@ -40,12 +45,20 @@ def canon_name_of_path(path: str) -> str: def caller_name() -> str: return stack()[-1].filename -def read_dependency_dict() -> dict: - path = os.path.join(directories['data'], - 'config-mode-dependencies.json') - with open(path) as f: - d = json.load(f) - return d +def read_dependency_dict(dependency_dir: str = dependency_dir) -> dict: + res = {} + for dep_file in os.listdir(dependency_dir): + if not dep_file.endswith('.json'): + continue + path = os.path.join(dependency_dir, dep_file) + with open(path) as f: + d = json.load(f) + if dep_file == 'vyos-1x.json': + res = dict_merge(res, d) + else: + res = dict_merge(d, res) + + return res def get_dependency_dict(config: 'Config') -> dict: if hasattr(config, 'cached_dependency_dict'): @@ -93,3 +106,37 @@ def call_dependents(): while l: f = l.pop(0) f() + +def graph_from_dependency_dict(d: dict) -> dict: + g = {} + for k in list(d): + g[k] = set() + # add the dependencies for every sub-case; should there be cases + # that are mutally exclusive in the future, the graphs will be + # distinguished + for el in list(d[k]): + g[k] |= set(d[k][el]) + + return g + +def is_acyclic(d: dict) -> bool: + g = graph_from_dependency_dict(d) + ts = TopologicalSorter(g) + try: + # get node iterator + order = ts.static_order() + # try iteration + _ = [*order] + except CycleError: + return False + + return True + +def check_dependency_graph(dependency_dir: str = dependency_dir, + supplement: str = None) -> bool: + d = read_dependency_dict(dependency_dir=dependency_dir) + if supplement is not None: + with open(supplement) as f: + d = dict_merge(json.load(f), d) + + return is_acyclic(d) diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 53ff8259e..3305eb269 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -87,7 +87,14 @@ def nft_action(vyos_action): def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): output = [] - def_suffix = '6' if ip_name == 'ip6' else '' + #def_suffix = '6' if ip_name == 'ip6' else '' + + if ip_name == 'ip6': + def_suffix = '6' + family = 'ipv6' + else: + def_suffix = '' + family = 'bri' if ip_name == 'bri' else 'ipv4' if 'state' in rule_conf and rule_conf['state']: states = ",".join([s for s, v in rule_conf['state'].items() if v == 'enable']) @@ -244,8 +251,9 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): if 'log' in rule_conf and rule_conf['log'] == 'enable': action = rule_conf['action'] if 'action' in rule_conf else 'accept' - output.append(f'log prefix "[{fw_name[:19]}-{rule_id}-{action[:1].upper()}]"') - + #output.append(f'log prefix "[{fw_name[:19]}-{rule_id}-{action[:1].upper()}]"') + output.append(f'log prefix "[{family}-{hook}-{fw_name}-{rule_id}-{action[:1].upper()}]"') + ##{family}-{hook}-{fw_name}-{rule_id} if 'log_options' in rule_conf: if 'level' in rule_conf['log_options']: @@ -379,6 +387,13 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): conn_mark_str = ','.join(rule_conf['connection_mark']) output.append(f'ct mark {{{conn_mark_str}}}') + if 'vlan' in rule_conf: + if 'id' in rule_conf['vlan']: + output.append(f'vlan id {rule_conf["vlan"]["id"]}') + if 'priority' in rule_conf['vlan']: + output.append(f'vlan pcp {rule_conf["vlan"]["priority"]}') + + output.append('counter') if 'set' in rule_conf: @@ -404,7 +419,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): else: output.append('return') - output.append(f'comment "{hook}-{fw_name}-{rule_id}"') + output.append(f'comment "{family}-{hook}-{fw_name}-{rule_id}"') return " ".join(output) def parse_tcp_flags(flags): diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py index c8366cb58..7402da55a 100644 --- a/python/vyos/ifconfig/control.py +++ b/python/vyos/ifconfig/control.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2023 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -49,6 +49,18 @@ class Control(Section): return popen(command, self.debug) def _cmd(self, command): + import re + if 'netns' in self.config: + # This command must be executed from default netns 'ip link set dev X netns X' + # exclude set netns cmd from netns to avoid: + # failed to run command: ip netns exec ns01 ip link set dev veth20 netns ns01 + pattern = r'ip link set dev (\S+) netns (\S+)' + matches = re.search(pattern, command) + if matches and matches.group(2) == self.config['netns']: + # Command already includes netns and matches desired namespace: + command = command + else: + command = f'ip netns exec {self.config["netns"]} {command}' return cmd(command, self.debug) def _get_command(self, config, name): diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 41ce352ad..050095364 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -38,7 +38,9 @@ from vyos.utils.dict import dict_search from vyos.utils.file import read_file from vyos.utils.network import get_interface_config from vyos.utils.network import get_interface_namespace +from vyos.utils.network import is_netns_interface from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import run from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.utils.network import is_intf_addr_assigned @@ -138,9 +140,6 @@ class Interface(Control): 'validate': assert_mtu, 'shellcmd': 'ip link set dev {ifname} mtu {value}', }, - 'netns': { - 'shellcmd': 'ip link set dev {ifname} netns {value}', - }, 'vrf': { 'convert': lambda v: f'master {v}' if v else 'nomaster', 'shellcmd': 'ip link set dev {ifname} {value}', @@ -175,10 +174,6 @@ class Interface(Control): 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding', }, - 'rp_filter': { - 'validate': lambda flt: assert_range(flt,0,3), - 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter', - }, 'ipv6_accept_ra': { 'validate': lambda ara: assert_range(ara,0,3), 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra', @@ -252,9 +247,6 @@ class Interface(Control): 'ipv4_directed_broadcast': { 'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding', }, - 'rp_filter': { - 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter', - }, 'ipv6_accept_ra': { 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra', }, @@ -286,8 +278,11 @@ class Interface(Control): } @classmethod - def exists(cls, ifname): - return os.path.exists(f'/sys/class/net/{ifname}') + def exists(cls, ifname: str, netns: str=None) -> bool: + cmd = f'ip link show dev {ifname}' + if netns: + cmd = f'ip netns exec {netns} {cmd}' + return run(cmd) == 0 @classmethod def get_config(cls): @@ -355,7 +350,13 @@ class Interface(Control): self.vrrp = VRRP(ifname) def _create(self): + # Do not create interface that already exist or exists in netns + netns = self.config.get('netns', None) + if self.exists(f'{self.ifname}', netns=netns): + return + cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) + if 'netns' in self.config: cmd = f'ip netns exec {netns} {cmd}' self._cmd(cmd) def remove(self): @@ -390,6 +391,9 @@ class Interface(Control): # after interface removal no other commands should be allowed # to be called and instead should raise an Exception: cmd = 'ip link del dev {ifname}'.format(**self.config) + # for delete we can't get data from self.config{'netns'} + netns = get_interface_namespace(self.ifname) + if netns: cmd = f'ip netns exec {netns} {cmd}' return self._cmd(cmd) def _set_vrf_ct_zone(self, vrf): @@ -397,6 +401,10 @@ class Interface(Control): Add/Remove rules in nftables to associate traffic in VRF to an individual conntack zone """ + # Don't allow for netns yet + if 'netns' in self.config: + return None + if vrf: # Get routing table ID for VRF vrf_table_id = get_interface_config(vrf).get('linkinfo', {}).get( @@ -540,36 +548,30 @@ class Interface(Control): if prev_state == 'up': self.set_admin_state('up') - def del_netns(self, netns): - """ - Remove interface from given NETNS. - """ - - # If NETNS does not exist then there is nothing to delete + def del_netns(self, netns: str) -> bool: + """ Remove interface from given network namespace """ + # If network namespace does not exist then there is nothing to delete if not os.path.exists(f'/run/netns/{netns}'): - return None - - # As a PoC we only allow 'dummy' interfaces - if 'dum' not in self.ifname: - return None + return False - # Check if interface realy exists in namespace - if get_interface_namespace(self.ifname) != None: - self._cmd(f'ip netns exec {get_interface_namespace(self.ifname)} ip link del dev {self.ifname}') - return + # Check if interface exists in network namespace + if is_netns_interface(self.ifname, netns): + self._cmd(f'ip netns exec {netns} ip link del dev {self.ifname}') + return True + return False - def set_netns(self, netns): + def set_netns(self, netns: str) -> bool: """ - Add interface from given NETNS. + Add interface from given network namespace Example: >>> from vyos.ifconfig import Interface >>> Interface('dum0').set_netns('foo') """ + self._cmd(f'ip link set dev {self.ifname} netns {netns}') + return True - self.set_interface('netns', netns) - - def set_vrf(self, vrf): + def set_vrf(self, vrf: str) -> bool: """ Add/Remove interface from given VRF instance. @@ -581,10 +583,11 @@ class Interface(Control): tmp = self.get_interface('vrf') if tmp == vrf: - return None + return False self.set_interface('vrf', vrf) self._set_vrf_ct_zone(vrf) + return True def set_arp_cache_tmo(self, tmo): """ @@ -621,6 +624,10 @@ class Interface(Control): >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_tcp_ipv4_mss(1340) """ + # Don't allow for netns yet + if 'netns' in self.config: + return None + self._cleanup_mss_rules('raw', self.ifname) nft_prefix = 'nft add rule raw VYOS_TCP_MSS' base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn' @@ -641,6 +648,10 @@ class Interface(Control): >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_tcp_mss(1320) """ + # Don't allow for netns yet + if 'netns' in self.config: + return None + self._cleanup_mss_rules('ip6 raw', self.ifname) nft_prefix = 'nft add rule ip6 raw VYOS_TCP_MSS' base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn' @@ -745,40 +756,36 @@ class Interface(Control): return None return self.set_interface('ipv4_directed_broadcast', forwarding) - def set_ipv4_source_validation(self, value): - """ - Help prevent attacks used by Spoofing IP Addresses. Reverse path - filtering is a Kernel feature that, when enabled, is designed to ensure - packets that are not routable to be dropped. The easiest example of this - would be and IP Address of the range 10.0.0.0/8, a private IP Address, - being received on the Internet facing interface of the router. + def _cleanup_ipv4_source_validation_rules(self, ifname): + results = self._cmd(f'nft -a list chain ip raw vyos_rpfilter').split("\n") + for line in results: + if f'iifname "{ifname}"' in line: + handle_search = re.search('handle (\d+)', line) + if handle_search: + self._cmd(f'nft delete rule ip raw vyos_rpfilter handle {handle_search[1]}') - As per RFC3074. + def set_ipv4_source_validation(self, mode): """ - if value == 'strict': - value = 1 - elif value == 'loose': - value = 2 - else: - value = 0 - - all_rp_filter = int(read_file('/proc/sys/net/ipv4/conf/all/rp_filter')) - if all_rp_filter > value: - global_setting = 'disable' - if all_rp_filter == 1: global_setting = 'strict' - elif all_rp_filter == 2: global_setting = 'loose' - - from vyos.base import Warning - Warning(f'Global source-validation is set to "{global_setting}", this '\ - f'overrides per interface setting on "{self.ifname}"!') + Set IPv4 reverse path validation - tmp = self.get_interface('rp_filter') - if int(tmp) == value: + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_ipv4_source_validation('strict') + """ + # Don't allow for netns yet + if 'netns' in self.config: return None - return self.set_interface('rp_filter', value) + + self._cleanup_ipv4_source_validation_rules(self.ifname) + nft_prefix = f'nft insert rule ip raw vyos_rpfilter iifname "{self.ifname}"' + if mode in ['strict', 'loose']: + self._cmd(f"{nft_prefix} counter return") + if mode == 'strict': + self._cmd(f"{nft_prefix} fib saddr . iif oif 0 counter drop") + elif mode == 'loose': + self._cmd(f"{nft_prefix} fib saddr oif 0 counter drop") def _cleanup_ipv6_source_validation_rules(self, ifname): - commands = [] results = self._cmd(f'nft -a list chain ip6 raw vyos_rpfilter').split("\n") for line in results: if f'iifname "{ifname}"' in line: @@ -794,8 +801,14 @@ class Interface(Control): >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_ipv6_source_validation('strict') """ + # Don't allow for netns yet + if 'netns' in self.config: + return None + self._cleanup_ipv6_source_validation_rules(self.ifname) - nft_prefix = f'nft add rule ip6 raw vyos_rpfilter iifname "{self.ifname}"' + nft_prefix = f'nft insert rule ip6 raw vyos_rpfilter iifname "{self.ifname}"' + if mode in ['strict', 'loose']: + self._cmd(f"{nft_prefix} counter return") if mode == 'strict': self._cmd(f"{nft_prefix} fib saddr . iif oif 0 counter drop") elif mode == 'loose': @@ -1143,13 +1156,17 @@ class Interface(Control): if addr in self._addr: return False + # get interface network namespace if specified + netns = self.config.get('netns', None) + # add to interface if addr == 'dhcp': self.set_dhcp(True) elif addr == 'dhcpv6': self.set_dhcpv6(True) - elif not is_intf_addr_assigned(self.ifname, addr): - tmp = f'ip addr add {addr} dev {self.ifname}' + elif not is_intf_addr_assigned(self.ifname, addr, netns=netns): + netns_cmd = f'ip netns exec {netns}' if netns else '' + tmp = f'{netns_cmd} ip addr add {addr} dev {self.ifname}' # Add broadcast address for IPv4 if is_ipv4(addr): tmp += ' brd +' @@ -1189,13 +1206,17 @@ class Interface(Control): if not addr: raise ValueError() + # get interface network namespace if specified + netns = self.config.get('netns', None) + # remove from interface if addr == 'dhcp': self.set_dhcp(False) elif addr == 'dhcpv6': self.set_dhcpv6(False) - elif is_intf_addr_assigned(self.ifname, addr): - self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"') + elif is_intf_addr_assigned(self.ifname, addr, netns=netns): + netns_cmd = f'ip netns exec {netns}' if netns else '' + self._cmd(f'{netns_cmd} ip addr del {addr} dev {self.ifname}') else: return False @@ -1215,8 +1236,11 @@ class Interface(Control): self.set_dhcp(False) self.set_dhcpv6(False) + netns = get_interface_namespace(self.ifname) + netns_cmd = f'ip netns exec {netns}' if netns else '' + cmd = f'{netns_cmd} ip addr flush dev {self.ifname}' # flush all addresses - self._cmd(f'ip addr flush dev "{self.ifname}"') + self._cmd(cmd) def add_to_bridge(self, bridge_dict): """ @@ -1371,6 +1395,11 @@ class Interface(Control): # - https://man7.org/linux/man-pages/man8/tc-mirred.8.html # Depening if we are the source or the target interface of the port # mirror we need to setup some variables. + + # Don't allow for netns yet + if 'netns' in self.config: + return None + source_if = self.config['ifname'] mirror_config = None @@ -1471,8 +1500,8 @@ class Interface(Control): # Since the interface is pushed onto a separate logical stack # Configure NETNS if dict_search('netns', config) != None: - self.set_netns(config.get('netns', '')) - return + if not is_netns_interface(self.ifname, self.config['netns']): + self.set_netns(config.get('netns', '')) else: self.del_netns(config.get('netns', '')) diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index 6a9911588..1fe5db7cd 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -1,4 +1,4 @@ -# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2023 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -13,9 +13,15 @@ # 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/>. +from json import loads + from vyos import ConfigError +from vyos.configdict import list_diff from vyos.ifconfig import Interface +from vyos.utils.assertion import assert_list from vyos.utils.dict import dict_search +from vyos.utils.network import get_interface_config +from vyos.utils.network import get_vxlan_vlan_tunnels @Interface.register class VXLANIf(Interface): @@ -49,6 +55,13 @@ class VXLANIf(Interface): } } + _command_set = {**Interface._command_set, **{ + 'vlan_tunnel': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'shellcmd': 'bridge link set dev {ifname} vlan_tunnel {value}', + }, + }} + def _create(self): # This table represents a mapping from VyOS internal config dict to # arguments used by iproute2. For more information please refer to: @@ -99,3 +112,54 @@ class VXLANIf(Interface): cmd = f'bridge fdb append to 00:00:00:00:00:00 dst {remote} ' \ 'port {port} dev {ifname}' self._cmd(cmd.format(**self.config)) + + def set_vlan_vni_mapping(self, state): + """ + Controls whether vlan to tunnel mapping is enabled on the port. + By default this flag is off. + """ + if not isinstance(state, bool): + raise ValueError('Value out of range') + + cur_vlan_ids = [] + if 'vlan_to_vni_removed' in self.config: + cur_vlan_ids = self.config['vlan_to_vni_removed'] + for vlan in cur_vlan_ids: + self._cmd(f'bridge vlan del dev {self.ifname} vid {vlan}') + + # Determine current OS Kernel vlan_tunnel setting - only adjust when needed + tmp = get_interface_config(self.ifname) + cur_state = 'on' if dict_search(f'linkinfo.info_slave_data.vlan_tunnel', tmp) == True else 'off' + new_state = 'on' if state else 'off' + if cur_state != new_state: + self.set_interface('vlan_tunnel', new_state) + + # Determine current OS Kernel configured VLANs + os_configured_vlan_ids = get_vxlan_vlan_tunnels(self.ifname) + + if 'vlan_to_vni' in self.config: + add_vlan = list_diff(list(self.config['vlan_to_vni'].keys()), os_configured_vlan_ids) + + for vlan, vlan_config in self.config['vlan_to_vni'].items(): + # VLAN mapping already exists - skip + if vlan not in add_vlan: + continue + + vni = vlan_config['vni'] + # The following commands must be run one after another, + # they can not be combined with linux 6.1 and iproute2 6.1 + self._cmd(f'bridge vlan add dev {self.ifname} vid {vlan}') + self._cmd(f'bridge vlan add dev {self.ifname} vid {vlan} tunnel_info id {vni}') + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class last + super().update(config) + + # Enable/Disable VLAN tunnel mapping + # This is only possible after the interface was assigned to the bridge + self.set_vlan_vni_mapping(dict_search('vlan_to_vni', config) != None) diff --git a/python/vyos/template.py b/python/vyos/template.py index e167488c6..c1b57b883 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -663,6 +663,84 @@ def nat_static_rule(rule_conf, rule_id, nat_type): from vyos.nat import parse_nat_static_rule return parse_nat_static_rule(rule_conf, rule_id, nat_type) +@register_filter('conntrack_ignore_rule') +def conntrack_ignore_rule(rule_conf, rule_id, ipv6=False): + ip_prefix = 'ip6' if ipv6 else 'ip' + def_suffix = '6' if ipv6 else '' + output = [] + + if 'inbound_interface' in rule_conf: + ifname = rule_conf['inbound_interface'] + output.append(f'iifname {ifname}') + + if 'protocol' in rule_conf: + proto = rule_conf['protocol'] + output.append(f'meta l4proto {proto}') + + for side in ['source', 'destination']: + if side in rule_conf: + side_conf = rule_conf[side] + prefix = side[0] + + if 'address' in side_conf: + address = side_conf['address'] + operator = '' + if address[0] == '!': + operator = '!=' + address = address[1:] + output.append(f'{ip_prefix} {prefix}addr {operator} {address}') + + if 'port' in side_conf: + port = side_conf['port'] + operator = '' + if port[0] == '!': + operator = '!=' + port = port[1:] + output.append(f'th {prefix}port {operator} {port}') + + if 'group' in side_conf: + group = side_conf['group'] + + if 'address_group' in group: + group_name = group['address_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'{ip_prefix} {prefix}addr {operator} @A{def_suffix}_{group_name}') + # Generate firewall group domain-group + elif 'domain_group' in group: + group_name = group['domain_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'{ip_prefix} {prefix}addr {operator} @D_{group_name}') + elif 'network_group' in group: + group_name = group['network_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'{ip_prefix} {prefix}addr {operator} @N{def_suffix}_{group_name}') + if 'port_group' in group: + group_name = group['port_group'] + + if proto == 'tcp_udp': + proto = 'th' + + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + + output.append(f'{proto} {prefix}port {operator} @P_{group_name}') + + output.append('counter notrack') + output.append(f'comment "ignore-{rule_id}"') + + return " ".join(output) + @register_filter('range_to_regex') def range_to_regex(num_range): """Convert range of numbers or list of ranges diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 2f181d8d9..4c579c760 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -40,13 +40,19 @@ def interface_exists(interface) -> bool: import os return os.path.exists(f'/sys/class/net/{interface}') -def interface_exists_in_netns(interface_name, netns): +def is_netns_interface(interface, netns): from vyos.utils.process import rc_cmd - rc, out = rc_cmd(f'ip netns exec {netns} ip link show dev {interface_name}') + rc, out = rc_cmd(f'sudo ip netns exec {netns} ip link show dev {interface}') if rc == 0: return True return False +def get_netns_all() -> list: + from json import loads + from vyos.utils.process import cmd + tmp = loads(cmd('ip --json netns ls')) + return [ netns['name'] for netns in tmp ] + def get_vrf_members(vrf: str) -> list: """ Get list of interface VRF members @@ -78,8 +84,7 @@ def get_interface_config(interface): """ Returns the used encapsulation protocol for given interface. If interface does not exist, None is returned. """ - import os - if not os.path.exists(f'/sys/class/net/{interface}'): + if not interface_exists(interface): return None from json import loads from vyos.utils.process import cmd @@ -90,33 +95,63 @@ def get_interface_address(interface): """ Returns the used encapsulation protocol for given interface. If interface does not exist, None is returned. """ - import os - if not os.path.exists(f'/sys/class/net/{interface}'): + if not interface_exists(interface): return None from json import loads from vyos.utils.process import cmd tmp = loads(cmd(f'ip --detail --json addr show dev {interface}'))[0] return tmp -def get_interface_namespace(iface): +def get_interface_namespace(interface: str): """ Returns wich netns the interface belongs to """ from json import loads from vyos.utils.process import cmd - # Check if netns exist - tmp = loads(cmd(f'ip --json netns ls')) - if len(tmp) == 0: - return None - for ns in tmp: + # Bail out early if netns does not exist + tmp = cmd(f'ip --json netns ls') + if not tmp: return None + + for ns in loads(tmp): netns = f'{ns["name"]}' # Search interface in each netns data = loads(cmd(f'ip netns exec {netns} ip --json link show')) for tmp in data: - if iface == tmp["ifname"]: + if interface == tmp["ifname"]: return netns +def is_ipv6_tentative(iface: str, ipv6_address: str) -> bool: + """Check if IPv6 address is in tentative state. + + This function checks if an IPv6 address on a specific network interface is + in the tentative state. IPv6 tentative addresses are not fully configured + and are undergoing Duplicate Address Detection (DAD) to ensure they are + unique on the network. + + Args: + iface (str): The name of the network interface. + ipv6_address (str): The IPv6 address to check. + + Returns: + bool: True if the IPv6 address is tentative, False otherwise. + """ + import json + from vyos.utils.process import rc_cmd + + rc, out = rc_cmd(f'ip -6 --json address show dev {iface} scope global') + if rc: + return False + + data = json.loads(out) + for addr_info in data[0]['addr_info']: + if ( + addr_info.get('local') == ipv6_address and + addr_info.get('tentative', False) + ): + return True + return False + def is_wwan_connected(interface): """ Determine if a given WWAN interface, e.g. wwan0 is connected to the carrier network or not """ @@ -141,8 +176,7 @@ def is_wwan_connected(interface): def get_bridge_fdb(interface): """ Returns the forwarding database entries for a given interface """ - import os - if not os.path.exists(f'/sys/class/net/{interface}'): + if not interface_exists(interface): return None from json import loads from vyos.utils.process import cmd @@ -274,57 +308,33 @@ def is_addr_assigned(ip_address, vrf=None) -> bool: return False -def is_intf_addr_assigned(intf, address) -> bool: +def is_intf_addr_assigned(ifname: str, addr: str, netns: str=None) -> bool: """ Verify if the given IPv4/IPv6 address is assigned to specific interface. It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR address 192.0.2.1/24. """ - from vyos.template import is_ipv4 - - from netifaces import ifaddresses - from netifaces import AF_INET - from netifaces import AF_INET6 - - # check if the requested address type is configured at all - # { - # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], - # 2: [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}], - # 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}] - # } - try: - addresses = ifaddresses(intf) - except ValueError as e: - print(e) - return False - - # determine IP version (AF_INET or AF_INET6) depending on passed address - addr_type = AF_INET if is_ipv4(address) else AF_INET6 - - # Check every IP address on this interface for a match - netmask = None - if '/' in address: - address, netmask = address.split('/') - for ip in addresses.get(addr_type, []): - # ip can have the interface name in the 'addr' field, we need to remove it - # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'} - ip_addr = ip['addr'].split('%')[0] - - if not _are_same_ip(address, ip_addr): - continue - - # we do not have a netmask to compare against, they are the same - if not netmask: - return True + import json + import jmespath - prefixlen = '' - if is_ipv4(ip_addr): - prefixlen = sum([bin(int(_)).count('1') for _ in ip['netmask'].split('.')]) - else: - prefixlen = sum([bin(int(_,16)).count('1') for _ in ip['netmask'].split('/')[0].split(':') if _]) + from vyos.utils.process import rc_cmd + from ipaddress import ip_interface - if str(prefixlen) == netmask: - return True + netns_cmd = f'ip netns exec {netns}' if netns else '' + rc, out = rc_cmd(f'{netns_cmd} ip --json address show dev {ifname}') + if rc == 0: + json_out = json.loads(out) + addresses = jmespath.search("[].addr_info[].{family: family, address: local, prefixlen: prefixlen}", json_out) + for address_info in addresses: + family = address_info['family'] + address = address_info['address'] + prefixlen = address_info['prefixlen'] + # Remove the interface name if present in the given address + if '%' in addr: + addr = addr.split('%')[0] + interface = ip_interface(f"{address}/{prefixlen}") + if ip_interface(addr) == interface or address == addr: + return True return False @@ -398,7 +408,7 @@ def is_subnet_connected(subnet, primary=False): return False -def is_afi_configured(interface, afi): +def is_afi_configured(interface: str, afi): """ Check if given address family is configured, or in other words - an IP address is assigned to the interface. """ from netifaces import ifaddresses @@ -415,3 +425,46 @@ def is_afi_configured(interface, afi): return False return afi in addresses + +def get_vxlan_vlan_tunnels(interface: str) -> list: + """ Return a list of strings with VLAN IDs configured in the Kernel """ + from json import loads + from vyos.utils.process import cmd + + if not interface.startswith('vxlan'): + raise ValueError('Only applicable for VXLAN interfaces!') + + # Determine current OS Kernel configured VLANs + # + # $ bridge -j -p vlan tunnelshow dev vxlan0 + # [ { + # "ifname": "vxlan0", + # "tunnels": [ { + # "vlan": 10, + # "vlanEnd": 11, + # "tunid": 10010, + # "tunidEnd": 10011 + # },{ + # "vlan": 20, + # "tunid": 10020 + # } ] + # } ] + # + os_configured_vlan_ids = [] + tmp = loads(cmd(f'bridge --json vlan tunnelshow dev {interface}')) + if tmp: + for tunnel in tmp[0].get('tunnels', {}): + vlanStart = tunnel['vlan'] + if 'vlanEnd' in tunnel: + vlanEnd = tunnel['vlanEnd'] + # Build a real list for user VLAN IDs + vlan_list = list(range(vlanStart, vlanEnd +1)) + # Convert list of integers to list or strings + os_configured_vlan_ids.extend(map(str, vlan_list)) + # Proceed with next tunnel - this one is complete + continue + + # Add single tunel id - not part of a range + os_configured_vlan_ids.append(str(vlanStart)) + + return os_configured_vlan_ids diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py index e09c7d86d..9ecdddf09 100644 --- a/python/vyos/utils/process.py +++ b/python/vyos/utils/process.py @@ -139,7 +139,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, expect: a list of error codes to consider as normal """ decoded, code = popen( - command, flag, + command.lstrip(), flag, stdout=stdout, stderr=stderr, input=input, timeout=timeout, env=env, shell=shell, @@ -170,7 +170,7 @@ def rc_cmd(command, flag='', shell=None, input=None, timeout=None, env=None, (1, 'Device "eth99" does not exist.') """ out, code = popen( - command, flag, + command.lstrip(), flag, stdout=stdout, stderr=stderr, input=input, timeout=timeout, env=env, shell=shell, diff --git a/smoketest/bin/vyos-configtest b/smoketest/bin/vyos-configtest index 3e42b0380..c1b602737 100755 --- a/smoketest/bin/vyos-configtest +++ b/smoketest/bin/vyos-configtest @@ -24,6 +24,7 @@ from vyos.configsession import ConfigSession, ConfigSessionError from vyos import ConfigError config_dir = '/usr/libexec/vyos/tests/config' +config_test_dir = '/usr/libexec/vyos/tests/config-tests' save_config = '/tmp/vyos-configtest-save' class DynamicClassBase(unittest.TestCase): @@ -42,7 +43,7 @@ class DynamicClassBase(unittest.TestCase): except OSError: pass -def make_test_function(filename): +def make_test_function(filename, test_path=None): def test_config_load(self): config_path = os.path.join(config_dir, filename) self.session.migrate_and_load_config(config_path) @@ -51,6 +52,16 @@ def make_test_function(filename): except (ConfigError, ConfigSessionError): self.session.discard() self.fail() + + if test_path: + config_commands = self.session.show(['configuration', 'commands']) + + with open(test_path, 'r') as f: + for line in f.readlines(): + if not line or line.startswith("#"): + continue + + self.assertIn(line, config_commands) return test_config_load def class_name_from_func_name(s): @@ -69,10 +80,18 @@ if __name__ == '__main__': config_list.sort() for config in config_list: - test_func = make_test_function(config) + test_path = os.path.join(config_test_dir, config) + + if not os.path.exists(test_path): + test_path = None + else: + log.info(f'Loaded migration result test for config "{config}"') + + test_func = make_test_function(config, test_path) func_name = config.replace('-', '_') klassname = f'TestConfig{class_name_from_func_name(func_name)}' + globals()[klassname] = type(klassname, (DynamicClassBase,), {f'test_{func_name}': test_func}) diff --git a/smoketest/config-tests/basic-vyos b/smoketest/config-tests/basic-vyos new file mode 100644 index 000000000..ef8bf374a --- /dev/null +++ b/smoketest/config-tests/basic-vyos @@ -0,0 +1,62 @@ +set interfaces ethernet eth0 address '192.168.0.1/24' +set interfaces ethernet eth0 duplex 'auto' +set interfaces ethernet eth0 speed 'auto' +set interfaces ethernet eth1 duplex 'auto' +set interfaces ethernet eth1 speed 'auto' +set interfaces ethernet eth2 duplex 'auto' +set interfaces ethernet eth2 speed 'auto' +set interfaces ethernet eth2 vif 100 address '100.100.0.1/24' +set interfaces ethernet eth2 vif-s 200 address '100.64.200.254/24' +set interfaces ethernet eth2 vif-s 200 vif-c 201 address '100.64.201.254/24' +set interfaces ethernet eth2 vif-s 200 vif-c 202 address '100.64.202.254/24' +set interfaces loopback lo +set protocols static arp interface eth0 address 192.168.0.20 mac '00:50:00:00:00:20' +set protocols static arp interface eth0 address 192.168.0.30 mac '00:50:00:00:00:30' +set protocols static arp interface eth0 address 192.168.0.40 mac '00:50:00:00:00:40' +set protocols static arp interface eth2.100 address 100.100.0.2 mac '00:50:00:00:02:02' +set protocols static arp interface eth2.100 address 100.100.0.3 mac '00:50:00:00:02:03' +set protocols static arp interface eth2.100 address 100.100.0.4 mac '00:50:00:00:02:04' +set protocols static arp interface eth2.200 address 100.64.200.1 mac '00:50:00:00:00:01' +set protocols static arp interface eth2.200 address 100.64.200.2 mac '00:50:00:00:00:02' +set protocols static arp interface eth2.200.201 address 100.64.201.10 mac '00:50:00:00:00:10' +set protocols static arp interface eth2.200.201 address 100.64.201.20 mac '00:50:00:00:00:20' +set protocols static arp interface eth2.200.202 address 100.64.202.30 mac '00:50:00:00:00:30' +set protocols static arp interface eth2.200.202 address 100.64.202.40 mac '00:50:00:00:00:40' +set protocols static route 0.0.0.0/0 next-hop 100.64.0.1 +set service dhcp-server shared-network-name LAN authoritative +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 default-router '192.168.0.1' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-name 'vyos.net' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-search 'vyos.net' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 name-server '192.168.0.1' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic start '192.168.0.20' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic stop '192.168.0.240' +set service dns forwarding allow-from '192.168.0.0/16' +set service dns forwarding cache-size '10000' +set service dns forwarding dnssec 'off' +set service dns forwarding listen-address '192.168.0.1' +set service ssh ciphers 'aes128-ctr' +set service ssh ciphers 'aes192-ctr' +set service ssh ciphers 'aes256-ctr' +set service ssh ciphers 'chacha20-poly1305@openssh.com' +set service ssh ciphers 'rijndael-cbc@lysator.liu.se' +set service ssh key-exchange 'curve25519-sha256@libssh.org' +set service ssh key-exchange 'diffie-hellman-group1-sha1' +set service ssh key-exchange 'diffie-hellman-group-exchange-sha1' +set service ssh key-exchange 'diffie-hellman-group-exchange-sha256' +set service ssh listen-address '192.168.0.1' +set service ssh port '22' +set system config-management commit-revisions '100' +set system console device ttyS0 speed '115200' +set system host-name 'vyos' +set system name-server '192.168.0.1' +set system syslog console facility all level 'emerg' +set system syslog console facility mail level 'info' +set system syslog global facility all level 'info' +set system syslog global facility auth level 'info' +set system syslog global facility local7 level 'debug' +set system syslog global preserve-fqdn +set system syslog host syslog.vyos.net facility auth level 'warning' +set system syslog host syslog.vyos.net facility local7 level 'notice' +set system syslog host syslog.vyos.net format octet-counted +set system syslog host syslog.vyos.net port '8000' +set system time-zone 'Europe/Berlin' diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn new file mode 100644 index 000000000..37baee0fd --- /dev/null +++ b/smoketest/config-tests/dialup-router-medium-vpn @@ -0,0 +1,321 @@ +set firewall global-options all-ping 'enable' +set firewall global-options broadcast-ping 'disable' +set firewall global-options ip-src-route 'disable' +set firewall global-options ipv6-receive-redirects 'disable' +set firewall global-options ipv6-src-route 'disable' +set firewall global-options log-martians 'enable' +set firewall global-options receive-redirects 'disable' +set firewall global-options send-redirects 'enable' +set firewall global-options source-validation 'disable' +set firewall global-options syn-cookies 'disable' +set firewall global-options twa-hazards-protection 'enable' +set firewall ipv4 name test_tcp_flags rule 1 action 'drop' +set firewall ipv4 name test_tcp_flags rule 1 protocol 'tcp' +set firewall ipv4 name test_tcp_flags rule 1 tcp flags ack +set firewall ipv4 name test_tcp_flags rule 1 tcp flags not fin +set firewall ipv4 name test_tcp_flags rule 1 tcp flags not rst +set firewall ipv4 name test_tcp_flags rule 1 tcp flags syn +set high-availability vrrp group LAN address 192.168.0.1/24 +set high-availability vrrp group LAN hello-source-address '192.168.0.250' +set high-availability vrrp group LAN interface 'eth1' +set high-availability vrrp group LAN peer-address '192.168.0.251' +set high-availability vrrp group LAN priority '200' +set high-availability vrrp group LAN vrid '1' +set high-availability vrrp sync-group failover-group member 'LAN' +set interfaces ethernet eth0 duplex 'auto' +set interfaces ethernet eth0 mtu '9000' +set interfaces ethernet eth0 offload gro +set interfaces ethernet eth0 offload gso +set interfaces ethernet eth0 offload sg +set interfaces ethernet eth0 offload tso +set interfaces ethernet eth0 speed 'auto' +set interfaces ethernet eth1 address '192.168.0.250/24' +set interfaces ethernet eth1 duplex 'auto' +set interfaces ethernet eth1 ip source-validation 'strict' +set interfaces ethernet eth1 mtu '9000' +set interfaces ethernet eth1 offload gro +set interfaces ethernet eth1 offload gso +set interfaces ethernet eth1 offload sg +set interfaces ethernet eth1 offload tso +set interfaces ethernet eth1 speed 'auto' +set interfaces loopback lo +set interfaces openvpn vtun0 encryption cipher 'aes256' +set interfaces openvpn vtun0 hash 'sha512' +set interfaces openvpn vtun0 ip adjust-mss '1380' +set interfaces openvpn vtun0 ip source-validation 'strict' +set interfaces openvpn vtun0 keep-alive failure-count '3' +set interfaces openvpn vtun0 keep-alive interval '30' +set interfaces openvpn vtun0 mode 'client' +set interfaces openvpn vtun0 openvpn-option 'comp-lzo adaptive' +set interfaces openvpn vtun0 openvpn-option 'fast-io' +set interfaces openvpn vtun0 openvpn-option 'persist-key' +set interfaces openvpn vtun0 openvpn-option 'reneg-sec 86400' +set interfaces openvpn vtun0 persistent-tunnel +set interfaces openvpn vtun0 remote-host '192.0.2.10' +set interfaces openvpn vtun0 tls auth-key 'openvpn_vtun0_auth' +set interfaces openvpn vtun0 tls ca-certificate 'openvpn_vtun0_1' +set interfaces openvpn vtun0 tls ca-certificate 'openvpn_vtun0_2' +set interfaces openvpn vtun0 tls certificate 'openvpn_vtun0' +set interfaces openvpn vtun1 authentication password 'vyos1' +set interfaces openvpn vtun1 authentication username 'vyos1' +set interfaces openvpn vtun1 encryption cipher 'aes256' +set interfaces openvpn vtun1 hash 'sha1' +set interfaces openvpn vtun1 ip adjust-mss '1380' +set interfaces openvpn vtun1 keep-alive failure-count '3' +set interfaces openvpn vtun1 keep-alive interval '30' +set interfaces openvpn vtun1 mode 'client' +set interfaces openvpn vtun1 openvpn-option 'comp-lzo adaptive' +set interfaces openvpn vtun1 openvpn-option 'tun-mtu 1500' +set interfaces openvpn vtun1 openvpn-option 'tun-mtu-extra 32' +set interfaces openvpn vtun1 openvpn-option 'mssfix 1300' +set interfaces openvpn vtun1 openvpn-option 'persist-key' +set interfaces openvpn vtun1 openvpn-option 'mute 10' +set interfaces openvpn vtun1 openvpn-option 'route-nopull' +set interfaces openvpn vtun1 openvpn-option 'fast-io' +set interfaces openvpn vtun1 openvpn-option 'reneg-sec 86400' +set interfaces openvpn vtun1 persistent-tunnel +set interfaces openvpn vtun1 protocol 'udp' +set interfaces openvpn vtun1 remote-host '01.foo.com' +set interfaces openvpn vtun1 remote-port '1194' +set interfaces openvpn vtun1 tls auth-key 'openvpn_vtun1_auth' +set interfaces openvpn vtun1 tls ca-certificate 'openvpn_vtun1_1' +set interfaces openvpn vtun1 tls ca-certificate 'openvpn_vtun1_2' +set interfaces openvpn vtun2 authentication password 'vyos2' +set interfaces openvpn vtun2 authentication username 'vyos2' +set interfaces openvpn vtun2 disable +set interfaces openvpn vtun2 encryption cipher 'aes256' +set interfaces openvpn vtun2 hash 'sha512' +set interfaces openvpn vtun2 ip adjust-mss '1380' +set interfaces openvpn vtun2 keep-alive failure-count '3' +set interfaces openvpn vtun2 keep-alive interval '30' +set interfaces openvpn vtun2 mode 'client' +set interfaces openvpn vtun2 openvpn-option 'tun-mtu 1500' +set interfaces openvpn vtun2 openvpn-option 'tun-mtu-extra 32' +set interfaces openvpn vtun2 openvpn-option 'mssfix 1300' +set interfaces openvpn vtun2 openvpn-option 'persist-key' +set interfaces openvpn vtun2 openvpn-option 'mute 10' +set interfaces openvpn vtun2 openvpn-option 'route-nopull' +set interfaces openvpn vtun2 openvpn-option 'fast-io' +set interfaces openvpn vtun2 openvpn-option 'remote-random' +set interfaces openvpn vtun2 openvpn-option 'reneg-sec 86400' +set interfaces openvpn vtun2 persistent-tunnel +set interfaces openvpn vtun2 protocol 'udp' +set interfaces openvpn vtun2 remote-host '01.myvpn.com' +set interfaces openvpn vtun2 remote-host '02.myvpn.com' +set interfaces openvpn vtun2 remote-host '03.myvpn.com' +set interfaces openvpn vtun2 remote-port '1194' +set interfaces openvpn vtun2 tls auth-key 'openvpn_vtun2_auth' +set interfaces openvpn vtun2 tls ca-certificate 'openvpn_vtun2_1' +set interfaces pppoe pppoe0 authentication password 'password' +set interfaces pppoe pppoe0 authentication username 'vyos' +set interfaces pppoe pppoe0 mtu '1500' +set interfaces pppoe pppoe0 source-interface 'eth0' +set interfaces wireguard wg0 address '192.168.10.1/24' +set interfaces wireguard wg0 ip adjust-mss '1380' +set interfaces wireguard wg0 peer blue allowed-ips '192.168.10.3/32' +set interfaces wireguard wg0 peer blue persistent-keepalive '20' +set interfaces wireguard wg0 peer blue preshared-key 'ztFDOY9UyaDvn8N3X97SFMDwIfv7EEfuUIPP2yab6UI=' +set interfaces wireguard wg0 peer blue public-key 'G4pZishpMRrLmd96Kr6V7LIuNGdcUb81gWaYZ+FWkG0=' +set interfaces wireguard wg0 peer green allowed-ips '192.168.10.21/32' +set interfaces wireguard wg0 peer green persistent-keepalive '25' +set interfaces wireguard wg0 peer green preshared-key 'LQ9qmlTh9G4nZu4UgElxRUwg7JB/qoV799aADJOijnY=' +set interfaces wireguard wg0 peer green public-key '5iQUD3VoCDBTPXAPHOwUJ0p7xzKGHEY/wQmgvBVmaFI=' +set interfaces wireguard wg0 peer pink allowed-ips '192.168.10.14/32' +set interfaces wireguard wg0 peer pink allowed-ips '192.168.10.16/32' +set interfaces wireguard wg0 peer pink persistent-keepalive '25' +set interfaces wireguard wg0 peer pink preshared-key 'Qi9Odyx0/5itLPN5C5bEy3uMX+tmdl15QbakxpKlWqQ=' +set interfaces wireguard wg0 peer pink public-key 'i4qNPmxyy9EETL4tIoZOLKJF4p7IlVmpAE15gglnAk4=' +set interfaces wireguard wg0 peer red allowed-ips '192.168.10.4/32' +set interfaces wireguard wg0 peer red persistent-keepalive '20' +set interfaces wireguard wg0 peer red preshared-key 'CumyXX7osvUT9AwnS+m2TEfCaL0Ptc2LfuZ78Sujuk8=' +set interfaces wireguard wg0 peer red public-key 'ALGWvMJCKpHF2tVH3hEIHqUe9iFfAmZATUUok/WQzks=' +set interfaces wireguard wg0 port '7777' +set interfaces wireguard wg1 address '10.89.90.2/30' +set interfaces wireguard wg1 ip adjust-mss '1380' +set interfaces wireguard wg1 peer sam address '192.0.2.45' +set interfaces wireguard wg1 peer sam allowed-ips '10.1.1.0/24' +set interfaces wireguard wg1 peer sam allowed-ips '10.89.90.1/32' +set interfaces wireguard wg1 peer sam persistent-keepalive '20' +set interfaces wireguard wg1 peer sam port '1200' +set interfaces wireguard wg1 peer sam preshared-key 'XpFtzx2Z+nR8pBv9/sSf7I94OkZkVYTz0AeU5Q/QQUE=' +set interfaces wireguard wg1 peer sam public-key 'v5zfKGvH6W/lfDXJ0en96lvKo1gfFxMUWxe02+Fj5BU=' +set interfaces wireguard wg1 port '7778' +set nat destination rule 50 destination port '49371' +set nat destination rule 50 inbound-interface 'pppoe0' +set nat destination rule 50 protocol 'tcp_udp' +set nat destination rule 50 translation address '192.168.0.5' +set nat destination rule 51 destination port '58050-58051' +set nat destination rule 51 inbound-interface 'pppoe0' +set nat destination rule 51 protocol 'tcp' +set nat destination rule 51 translation address '192.168.0.5' +set nat destination rule 52 destination port '22067-22070' +set nat destination rule 52 inbound-interface 'pppoe0' +set nat destination rule 52 protocol 'tcp' +set nat destination rule 52 translation address '192.168.0.5' +set nat destination rule 53 destination port '34342' +set nat destination rule 53 inbound-interface 'pppoe0' +set nat destination rule 53 protocol 'tcp_udp' +set nat destination rule 53 translation address '192.168.0.121' +set nat destination rule 54 destination port '45459' +set nat destination rule 54 inbound-interface 'pppoe0' +set nat destination rule 54 protocol 'tcp_udp' +set nat destination rule 54 translation address '192.168.0.120' +set nat destination rule 55 destination port '22' +set nat destination rule 55 inbound-interface 'pppoe0' +set nat destination rule 55 protocol 'tcp' +set nat destination rule 55 translation address '192.168.0.5' +set nat destination rule 56 destination port '8920' +set nat destination rule 56 inbound-interface 'pppoe0' +set nat destination rule 56 protocol 'tcp' +set nat destination rule 56 translation address '192.168.0.5' +set nat destination rule 60 destination port '80,443' +set nat destination rule 60 inbound-interface 'pppoe0' +set nat destination rule 60 protocol 'tcp' +set nat destination rule 60 translation address '192.168.0.5' +set nat destination rule 70 destination port '5001' +set nat destination rule 70 inbound-interface 'pppoe0' +set nat destination rule 70 protocol 'tcp' +set nat destination rule 70 translation address '192.168.0.5' +set nat destination rule 80 destination port '25' +set nat destination rule 80 inbound-interface 'pppoe0' +set nat destination rule 80 protocol 'tcp' +set nat destination rule 80 translation address '192.168.0.5' +set nat destination rule 90 destination port '8123' +set nat destination rule 90 inbound-interface 'pppoe0' +set nat destination rule 90 protocol 'tcp' +set nat destination rule 90 translation address '192.168.0.7' +set nat destination rule 91 destination port '1880' +set nat destination rule 91 inbound-interface 'pppoe0' +set nat destination rule 91 protocol 'tcp' +set nat destination rule 91 translation address '192.168.0.7' +set nat destination rule 500 destination address '!192.168.0.0/24' +set nat destination rule 500 destination port '53' +set nat destination rule 500 inbound-interface 'eth1' +set nat destination rule 500 protocol 'tcp_udp' +set nat destination rule 500 source address '!192.168.0.1-192.168.0.5' +set nat destination rule 500 translation address '192.168.0.1' +set nat source rule 1000 outbound-interface 'pppoe0' +set nat source rule 1000 translation address 'masquerade' +set nat source rule 2000 outbound-interface 'vtun0' +set nat source rule 2000 source address '192.168.0.0/16' +set nat source rule 2000 translation address 'masquerade' +set nat source rule 3000 outbound-interface 'vtun1' +set nat source rule 3000 translation address 'masquerade' +set policy prefix-list user1-routes rule 1 action 'permit' +set policy prefix-list user1-routes rule 1 prefix '192.168.0.0/24' +set policy prefix-list user2-routes rule 1 action 'permit' +set policy prefix-list user2-routes rule 1 prefix '10.1.1.0/24' +set policy route LAN-POLICY-BASED-ROUTING interface 'eth1' +set policy route LAN-POLICY-BASED-ROUTING rule 10 destination +set policy route LAN-POLICY-BASED-ROUTING rule 10 disable +set policy route LAN-POLICY-BASED-ROUTING rule 10 set table '10' +set policy route LAN-POLICY-BASED-ROUTING rule 10 source address '192.168.0.119/32' +set policy route LAN-POLICY-BASED-ROUTING rule 20 destination +set policy route LAN-POLICY-BASED-ROUTING rule 20 set table '100' +set policy route LAN-POLICY-BASED-ROUTING rule 20 source address '192.168.0.240' +set policy route-map rm-static-to-bgp rule 10 action 'permit' +set policy route-map rm-static-to-bgp rule 10 match ip address prefix-list 'user1-routes' +set policy route-map rm-static-to-bgp rule 100 action 'deny' +set policy route6 LAN6-POLICY-BASED-ROUTING interface 'eth1' +set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 destination +set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 disable +set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 set table '10' +set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 source address '2002::1' +set policy route6 LAN6-POLICY-BASED-ROUTING rule 20 destination +set policy route6 LAN6-POLICY-BASED-ROUTING rule 20 set table '100' +set policy route6 LAN6-POLICY-BASED-ROUTING rule 20 source address '2008::f' +set protocols bgp address-family ipv4-unicast redistribute connected route-map 'rm-static-to-bgp' +set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast nexthop-self +set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast prefix-list export 'user1-routes' +set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast prefix-list import 'user2-routes' +set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast soft-reconfiguration inbound +set protocols bgp neighbor 10.89.90.1 password 'ericandre2020' +set protocols bgp neighbor 10.89.90.1 remote-as '64589' +set protocols bgp parameters log-neighbor-changes +set protocols bgp parameters router-id '10.89.90.2' +set protocols bgp system-as '64590' +set protocols static route 100.64.160.23/32 interface pppoe0 +set protocols static route 100.64.165.25/32 interface pppoe0 +set protocols static route 100.64.165.26/32 interface pppoe0 +set protocols static route 100.64.198.0/24 interface vtun0 +set protocols static table 10 route 0.0.0.0/0 interface vtun1 +set protocols static table 100 route 0.0.0.0/0 next-hop 192.168.10.5 +set service conntrack-sync accept-protocol 'tcp' +set service conntrack-sync accept-protocol 'udp' +set service conntrack-sync accept-protocol 'icmp' +set service conntrack-sync disable-external-cache +set service conntrack-sync event-listen-queue-size '8' +set service conntrack-sync expect-sync 'all' +set service conntrack-sync failover-mechanism vrrp sync-group 'failover-group' +set service conntrack-sync interface eth1 peer '192.168.0.251' +set service conntrack-sync sync-queue-size '8' +set service dhcp-server failover name 'DHCP02' +set service dhcp-server failover remote '192.168.0.251' +set service dhcp-server failover source-address '192.168.0.250' +set service dhcp-server failover status 'primary' +set service dhcp-server shared-network-name LAN authoritative +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 default-router '192.168.0.1' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-name 'vyos.net' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-search 'vyos.net' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 enable-failover +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 lease '86400' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 name-server '192.168.0.1' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic start '192.168.0.200' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic stop '192.168.0.240' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Audio ip-address '192.168.0.107' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Audio mac-address '00:50:01:dc:91:14' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping IPTV ip-address '192.168.0.104' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping IPTV mac-address '00:50:01:31:b5:f6' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus ip-address '192.168.0.60' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus mac-address '00:50:01:58:ac:95' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 ip-address '192.168.0.109' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 mac-address '00:50:01:bc:ac:51' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 ip-address '192.168.0.11' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 mac-address '00:50:01:70:b9:4d' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 ip-address '192.168.0.12' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 mac-address '00:50:01:70:b7:4f' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping pearTV ip-address '192.168.0.101' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping pearTV mac-address '00:50:01:ba:62:79' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping sand ip-address '192.168.0.110' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping sand mac-address '00:50:01:af:c5:d2' +set service dns forwarding allow-from '192.168.0.0/16' +set service dns forwarding cache-size '8192' +set service dns forwarding dnssec 'off' +set service dns forwarding listen-address '192.168.0.1' +set service dns forwarding name-server 100.64.0.1 +set service dns forwarding name-server 100.64.0.2 +set service ntp allow-client address '192.168.0.0/16' +set service ntp server nz.pool.ntp.org prefer +set service snmp community AwesomeCommunity authorization 'ro' +set service snmp community AwesomeCommunity client '127.0.0.1' +set service snmp community AwesomeCommunity network '192.168.0.0/24' +set service ssh access-control allow user 'vyos' +set service ssh client-keepalive-interval '60' +set service ssh listen-address '192.168.0.1' +set service ssh listen-address '192.168.10.1' +set service ssh listen-address '192.168.0.250' +set system config-management commit-revisions '100' +set system console device ttyS0 speed '115200' +set system host-name 'vyos' +set system ip arp table-size '1024' +set system name-server '192.168.0.1' +set system name-server 'pppoe0' +set system option ctrl-alt-delete 'ignore' +set system option reboot-on-panic +set system option startup-beep +set system static-host-mapping host-name host60.vyos.net inet '192.168.0.60' +set system static-host-mapping host-name host104.vyos.net inet '192.168.0.104' +set system static-host-mapping host-name host107.vyos.net inet '192.168.0.107' +set system static-host-mapping host-name host109.vyos.net inet '192.168.0.109' +set system sysctl parameter net.core.default_qdisc value 'fq' +set system sysctl parameter net.ipv4.tcp_congestion_control value 'bbr' +set system syslog global facility all level 'info' +set system syslog host 192.168.0.252 facility all level 'debug' +set system syslog host 192.168.0.252 protocol 'udp' +set system task-scheduler task Update-Blacklists executable path '/config/scripts/vyos-foo-update.script' +set system task-scheduler task Update-Blacklists interval '3h' +set system time-zone 'Pacific/Auckland' diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos index 033c1a518..78dba3ee2 100644 --- a/smoketest/configs/basic-vyos +++ b/smoketest/configs/basic-vyos @@ -116,6 +116,18 @@ system { speed 115200 } } + conntrack { + ignore { + rule 1 { + destination { + address 192.0.2.2 + } + source { + address 192.0.2.1 + } + } + } + } host-name vyos login { user vyos { diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 820024dc9..51ccbc9e6 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -834,8 +834,12 @@ class BasicInterfaceTest: self.assertEqual('1', tmp) if cli_defined(self._base_path + ['ip'], 'source-validation'): - tmp = read_file(f'{proc_base}/rp_filter') - self.assertEqual('2', tmp) + base_options = f'iifname "{interface}"' + out = cmd('sudo nft list chain ip raw vyos_rpfilter') + for line in out.splitlines(): + if line.startswith(base_options): + self.assertIn('fib saddr oif 0', line) + self.assertIn('drop', line) def test_interface_ipv6_options(self): if not self._test_ipv6: diff --git a/smoketest/scripts/cli/test_dependency_graph.py b/smoketest/scripts/cli/test_dependency_graph.py deleted file mode 100755 index 45a40acc4..000000000 --- a/smoketest/scripts/cli/test_dependency_graph.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2022 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 json -import unittest -from graphlib import TopologicalSorter, CycleError - -DEP_FILE = '/usr/share/vyos/config-mode-dependencies.json' - -def graph_from_dict(d): - g = {} - for k in list(d): - g[k] = set() - # add the dependencies for every sub-case; should there be cases - # that are mutally exclusive in the future, the graphs will be - # distinguished - for el in list(d[k]): - g[k] |= set(d[k][el]) - return g - -class TestDependencyGraph(unittest.TestCase): - def setUp(self): - with open(DEP_FILE) as f: - dd = json.load(f) - self.dependency_graph = graph_from_dict(dd) - - def test_cycles(self): - ts = TopologicalSorter(self.dependency_graph) - out = None - try: - # get node iterator - order = ts.static_order() - # try iteration - _ = [*order] - except CycleError as e: - out = e.args - - self.assertIsNone(out) - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index ee6ccb710..f74ce4b72 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -274,8 +274,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['meta l4proto gre', f'oifname != "{interface}"', 'drop'], ['meta l4proto icmp', f'ct mark {mark_hex}', 'return'], ['chain NAME_smoketest'], - ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'], - ['tcp flags syn / syn,ack', 'tcp dport 8888', 'log prefix "[smoketest-2-R]" log level err', 'ip ttl > 102', 'reject'], + ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[ipv4-NAM-smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'], + ['tcp flags syn / syn,ack', 'tcp dport 8888', 'log prefix "[ipv4-NAM-smoketest-2-R]" log level err', 'ip ttl > 102', 'reject'], ['log prefix "[smoketest-default-D]"','smoketest default-action', 'drop'] ] @@ -331,7 +331,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): [f'meta l4proto tcp','queue to 3'], [f'meta l4proto udp','queue flags bypass,fanout to 0-15'], [f'chain NAME_{name}'], - ['ip length { 64, 512, 1024 }', 'ip dscp { 0x11, 0x34 }', f'log prefix "[{name}-6-A]" log group 66 snaplen 6666 queue-threshold 32000', 'accept'], + ['ip length { 64, 512, 1024 }', 'ip dscp { 0x11, 0x34 }', f'log prefix "[ipv4-NAM-{name}-6-A]" log group 66 snaplen 6666 queue-threshold 32000', 'accept'], ['ip length 1-30000', 'ip length != 60000-65535', 'ip dscp 0x03-0x0b', 'ip dscp != 0x15-0x19', 'accept'], [f'log prefix "[{name}-default-D]"', 'drop'] ] @@ -412,7 +412,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['type filter hook output priority filter; policy drop;'], ['meta l4proto gre', f'oifname "{interface}"', 'return'], [f'chain NAME6_{name}'], - ['saddr 2002::1', 'daddr 2002::1:1', 'log prefix "[v6-smoketest-1-A]" log level crit', 'accept'], + ['saddr 2002::1', 'daddr 2002::1:1', 'log prefix "[ipv6-NAM-v6-smoketest-1-A]" log level crit', 'accept'], [f'"{name} default-action drop"', f'log prefix "[{name}-default-D]"', 'drop'] ] @@ -526,26 +526,65 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables_chain([['accept']], 'raw', 'FW_CONNTRACK') self.verify_nftables_chain([['return']], 'ip6 raw', 'FW_CONNTRACK') + def test_bridge_basic_rules(self): + name = 'smoketest' + interface_in = 'eth0' + mac_address = '00:53:00:00:00:01' + vlan_id = '12' + vlan_prior = '3' + + self.cli_set(['firewall', 'bridge', 'name', name, 'default-action', 'accept']) + self.cli_set(['firewall', 'bridge', 'name', name, 'enable-default-log']) + self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'source', 'mac-address', mac_address]) + self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'inbound-interface', 'interface-name', interface_in]) + self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log', 'enable']) + self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log-options', 'level', 'crit']) + + self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'default-action', 'drop']) + self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'vlan', 'id', vlan_id]) + self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'action', 'jump']) + self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'jump-target', name]) + self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'vlan', 'priority', vlan_prior]) + + self.cli_commit() + + nftables_search = [ + ['chain VYOS_FORWARD_filter'], + ['type filter hook forward priority filter; policy drop;'], + [f'vlan id {vlan_id}', 'accept'], + [f'vlan pcp {vlan_prior}', f'jump NAME_{name}'], + [f'chain NAME_{name}'], + [f'ether saddr {mac_address}', f'iifname "{interface_in}"', f'log prefix "[bri-NAM-{name}-1-A]" log level crit', 'accept'] + ] + + self.verify_nftables(nftables_search, 'bridge vyos_filter') + def test_source_validation(self): # Strict self.cli_set(['firewall', 'global-options', 'source-validation', 'strict']) + self.cli_set(['firewall', 'global-options', 'ipv6-source-validation', 'strict']) self.cli_commit() nftables_strict_search = [ ['fib saddr . iif oif 0', 'drop'] ] - self.verify_nftables(nftables_strict_search, 'inet vyos_global_rpfilter') + self.verify_nftables_chain(nftables_strict_search, 'ip raw', 'vyos_global_rpfilter') + self.verify_nftables_chain(nftables_strict_search, 'ip6 raw', 'vyos_global_rpfilter') # Loose self.cli_set(['firewall', 'global-options', 'source-validation', 'loose']) + self.cli_set(['firewall', 'global-options', 'ipv6-source-validation', 'loose']) self.cli_commit() nftables_loose_search = [ ['fib saddr oif 0', 'drop'] ] - self.verify_nftables(nftables_loose_search, 'inet vyos_global_rpfilter') + self.verify_nftables_chain(nftables_loose_search, 'ip raw', 'vyos_global_rpfilter') + self.verify_nftables_chain(nftables_loose_search, 'ip6 raw', 'vyos_global_rpfilter') def test_sysfs(self): for name, conf in sysfs_config.items(): diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index f6b203de4..e9c9e68fd 100755 --- a/smoketest/scripts/cli/test_interfaces_vxlan.py +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py @@ -20,6 +20,8 @@ from vyos.configsession import ConfigSessionError from vyos.ifconfig import Interface from vyos.utils.network import get_bridge_fdb from vyos.utils.network import get_interface_config +from vyos.utils.network import interface_exists +from vyos.utils.network import get_vxlan_vlan_tunnels from vyos.template import is_ipv6 from base_interfaces_test import BasicInterfaceTest @@ -133,5 +135,53 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): self.assertTrue(options['linkinfo']['info_data']['external']) self.assertEqual('vxlan', options['linkinfo']['info_kind']) + def test_vxlan_vlan_vni_mapping(self): + bridge = 'br0' + interface = 'vxlan0' + source_interface = 'eth0' + + vlan_to_vni = { + '10': '10010', + '11': '10011', + '12': '10012', + '13': '10013', + '20': '10020', + '30': '10030', + '31': '10031', + } + + self.cli_set(self._base_path + [interface, 'external']) + self.cli_set(self._base_path + [interface, 'source-interface', source_interface]) + + for vlan, vni in vlan_to_vni.items(): + self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni]) + + # This must fail as this VXLAN interface is not associated with any bridge + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(['interfaces', 'bridge', bridge, 'member', 'interface', interface]) + + # It is not allowed to use duplicate VNIs + self.cli_set(self._base_path + [interface, 'vlan-to-vni', '11', 'vni', vlan_to_vni['10']]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + # restore VLAN - VNI mappings + for vlan, vni in vlan_to_vni.items(): + self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni]) + + # commit configuration + self.cli_commit() + + self.assertTrue(interface_exists(bridge)) + self.assertTrue(interface_exists(interface)) + + tmp = get_interface_config(interface) + self.assertEqual(tmp['master'], bridge) + + tmp = get_vxlan_vlan_tunnels('vxlan0') + self.assertEqual(tmp, list(vlan_to_vni)) + + self.cli_delete(['interfaces', 'bridge', bridge]) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index f8686edd8..95246a7b9 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -97,6 +97,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): vht_opt = { # VyOS CLI option hostapd - ht_capab setting + 'channel-set-width 3' : '[VHT160-80PLUS80]', 'stbc tx' : '[TX-STBC-2BY1]', 'stbc rx 12' : '[RX-STBC-12]', 'ldpc' : '[RXLDPC]', @@ -104,7 +105,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): 'vht-cf' : '[HTC-VHT]', 'antenna-pattern-fixed' : '[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN]', 'max-mpdu 11454' : '[MAX-MPDU-11454]', - 'max-mpdu-exp 2' : '[MAX-A-MPDU-LEN-EXP-2][VHT160]', + 'max-mpdu-exp 2' : '[MAX-A-MPDU-LEN-EXP-2]', 'link-adaptation both' : '[VHT-LINK-ADAPT3]', 'short-gi 80' : '[SHORT-GI-80]', 'short-gi 160' : '[SHORT-GI-160]', diff --git a/smoketest/scripts/cli/test_load_balancing_wan.py b/smoketest/scripts/cli/test_load_balancing_wan.py index 9b2cb0fac..47ca19b27 100755 --- a/smoketest/scripts/cli/test_load_balancing_wan.py +++ b/smoketest/scripts/cli/test_load_balancing_wan.py @@ -124,11 +124,12 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.assertEqual(tmp, original) # Delete veth interfaces and netns - for iface in [iface1, iface2, iface3, container_iface1, container_iface2, container_iface3]: + for iface in [iface1, iface2, iface3]: call(f'sudo ip link del dev {iface}') delete_netns(ns1) delete_netns(ns2) + delete_netns(ns3) def test_check_chains(self): @@ -246,11 +247,13 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.assertEqual(tmp, nat_vyos_pre_snat_hook) # Delete veth interfaces and netns - for iface in [iface1, iface2, iface3, container_iface1, container_iface2, container_iface3]: + for iface in [iface1, iface2, iface3]: call(f'sudo ip link del dev {iface}') delete_netns(ns1) delete_netns(ns2) + delete_netns(ns3) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_netns.py b/smoketest/scripts/cli/test_netns.py index b8bebb221..fd04dd520 100755 --- a/smoketest/scripts/cli/test_interfaces_netns.py +++ b/smoketest/scripts/cli/test_netns.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 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 @@ -16,7 +16,6 @@ import unittest -from netifaces import interfaces from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSession @@ -24,56 +23,61 @@ from vyos.configsession import ConfigSessionError from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.utils.process import cmd +from vyos.utils.network import is_netns_interface +from vyos.utils.network import get_netns_all base_path = ['netns'] -namespaces = ['mgmt', 'front', 'back', 'ams-ix'] +interfaces = ['dum10', 'dum12', 'dum50'] -class NETNSTest(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self._interfaces = ['dum10', 'dum12', 'dum50'] +class NetNSTest(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + self.cli_delete(base_path) + # commit changes + self.cli_commit() + + # There should be no network namespace remaining + tmp = cmd('ip netns ls') + self.assertFalse(tmp) + + super(NetNSTest, self).tearDown() - def test_create_netns(self): + def test_netns_create(self): + namespaces = ['mgmt', 'front', 'back'] for netns in namespaces: - base = base_path + ['name', netns] - self.cli_set(base) + self.cli_set(base_path + ['name', netns]) # commit changes self.cli_commit() - netns_list = cmd('ip netns ls') - # Verify NETNS configuration for netns in namespaces: - self.assertTrue(netns in netns_list) - + self.assertIn(netns, get_netns_all()) - def test_netns_assign_interface(self): + def test_netns_interface(self): netns = 'foo' - self.cli_set(['netns', 'name', netns]) + self.cli_set(base_path + ['name', netns]) # Set - for iface in self._interfaces: + for iface in interfaces: self.cli_set(['interfaces', 'dummy', iface, 'netns', netns]) # commit changes self.cli_commit() - netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show') - - for iface in self._interfaces: - self.assertTrue(iface in netns_iface_list) + for interface in interfaces: + self.assertTrue(is_netns_interface(interface, netns)) # Delete - for iface in self._interfaces: - self.cli_delete(['interfaces', 'dummy', iface, 'netns', netns]) + for interface in interfaces: + self.cli_delete(['interfaces', 'dummy', interface]) # commit changes self.cli_commit() netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show') - for iface in self._interfaces: - self.assertNotIn(iface, netns_iface_list) + for interface in interfaces: + self.assertFalse(is_netns_interface(interface, netns)) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py index d9b64544a..118b1d3a2 100755 --- a/smoketest/scripts/cli/test_policy_route.py +++ b/smoketest/scripts/cli/test_policy_route.py @@ -250,7 +250,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): ['meta l4proto udp', 'drop'], ['tcp flags syn / syn,ack', 'meta mark set ' + mark_hex], ['ct state new', 'tcp dport 22', 'ip saddr 198.51.100.0/24', 'ip ttl > 2', 'meta mark set ' + mark_hex], - ['meta l4proto icmp', 'log prefix "[smoketest-4-A]"', 'icmp type echo-request', 'ip length { 128, 1024-2048 }', 'meta pkttype other', 'meta mark set ' + mark_hex], + ['meta l4proto icmp', 'log prefix "[ipv4-route-smoketest-4-A]"', 'icmp type echo-request', 'ip length { 128, 1024-2048 }', 'meta pkttype other', 'meta mark set ' + mark_hex], ['ip dscp { 0x29, 0x39-0x3b }', 'meta mark set ' + mark_hex] ] @@ -262,7 +262,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): ['meta l4proto udp', 'drop'], ['tcp flags syn / syn,ack', 'meta mark set ' + mark_hex], ['ct state new', 'tcp dport 22', 'ip6 saddr 2001:db8::/64', 'ip6 hoplimit > 2', 'meta mark set ' + mark_hex], - ['meta l4proto ipv6-icmp', 'log prefix "[smoketest6-4-A]"', 'icmpv6 type echo-request', 'ip6 length != { 128, 1024-2048 }', 'meta pkttype multicast', 'meta mark set ' + mark_hex], + ['meta l4proto ipv6-icmp', 'log prefix "[ipv6-route6-smoketest6-4-A]"', 'icmpv6 type echo-request', 'ip6 length != { 128, 1024-2048 }', 'meta pkttype multicast', 'meta mark set ' + mark_hex], ['ip6 dscp != { 0x0e-0x13, 0x3d }', 'meta mark set ' + mark_hex] ] diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index 2a89aa98b..ea304783d 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -35,6 +35,17 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + def verify_nftables(self, nftables_search, table, inverse=False, args=''): + nftables_output = cmd(f'sudo nft {args} list table {table}') + + for search in nftables_search: + matched = False + for line in nftables_output.split("\n"): + if all(item in line for item in search): + matched = True + break + self.assertTrue(not matched if inverse else matched, msg=search) + def test_conntrack_options(self): conntrack_config = { 'net.netfilter.nf_conntrack_expect_max' : { @@ -232,5 +243,51 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): tmp = read_file('/etc/modprobe.d/vyatta_nf_conntrack.conf') self.assertIn(hash_size_default, tmp) + def test_conntrack_ignore(self): + address_group = 'conntracktest' + address_group_member = '192.168.0.1' + ipv6_address_group = 'conntracktest6' + ipv6_address_group_member = 'dead:beef::1' + + self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member]) + self.cli_set(['firewall', 'group', 'ipv6-address-group', ipv6_address_group, 'address', ipv6_address_group_member]) + + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'source', 'address', '192.0.2.1']) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'destination', 'address', '192.0.2.2']) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'destination', 'port', '22']) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'protocol', 'tcp']) + + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'source', 'address', '192.0.2.1']) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'destination', 'group', 'address-group', address_group]) + + self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'source', 'address', 'fe80::1']) + self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'destination', 'address', 'fe80::2']) + self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'destination', 'port', '22']) + self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'protocol', 'tcp']) + + self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '12', 'source', 'address', 'fe80::1']) + self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '12', 'destination', 'group', 'address-group', ipv6_address_group]) + + self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '13', 'source', 'address', 'fe80::1']) + self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '13', 'destination', 'address', '!fe80::3']) + + self.cli_commit() + + nftables_search = [ + ['ip saddr 192.0.2.1', 'ip daddr 192.0.2.2', 'tcp dport 22', 'notrack'], + ['ip saddr 192.0.2.1', 'ip daddr @A_conntracktest', 'notrack'] + ] + + nftables6_search = [ + ['ip6 saddr fe80::1', 'ip6 daddr fe80::2', 'tcp dport 22', 'notrack'], + ['ip6 saddr fe80::1', 'ip6 daddr @A6_conntracktest6', 'notrack'], + ['ip6 saddr fe80::1', 'ip6 daddr != fe80::3', 'notrack'] + ] + + self.verify_nftables(nftables_search, 'raw') + self.verify_nftables(nftables6_search, 'ip6 raw') + + self.cli_delete(['firewall']) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py index 9c43640a9..a0de914bc 100755 --- a/src/conf_mode/conntrack.py +++ b/src/conf_mode/conntrack.py @@ -24,7 +24,9 @@ from vyos.firewall import find_nftables_rule from vyos.firewall import remove_nftables_rule from vyos.utils.process import process_named_running from vyos.utils.dict import dict_search +from vyos.utils.dict import dict_search_args from vyos.utils.process import cmd +from vyos.utils.process import rc_cmd from vyos.utils.process import run from vyos.template import render from vyos import ConfigError @@ -62,6 +64,13 @@ module_map = { }, } +valid_groups = [ + 'address_group', + 'domain_group', + 'network_group', + 'port_group' +] + def resync_conntrackd(): tmp = run('/usr/libexec/vyos/conf_mode/conntrack_sync.py') if tmp > 0: @@ -78,15 +87,53 @@ def get_config(config=None): get_first_key=True, with_recursive_defaults=True) + conntrack['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + return conntrack def verify(conntrack): - if dict_search('ignore.rule', conntrack) != None: - for rule, rule_config in conntrack['ignore']['rule'].items(): - if dict_search('destination.port', rule_config) or \ - dict_search('source.port', rule_config): - if 'protocol' not in rule_config or rule_config['protocol'] not in ['tcp', 'udp']: - raise ConfigError(f'Port requires tcp or udp as protocol in rule {rule}') + for inet in ['ipv4', 'ipv6']: + if dict_search_args(conntrack, 'ignore', inet, 'rule') != None: + for rule, rule_config in conntrack['ignore'][inet]['rule'].items(): + if dict_search('destination.port', rule_config) or \ + dict_search('destination.group.port_group', rule_config) or \ + dict_search('source.port', rule_config) or \ + dict_search('source.group.port_group', rule_config): + if 'protocol' not in rule_config or rule_config['protocol'] not in ['tcp', 'udp']: + raise ConfigError(f'Port requires tcp or udp as protocol in rule {rule}') + + for side in ['destination', 'source']: + if side in rule_config: + side_conf = rule_config[side] + + if 'group' in side_conf: + if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1: + raise ConfigError('Only one address-group, network-group or domain-group can be specified') + + for group in valid_groups: + if group in side_conf['group']: + group_name = side_conf['group'][group] + error_group = group.replace("_", "-") + + if group in ['address_group', 'network_group', 'domain_group']: + if 'address' in side_conf: + raise ConfigError(f'{error_group} and address cannot both be defined') + + if group_name and group_name[0] == '!': + group_name = group_name[1:] + + if inet == 'ipv6': + group = f'ipv6_{group}' + + group_obj = dict_search_args(conntrack['firewall_group'], group, group_name) + + if group_obj is None: + raise ConfigError(f'Invalid {error_group} "{group_name}" on ignore rule') + + if not group_obj: + Warning(f'{error_group} "{group_name}" has no members!') return None @@ -94,26 +141,18 @@ def generate(conntrack): render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack) render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack) render(nftables_ct_file, 'conntrack/nftables-ct.j2', conntrack) - - # dry-run newly generated configuration - tmp = run(f'nft -c -f {nftables_ct_file}') - if tmp > 0: - if os.path.exists(nftables_ct_file): - os.unlink(nftables_ct_file) - raise ConfigError('Configuration file errors encountered!') - return None -def find_nftables_ct_rule(rule): +def find_nftables_ct_rule(table, chain, rule): helper_search = re.search('ct helper set "(\w+)"', rule) if helper_search: rule = helper_search[1] - return find_nftables_rule('raw', 'VYOS_CT_HELPER', [rule]) + return find_nftables_rule(table, chain, [rule]) -def find_remove_rule(rule): - handle = find_nftables_ct_rule(rule) +def find_remove_rule(table, chain, rule): + handle = find_nftables_ct_rule(table, chain, rule) if handle: - remove_nftables_rule('raw', 'VYOS_CT_HELPER', handle) + remove_nftables_rule(table, chain, handle) def apply(conntrack): # Depending on the enable/disable state of the ALG (Application Layer Gateway) @@ -127,18 +166,24 @@ def apply(conntrack): cmd(f'rmmod {mod}') if 'nftables' in module_config: for rule in module_config['nftables']: - find_remove_rule(rule) + find_remove_rule('raw', 'VYOS_CT_HELPER', rule) + find_remove_rule('ip6 raw', 'VYOS_CT_HELPER', rule) else: if 'ko' in module_config: for mod in module_config['ko']: cmd(f'modprobe {mod}') if 'nftables' in module_config: for rule in module_config['nftables']: - if not find_nftables_ct_rule(rule): - cmd(f'nft insert rule ip raw VYOS_CT_HELPER {rule}') + if not find_nftables_ct_rule('raw', 'VYOS_CT_HELPER', rule): + cmd(f'nft insert rule raw VYOS_CT_HELPER {rule}') + + if not find_nftables_ct_rule('ip6 raw', 'VYOS_CT_HELPER', rule): + cmd(f'nft insert rule ip6 raw VYOS_CT_HELPER {rule}') # Load new nftables ruleset - cmd(f'nft -f {nftables_ct_file}') + install_result, output = rc_cmd(f'nft -f {nftables_ct_file}') + if install_result == 1: + raise ConfigError(f'Failed to apply configuration: {output}') if process_named_running('conntrackd'): # Reload conntrack-sync daemon to fetch new sysctl values diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 46eb10714..daad9186e 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -274,10 +274,10 @@ def generate_run_arguments(name, container_config): env_opt += f" --env \"{k}={v['value']}\"" # Check/set label options "--label foo=bar" - env_opt = '' + label = '' if 'label' in container_config: for k, v in container_config['label'].items(): - env_opt += f" --label \"{k}={v['value']}\"" + label += f" --label \"{k}={v['value']}\"" hostname = '' if 'host_name' in container_config: @@ -314,7 +314,7 @@ def generate_run_arguments(name, container_config): container_base_cmd = f'--detach --interactive --tty --replace {cap_add} ' \ f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ - f'--name {name} {hostname} {device} {port} {volume} {env_opt}' + f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label}' entrypoint = '' if 'entrypoint' in container_config: diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index c4c72aae9..ac7d95632 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 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 @@ -34,6 +34,7 @@ from vyos import airbag airbag.enable() config_file = '/run/dhcp-server/dhcpd.conf' +systemd_override = r'/run/systemd/system/isc-dhcp-server.service.d/10-override.conf' def dhcp_slice_range(exclude_list, range_dict): """ @@ -295,6 +296,7 @@ def generate(dhcp): # render the "real" configuration render(config_file, 'dhcp-server/dhcpd.conf.j2', dhcp, formater=lambda _: _.replace(""", '"')) + render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp) # Clean up configuration test file if os.path.exists(tmp_file): @@ -303,6 +305,7 @@ def generate(dhcp): return None def apply(dhcp): + call('systemctl daemon-reload') # bail out early - looks like removal from running config if not dhcp or 'disable' in dhcp: call('systemctl stop isc-dhcp-server.service') diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index 626a3757e..70f43ab52 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -15,6 +15,9 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os +import time + from sys import exit from ipaddress import ip_interface from ipaddress import IPv4Interface @@ -22,15 +25,21 @@ from ipaddress import IPv6Interface from vyos.base import Warning from vyos.config import Config +from vyos.configdict import leaf_node_changed from vyos.ifconfig.vrrp import VRRP from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 +from vyos.utils.network import is_ipv6_tentative from vyos.utils.process import call from vyos import ConfigError from vyos import airbag airbag.enable() + +systemd_override = r'/run/systemd/system/keepalived.service.d/10-override.conf' + + def get_config(config=None): if config: conf = config @@ -50,6 +59,9 @@ def get_config(config=None): if conf.exists(conntrack_path): ha['conntrack_sync_group'] = conf.return_value(conntrack_path) + if leaf_node_changed(conf, base + ['vrrp', 'disable-snmp']): + ha.update({'restart_required': {}}) + return ha def verify(ha): @@ -160,18 +172,38 @@ def verify(ha): def generate(ha): if not ha or 'disable' in ha: + if os.path.isfile(systemd_override): + os.unlink(systemd_override) return None render(VRRP.location['config'], 'high-availability/keepalived.conf.j2', ha) + render(systemd_override, 'high-availability/10-override.conf.j2', ha) return None def apply(ha): service_name = 'keepalived.service' + call('systemctl daemon-reload') if not ha or 'disable' in ha: call(f'systemctl stop {service_name}') return None - call(f'systemctl reload-or-restart {service_name}') + # Check if IPv6 address is tentative T5533 + for group, group_config in ha.get('vrrp', {}).get('group', {}).items(): + if 'hello_source_address' in group_config: + if is_ipv6(group_config['hello_source_address']): + ipv6_address = group_config['hello_source_address'] + interface = group_config['interface'] + checks = 20 + interval = 0.1 + for _ in range(checks): + if is_ipv6_tentative(interface, ipv6_address): + time.sleep(interval) + + systemd_action = 'reload-or-restart' + if 'restart_required' in ha: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {service_name}') return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index e771581e1..db768b94d 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright (C) 2019-2023 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 @@ -55,7 +55,7 @@ def generate(dummy): return None def apply(dummy): - d = DummyIf(dummy['ifname']) + d = DummyIf(**dummy) # Remove dummy interface if 'deleted' in dummy: diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index a3b0867e0..05f68112a 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 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 @@ -24,6 +24,7 @@ from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import leaf_node_changed from vyos.configdict import is_node_changed +from vyos.configdict import node_changed from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 @@ -58,6 +59,9 @@ def get_config(config=None): vxlan.update({'rebuild_required': {}}) break + tmp = node_changed(conf, base + [ifname, 'vlan-to-vni'], recursive=True) + if tmp: vxlan.update({'vlan_to_vni_removed': tmp}) + # We need to verify that no other VXLAN tunnel is configured when external # mode is in use - Linux Kernel limitation conf.set_level(base) @@ -146,6 +150,20 @@ def verify(vxlan): raise ConfigError(error_msg) protocol = 'ipv4' + if 'vlan_to_vni' in vxlan: + if 'is_bridge_member' not in vxlan: + raise ConfigError('VLAN to VNI mapping requires that VXLAN interface '\ + 'is member of a bridge interface!') + + vnis_used = [] + for vif, vif_config in vxlan['vlan_to_vni'].items(): + if 'vni' not in vif_config: + raise ConfigError(f'Must define VNI for VLAN "{vif}"!') + vni = vif_config['vni'] + if vni in vnis_used: + raise ConfigError(f'VNI "{vni}" is already assigned to a different VLAN!') + vnis_used.append(vni) + verify_mtu_ipv6(vxlan) verify_address(vxlan) verify_bond_bridge_member(vxlan) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 9da7fbe80..08e96f10b 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -112,7 +112,7 @@ def verify_rule(config, err_msg, groups_dict): group_obj = dict_search_args(groups_dict, group, group_name) if group_obj is None: - raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule') + raise ConfigError(f'Invalid {error_group} "{group_name}" on nat rule') if not group_obj: Warning(f'{error_group} "{group_name}" has no members!') diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py index f6097e282..435189025 100755 --- a/src/conf_mode/protocols_igmp.py +++ b/src/conf_mode/protocols_igmp.py @@ -102,7 +102,7 @@ def verify(igmp): # Check, is this multicast group for intfc in igmp['ifaces']: for gr_addr in igmp['ifaces'][intfc]['gr_join']: - if IPv4Address(gr_addr) < IPv4Address('224.0.0.0'): + if not IPv4Address(gr_addr).is_multicast: raise ConfigError(gr_addr + " not a multicast group") def generate(igmp): diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index f5d84be4b..ad43390bb 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -110,3 +110,7 @@ net.ipv6.neigh.default.gc_thresh3 = 8192 # Enable global RFS (Receive Flow Steering) configuration. RFS is inactive # until explicitly configured at the interface level net.core.rps_sock_flow_entries = 32768 + +# Congestion control +net.core.default_qdisc=fq +net.ipv4.tcp_congestion_control=bbr diff --git a/src/helpers/config_dependency.py b/src/helpers/config_dependency.py new file mode 100755 index 000000000..50c72956e --- /dev/null +++ b/src/helpers/config_dependency.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 os +import sys +from argparse import ArgumentParser +from argparse import ArgumentTypeError + +try: + from vyos.configdep import check_dependency_graph + from vyos.defaults import directories +except ImportError: + # allow running during addon package build + _here = os.path.dirname(__file__) + sys.path.append(os.path.join(_here, '../../python/vyos')) + from configdep import check_dependency_graph + from defaults import directories + +# addon packages will need to specify the dependency directory +dependency_dir = os.path.join(directories['data'], + 'config-mode-dependencies') + +def path_exists(s): + if not os.path.exists(s): + raise ArgumentTypeError("Must specify a valid vyos-1x dependency directory") + return s + +def main(): + parser = ArgumentParser(description='generate and save dict from xml defintions') + parser.add_argument('--dependency-dir', type=path_exists, + default=dependency_dir, + help='location of vyos-1x dependency directory') + parser.add_argument('--supplement', type=str, + help='supplemental dependency file') + args = vars(parser.parse_args()) + + if not check_dependency_graph(**args): + sys.exit(1) + + sys.exit(0) + +if __name__ == '__main__': + main() diff --git a/src/helpers/vyos-domain-resolver.py b/src/helpers/vyos-domain-resolver.py index 7e2fe2462..eac3d37af 100755 --- a/src/helpers/vyos-domain-resolver.py +++ b/src/helpers/vyos-domain-resolver.py @@ -37,12 +37,14 @@ domain_state = {} ipv4_tables = { 'ip vyos_mangle', 'ip vyos_filter', - 'ip vyos_nat' + 'ip vyos_nat', + 'ip raw' } ipv6_tables = { 'ip6 vyos_mangle', - 'ip6 vyos_filter' + 'ip6 vyos_filter', + 'ip6 raw' } def get_config(conf): diff --git a/src/helpers/vyos-save-config.py b/src/helpers/vyos-save-config.py index 2812155e8..8af4a7916 100755 --- a/src/helpers/vyos-save-config.py +++ b/src/helpers/vyos-save-config.py @@ -44,7 +44,10 @@ ct = config.get_config_tree(effective=True) write_file = save_file if remote_save is None else NamedTemporaryFile(delete=False).name with open(write_file, 'w') as f: - f.write(ct.to_string()) + # config_tree is None before boot configuration is complete; + # automated saves should check boot_configuration_complete + if ct is not None: + f.write(ct.to_string()) f.write("\n") f.write(system_footer()) diff --git a/src/init/vyos-router b/src/init/vyos-router index 96f163213..a5d1a31fa 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -335,6 +335,9 @@ start () nfct helper add rpc inet tcp nfct helper add rpc inet udp nfct helper add tns inet tcp + nfct helper add rpc inet6 tcp + nfct helper add rpc inet6 udp + nfct helper add tns inet6 tcp nft -f /usr/share/vyos/vyos-firewall-init.conf || log_failure_msg "could not initiate firewall rules" rm -f /etc/hostname diff --git a/src/migration-scripts/conntrack/3-to-4 b/src/migration-scripts/conntrack/3-to-4 new file mode 100755 index 000000000..e90c383af --- /dev/null +++ b/src/migration-scripts/conntrack/3-to-4 @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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/>. + +# Add support for IPv6 conntrack ignore, move existing nodes to `system conntrack ignore ipv4` + +from sys import argv +from sys import 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() + +base = ['system', 'conntrack'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['ignore', 'rule']): + config.set(base + ['ignore', 'ipv4']) + config.copy(base + ['ignore', 'rule'], base + ['ignore', 'ipv4', 'rule']) + config.delete(base + ['ignore', 'rule']) + +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/system/13-to-14 b/src/migration-scripts/system/13-to-14 index 1fa781869..5b781158b 100755 --- a/src/migration-scripts/system/13-to-14 +++ b/src/migration-scripts/system/13-to-14 @@ -34,7 +34,7 @@ else: # retrieve all valid timezones try: - tz_datas = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::') + tz_datas = cmd('timedatectl list-timezones') except OSError: tz_datas = '' tz_data = tz_datas.split('\n') diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index f558c18b7..77f38992b 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -338,10 +338,12 @@ def _get_formatted_client_leases(lease_data, family): from time import localtime from time import strftime - from vyos.validate import is_intf_addr_assigned + from vyos.utils.network import is_intf_addr_assigned data_entries = [] for lease in lease_data: + if not lease.get('new_ip_address'): + continue data_entries.append(["Interface", lease['interface']]) if 'new_ip_address' in lease: tmp = '[Active]' if is_intf_addr_assigned(lease['interface'], lease['new_ip_address']) else '[Inactive]' diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 581710b31..23b4b8459 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -127,7 +127,15 @@ def output_firewall_name_statistics(hook, prior, prior_conf, ipv6=False, single_ if not source_addr: source_addr = dict_search_args(rule_conf, 'source', 'group', 'domain_group') if not source_addr: - source_addr = '::/0' if ipv6 else '0.0.0.0/0' + source_addr = dict_search_args(rule_conf, 'source', 'fqdn') + if not source_addr: + source_addr = dict_search_args(rule_conf, 'source', 'geoip', 'country_code') + if source_addr: + source_addr = str(source_addr)[1:-1].replace('\'','') + if 'inverse_match' in dict_search_args(rule_conf, 'source', 'geoip'): + source_addr = 'NOT ' + str(source_addr) + if not source_addr: + source_addr = 'any' # Get destination dest_addr = dict_search_args(rule_conf, 'destination', 'address') @@ -138,7 +146,15 @@ def output_firewall_name_statistics(hook, prior, prior_conf, ipv6=False, single_ if not dest_addr: dest_addr = dict_search_args(rule_conf, 'destination', 'group', 'domain_group') if not dest_addr: - dest_addr = '::/0' if ipv6 else '0.0.0.0/0' + dest_addr = dict_search_args(rule_conf, 'destination', 'fqdn') + if not dest_addr: + dest_addr = dict_search_args(rule_conf, 'destination', 'geoip', 'country_code') + if dest_addr: + dest_addr = str(dest_addr)[1:-1].replace('\'','') + if 'inverse_match' in dict_search_args(rule_conf, 'destination', 'geoip'): + dest_addr = 'NOT ' + str(dest_addr) + if not dest_addr: + dest_addr = 'any' # Get inbound interface iiface = dict_search_args(rule_conf, 'inbound_interface', 'interface_name') @@ -169,7 +185,22 @@ def output_firewall_name_statistics(hook, prior, prior_conf, ipv6=False, single_ row.append(oiface) rows.append(row) - if 'default_action' in prior_conf and not single_rule_id: + + if hook in ['input', 'forward', 'output']: + row = ['default'] + row.append('N/A') + row.append('N/A') + if 'default_action' in prior_conf: + row.append(prior_conf['default_action']) + else: + row.append('accept') + row.append('any') + row.append('any') + row.append('any') + row.append('any') + rows.append(row) + + elif 'default_action' in prior_conf and not single_rule_id: row = ['default'] if 'default-action' in details: rule_details = details['default-action'] @@ -179,8 +210,10 @@ def output_firewall_name_statistics(hook, prior, prior_conf, ipv6=False, single_ row.append('0') row.append('0') row.append(prior_conf['default_action']) - row.append('0.0.0.0/0') # Source - row.append('0.0.0.0/0') # Dest + row.append('any') # Source + row.append('any') # Dest + row.append('any') # inbound-interface + row.append('any') # outbound-interface rows.append(row) if rows: @@ -303,7 +336,7 @@ def show_firewall_group(name=None): continue references = find_references(group_type, group_name) - row = [group_name, group_type, '\n'.join(references) or 'N/A'] + row = [group_name, group_type, '\n'.join(references) or 'N/D'] if 'address' in group_conf: row.append("\n".join(sorted(group_conf['address']))) elif 'network' in group_conf: @@ -315,7 +348,7 @@ def show_firewall_group(name=None): elif 'interface' in group_conf: row.append("\n".join(sorted(group_conf['interface']))) else: - row.append('N/A') + row.append('N/D') rows.append(row) if rows: diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index 57d3cfed9..44d41219e 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -779,6 +779,45 @@ def show_ra_summary(raw: bool): return _get_formatted_output_ra_summary(list_sa) +# PSK block +def _get_raw_psk(): + conf: ConfigTreeQuery = ConfigTreeQuery() + config_path = ['vpn', 'ipsec', 'authentication', 'psk'] + psk_config = conf.get_config_dict(config_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + psk_list = [] + for psk, psk_data in psk_config.items(): + psk_data['psk'] = psk + psk_list.append(psk_data) + + return psk_list + + +def _get_formatted_psk(psk_list): + headers = ["PSK", "Id", "Secret"] + formatted_data = [] + + for psk_data in psk_list: + formatted_data.append([psk_data["psk"], "\n".join(psk_data["id"]), psk_data["secret"]]) + + return tabulate(formatted_data, headers=headers) + + +def show_psk(raw: bool): + config = ConfigTreeQuery() + if not config.exists('vpn ipsec authentication psk'): + raise vyos.opmode.UnconfiguredSubsystem('VPN ipsec psk authentication is not configured') + + psk = _get_raw_psk() + if raw: + return psk + return _get_formatted_psk(psk) + +# PSK block end + + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/otp.py b/src/op_mode/otp.py new file mode 100755 index 000000000..6d4298894 --- /dev/null +++ b/src/op_mode/otp.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +# Copyright 2017, 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# 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 sys +import os +import vyos.opmode +from jinja2 import Template +from vyos.configquery import ConfigTreeQuery +from vyos.xml import defaults +from vyos.configdict import dict_merge +from vyos.utils.process import popen + + +users_otp_template = Template(""" +{% if info == "full" %} +# You can share it with the user, he just needs to scan the QR in his OTP app +# username: {{username}} +# OTP KEY: {{key_base32}} +# OTP URL: {{otp_url}} +{{qrcode}} +# To add this OTP key to configuration, run the following commands: +set system login user {{username}} authentication otp key '{{key_base32}}' +{% if rate_limit != "3" %} +set system login user {{username}} authentication otp rate-limit '{{rate_limit}}' +{% endif %} +{% if rate_time != "30" %} +set system login user {{username}} authentication otp rate-time '{{rate_time}}' +{% endif %} +{% if window_size != "3" %} +set system login user {{username}} authentication otp window-size '{{window_size}}' +{% endif %} +{% elif info == "key-b32" %} +# OTP key in Base32 for system user {{username}}: +{{key_base32}} +{% elif info == "qrcode" %} +# QR code for system user '{{username}}' +{{qrcode}} +{% elif info == "uri" %} +# URI for system user '{{username}}' +{{otp_url}} +{% endif %} +""", trim_blocks=True, lstrip_blocks=True) + + +def _check_uname_otp(username:str): + """ + Check if "username" exists and have an OTP key + """ + config = ConfigTreeQuery() + base_key = ['system', 'login', 'user', username, 'authentication', 'otp', 'key'] + if not config.exists(base_key): + return None + return True + +def _get_login_otp(username: str, info:str): + """ + Retrieve user settings from configuration and set some defaults + """ + config = ConfigTreeQuery() + base = ['system', 'login', 'user', username] + if not config.exists(base): + return None + user_otp = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(['system', 'login', 'user']) + user_otp = dict_merge(default_values, user_otp) + result = user_otp['authentication']['otp'] + # Filling in the system and default options + result['info'] = info + result['hostname'] = os.uname()[1] + result['username'] = username + result['key_base32'] = result['key'] + result['otp_length'] = '6' + result['interval'] = '30' + result['token_type'] = 'hotp-time' + if result['token_type'] == 'hotp-time': + token_type_acrn = 'totp' + result['otp_url'] = ''.join(["otpauth://",token_type_acrn,"/",username,"@",\ + result['hostname'],"?secret=",result['key_base32'],"&digits=",\ + result['otp_length'],"&period=",result['interval']]) + result['qrcode'],err = popen('qrencode -t ansiutf8', input=result['otp_url']) + return result + +def show_login(raw: bool, username: str, info:str): + ''' + Display OTP parameters for <username> + ''' + check_otp = _check_uname_otp(username) + if check_otp: + user_otp_params = _get_login_otp(username, info) + else: + print(f'There is no such user ("{username}") with an OTP key configured') + print('You can use the following command to generate a key for a user:\n') + print(f'generate system login username {username} otp-key hotp-time') + sys.exit(0) + if raw: + return user_otp_params + return users_otp_template.render(user_otp_params) + + +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) diff --git a/src/op_mode/vyos-op-cmd-wrapper.sh b/src/op_mode/vyos-op-cmd-wrapper.sh new file mode 100755 index 000000000..a89211b2b --- /dev/null +++ b/src/op_mode/vyos-op-cmd-wrapper.sh @@ -0,0 +1,6 @@ +#!/bin/vbash +shopt -s expand_aliases +source /etc/default/vyatta +source /etc/bash_completion.d/vyatta-op +_vyatta_op_init +_vyatta_op_run "$@" diff --git a/src/pam-configs/radius b/src/pam-configs/radius index 08247f77c..eee9cb93e 100644 --- a/src/pam-configs/radius +++ b/src/pam-configs/radius @@ -3,15 +3,18 @@ Default: no Priority: 257 Auth-Type: Primary Auth: + [default=ignore success=2] pam_succeed_if.so service = sudo [default=ignore success=ignore] pam_succeed_if.so user ingroup aaa quiet [authinfo_unavail=ignore success=end default=ignore] pam_radius_auth.so Account-Type: Primary Account: + [default=ignore success=2] pam_succeed_if.so service = sudo [default=ignore success=ignore] pam_succeed_if.so user ingroup aaa quiet [authinfo_unavail=ignore success=end perm_denied=bad default=ignore] pam_radius_auth.so Session-Type: Additional Session: + [default=ignore success=2] pam_succeed_if.so service = sudo [default=ignore success=ignore] pam_succeed_if.so user ingroup aaa quiet [authinfo_unavail=ignore success=ok default=ignore] pam_radius_auth.so diff --git a/src/tests/test_dependency_graph.py b/src/tests/test_dependency_graph.py new file mode 100644 index 000000000..f682e87bb --- /dev/null +++ b/src/tests/test_dependency_graph.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 os +from vyos.configdep import check_dependency_graph + +_here = os.path.dirname(__file__) +ddir = os.path.join(_here, '../../data/config-mode-dependencies') + +from unittest import TestCase + +class TestDependencyGraph(TestCase): + def setUp(self): + pass + + def test_acyclic(self): + res = check_dependency_graph(dependency_dir=ddir) + self.assertTrue(res) |