diff options
445 files changed, 15203 insertions, 4805 deletions
@@ -29,9 +29,6 @@ interface_definitions: $(config_xml_obj) # XXX: delete top level node.def's that now live in other packages # IPSec VPN EAP-RADIUS does not support source-address rm -rf $(TMPL_DIR)/vpn/ipsec/remote-access/radius/source-address - # T3568: firewall is yet not migrated to XML and Python - this is only a dummy - rm -rf $(TMPL_DIR)/firewall/node.def - rm -rf $(TMPL_DIR)/nfirewall # XXX: test if there are empty node.def files - this is not allowed as these # could mask help strings or mandatory priority statements find $(TMPL_DIR) -name node.def -type f -empty -exec false {} + || sh -c 'echo "There are empty node.def files! Check your interface definitions." && exit 1' @@ -1,4 +1,4 @@ -# vyos-1x: VyOS 1.2.0+ configuration scripts and data +# vyos-1x: VyOS 1.2.0+ Configuration Scripts and Data [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=vyos%3Avyos-1x&metric=coverage)](https://sonarcloud.io/component_measures?id=vyos%3Avyos-1x&metric=coverage) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_shield) @@ -69,7 +69,7 @@ pipenv shell make test ``` -### Runtime (Smoketests) +### Runtime (Smoke Tests) Runtime tests are executed by the CI system on a running VyOS instance inside QEMU. The testcases can be found inside the smoketest subdirectory which will diff --git a/data/configd-include.json b/data/configd-include.json index 6893aaa86..ee4cb0d42 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -6,6 +6,7 @@ "dhcpv6_relay.py", "dns_forwarding.py", "dynamic_dns.py", +"flow_accounting_conf.py", "host_name.py", "https.py", "igmp_proxy.py", diff --git a/data/templates/accel-ppp/l2tp.config.tmpl b/data/templates/accel-ppp/l2tp.config.tmpl index 44c96b935..9fcda76d4 100644 --- a/data/templates/accel-ppp/l2tp.config.tmpl +++ b/data/templates/accel-ppp/l2tp.config.tmpl @@ -57,6 +57,9 @@ bind={{ outside_addr }} {% if lns_shared_secret %} secret={{ lns_shared_secret }} {% endif %} +{% if lns_host_name %} +host-name={{ lns_host_name }} +{% endif %} [client-ip-range] 0.0.0.0/0 diff --git a/data/templates/accel-ppp/pppoe.config.tmpl b/data/templates/accel-ppp/pppoe.config.tmpl index 238e7ee15..0a8e0079b 100644 --- a/data/templates/accel-ppp/pppoe.config.tmpl +++ b/data/templates/accel-ppp/pppoe.config.tmpl @@ -108,19 +108,17 @@ ac-name={{ access_concentrator }} {% if iface_config.vlan_id is not defined and iface_config.vlan_range is not defined %} interface={{ iface }} {% endif %} -{% if iface_config.vlan_id is defined and iface_config.vlan_range is not defined %} -{% for vlan in iface_config.vlan_id %} -interface={{ iface }}.{{ vlan }} -vlan-mon={{ iface }},{{ vlan }} +{% if iface_config.vlan_range is defined %} +{% for regex in iface_config.regex %} +interface=re:^{{ iface | replace('.', '\\.') }}\.({{ regex }})$ {% endfor %} -{% endif %} -{% if iface_config.vlan_range is defined and iface_config.vlan_id is not defined %} vlan-mon={{ iface }},{{ iface_config.vlan_range | join(',') }} -interface=re:{{ iface | replace('.', '\\.') }}\.\d+ {% endif %} -{% if iface_config.vlan_id is defined and iface_config.vlan_range is defined %} -vlan-mon={{ iface }},{{ iface_config.vlan_id | join(',') }},{{ iface_config.vlan_range | join(',') }} -interface=re:{{ iface | replace('.', '\\.') }}\.\d+ +{% if iface_config.vlan_id is defined %} +{% for vlan in iface_config.vlan_id %} +vlan-mon={{ iface }},{{ vlan }} +interface=re:^{{ iface | replace('.', '\\.') }}\.{{ vlan }}$ +{% endfor %} {% endif %} {% endfor %} {% endif %} diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl index fad91d118..8fd7d230d 100644 --- a/data/templates/accel-ppp/sstp.config.tmpl +++ b/data/templates/accel-ppp/sstp.config.tmpl @@ -50,9 +50,9 @@ verbose=1 check-ip=1 {# MTU #} mtu={{ mtu }} -{% if client_ipv6_pool is defined %} -ipv6=allow -{% endif %} +ipv6={{ 'allow' if ppp_options.ipv6 == "deny" and client_ipv6_pool is defined else ppp_options.ipv6 }} +ipv4={{ ppp_options.ipv4 }} + mppe={{ ppp_options.mppe }} lcp-echo-interval={{ ppp_options.lcp_echo_interval }} lcp-echo-timeout={{ ppp_options.lcp_echo_timeout }} diff --git a/data/templates/conserver/dropbear@.service.tmpl b/data/templates/conserver/dropbear@.service.tmpl index 4bb73f751..e355dab43 100644 --- a/data/templates/conserver/dropbear@.service.tmpl +++ b/data/templates/conserver/dropbear@.service.tmpl @@ -1,4 +1,4 @@ [Service] ExecStart= -ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I +ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -b /etc/issue.net -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I PIDFile=/run/conserver/dropbear.%I.pid diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl index c934b7cdb..b3e74c22b 100644 --- a/data/templates/dhcp-client/ipv4.tmpl +++ b/data/templates/dhcp-client/ipv4.tmpl @@ -2,12 +2,18 @@ option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; timeout 60; -retry 300; +retry 60; +initial-interval 2; interface "{{ ifname }}" { send host-name "{{ dhcp_options.host_name }}"; {% if dhcp_options.client_id is defined and dhcp_options.client_id is not none %} - send dhcp-client-identifier "{{ dhcp_options.client_id }}"; +{% set client_id = dhcp_options.client_id %} +{# Use HEX representation of client-id as it is send in MAC-address style using hex characters. If not HEX, use double quotes ASCII format #} +{% if not dhcp_options.client_id.split(':') | length >= 5 %} +{% set client_id = '"' + dhcp_options.client_id + '"' %} +{% endif %} + send dhcp-client-identifier {{ client_id }}; {% endif %} {% if dhcp_options.vendor_class_id is defined and dhcp_options.vendor_class_id is not none %} send vendor-class-identifier "{{ dhcp_options.vendor_class_id }}"; diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl index 003c585dd..233e2cc53 100644 --- a/data/templates/dhcp-server/dhcpd.conf.tmpl +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -90,6 +90,9 @@ shared-network {{ network | replace('_','-') }} { {% endif %} {% if network_config.subnet is defined and network_config.subnet is not none %} {% for subnet, subnet_config in network_config.subnet.items() %} +{% if subnet_config.description is defined and subnet_config.description is not none %} + # {{ subnet_config.description }} +{% endif %} subnet {{ subnet | address_from_cidr }} netmask {{ subnet | netmask_from_cidr }} { {% if subnet_config.name_server is defined and subnet_config.name_server is not none %} option domain-name-servers {{ subnet_config.name_server | join(', ') }}; diff --git a/data/templates/dns-forwarding/recursor.conf.tmpl b/data/templates/dns-forwarding/recursor.conf.tmpl index 9e0ad5d17..02efe903b 100644 --- a/data/templates/dns-forwarding/recursor.conf.tmpl +++ b/data/templates/dns-forwarding/recursor.conf.tmpl @@ -10,8 +10,7 @@ threads=1 allow-from={{ allow_from | join(',') }} log-common-errors=yes non-local-bind=yes -query-local-address={{ source_address_v4 | join(',') }} -query-local-address6={{ source_address_v6 | join(',') }} +query-local-address={{ source_address | join(',') }} lua-config-file=recursor.conf.lua # cache-size @@ -32,5 +31,8 @@ dnssec={{ dnssec }} # serve rfc1918 records serve-rfc1918={{ 'no' if no_serve_rfc1918 is defined else 'yes' }} +# zones +auth-zones={% for z in authoritative_zones %}{{ z.name }}={{ z.file }}{{- "," if not loop.last -}}{% endfor %} + forward-zones-file=recursor.forward-zones.conf diff --git a/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl index 784d5c360..7f29c387e 100644 --- a/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl +++ b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl @@ -22,3 +22,9 @@ addNTA("{{ zone }}", "static") {% endfor %} {% endif %} +{% if authoritative_zones is defined %} +-- from 'service dns forwarding authoritative-domain' +{% for zone in authoritative_zones %} +addNTA("{{ zone }}", "static") +{% endfor %} +{% endif %} diff --git a/data/templates/dns-forwarding/recursor.zone.conf.tmpl b/data/templates/dns-forwarding/recursor.zone.conf.tmpl new file mode 100644 index 000000000..758871bef --- /dev/null +++ b/data/templates/dns-forwarding/recursor.zone.conf.tmpl @@ -0,0 +1,7 @@ +; +; Autogenerated by dns_forwarding.py +; +; +{% for r in records %} +{{ r.name }} {{ r.ttl }} {{ r.type }} {{ r.value }} +{% endfor %} diff --git a/data/templates/dynamic-dns/ddclient.conf.tmpl b/data/templates/dynamic-dns/ddclient.conf.tmpl index 9d379de00..517e4bad4 100644 --- a/data/templates/dynamic-dns/ddclient.conf.tmpl +++ b/data/templates/dynamic-dns/ddclient.conf.tmpl @@ -9,7 +9,7 @@ ssl=yes {% set web_skip = ", web-skip='" + interface[iface].use_web.skip + "'" if interface[iface].use_web.skip is defined else '' %} use=web, web='{{ interface[iface].use_web.url }}'{{ web_skip }} {% else %} -use=if, if={{ iface }} +{{ 'usev6=if' if interface[iface].ipv6_enable is defined else 'use=if' }}, if={{ iface }} {% endif %} {% if interface[iface].rfc2136 is defined and interface[iface].rfc2136 is not none %} diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl index 40ed1b916..9ea880697 100644 --- a/data/templates/firewall/nftables-nat.tmpl +++ b/data/templates/firewall/nftables-nat.tmpl @@ -157,8 +157,8 @@ delete chain ip raw NAT_CONNTRACK add chain ip raw NAT_CONNTRACK add rule ip raw NAT_CONNTRACK counter accept {% set base_command = 'add rule ip raw' %} -{{ base_command }} PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER -{{ base_command }} OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER +{{ base_command }} PREROUTING position {{ pre_ct_ignore }} counter jump VYOS_CT_HELPER +{{ base_command }} OUTPUT position {{ out_ct_ignore }} counter jump VYOS_CT_HELPER {{ base_command }} PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK {{ base_command }} OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK {% endif %} diff --git a/data/templates/firewall/nftables-policy.tmpl b/data/templates/firewall/nftables-policy.tmpl new file mode 100644 index 000000000..aa6bb6fc1 --- /dev/null +++ b/data/templates/firewall/nftables-policy.tmpl @@ -0,0 +1,53 @@ +#!/usr/sbin/nft -f + +table ip mangle { +{% if first_install is defined %} + chain VYOS_PBR_PREROUTING { + type filter hook prerouting priority -150; policy accept; + } + chain VYOS_PBR_POSTROUTING { + type filter hook postrouting priority -150; policy accept; + } +{% endif %} +{% if route is defined -%} +{% for route_text, conf in route.items() %} + chain VYOS_PBR_{{ route_text }} { +{% if conf.rule is defined %} +{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %} + {{ rule_conf | nft_rule(route_text, rule_id, 'ip') }} +{% endfor %} +{% endif %} +{% if conf.default_action is defined %} + counter {{ conf.default_action | nft_action }} comment "{{ name_text }} default-action {{ conf.default_action }}" +{% else %} + counter return +{% endif %} + } +{% endfor %} +{%- endif %} +} + +table ip6 mangle { +{% if first_install is defined %} + chain VYOS_PBR6_PREROUTING { + type filter hook prerouting priority -150; policy accept; + } + chain VYOS_PBR6_POSTROUTING { + type filter hook postrouting priority -150; policy accept; + } +{% endif %} +{% if ipv6_route is defined %} +{% for route_text, conf in ipv6_route.items() %} + chain VYOS_PBR6_{{ route_text }} { +{% if conf.rule is defined %} +{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %} + {{ rule_conf | nft_rule(route_text, rule_id, 'ip6') }} +{% endfor %} +{% endif %} +{% if conf.default_action is defined %} + counter {{ conf.default_action | nft_action }} comment "{{ name_text }} default-action {{ conf.default_action }}" +{% endif %} + } +{% endfor %} +{% endif %} +} diff --git a/data/templates/firewall/nftables.tmpl b/data/templates/firewall/nftables.tmpl new file mode 100644 index 000000000..34bd9b71e --- /dev/null +++ b/data/templates/firewall/nftables.tmpl @@ -0,0 +1,292 @@ +#!/usr/sbin/nft -f + +{% if cleanup_commands is defined %} +{% for command in cleanup_commands %} +{{ command }} +{% endfor %} +{% endif %} + +{% if group is defined %} +{% if group.address_group is defined %} +{% for group_name, group_conf in group.address_group.items() %} +define A_{{ group_name }} = { + {{ group_conf.address | join(",") }} +} +{% endfor %} +{% endif %} +{% if group.ipv6_address_group is defined %} +{% for group_name, group_conf in group.ipv6_address_group.items() %} +define A6_{{ group_name }} = { + {{ group_conf.address | join(",") }} +} +{% endfor %} +{% endif %} +{% if group.network_group is defined %} +{% for group_name, group_conf in group.network_group.items() %} +define N_{{ group_name }} = { + {{ group_conf.network | join(",") }} +} +{% endfor %} +{% endif %} +{% if group.ipv6_network_group is defined %} +{% for group_name, group_conf in group.ipv6_network_group.items() %} +define N6_{{ group_name }} = { + {{ group_conf.network | join(",") }} +} +{% endfor %} +{% endif %} +{% if group.port_group is defined %} +{% for group_name, group_conf in group.port_group.items() %} +define P_{{ group_name }} = { + {{ group_conf.port | join(",") }} +} +{% endfor %} +{% endif %} +{% endif %} + +table ip filter { +{% if first_install is defined %} + chain VYOS_FW_IN { + type filter hook forward priority 0; policy accept; + } + chain VYOS_FW_OUT { + type filter hook forward priority 1; policy accept; + jump VYOS_POST_FW + } + chain VYOS_FW_LOCAL { + type filter hook input priority 0; policy accept; + jump VYOS_POST_FW + } + chain VYOS_FW_OUTPUT { + type filter hook output priority 0; policy accept; + jump VYOS_POST_FW + } + chain VYOS_POST_FW { + return + } + chain VYOS_FRAG_MARK { + type filter hook prerouting priority -450; policy accept; + ip frag-off & 0x3fff != 0 meta mark set 0xffff1 return + } +{% endif %} +{% if name is defined %} +{% for name_text, conf in name.items() %} +{% set default_log = 'log' if 'enable_default_log' in conf else '' %} + chain {{ name_text }} { +{% if conf.rule is defined %} +{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %} + {{ rule_conf | nft_rule(name_text, rule_id) }} +{% endfor %} +{% endif %} +{% if conf.default_action is defined %} + counter {{ default_log }} {{ conf.default_action | nft_action }} comment "{{ name_text }} default-action {{ conf.default_action }}" +{% else %} + return +{% endif %} + } +{% endfor %} +{% endif %} +{% if state_policy is defined %} + chain VYOS_STATE_POLICY { +{% if state_policy.established is defined %} + {{ state_policy.established | nft_state_policy('established') }} +{% endif %} +{% if state_policy.invalid is defined %} + {{ state_policy.invalid | nft_state_policy('invalid') }} +{% endif %} +{% if state_policy.related is defined %} + {{ state_policy.related | nft_state_policy('related') }} +{% endif %} + return + } +{% endif %} +} + +table ip6 filter { +{% if first_install is defined %} + chain VYOS_FW6_IN { + type filter hook forward priority 0; policy accept; + } + chain VYOS_FW6_OUT { + type filter hook forward priority 1; policy accept; + jump VYOS_POST_FW6 + } + chain VYOS_FW6_LOCAL { + type filter hook input priority 0; policy accept; + jump VYOS_POST_FW6 + } + chain VYOS_FW6_OUTPUT { + type filter hook output priority 0; policy accept; + jump VYOS_POST_FW6 + } + chain VYOS_POST_FW6 { + return + } + chain VYOS_FRAG6_MARK { + type filter hook prerouting priority -450; policy accept; + exthdr frag exists meta mark set 0xffff1 return + } +{% endif %} +{% if ipv6_name is defined %} +{% for name_text, conf in ipv6_name.items() %} +{% set default_log = 'log' if 'enable_default_log' in conf else '' %} + chain {{ name_text }} { +{% if conf.rule is defined %} +{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %} + {{ rule_conf | nft_rule(name_text, rule_id, 'ip6') }} +{% endfor %} +{% endif %} +{% if conf.default_action is defined %} + counter {{ default_log }} {{ conf.default_action | nft_action }} comment "{{ name_text }} default-action {{ conf.default_action }}" +{% else %} + return +{% endif %} + } +{% endfor %} +{% endif %} +{% if state_policy is defined %} + chain VYOS_STATE_POLICY6 { +{% if state_policy.established is defined %} + {{ state_policy.established | nft_state_policy('established') }} +{% endif %} +{% if state_policy.invalid is defined %} + {{ state_policy.invalid | nft_state_policy('invalid') }} +{% endif %} +{% if state_policy.related is defined %} + {{ state_policy.related | nft_state_policy('related') }} +{% endif %} + return + } +{% endif %} +} + +{% if first_install is defined %} +table ip nat { + chain PREROUTING { + type nat hook prerouting priority -100; policy accept; + counter jump VYOS_PRE_DNAT_HOOK + } + + chain POSTROUTING { + type nat hook postrouting priority 100; policy accept; + counter jump VYOS_PRE_SNAT_HOOK + } + + chain VYOS_PRE_DNAT_HOOK { + return + } + + chain VYOS_PRE_SNAT_HOOK { + return + } +} + +table ip6 nat { + chain PREROUTING { + type nat hook prerouting priority -100; policy accept; + counter jump VYOS_DNPT_HOOK + } + + chain POSTROUTING { + type nat hook postrouting priority 100; policy accept; + counter jump VYOS_SNPT_HOOK + } + + chain VYOS_DNPT_HOOK { + return + } + + chain VYOS_SNPT_HOOK { + return + } +} + +table inet mangle { + chain FORWARD { + type filter hook forward priority -150; policy accept; + } +} + +table raw { + chain VYOS_TCP_MSS { + type filter hook forward priority -300; policy accept; + } + + chain PREROUTING { + type filter hook prerouting priority -200; policy accept; + counter jump VYOS_CT_IGNORE + counter jump VYOS_CT_TIMEOUT + counter jump VYOS_CT_PREROUTING_HOOK + notrack + } + + chain OUTPUT { + type filter hook output priority -200; policy accept; + counter jump VYOS_CT_IGNORE + counter jump VYOS_CT_TIMEOUT + counter jump VYOS_CT_OUTPUT_HOOK + 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 + } + + chain VYOS_CT_OUTPUT_HOOK { + return + } +} + +table ip6 raw { + chain VYOS_TCP_MSS { + type filter hook forward priority -300; policy accept; + } + + chain PREROUTING { + type filter hook prerouting priority -300; policy accept; + counter jump VYOS_CT_PREROUTING_HOOK + notrack + } + + chain OUTPUT { + type filter hook output priority -300; policy accept; + counter jump VYOS_CT_OUTPUT_HOOK + notrack + } + + chain VYOS_CT_PREROUTING_HOOK { + return + } + + chain VYOS_CT_OUTPUT_HOOK { + return + } +} +{% endif %} diff --git a/data/templates/frr/bfd.frr.tmpl b/data/templates/frr/bfdd.frr.tmpl index 16f8be92c..439f79d67 100644 --- a/data/templates/frr/bfd.frr.tmpl +++ b/data/templates/frr/bfdd.frr.tmpl @@ -1,4 +1,4 @@ -! +{% if profile is defined or peer is defined %} bfd {% if profile is defined and profile is not none %} {% for profile_name, profile_config in profile.items() %} @@ -6,39 +6,53 @@ bfd detect-multiplier {{ profile_config.interval.multiplier }} receive-interval {{ profile_config.interval.receive }} transmit-interval {{ profile_config.interval.transmit }} -{% if profile_config.interval['echo-interval'] is defined and profile_config.interval['echo-interval'] is not none %} - echo-interval {{ profile_config.interval['echo-interval'] }} +{% if profile_config.interval.echo_interval is defined and profile_config.interval.echo_interval is not none %} + echo transmit-interval {{ profile_config.interval.echo_interval }} + echo receive-interval {{ profile_config.interval.echo_interval }} {% endif %} -{% if profile_config['echo-mode'] is defined %} +{% if profile_config.echo_mode is defined %} echo-mode {% endif %} +{% if profile_config.passive is defined %} + passive-mode +{% endif %} {% if profile_config.shutdown is defined %} shutdown {% else %} no shutdown {% endif %} - exit + exit + ! {% endfor %} {% endif %} {% if peer is defined and peer is not none %} {% for peer_name, peer_config in peer.items() %} - peer {{ peer_name }}{{ ' multihop' if peer_config.multihop is defined }}{{ ' local-address ' + peer_config.source.address if peer_config.source is defined and peer_config.source.address is defined }}{{ ' interface ' + peer_config.source.interface if peer_config.source is defined and peer_config.source.interface is defined }} + peer {{ peer_name }}{{ ' multihop' if peer_config.multihop is defined }}{{ ' local-address ' + peer_config.source.address if peer_config.source is defined and peer_config.source.address is defined }}{{ ' interface ' + peer_config.source.interface if peer_config.source is defined and peer_config.source.interface is defined }} {{ ' vrf ' + peer_config.vrf if peer_config.vrf is defined and peer_config.vrf is not none }} detect-multiplier {{ peer_config.interval.multiplier }} receive-interval {{ peer_config.interval.receive }} transmit-interval {{ peer_config.interval.transmit }} -{% if peer_config.interval['echo-interval'] is defined and peer_config.interval['echo-interval'] is not none %} - echo-interval {{ peer_config.interval['echo-interval'] }} +{% if peer_config.interval.echo_interval is defined and peer_config.interval.echo_interval is not none %} + echo transmit-interval {{ peer_config.interval.echo_interval }} + echo receive-interval {{ peer_config.interval.echo_interval }} {% endif %} -{% if peer_config['echo-mode'] is defined %} +{% if peer_config.echo_mode is defined %} echo-mode {% endif %} +{% if peer_config.passive is defined %} + passive-mode +{% endif %} +{% if peer_config.profile is defined and peer_config.profile is not none %} + profile {{ peer_config.profile }} +{% endif %} {% if peer_config.shutdown is defined %} shutdown {% else %} no shutdown {% endif %} - exit + exit + ! {% endfor %} {% endif %} - end +exit ! +{% endif %} diff --git a/data/templates/frr/bgpd.frr.tmpl b/data/templates/frr/bgpd.frr.tmpl index 987b922da..45e0544b7 100644 --- a/data/templates/frr/bgpd.frr.tmpl +++ b/data/templates/frr/bgpd.frr.tmpl @@ -17,6 +17,12 @@ {% endif %} {% if config.bfd is defined %} neighbor {{ neighbor }} bfd +{% if config.bfd.check_control_plane_failure is defined %} + neighbor {{ neighbor }} bfd check-control-plane-failure +{% endif %} +{% if config.bfd.profile is defined and config.bfd.profile is not none %} + neighbor {{ neighbor }} bfd profile {{ config.bfd.profile }} +{% endif %} {% endif %} {% if config.capability is defined and config.capability is not none %} {% if config.capability.dynamic is defined %} @@ -90,6 +96,9 @@ {% if config.interface.peer_group is defined and config.interface.peer_group is not none %} neighbor {{ neighbor }} interface peer-group {{ config.interface.peer_group }} {% endif %} +{% if config.interface.source_interface is defined and config.interface.source_interface is not none %} + neighbor {{ neighbor }} interface {{ config.interface.source_interface }} +{% endif %} {% if config.interface.v6only is defined and config.interface.v6only is not none %} {% if config.interface.v6only.peer_group is defined and config.interface.v6only.peer_group is not none %} neighbor {{ neighbor }} interface v6only peer-group {{ config.interface.v6only.peer_group }} @@ -137,6 +146,17 @@ {% if afi_config.as_override is defined %} neighbor {{ neighbor }} as-override {% endif %} +{% if afi_config.conditionally_advertise is defined and afi_config.conditionally_advertise is not none %} +{% if afi_config.conditionally_advertise.advertise_map is defined and afi_config.conditionally_advertise.advertise_map is not none %} +{% set exist_non_exist_map = 'exist-map' %} +{% if afi_config.conditionally_advertise.exist_map is defined and afi_config.conditionally_advertise.exist_map is not none %} +{% set exist_non_exist_map = 'exist-map ' ~ afi_config.conditionally_advertise.exist_map %} +{% elif afi_config.conditionally_advertise.non_exist_map is defined and afi_config.conditionally_advertise.non_exist_map is not none %} +{% set exist_non_exist_map = 'non-exist-map ' ~ afi_config.conditionally_advertise.non_exist_map %} +{% endif %} + neighbor {{ neighbor }} advertise-map {{ afi_config.conditionally_advertise.advertise_map }} {{ exist_non_exist_map }} +{% endif %} +{% endif %} {% if afi_config.remove_private_as is defined %} neighbor {{ neighbor }} remove-private-AS {% endif %} @@ -227,10 +247,8 @@ router bgp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is defined and vrf is not none {% else %} no bgp ebgp-requires-policy {% endif %} -{% if parameters is defined and parameters.default is defined and parameters.default.no_ipv4_unicast is defined %} {# Option must be set before any neighbor - see https://phabricator.vyos.net/T3463 #} no bgp default ipv4-unicast -{% endif %} {# Workaround for T2100 until we have decided about a migration script #} no bgp network import-check {% if address_family is defined and address_family is not none %} @@ -263,8 +281,11 @@ router bgp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is defined and vrf is not none {% endif %} {% endif %} {% if afi_config.aggregate_address is defined and afi_config.aggregate_address is not none %} -{% for ip in afi_config.aggregate_address %} - aggregate-address {{ ip }}{{ ' as-set' if afi_config.aggregate_address[ip].as_set is defined }}{{ ' summary-only' if afi_config.aggregate_address[ip].summary_only is defined }} +{% for aggregate, aggregate_config in afi_config.aggregate_address.items() %} + aggregate-address {{ aggregate }}{{ ' as-set' if aggregate_config.as_set is defined }}{{ ' summary-only' if aggregate_config.summary_only is defined }} +{% if aggregate_config.route_map is defined and aggregate_config.route_map is not none %} + aggregate-address {{ aggregate }} route-map {{ aggregate_config.route_map }} +{% endif %} {% endfor %} {% endif %} {% if afi_config.maximum_paths is defined and afi_config.maximum_paths.ebgp is defined and afi_config.maximum_paths.ebgp is not none %} @@ -465,6 +486,11 @@ router bgp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is defined and vrf is not none {% if parameters.cluster_id is defined and parameters.cluster_id is not none %} bgp cluster-id {{ parameters.cluster_id }} {% endif %} +{% if parameters.conditional_advertisement is defined and parameters.conditional_advertisement is not none %} +{% if parameters.conditional_advertisement.timer is defined and parameters.conditional_advertisement.timer is not none %} + bgp conditional-advertisement timer {{ parameters.conditional_advertisement.timer }} +{% endif %} +{% endif %} {% if parameters.confederation is defined and parameters.confederation is not none %} {% if parameters.confederation.identifier is defined and parameters.confederation.identifier is not none %} bgp confederation identifier {{ parameters.confederation.identifier }} @@ -495,6 +521,9 @@ router bgp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is defined and vrf is not none {% endfor %} {% endif %} {% endif %} +{% if parameters.fast_convergence is defined %} + bgp fast-convergence +{% endif %} {% if parameters.graceful_restart is defined %} bgp graceful-restart {{ 'stalepath-time ' ~ parameters.graceful_restart.stalepath_time if parameters.graceful_restart.stalepath_time is defined }} {% endif %} @@ -504,6 +533,9 @@ router bgp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is defined and vrf is not none {% if parameters.log_neighbor_changes is defined %} bgp log-neighbor-changes {% endif %} +{% if parameters.minimum_holdtime is defined and parameters.minimum_holdtime is not none %} + bgp minimum-holdtime {{ parameters.minimum_holdtime }} +{% endif %} {% if parameters.network_import_check is defined %} bgp network import-check {% endif %} @@ -513,12 +545,20 @@ router bgp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is defined and vrf is not none {% if parameters.no_fast_external_failover is defined %} no bgp fast-external-failover {% endif %} +{% if parameters.reject_as_sets is defined %} + bgp reject-as-sets +{% endif %} {% if parameters.router_id is defined and parameters.router_id is not none %} bgp router-id {{ parameters.router_id }} {% endif %} +{% if parameters.shutdown is defined %} + bgp shutdown +{% endif %} +{% if parameters.suppress_fib_pending is defined %} + bgp suppress-fib-pending +{% endif %} {% endif %} {% if timers is defined and timers.keepalive is defined and timers.holdtime is defined %} timers bgp {{ timers.keepalive }} {{ timers.holdtime }} {% endif %} - end -!
\ No newline at end of file +exit diff --git a/data/templates/frr/igmp.frr.tmpl b/data/templates/frr/igmp.frr.tmpl index cdb7ee6cc..49b5aeaa5 100644 --- a/data/templates/frr/igmp.frr.tmpl +++ b/data/templates/frr/igmp.frr.tmpl @@ -1,41 +1,41 @@ ! {% for iface in old_ifaces %} interface {{ iface }} -{% for group in old_ifaces[iface].gr_join %} -{% if old_ifaces[iface].gr_join[group] %} -{% for source in old_ifaces[iface].gr_join[group] %} -no ip igmp join {{ group }} {{ source }} -{% endfor %} -{% else %} -no ip igmp join {{ group }} -{% endif %} -{% endfor %} -no ip igmp +{% for group in old_ifaces[iface].gr_join %} +{% if old_ifaces[iface].gr_join[group] %} +{% for source in old_ifaces[iface].gr_join[group] %} + no ip igmp join {{ group }} {{ source }} +{% endfor %} +{% else %} + no ip igmp join {{ group }} +{% endif %} +{% endfor %} + no ip igmp ! {% endfor %} {% for iface in ifaces %} interface {{ iface }} -{% if ifaces[iface].version %} -ip igmp version {{ ifaces[iface].version }} -{% else %} +{% if ifaces[iface].version %} + ip igmp version {{ ifaces[iface].version }} +{% else %} {# IGMP default version 3 #} -ip igmp -{% endif %} -{% if ifaces[iface].query_interval %} -ip igmp query-interval {{ ifaces[iface].query_interval }} -{% endif %} -{% if ifaces[iface].query_max_resp_time %} -ip igmp query-max-response-time {{ ifaces[iface].query_max_resp_time }} -{% endif %} -{% for group in ifaces[iface].gr_join %} -{% if ifaces[iface].gr_join[group] %} -{% for source in ifaces[iface].gr_join[group] %} -ip igmp join {{ group }} {{ source }} -{% endfor %} -{% else %} -ip igmp join {{ group }} -{% endif %} -{% endfor %} + ip igmp +{% endif %} +{% if ifaces[iface].query_interval %} + ip igmp query-interval {{ ifaces[iface].query_interval }} +{% endif %} +{% if ifaces[iface].query_max_resp_time %} + ip igmp query-max-response-time {{ ifaces[iface].query_max_resp_time }} +{% endif %} +{% for group in ifaces[iface].gr_join %} +{% if ifaces[iface].gr_join[group] %} +{% for source in ifaces[iface].gr_join[group] %} + ip igmp join {{ group }} {{ source }} +{% endfor %} +{% else %} + ip igmp join {{ group }} +{% endif %} +{% endfor %} ! {% endfor %} ! diff --git a/data/templates/frr/isisd.frr.tmpl b/data/templates/frr/isisd.frr.tmpl index 51ac40060..b1e3f825b 100644 --- a/data/templates/frr/isisd.frr.tmpl +++ b/data/templates/frr/isisd.frr.tmpl @@ -1,4 +1,53 @@ ! +{% if interface is defined and interface is not none %} +{% for iface, iface_config in interface.items() %} +interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} + ip router isis VyOS + ipv6 router isis VyOS +{% if iface_config.bfd is defined %} + isis bfd +{% if iface_config.bfd.profile is defined and iface_config.bfd.profile is not none %} + isis bfd profile {{ iface_config.bfd.profile }} +{% endif %} +{% endif %} +{% if iface_config.network is defined and iface_config.network.point_to_point is defined %} + isis network point-to-point +{% endif %} +{% if iface_config.circuit_type is defined %} + isis circuit-type {{ iface_config.circuit_type }} +{% endif %} +{% if iface_config.hello_interval is defined and iface_config.hello_interval is not none %} + isis hello-interval {{ iface_config.hello_interval }} +{% endif %} +{% if iface_config.hello_multiplier is defined and iface_config.hello_multiplier is not none %} + isis hello-multiplier {{ iface_config.hello_multiplier }} +{% endif %} +{% if iface_config.hello_padding is defined %} + isis hello padding +{% endif %} +{% if iface_config.metric is defined and iface_config.metric is not none %} + isis metric {{ iface_config.metric }} +{% endif %} +{% if iface_config.passive is defined %} + isis passive +{% endif %} +{% if iface_config.password is defined and iface_config.password.plaintext_password is defined and iface_config.password.plaintext_password is not none %} + isis password clear {{ iface_config.password.plaintext_password }} +{% endif %} +{% if iface_config.priority is defined and iface_config.priority is not none %} + isis priority {{ iface_config.priority }} +{% endif %} +{% if iface_config.psnp_interval is defined and iface_config.psnp_interval is not none %} + isis psnp-interval {{ iface_config.psnp_interval }} +{% endif %} +{% if iface_config.no_three_way_handshake is defined %} + no isis three-way-handshake +{% endif %} +exit +! +{% endfor %} +{% endif %} +! router isis VyOS {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} net {{ net }} {% if dynamic_hostname is defined %} @@ -151,48 +200,5 @@ router isis VyOS {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} is-type {{ level }} {% endif %} {% endif %} -! -{% if interface is defined and interface is not none %} -{% for iface, iface_config in interface.items() %} -interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} - ip router isis VyOS - ipv6 router isis VyOS -{% if iface_config.bfd is defined %} - isis bfd -{% endif %} -{% if iface_config.network is defined and iface_config.network.point_to_point is defined %} - isis network point-to-point -{% endif %} -{% if iface_config.circuit_type is defined %} - isis circuit-type {{ iface_config.circuit_type }} -{% endif %} -{% if iface_config.hello_interval is defined and iface_config.hello_interval is not none %} - isis hello-interval {{ iface_config.hello_interval }} -{% endif %} -{% if iface_config.hello_multiplier is defined and iface_config.hello_multiplier is not none %} - isis hello-multiplier {{ iface_config.hello_multiplier }} -{% endif %} -{% if iface_config.hello_padding is defined %} - isis hello padding -{% endif %} -{% if iface_config.metric is defined and iface_config.metric is not none %} - isis metric {{ iface_config.metric }} -{% endif %} -{% if iface_config.passive is defined %} - isis passive -{% endif %} -{% if iface_config.password is defined and iface_config.password.plaintext_password is defined and iface_config.password.plaintext_password is not none %} - isis password clear {{ iface_config.password.plaintext_password }} -{% endif %} -{% if iface_config.priority is defined and iface_config.priority is not none %} - isis priority {{ iface_config.priority }} -{% endif %} -{% if iface_config.psnp_interval is defined and iface_config.psnp_interval is not none %} - isis psnp-interval {{ iface_config.psnp_interval }} -{% endif %} -{% if iface_config.no_three_way_handshake is defined %} - no isis three-way-handshake -{% endif %} -{% endfor %} -{% endif %} +exit !
\ No newline at end of file diff --git a/data/templates/frr/ldpd.frr.tmpl b/data/templates/frr/ldpd.frr.tmpl index 0a5411552..537ea4025 100644 --- a/data/templates/frr/ldpd.frr.tmpl +++ b/data/templates/frr/ldpd.frr.tmpl @@ -2,69 +2,69 @@ {% if ldp is defined %} mpls ldp {% if ldp.router_id is defined %} -router-id {{ ldp.router_id }} + router-id {{ ldp.router_id }} {% endif %} {% if ldp.parameters is defined %} {% if ldp.parameters.cisco_interop_tlv is defined %} -dual-stack cisco-interop + dual-stack cisco-interop {% endif %} {% if ldp.parameters.transport_prefer_ipv4 is defined%} -dual-stack transport-connection prefer ipv4 + dual-stack transport-connection prefer ipv4 {% endif %} {% if ldp.parameters.ordered_control is defined%} -ordered-control + ordered-control {% endif %} {% endif %} {% if ldp.neighbor is defined %} {% for neighbors in ldp.neighbor %} {% if ldp.neighbor[neighbors].password is defined %} -neighbor {{neighbors}} password {{ldp.neighbor[neighbors].password}} + neighbor {{ neighbors }} password {{ ldp.neighbor[neighbors].password }} {% endif %} {% if ldp.neighbor[neighbors].ttl_security is defined %} {% if 'disable' in ldp.neighbor[neighbors].ttl_security %} -neighbor {{neighbors}} ttl-security disable + neighbor {{ neighbors }} ttl-security disable {% else %} -neighbor {{neighbors}} ttl-security hops {{ldp.neighbor[neighbors].ttl_security}} + neighbor {{ neighbors }} ttl-security hops {{ ldp.neighbor[neighbors].ttl_security }} {% endif %} {% endif %} {% if ldp.neighbor[neighbors].session_holdtime is defined %} -neighbor {{neighbors}} session holdtime {{ldp.neighbor[neighbors].session_holdtime}} + neighbor {{ neighbors }} session holdtime {{ ldp.neighbor[neighbors].session_holdtime }} {% endif %} {% endfor %} {% endif %} -! + ! {% if ldp.discovery is defined %} {% if ldp.discovery.transport_ipv4_address is defined %} -address-family ipv4 + address-family ipv4 {% if ldp.allocation is defined %} {% if ldp.allocation.ipv4 is defined %} {% if ldp.allocation.ipv4.access_list is defined %} -label local allocate for {{ ldp.allocation.ipv4.access_list }} + label local allocate for {{ ldp.allocation.ipv4.access_list }} {% endif %} {% endif %} {% else %} -label local allocate host-routes + label local allocate host-routes {% endif %} {% if ldp.discovery.transport_ipv4_address is defined %} -discovery transport-address {{ ldp.discovery.transport_ipv4_address }} + discovery transport-address {{ ldp.discovery.transport_ipv4_address }} {% endif %} {% if ldp.discovery.hello_ipv4_holdtime is defined %} -discovery hello holdtime {{ ldp.discovery.hello_ipv4_holdtime }} + discovery hello holdtime {{ ldp.discovery.hello_ipv4_holdtime }} {% endif %} {% if ldp.discovery.hello_ipv4_interval is defined %} -discovery hello interval {{ ldp.discovery.hello_ipv4_interval }} + discovery hello interval {{ ldp.discovery.hello_ipv4_interval }} {% endif %} {% if ldp.discovery.session_ipv4_holdtime is defined %} -session holdtime {{ ldp.discovery.session_ipv4_holdtime }} + session holdtime {{ ldp.discovery.session_ipv4_holdtime }} {% endif %} {% if ldp.import is defined %} {% if ldp.import.ipv4 is defined %} {% if ldp.import.ipv4.import_filter is defined %} {% if ldp.import.ipv4.import_filter.filter_access_list is defined %} {% if ldp.import.ipv4.import_filter.neighbor_access_list is defined %} -label remote accept for {{ ldp.import.ipv4.import_filter.filter_access_list }} from {{ ldp.import.ipv4.import_filter.neighbor_access_list }} + label remote accept for {{ ldp.import.ipv4.import_filter.filter_access_list }} from {{ ldp.import.ipv4.import_filter.neighbor_access_list }} {% else %} -label remote accept for {{ ldp.import.ipv4.import_filter.filter_access_list }} + label remote accept for {{ ldp.import.ipv4.import_filter.filter_access_list }} {% endif %} {% endif %} {% endif %} @@ -73,14 +73,14 @@ label remote accept for {{ ldp.import.ipv4.import_filter.filter_access_list }} {% if ldp.export is defined %} {% if ldp.export.ipv4 is defined %} {% if ldp.export.ipv4.explicit_null is defined %} -label local advertise explicit-null + label local advertise explicit-null {% endif %} {% if ldp.export.ipv4.export_filter is defined %} {% if ldp.export.ipv4.export_filter.filter_access_list is defined %} {% if ldp.export.ipv4.export_filter.neighbor_access_list is defined %} -label local advertise for {{ ldp.export.ipv4.export_filter.filter_access_list }} to {{ ldp.export.ipv4.export_filter.neighbor_access_list }} + label local advertise for {{ ldp.export.ipv4.export_filter.filter_access_list }} to {{ ldp.export.ipv4.export_filter.neighbor_access_list }} {% else %} -label local advertise for {{ ldp.export.ipv4.export_filter.filter_access_list }} + label local advertise for {{ ldp.export.ipv4.export_filter.filter_access_list }} {% endif %} {% endif %} {% endif %} @@ -88,59 +88,59 @@ label local advertise for {{ ldp.export.ipv4.export_filter.filter_access_list }} {% endif %} {% if ldp.targeted_neighbor is defined %} {% if ldp.targeted_neighbor.ipv4.enable is defined %} -discovery targeted-hello accept + discovery targeted-hello accept {% endif %} {% if ldp.targeted_neighbor.ipv4.hello_holdtime is defined %} -discovery targeted-hello holdtime {{ ldp.targeted_neighbor.ipv4.hello_holdtime }} + discovery targeted-hello holdtime {{ ldp.targeted_neighbor.ipv4.hello_holdtime }} {% endif %} {% if ldp.targeted_neighbor.ipv4.hello_interval is defined %} -discovery targeted-hello interval {{ ldp.targeted_neighbor.ipv4.hello_interval }} + discovery targeted-hello interval {{ ldp.targeted_neighbor.ipv4.hello_interval }} {% endif %} {% for addresses in ldp.targeted_neighbor.ipv4.address %} -neighbor {{addresses}} targeted + neighbor {{addresses}} targeted {% endfor %} {% endif %} {% for interfaces in ldp.interface %} -interface {{interfaces}} + interface {{interfaces}} {% endfor %} -exit-address-family + exit-address-family {% else %} -no address-family ipv4 + no address-family ipv4 {% endif %} {% endif %} -! + ! {% if ldp.discovery is defined %} {% if ldp.discovery.transport_ipv6_address is defined %} -address-family ipv6 + address-family ipv6 {% if ldp.allocation is defined %} {% if ldp.allocation.ipv6 is defined %} {% if ldp.allocation.ipv6.access_list6 is defined %} -label local allocate for {{ ldp.allocation.ipv6.access_list6 }} + label local allocate for {{ ldp.allocation.ipv6.access_list6 }} {% endif %} {% endif %} {% else %} -label local allocate host-routes + label local allocate host-routes {% endif %} {% if ldp.discovery.transport_ipv6_address is defined %} -discovery transport-address {{ ldp.discovery.transport_ipv6_address }} + discovery transport-address {{ ldp.discovery.transport_ipv6_address }} {% endif %} {% if ldp.discovery.hello_ipv6_holdtime is defined %} -discovery hello holdtime {{ ldp.discovery.hello_ipv6_holdtime }} + discovery hello holdtime {{ ldp.discovery.hello_ipv6_holdtime }} {% endif %} {% if ldp.discovery.hello_ipv6_interval is defined %} -discovery hello interval {{ ldp.discovery.hello_ipv6_interval }} + discovery hello interval {{ ldp.discovery.hello_ipv6_interval }} {% endif %} {% if ldp.discovery.session_ipv6_holdtime is defined %} -session holdtime {{ ldp.discovery.session_ipv6_holdtime }} + session holdtime {{ ldp.discovery.session_ipv6_holdtime }} {% endif %} {% if ldp.import is defined %} {% if ldp.import.ipv6 is defined %} {% if ldp.import.ipv6.import_filter is defined %} {% if ldp.import.ipv6.import_filter.filter_access_list6 is defined %} {% if ldp.import.ipv6.import_filter.neighbor_access_list6 is defined %} -label remote accept for {{ ldp.import.ipv6.import_filter.filter_access_list6 }} from {{ ldp.import.ipv6.import_filter.neighbor_access_list6 }} + label remote accept for {{ ldp.import.ipv6.import_filter.filter_access_list6 }} from {{ ldp.import.ipv6.import_filter.neighbor_access_list6 }} {% else %} -label remote accept for {{ ldp.import.ipv6.import_filter.filter_access_list6 }} + label remote accept for {{ ldp.import.ipv6.import_filter.filter_access_list6 }} {% endif %} {% endif %} {% endif %} @@ -149,14 +149,14 @@ label remote accept for {{ ldp.import.ipv6.import_filter.filter_access_list6 }} {% if ldp.export is defined %} {% if ldp.export.ipv6 is defined %} {% if ldp.export.ipv6.explicit_null is defined %} -label local advertise explicit-null + label local advertise explicit-null {% endif %} {% if ldp.export.ipv6.export_filter is defined %} {% if ldp.export.ipv6.export_filter.filter_access_list6 is defined %} {% if ldp.export.ipv6.export_filter.neighbor_access_list6 is defined %} -label local advertise for {{ ldp.export.ipv6.export_filter.filter_access_list6 }} to {{ ldp.export.ipv6.export_filter.neighbor_access_list6 }} + label local advertise for {{ ldp.export.ipv6.export_filter.filter_access_list6 }} to {{ ldp.export.ipv6.export_filter.neighbor_access_list6 }} {% else %} -label local advertise for {{ ldp.export.ipv6.export_filter.filter_access_list6 }} + label local advertise for {{ ldp.export.ipv6.export_filter.filter_access_list6 }} {% endif %} {% endif %} {% endif %} @@ -164,24 +164,27 @@ label local advertise for {{ ldp.export.ipv6.export_filter.filter_access_list6 } {% endif %} {% if ldp.targeted_neighbor is defined %} {% if ldp.targeted_neighbor.ipv6.enable is defined %} -discovery targeted-hello accept + discovery targeted-hello accept {% endif %} {% if ldp.targeted_neighbor.ipv6.hello_holdtime is defined %} -discovery targeted-hello holdtime {{ ldp.targeted_neighbor.ipv6.hello_holdtime }} + discovery targeted-hello holdtime {{ ldp.targeted_neighbor.ipv6.hello_holdtime }} {% endif %} {% if ldp.targeted_neighbor.ipv6.hello_interval is defined %} -discovery targeted-hello interval {{ ldp.targeted_neighbor.ipv6.hello_interval }} + discovery targeted-hello interval {{ ldp.targeted_neighbor.ipv6.hello_interval }} {% endif %} {% for addresses in ldp.targeted_neighbor.ipv6.address %} -neighbor {{addresses}} targeted + neighbor {{addresses}} targeted {% endfor %} {% endif %} {% for interfaces in ldp.interface %} -interface {{interfaces}} + interface {{interfaces}} {% endfor %} -exit-address-family + exit-address-family {% else %} -no address-family ipv6 + no address-family ipv6 {% endif %} + ! {% endif %} +exit {% endif %} +! diff --git a/data/templates/frr/ospf6d.frr.tmpl b/data/templates/frr/ospf6d.frr.tmpl index 0026c0d2c..8279e5abb 100644 --- a/data/templates/frr/ospf6d.frr.tmpl +++ b/data/templates/frr/ospf6d.frr.tmpl @@ -1,7 +1,10 @@ ! {% if interface is defined and interface is not none %} {% for iface, iface_config in interface.items() %} -interface {{ iface }} +interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} +{% if iface_config.area is defined and iface_config.area is not none %} + ipv6 ospf6 area {{ iface_config.area }} +{% endif %} {% if iface_config.cost is defined and iface_config.cost is not none %} ipv6 ospf6 cost {{ iface_config.cost }} {% endif %} @@ -22,6 +25,9 @@ interface {{ iface }} {% endif %} {% if iface_config.bfd is defined %} ipv6 ospf6 bfd +{% if iface_config.bfd.profile is defined and iface_config.bfd.profile is not none %} + ipv6 ospf6 bfd profile {{ iface_config.bfd.profile }} +{% endif %} {% endif %} {% if iface_config.mtu_ignore is defined %} ipv6 ospf6 mtu-ignore @@ -38,21 +44,17 @@ interface {{ iface }} {% if iface_config.passive is defined %} ipv6 ospf6 passive {% endif %} +exit ! {% endfor %} {% endif %} ! -router ospf6 +router ospf6 {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} {% if area is defined and area is not none %} {% for area_id, area_config in area.items() %} -{% if area_config.interface is defined and area_config.interface is not none %} -{% for interface in area_config.interface %} - interface {{ interface }} area {{ area_id }} -{% endfor %} -{% endif %} {% if area_config.area_type is defined and area_config.area_type is not none %} {% for type, type_config in area_config.area_type.items() %} - area {{ area_id }} {{ type }} {{ 'no-summary' if type_config.no_summary is defined }} + area {{ area_id }} {{ type }} {{ 'default-information-originate' if type_config.default_information_originate is defined }} {{ 'no-summary' if type_config.no_summary is defined }} {% endfor %} {% endif %} {% if area_config.range is defined and area_config.range is not none %} @@ -68,6 +70,10 @@ router ospf6 {% endif %} {% endfor %} {% endif %} + auto-cost reference-bandwidth {{ auto_cost.reference_bandwidth }} +{% if default_information is defined and default_information.originate is defined and default_information.originate is not none %} + default-information originate {{ 'always' if default_information.originate.always is defined }} {{ 'metric ' + default_information.originate.metric if default_information.originate.metric is defined }} {{ 'metric-type ' + default_information.originate.metric_type if default_information.originate.metric_type is defined }} {{ 'route-map ' + default_information.originate.route_map if default_information.originate.route_map is defined }} +{% endif %} {% if distance is defined and distance is not none %} {% if distance.global is defined and distance.global is not none %} distance {{ distance.global }} @@ -76,6 +82,9 @@ router ospf6 distance ospf6 {{ 'intra-area ' + distance.ospfv3.intra_area if distance.ospfv3.intra_area is defined }} {{ 'inter-area ' + distance.ospfv3.inter_area if distance.ospfv3.inter_area is defined }} {{ 'external ' + distance.ospfv3.external if distance.ospfv3.external is defined }} {% endif %} {% endif %} +{% if log_adjacency_changes is defined %} + log-adjacency-changes {{ "detail" if log_adjacency_changes.detail is defined }} +{% endif %} {% if parameters is defined and parameters is not none %} {% if parameters.router_id is defined and parameters.router_id is not none %} ospf6 router-id {{ parameters.router_id }} @@ -86,4 +95,5 @@ router ospf6 redistribute {{ protocol }} {{ 'route-map ' + options.route_map if options.route_map is defined }} {% endfor %} {% endif %} +exit ! diff --git a/data/templates/frr/ospfd.frr.tmpl b/data/templates/frr/ospfd.frr.tmpl index 90a6bbd56..af66baf53 100644 --- a/data/templates/frr/ospfd.frr.tmpl +++ b/data/templates/frr/ospfd.frr.tmpl @@ -42,6 +42,9 @@ interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} {% endif %} {% if iface_config.bfd is defined %} ip ospf bfd +{% if iface_config.bfd.profile is defined and iface_config.bfd.profile is not none %} + ip ospf bfd profile {{ iface_config.bfd.profile }} +{% endif %} {% endif %} {% if iface_config.mtu_ignore is defined %} ip ospf mtu-ignore @@ -49,6 +52,10 @@ interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} {% if iface_config.network is defined and iface_config.network is not none %} ip ospf network {{ iface_config.network }} {% endif %} +{% if iface_config.passive is defined %} + {{ 'no ' if iface_config.passive.disable is defined }}ip ospf passive +{% endif %} +exit ! {% endfor %} {% endif %} @@ -158,18 +165,8 @@ router ospf {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} ospf router-id {{ parameters.router_id }} {% endif %} {% endif %} -{% if passive_interface is defined and passive_interface is not none %} -{% for interface in passive_interface %} - passive-interface {{ interface }} -{% endfor %} -{% endif %} -{% if passive_interface_exclude is defined and passive_interface_exclude is not none %} -{% for interface in passive_interface_exclude if passive_interface_exclude is defined %} -{% if interface.startswith('vlink') %} -{% set interface = interface.upper() %} -{% endif %} - no passive-interface {{ interface }} -{% endfor %} +{% if passive_interface is defined and passive_interface.default is defined %} + passive-interface default {% endif %} {% if redistribute is defined and redistribute is not none %} {% for protocol, protocols_options in redistribute.items() %} @@ -189,4 +186,5 @@ router ospf {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} {# Timer values have default values #} timers throttle spf {{ timers.throttle.spf.delay }} {{ timers.throttle.spf.initial_holdtime }} {{ timers.throttle.spf.max_holdtime }} {% endif %} +exit ! diff --git a/data/templates/frr/policy.frr.tmpl b/data/templates/frr/policy.frr.tmpl index 51adc1902..d3d3957a5 100644 --- a/data/templates/frr/policy.frr.tmpl +++ b/data/templates/frr/policy.frr.tmpl @@ -1,4 +1,3 @@ -! {% if access_list is defined and access_list is not none %} {% for acl, acl_config in access_list.items() | natural_sort %} {% if acl_config.description is defined and acl_config.description is not none %} @@ -60,7 +59,7 @@ ipv6 access-list {{ acl }} seq {{ rule }} {{ rule_config.action }} {{ src }} {{ {% for acl, acl_config in as_path_list.items() | natural_sort %} {% if acl_config.rule is defined and acl_config.rule is not none %} {% for rule, rule_config in acl_config.rule.items() | natural_sort %} -bgp as-path access-list {{ acl }} {{ rule_config.action }} {{ rule_config.regex }} +bgp as-path access-list {{ acl }} seq {{ rule }} {{ rule_config.action }} {{ rule_config.regex }} {% endfor %} {% endif %} {% endfor %} @@ -314,9 +313,9 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }} set weight {{ rule_config.set.weight }} {% endif %} {% endif %} -{% endfor %} +exit ! +{% endfor %} {% endif %} {% endfor %} {% endif %} -! diff --git a/data/templates/frr/rip.frr.tmpl b/data/templates/frr/ripd.frr.tmpl index cabc236f0..c44bb6d27 100644 --- a/data/templates/frr/rip.frr.tmpl +++ b/data/templates/frr/ripd.frr.tmpl @@ -1,4 +1,3 @@ -! {# RIP key-chain definition #} {% if interface is defined and interface is not none %} {% for iface, iface_config in interface.items() %} @@ -9,7 +8,9 @@ key chain {{ iface }}-rip {% if key_options.password is defined and key_options.password is not none %} key-string {{ key_options.password }} {% endif %} + exit {% endfor %} +exit {% endif %} {% endfor %} {% endif %} @@ -31,6 +32,8 @@ interface {{ iface }} {% if iface_config.split_horizon is defined and iface_config.split_horizon.poison_reverse is defined %} ip rip split-horizon poisoned-reverse {% endif %} +exit +! {% endfor %} {% endif %} ! @@ -89,6 +92,7 @@ router rip {% endif %} {% endif %} {% include 'frr/rip_ripng.frr.j2' %} +exit ! {% if route_map is defined and route_map is not none %} ip protocol rip route-map {{ route_map }} diff --git a/data/templates/frr/ripng.frr.tmpl b/data/templates/frr/ripngd.frr.tmpl index 25df15121..ca7b9b5fb 100644 --- a/data/templates/frr/ripng.frr.tmpl +++ b/data/templates/frr/ripngd.frr.tmpl @@ -1,4 +1,3 @@ -! {# Interface specific configuration #} {% if interface is defined and interface is not none %} {% for iface, iface_config in interface.items() %} @@ -9,6 +8,7 @@ interface {{ iface }} {% if iface_config.split_horizon is defined and iface_config.split_horizon.poison_reverse is defined %} ipv6 rip split-horizon poisoned-reverse {% endif %} +exit {% endfor %} {% endif %} ! @@ -57,4 +57,9 @@ router ripng {% endif %} {% endif %} {% include 'frr/rip_ripng.frr.j2' %} +exit +! +{% if route_map is defined and route_map is not none %} +ipv6 protocol ripng route-map {{ route_map }} +{% endif %} ! diff --git a/data/templates/frr/rpki.frr.tmpl b/data/templates/frr/rpki.frr.tmpl index fbdfa27c3..7f9823f6b 100644 --- a/data/templates/frr/rpki.frr.tmpl +++ b/data/templates/frr/rpki.frr.tmpl @@ -14,4 +14,5 @@ rpki {% if polling_period is defined and polling_period is not none %} rpki polling_period {{ polling_period }} {% endif %} +exit ! diff --git a/data/templates/frr/static.frr.tmpl b/data/templates/frr/staticd.frr.tmpl index db59a44c2..bfe959c1d 100644 --- a/data/templates/frr/static.frr.tmpl +++ b/data/templates/frr/staticd.frr.tmpl @@ -15,6 +15,15 @@ vrf {{ vrf }} {{ static_routes(ip_prefix, prefix, prefix_config) }} {%- endfor -%} {% endif %} +{# IPv4 default routes from DHCP interfaces #} +{% if dhcp is defined and dhcp is not none %} +{% for interface in dhcp %} +{% set next_hop = interface | get_dhcp_router %} +{% if next_hop is defined and next_hop is not none %} +{{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 210 +{% endif %} +{% endfor %} +{% endif %} {# IPv6 routing #} {% if route6 is defined and route6 is not none %} {% for prefix, prefix_config in route6.items() %} diff --git a/data/templates/frr/vrf-vni.frr.tmpl b/data/templates/frr/vrf-vni.frr.tmpl index 51d4ede1b..299c9719e 100644 --- a/data/templates/frr/vrf-vni.frr.tmpl +++ b/data/templates/frr/vrf-vni.frr.tmpl @@ -1,7 +1,9 @@ -{% if vrf is defined and vrf is not none %} +{% if name is defined and name is not none %} +{% for vrf, vrf_config in name.items() %} vrf {{ vrf }} -{% if vni is defined and vni is not none %} - vni {{ vni }} -{% endif %} +{% if vrf_config.vni is defined and vrf_config.vni is not none %} + vni {{ vrf_config.vni }} +{% endif %} exit-vrf +{% endfor %} {% endif %} diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl index 9d73baeee..ac9203e83 100644 --- a/data/templates/https/nginx.default.tmpl +++ b/data/templates/https/nginx.default.tmpl @@ -44,7 +44,11 @@ server { # proxy settings for HTTP API, if enabled; 503, if not location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc|graphql) { {% if server.api %} +{% if server.api.socket %} + proxy_pass http://unix:/run/api.sock; +{% else %} proxy_pass http://localhost:{{ server.api.port }}; +{% endif %} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 600; diff --git a/data/templates/https/override.conf.tmpl b/data/templates/https/override.conf.tmpl new file mode 100644 index 000000000..824b1ba3b --- /dev/null +++ b/data/templates/https/override.conf.tmpl @@ -0,0 +1,15 @@ +{% set vrf_command = 'ip vrf exec ' + vrf + ' ' if vrf is defined else '' %} +[Unit] +StartLimitIntervalSec=0 +After=vyos-router.service + +[Service] +ExecStartPre= +ExecStartPre={{vrf_command}}/usr/sbin/nginx -t -q -g 'daemon on; master_process on;' +ExecStart= +ExecStart={{vrf_command}}/usr/sbin/nginx -g 'daemon on; master_process on;' +ExecReload= +ExecReload={{vrf_command}}/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload +Restart=always +RestartPreventExitStatus= +RestartSec=10 diff --git a/data/templates/https/vyos-http-api.service.tmpl b/data/templates/https/vyos-http-api.service.tmpl new file mode 100644 index 000000000..15bd80d65 --- /dev/null +++ b/data/templates/https/vyos-http-api.service.tmpl @@ -0,0 +1,22 @@ +{% set vrf_command = 'ip vrf exec ' + vrf + ' ' if vrf is defined else '' %} +[Unit] +Description=VyOS HTTP API service +After=vyos-router.service +Requires=vyos-router.service + +[Service] +ExecStart={{vrf_command}}/usr/libexec/vyos/services/vyos-http-api-server +Type=idle + +SyslogIdentifier=vyos-http-api +SyslogFacility=daemon + +Restart=on-failure + +# Does't work but leave it here +User=root +Group=vyattacfg + +[Install] +WantedBy=vyos.target + diff --git a/data/templates/ipsec/swanctl.conf.tmpl b/data/templates/ipsec/swanctl.conf.tmpl index 161f19f95..68b108365 100644 --- a/data/templates/ipsec/swanctl.conf.tmpl +++ b/data/templates/ipsec/swanctl.conf.tmpl @@ -57,7 +57,7 @@ secrets { {% endif %} {% if site_to_site is defined and site_to_site.peer is defined %} {% for peer, peer_conf in site_to_site.peer.items() if peer not in dhcp_no_address and peer_conf.disable is not defined %} -{% set peer_name = peer.replace(".", "-").replace("@", "") %} +{% set peer_name = peer.replace("@", "") | dot_colon_to_dash %} {% if peer_conf.authentication.mode == 'pre-shared-secret' %} ike_{{ peer_name }} { {% if peer_conf.local_address is defined %} diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl index 8c3776bf1..c6b71f2a1 100644 --- a/data/templates/ipsec/swanctl/peer.tmpl +++ b/data/templates/ipsec/swanctl/peer.tmpl @@ -1,5 +1,5 @@ {% macro conn(peer, peer_conf, ike_group, esp_group) %} -{% set name = peer.replace(".", "-").replace("@", "") %} +{% set name = peer.replace("@", "") | dot_colon_to_dash %} {# peer needs to reference the global IKE configuration for certain values #} {% set ike = ike_group[peer_conf.ike_group] %} peer_{{ name }} { @@ -101,6 +101,9 @@ {% set remote_prefix = tunnel_conf.remote.prefix if 'any' not in tunnel_conf.remote.prefix else ['0.0.0.0/0', '::/0'] %} remote_ts = {{ remote_prefix | join(remote_suffix + ",") }}{{ remote_suffix }} {% endif %} +{% if tunnel_conf.priority is defined and tunnel_conf.priority is not none %} + priority = {{ tunnel_conf.priority }} +{% endif %} {% elif tunnel_esp.mode == 'transport' %} local_ts = {{ peer_conf.local_address }}{{ local_suffix }} remote_ts = {{ peer }}{{ remote_suffix }} diff --git a/data/templates/ipsec/swanctl/profile.tmpl b/data/templates/ipsec/swanctl/profile.tmpl index 948dd8f87..a5cae31c0 100644 --- a/data/templates/ipsec/swanctl/profile.tmpl +++ b/data/templates/ipsec/swanctl/profile.tmpl @@ -7,7 +7,7 @@ dmvpn-{{ name }}-{{ interface }} { proposals = {{ ike_group[profile_conf.ike_group] | get_esp_ike_cipher | join(',') }} version = {{ ike.key_exchange[4:] if ike is defined and ike.key_exchange is defined else "0" }} - life_time = {{ ike.lifetime }}s + rekey_time = {{ ike.lifetime }}s keyingtries = 0 {% if profile_conf.authentication is defined and profile_conf.authentication.mode is defined and profile_conf.authentication.mode == 'pre-shared-secret' %} local { diff --git a/data/templates/lcd/LCDd.conf.tmpl b/data/templates/lcd/LCDd.conf.tmpl index 6cf6a440f..2c7ad920f 100644 --- a/data/templates/lcd/LCDd.conf.tmpl +++ b/data/templates/lcd/LCDd.conf.tmpl @@ -53,6 +53,8 @@ DriverPath=/usr/lib/x86_64-linux-gnu/lcdproc/ Driver=CFontzPacket {% elif model == 'sdec' %} Driver=sdeclcd +{% elif model == 'hd44780' %} +Driver=hd44780 {% endif %} {% endif %} @@ -128,5 +130,10 @@ USB=yes ## SDEC driver for Lanner, Watchguard, Sophos sppliances ## [sdeclcd] # No options +{% elif model == 'hd44780' %} +[hd44780] +ConnectionType=ezio +Device={{ device }} +Size=16x2 {% endif %} {% endif %} diff --git a/data/templates/logs/logrotate/vyos-atop.tmpl b/data/templates/logs/logrotate/vyos-atop.tmpl new file mode 100644 index 000000000..2d078f379 --- /dev/null +++ b/data/templates/logs/logrotate/vyos-atop.tmpl @@ -0,0 +1,20 @@ +/var/log/atop/atop.log { + daily + dateext + dateformat _%Y-%m-%d_%H-%M-%S + maxsize {{ max_size }}M + missingok + nocompress + nocreate + nomail + rotate {{ rotate }} + prerotate + # stop the service + systemctl stop atop.service + endscript + postrotate + # start atop service again + systemctl start atop.service + endscript +} + diff --git a/data/templates/logs/logrotate/vyos-rsyslog.tmpl b/data/templates/logs/logrotate/vyos-rsyslog.tmpl new file mode 100644 index 000000000..f2e4d2ab2 --- /dev/null +++ b/data/templates/logs/logrotate/vyos-rsyslog.tmpl @@ -0,0 +1,13 @@ +/var/log/messages { + create + missingok + nomail + notifempty + rotate {{ rotate }} + size {{ max_size }}M + postrotate + # inform rsyslog service about rotation + /usr/lib/rsyslog/rsyslog-rotate + endscript +} + diff --git a/data/templates/mdns-repeater/avahi-daemon.tmpl b/data/templates/mdns-repeater/avahi-daemon.tmpl new file mode 100644 index 000000000..65bb5a306 --- /dev/null +++ b/data/templates/mdns-repeater/avahi-daemon.tmpl @@ -0,0 +1,18 @@ +[server] +use-ipv4=yes +use-ipv6=yes +allow-interfaces={{ interface | join(', ') }} +disallow-other-stacks=no + +[wide-area] +enable-wide-area=yes + +[publish] +disable-publishing=yes +disable-user-service-publishing=yes +publish-addresses=no +publish-hinfo=no +publish-workstation=no + +[reflector] +enable-reflector=yes diff --git a/data/templates/mdns-repeater/mdns-repeater.tmpl b/data/templates/mdns-repeater/mdns-repeater.tmpl deleted file mode 100644 index 80f4ab047..000000000 --- a/data/templates/mdns-repeater/mdns-repeater.tmpl +++ /dev/null @@ -1,2 +0,0 @@ -### Autogenerated by mdns_repeater.py ### -DAEMON_ARGS="{{ interface | join(' ') }}" diff --git a/data/templates/netflow/uacctd.conf.tmpl b/data/templates/netflow/uacctd.conf.tmpl index 1c183bb20..f81002dc1 100644 --- a/data/templates/netflow/uacctd.conf.tmpl +++ b/data/templates/netflow/uacctd.conf.tmpl @@ -1,72 +1,74 @@ # Genereated from VyOS configuration daemonize: true promisc: false -pidfile: /var/run/uacctd.pid +pidfile: /run/pmacct/uacctd.pid uacctd_group: 2 uacctd_nl_size: 2097152 -snaplen: {{ snaplen }} -{% if templatecfg['enable-egress'] != none %} -aggregate: in_iface,out_iface,src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows -{% else %} -aggregate: in_iface,src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows +snaplen: {{ packet_length }} +aggregate: in_iface{{ ',out_iface' if enable_egress is defined }},src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows +{% set pipe_size = buffer_size | int *1024 *1024 %} +plugin_pipe_size: {{ pipe_size }} +{# We need an integer division (//) without any remainder or fraction #} +plugin_buffer_size: {{ pipe_size // 1000 }} +{% if syslog_facility is defined and syslog_facility is not none %} +syslog: {{ syslog_facility }} {% endif %} -plugin_pipe_size: {{ templatecfg['plugin_pipe_size'] }} -plugin_buffer_size: {{ templatecfg['plugin_buffer_size'] }} -{% if templatecfg['syslog-facility'] != none %} -syslog: {{ templatecfg['syslog-facility'] }} -{% endif %} -{% if templatecfg['disable-imt'] == none %} +{% if disable_imt is not defined %} imt_path: /tmp/uacctd.pipe imt_mem_pools_number: 169 {% endif %} -plugins: {% if templatecfg['netflow']['servers'] != none %} -{% for server in templatecfg['netflow']['servers'] %} -{% if loop.last %}nfprobe[nf_{{ server['address'] }}]{% else %}nfprobe[nf_{{ server['address'] }}],{% endif %} -{% endfor %} -{% set plugins_presented = true %} -{% endif %} -{% if templatecfg['sflow']['servers'] != none %} -{% if plugins_presented %} -{% for server in templatecfg['sflow']['servers'] %},sfprobe[sf_{{ server['address'] }}]{% endfor %} -{% else %} -{% for server in templatecfg['sflow']['servers'] %} -{% if loop.last %}sfprobe[sf_{{ server['address'] }}]{% else %}sfprobe[sf_{{ server['address'] }}],{% endif %} -{% endfor %} -{% endif %} -{% set plugins_presented = true %} -{% endif %} -{% if templatecfg['disable-imt'] == none %} -{% if plugins_presented %},memory{% else %}memory{% endif %} -{% endif %} -{% if templatecfg['netflow']['servers'] != none %} -{% for server in templatecfg['netflow']['servers'] %} -nfprobe_receiver[nf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }} -nfprobe_version[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['version'] }} -{% if templatecfg['netflow']['engine-id'] != none %} -nfprobe_engine[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['engine-id'] }} -{% endif %} -{% if templatecfg['netflow']['max-flows'] != none %} -nfprobe_maxflows[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['max-flows'] }} -{% endif %} -{% if templatecfg['netflow']['sampling-rate'] != none %} -sampling_rate[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['sampling-rate'] }} -{% endif %} -{% if templatecfg['netflow']['source-ip'] != none %} -nfprobe_source_ip[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['source-ip'] }} +{% set plugin = [] %} +{% if disable_imt is not defined %} +{% set plugin = ['memory'] %} {% endif %} -{% if templatecfg['netflow']['timeout_string'] != '' %} -nfprobe_timeouts[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['timeout_string'] }} +{% if netflow is defined and netflow.server is defined and netflow.server is not none %} +{% for server in netflow.server %} +{% set plugin = plugin.append('nfprobe[nf_' ~ server ~ ']') %} +{% endfor %} {% endif %} -{% endfor %} +{% if sflow is defined and sflow.server is defined and sflow.server is not none %} +{% for server in sflow.server %} +{% set plugin = plugin.append('sfprobe[sf_' ~ server ~ ']') %} +{% endfor %} {% endif %} +plugins: {{ plugin | join(',') }} -{% if templatecfg['sflow']['servers'] != none %} -{% for server in templatecfg['sflow']['servers'] %} -sfprobe_receiver[sf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }} -sfprobe_agentip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['agent-address'] }} -{% if templatecfg['sflow']['sampling-rate'] != none %} -sampling_rate[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['sampling-rate'] }} +{% if netflow is defined and netflow.server is defined and netflow.server is not none %} +# NetFlow servers +{% for server, server_config in netflow.server.items() %} +nfprobe_receiver[nf_{{ server }}]: {{ server }}:{{ server_config.port }} +nfprobe_version[nf_{{ server }}]: {{ netflow.version }} +{% if netflow.engine_id is defined and netflow.engine_id is not none %} +nfprobe_engine[nf_{{ server }}]: {{ netflow.engine_id }} +{% endif %} +{% if netflow.max_flows is defined and netflow.max_flows is not none %} +nfprobe_maxflows[nf_{{ server }}]: {{ netflow.max_flows }} +{% endif %} +{% if netflow.sampling_rate is defined and netflow.sampling_rate is not none %} +sampling_rate[nf_{{ server }}]: {{ netflow.sampling_rate }} +{% endif %} +{% if netflow.source_address is defined and netflow.source_address is not none %} +nfprobe_source_ip[nf_{{ server }}]: {{ netflow.source_address }} +{% endif %} +{% if netflow.timeout is defined and netflow.timeout is not none %} +nfprobe_timeouts[nf_{{ server }}]: expint={{ netflow.timeout.expiry_interval }}:general={{ netflow.timeout.flow_generic }}:icmp={{ netflow.timeout.icmp }}:maxlife={{ netflow.timeout.max_active_life }}:tcp.fin={{ netflow.timeout.tcp_fin }}:tcp={{ netflow.timeout.tcp_generic }}:tcp.rst={{ netflow.timeout.tcp_rst }}:udp={{ netflow.timeout.udp }} +{% endif %} + +{% endfor %} {% endif %} -{% endfor %} + +{% if sflow is defined and sflow.server is defined and sflow.server is not none %} +# sFlow servers +{% for server, server_config in sflow.server.items() %} +sfprobe_receiver[sf_{{ server }}]: {{ server }}:{{ server_config.port }} +sfprobe_agentip[sf_{{ server }}]: {{ sflow.agent_address }} +{% if sflow.sampling_rate is defined and sflow.sampling_rate is not none %} +sampling_rate[sf_{{ server }}]: {{ sflow.sampling_rate }} +{% endif %} +{% if sflow.source_address is defined and sflow.source_address is not none %} +sfprobe_source_ip[sf_{{ server }}]: {{ sflow.source_address }} +{% endif %} + +{% endfor %} {% endif %} diff --git a/data/templates/ntp/ntpd.conf.tmpl b/data/templates/ntp/ntpd.conf.tmpl index 2b56b53c3..38e68f24f 100644 --- a/data/templates/ntp/ntpd.conf.tmpl +++ b/data/templates/ntp/ntpd.conf.tmpl @@ -6,6 +6,8 @@ driftfile /var/lib/ntp/ntp.drift # By default, only allow ntpd to query time sources, ignore any incoming requests restrict default noquery nopeer notrap nomodify +# Allow pool associations +restrict source nomodify notrap noquery # Local users have unrestricted access, allowing reconfiguration via ntpdc restrict 127.0.0.1 restrict -6 ::1 diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index 9e4cc6813..7a0470d0e 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -76,7 +76,7 @@ server {{ subnet | address_from_cidr }} {{ subnet | netmask_from_cidr }} nopool {% if server.push_route is defined and server.push_route is not none %} {% for route, route_config in server.push_route.items() %} {% if route | is_ipv4 %} -push "route {{ route | address_from_cidr }} {{ route | netmask_from_cidr }} {{ subnet | first_host_address }} {{ route_config.metric if route_config.metric is defined else "0" }}" +push "route {{ route | address_from_cidr }} {{ route | netmask_from_cidr }}{% if route_config.metric is defined %} {{ subnet | first_host_address }} {{ route_config.metric }}{% endif %}" {% elif route | is_ipv6 %} push "route-ipv6 {{ route }}" {% endif %} @@ -126,6 +126,12 @@ push "dhcp-option DNS6 {{ nameserver }}" {% if server.domain_name is defined and server.domain_name is not none %} push "dhcp-option DOMAIN {{ server.domain_name }}" {% endif %} +{% if server.mfa is defined and server.mfa is not none %} +{% if server.mfa.totp is defined and server.mfa.totp is not none %} +{% set totp_config = server.mfa.totp %} +plugin "{{ plugin_dir}}/openvpn-otp.so" "otp_secrets=/config/auth/openvpn/{{ ifname }}-otp-secrets {{ 'otp_slop=' ~ totp_config.slop }} {{ 'totp_t0=' ~ totp_config.drift }} {{ 'totp_step=' ~ totp_config.step }} {{ 'totp_digits=' ~ totp_config.digits }} password_is_cr={{ '1' if totp_config.challenge == 'enable' else '0' }}" +{% endif %} +{% endif %} {% endif %} {% else %} # @@ -176,6 +182,8 @@ tls-version-min {{ tls.tls_version_min }} {% endif %} {% if tls.dh_params is defined and tls.dh_params is not none %} dh /run/openvpn/{{ ifname }}_dh.pem +{% elif mode == 'server' and tls.private_key is defined %} +dh none {% endif %} {% if tls.auth_key is defined and tls.auth_key is not none %} {% if mode == 'client' %} @@ -216,16 +224,3 @@ auth {{ hash }} auth-user-pass {{ auth_user_pass_file }} auth-retry nointeract {% endif %} - -{% if openvpn_option is defined and openvpn_option is not none %} -# -# Custom options added by user (not validated) -# -{% for option in openvpn_option %} -{% for argument in option.split('--') %} -{% if argument is defined and argument != '' %} ---{{ argument }} -{% endif %} -{% endfor %} -{% endfor %} -{% endif %} diff --git a/data/templates/openvpn/service-override.conf.tmpl b/data/templates/openvpn/service-override.conf.tmpl new file mode 100644 index 000000000..069bdbd08 --- /dev/null +++ b/data/templates/openvpn/service-override.conf.tmpl @@ -0,0 +1,20 @@ +[Service] +ExecStart= +ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid +{%- if openvpn_option is defined and openvpn_option is not none %} +{% for option in openvpn_option %} +{# Remove the '--' prefix from variable if it is presented #} +{% if option.startswith('--') %} +{% set option = option.split('--', maxsplit=1)[1] %} +{% endif %} +{# Workaround to pass '--push' options properly. Previously openvpn accepted this option without values in double-quotes #} +{# But now it stopped doing this, so we need to add them for compatibility #} +{# HOWEVER! This is a raw option and we do not promise that this or any other trick will work for all the cases. #} +{# Using 'openvpn-option' you take all responsibility for compatibility for yourself. #} +{% if option.startswith('push') and not (option.startswith('push "') and option.endswith('"')) %} +{% set option = 'push \"%s\"'|format(option.split('push ', maxsplit=1)[1]) %} +{% endif %} + --{{ option }} +{%- endfor %} +{% endif %} + diff --git a/data/templates/snmp/etc.snmp.conf.tmpl b/data/templates/snmp/etc.snmp.conf.tmpl index 6e4c6f063..f7d9a3c17 100644 --- a/data/templates/snmp/etc.snmp.conf.tmpl +++ b/data/templates/snmp/etc.snmp.conf.tmpl @@ -1,4 +1,4 @@ ### Autogenerated by snmp.py ### -{% if trap_source %} +{% if trap_source is defined and trap_source is not none %} clientaddr {{ trap_source }} {% endif %} diff --git a/data/templates/snmp/etc.snmpd.conf.tmpl b/data/templates/snmp/etc.snmpd.conf.tmpl index db2114fa1..befea0122 100644 --- a/data/templates/snmp/etc.snmpd.conf.tmpl +++ b/data/templates/snmp/etc.snmpd.conf.tmpl @@ -33,87 +33,152 @@ interface_replace_old yes # Default system description is VyOS version sysDescr VyOS {{ version }} -{% if description %} +{% if description is defined and description is not none %} # Description SysDescr {{ description }} {% endif %} # Listen -agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161{% if ipv6_enabled %},udp6:161{% endif %}{% endif %} +{% set options = [] %} +{% if listen_address is defined and listen_address is not none %} +{% for address, address_options in listen_address.items() %} +{% if address | is_ipv6 %} +{% set protocol = protocol ~ '6' %} +{% endif %} +{% set _ = options.append(protocol ~ ':' ~ address | bracketize_ipv6 ~ ':' ~ address_options.port) %} +{% endfor %} +{% else %} +{% set _ = options.append(protocol ~ ':161') %} +{% if ipv6_disabled is not defined %} +{% set _ = options.append(protocol ~ '6:161') %} +{% endif %} +{% endif %} +agentaddress unix:/run/snmpd.socket{{ ',' ~ options | join(',') if options is defined and options is not none }} # SNMP communities -{% for c in communities %} -{% if c.network_v4 %} -{% for network in c.network_v4 %} -{{ c.authorization }}community {{ c.name }} {{ network }} -{% endfor %} -{% elif not c.has_source %} -{{ c.authorization }}community {{ c.name }} -{% endif %} -{% if c.network_v6 %} -{% for network in c.network_v6 %} -{{ c.authorization }}community6 {{ c.name }} {{ network }} -{% endfor %} -{% elif not c.has_source %} -{{ c.authorization }}community6 {{ c.name }} -{% endif %} -{% endfor %} +{% if community is defined and community is not none %} +{% for comm, comm_config in community.items() %} +{% if comm_config.client is defined and comm_config.client is not none %} +{% for client in comm_config.client %} +{% if client | is_ipv4 %} +{{ comm_config.authorization }}community {{ comm }} {{ client }} +{% elif client | is_ipv6 %} +{{ comm_config.authorization }}community6 {{ comm }} {{ client }} +{% endif %} +{% endfor %} +{% endif %} +{% if comm_config.network is defined and comm_config.network is not none %} +{% for network in comm_config.network %} +{% if network | is_ipv4 %} +{{ comm_config.authorization }}community {{ comm }} {{ network }} +{% elif client | is_ipv6 %} +{{ comm_config.authorization }}community6 {{ comm }} {{ network }} +{% endif %} +{% endfor %} +{% endif %} +{% if comm_config.client is not defined and comm_config.network is not defined %} +{{ comm_config.authorization }}community {{ comm }} +{% endif %} +{% endfor %} +{% endif %} -{% if contact %} +{% if contact is defined and contact is not none %} # system contact information SysContact {{ contact }} {% endif %} -{% if location %} +{% if location is defined and location is not none %} # system location information SysLocation {{ location }} {% endif %} -{% if smux_peers %} +{% if smux_peer is defined and smux_peer is not none %} # additional smux peers -{% for sp in smux_peers %} -smuxpeer {{ sp }} +{% for peer in smux_peer %} +smuxpeer {{ peer }} {% endfor %} {% endif %} -{% if trap_targets %} +{% if trap_target is defined and trap_target is not none %} # if there is a problem - tell someone! -{% for trap in trap_targets %} -trap2sink {{ trap.target }}{{ ":" + trap.port if trap.port is defined }} {{ trap.community }} +{% for trap, trap_config in trap_target.items() %} +trap2sink {{ trap }}:{{ trap_config.port }} {{ trap_config.community }} {% endfor %} {% endif %} -{% if v3_enabled %} +{% if v3 is defined and v3 is not none %} # # SNMPv3 stuff goes here # +{% if v3.view is defined and v3.view is not none %} # views -{% for view in v3_views %} -{% for oid in view.oids %} -view {{ view.name }} included .{{ oid.oid }} +{% for view, view_config in v3.view.items() %} +{% if view_config.oid is defined and view_config.oid is not none %} +{% for oid in view_config.oid %} +view {{ view }} included .{{ oid }} +{% endfor %} +{% endif %} {% endfor %} -{% endfor %} +{% endif %} # access +{% if v3.group is defined and v3.group is not none %} # context sec.model sec.level match read write notif -{% for group in v3_groups %} -access {{ group.name }} "" usm {{ group.seclevel }} exact {{ group.view }} {% if group.mode == 'ro' %}none{% else %}{{ group.view }}{% endif %} none -{% endfor %} +{% for group, group_config in v3.group.items() %} +access {{ group }} "" usm {{ group_config.seclevel }} exact {{ group_config.view }} {% if group_config.mode == 'ro' %}none{% else %}{{ group_config.view }}{% endif %} none +{% endfor %} +{% endif %} # trap-target -{% for t in v3_traps %} -trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ v3_engineid }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} -{% endfor %} +{% if v3.trap_target is defined and v3.trap_target is not none %} +{% for trap, trap_config in v3.trap_target.items() %} +{% set options = '' %} +{% if trap_config.type == 'inform' %} +{% set options = options ~ ' -Ci' %} +{% endif %} +{% if v3.engineid is defined and v3.engineid is not none %} +{% set options = options ~ ' -e "' ~ v3.engineid ~ '"' %} +{% endif %} +{% if trap_config.user is defined and trap_config.user is not none %} +{% set options = options ~ ' -u ' ~ trap_config.user %} +{% endif %} +{% if trap_config.auth is defined and trap_config.auth.plaintext_password is defined or trap_config.auth.encrypted_password is defined %} +{% set options = options ~ ' -a ' ~ trap_config.auth.type %} +{% if trap_config.auth.plaintext_password is defined and trap_config.auth.plaintext_password is not none %} +{% set options = options ~ ' -A ' ~ trap_config.auth.plaintext_password %} +{% elif trap_config.auth.encrypted_password is defined and trap_config.auth.encrypted_password is not none %} +{% set options = options ~ ' -3m ' ~ trap_config.auth.encrypted_password %} +{% endif %} +{% if trap_config.privacy is defined and trap_config.privacy.plaintext_password is defined or trap_config.privacy.encrypted_password is defined %} +{% set options = options ~ ' -x ' ~ trap_config.privacy.type %} +{% if trap_config.privacy.plaintext_password is defined and trap_config.privacy.plaintext_password is not none %} +{% set options = options ~ ' -X ' ~ trap_config.privacy.plaintext_password %} +{% elif trap_config.privacy.encrypted_password is defined and trap_config.privacy.encrypted_password is not none %} +{% set options = options ~ ' -3M ' ~ trap_config.privacy.encrypted_password %} +{% endif %} +{% set options = options ~ ' -l authPriv' %} +{% else %} +{% set options = options ~ ' -l authNoPriv' %} +{% endif %} +{% else %} +{% set options = options ~ ' -l noAuthNoPriv' %} +{% endif %} +trapsess -v 3 {{ options }} {{ trap }}:{{ trap_config.protocol }}:{{ trap_config.port }} +{% endfor %} +{% endif %} # group -{% for u in v3_users %} -group {{ u.group }} usm {{ u.name }} -{% endfor %} +{% if v3.user is defined and v3.user is not none %} +{% for user, user_config in v3.user.items() %} +group {{ user_config.group }} usm {{ user }} +{% endfor %} +{% endif %} +{# SNMPv3 end #} {% endif %} -{% if script_ext %} +{% if script_extensions is defined and script_extensions.extension_name is defined and script_extensions.extension_name is not none %} # extension scripts -{% for ext in script_ext|sort(attribute='name') %} -extend {{ ext.name }} {{ ext.script }} +{% for script, script_config in script_extensions.extension_name.items() | sort(attribute=script) %} +extend {{ script }} {{ script_config.script }} {% endfor %} {% endif %} diff --git a/data/templates/snmp/override.conf.tmpl b/data/templates/snmp/override.conf.tmpl index 2ac45a89f..3b00aab83 100644 --- a/data/templates/snmp/override.conf.tmpl +++ b/data/templates/snmp/override.conf.tmpl @@ -1,5 +1,5 @@ {% set vrf_command = 'ip vrf exec ' + vrf + ' ' if vrf is defined else '' %} -{% set oid_route_table = ' ' if route_table is sameas true else '-I -ipCidrRouteTable,inetCidrRouteTable' %} +{% set oid_route_table = ' ' if oid_enable is defined and oid_enable == 'route-table' else '-I -ipCidrRouteTable,inetCidrRouteTable' %} [Unit] StartLimitIntervalSec=0 After=vyos-router.service diff --git a/data/templates/snmp/usr.snmpd.conf.tmpl b/data/templates/snmp/usr.snmpd.conf.tmpl index e2c5ec102..1c688a61e 100644 --- a/data/templates/snmp/usr.snmpd.conf.tmpl +++ b/data/templates/snmp/usr.snmpd.conf.tmpl @@ -1,6 +1,8 @@ ### Autogenerated by snmp.py ### -{% for u in v3_users %} -{{ u.mode }}user {{ u.name }} -{% endfor %} +{% if v3 is defined and v3.user is defined and v3.user is not none %} +{% for user, user_config in v3.user.items() %} +{{ user_config.mode }}user {{ user }} +{% endfor %} +{% endif %} rwuser {{ vyos_user }} diff --git a/data/templates/snmp/var.snmpd.conf.tmpl b/data/templates/snmp/var.snmpd.conf.tmpl index c779587df..5871a8234 100644 --- a/data/templates/snmp/var.snmpd.conf.tmpl +++ b/data/templates/snmp/var.snmpd.conf.tmpl @@ -1,14 +1,16 @@ ### Autogenerated by snmp.py ### # user -{% for u in v3_users %} -{% if u.authOID == 'none' %} -createUser {{ u.name }} -{% else %} -usmUser 1 3 0x{{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} 0x{{ u.authMasterKey }} {{ u.privOID }} 0x{{ u.privMasterKey }} 0x -{% endif %} -{% endfor %} +{% if v3 is defined and v3 is not none %} +{% if v3.user is defined and v3.user is not none %} +{% for user, user_config in v3.user.items() %} +usmUser 1 3 0x{{ v3.engineid }} "{{ user }}" "{{ user }}" NULL {{ user_config.auth.type | snmp_auth_oid }} 0x{{ user_config.auth.encrypted_password }} {{ user_config.privacy.type | snmp_auth_oid }} 0x{{ user_config.privacy.encrypted_password }} 0x +{% endfor %} +{% endif %} +# VyOS default user createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES -{% if v3_engineid %} -oldEngineID 0x{{ v3_engineid }} + +{% if v3.engineid is defined and v3.engineid is not none %} +oldEngineID 0x{{ v3.engineid }} +{% endif %} {% endif %} diff --git a/data/templates/squid/squid.conf.tmpl b/data/templates/squid/squid.conf.tmpl index 80826fc75..26aff90bf 100644 --- a/data/templates/squid/squid.conf.tmpl +++ b/data/templates/squid/squid.conf.tmpl @@ -88,7 +88,7 @@ tcp_outgoing_address {{ outgoing_address }} {% if listen_address is defined and listen_address is not none %} {% for address, config in listen_address.items() %} -http_port {{ address }}:{{ config.port if config.port is defined else default_port }} {{ 'intercept' if config.disable_transparent is not defined }} +http_port {{ address | bracketize_ipv6 }}:{{ config.port if config.port is defined else default_port }} {{ 'intercept' if config.disable_transparent is not defined }} {% endfor %} {% endif %} http_port 127.0.0.1:{{ default_port }} diff --git a/data/templates/syslog/rsyslog.conf.tmpl b/data/templates/syslog/rsyslog.conf.tmpl index e25ef48d4..2fb621760 100644 --- a/data/templates/syslog/rsyslog.conf.tmpl +++ b/data/templates/syslog/rsyslog.conf.tmpl @@ -25,12 +25,18 @@ $outchannel {{ file }},{{ file_options['log-file'] }},{{ file_options['max-size' {% if host_options.proto == 'tcp' %} {% if host_options.port is defined %} {% if host_options.oct_count is defined %} -{{ host_options.selectors }} @@(o){{ host }}:{{ host_options.port }};RSYSLOG_SyslogProtocol23Format +{{ host_options.selectors }} @@(o){{ host | bracketize_ipv6 }}:{{ host_options.port }};RSYSLOG_SyslogProtocol23Format {% else %} -{{ host_options.selectors }} @@{{ host }}:{{ host_options.port }} +{{ host_options.selectors }} @@{{ host | bracketize_ipv6 }}:{{ host_options.port }} {% endif %} {% else %} -{{ host_options.selectors }} @@{{ host }} +{{ host_options.selectors }} @@{{ host | bracketize_ipv6 }} +{% endif %} +{% elif host_options.proto == 'udp' %} +{% if host_options.port is defined %} +{{ host_options.selectors }} @{{ host | bracketize_ipv6 }}:{{ host_options.port }}{{ ';RSYSLOG_SyslogProtocol23Format' if host_options.oct_count is sameas true }} +{% else %} +{{ host_options.selectors }} @{{ host | bracketize_ipv6 }} {% endif %} {% else %} {% if host_options['port'] %} diff --git a/data/templates/tftp-server/default.tmpl b/data/templates/tftp-server/default.tmpl index 6b2d6a903..a7edf60ad 100644 --- a/data/templates/tftp-server/default.tmpl +++ b/data/templates/tftp-server/default.tmpl @@ -1,2 +1,7 @@ ### Autogenerated by tftp_server.py ### DAEMON_ARGS="--listen --user tftp --address {{ listen_address }} {{ "--create --umask 000" if allow_upload is defined }} --secure {{ directory }}" +{% if vrf is defined %} +VRF_ARGS="ip vrf exec {{ vrf }}" +{% else %} +VRF_ARGS="" +{% endif %} diff --git a/data/templates/vrrp/keepalived.conf.tmpl b/data/templates/vrrp/keepalived.conf.tmpl index b4824a994..6585fc60b 100644 --- a/data/templates/vrrp/keepalived.conf.tmpl +++ b/data/templates/vrrp/keepalived.conf.tmpl @@ -5,9 +5,6 @@ global_defs { dynamic_interfaces script_user root - # Don't run scripts configured to be run as root if any part of the path - # is writable by a non-root user. - enable_script_security notify_fifo /run/keepalived/keepalived_notify_fifo notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py } @@ -86,15 +83,24 @@ vrrp_instance {{ name }} { {% endif %} {% if sync_group is defined and sync_group is not none %} -{% for name, group_config in sync_group.items() if group_config.disable is not defined %} +{% for name, sync_group_config in sync_group.items() if sync_group_config.disable is not defined %} vrrp_sync_group {{ name }} { group { -{% if group_config.member is defined and group_config.member is not none %} -{% for member in group_config.member %} +{% if sync_group_config.member is defined and sync_group_config.member is not none %} +{% for member in sync_group_config.member %} {{ member }} {% endfor %} {% endif %} } + +{# Health-check scripts should be in section sync-group if member is part of the sync-group T4081 #} +{% for name, group_config in group.items() if group_config.disable is not defined %} +{% if group_config.health_check is defined and group_config.health_check.script is defined and group_config.health_check.script is not none and name in sync_group_config.member %} + track_script { + healthcheck_{{ name }} + } +{% endif %} +{% endfor %} {% if conntrack_sync_group is defined and conntrack_sync_group == name %} {% set vyos_helper = "/usr/libexec/vyos/vyos-vrrp-conntracksync.sh" %} notify_master "{{ vyos_helper }} master {{ name }}" diff --git a/data/templates/vyos-hostsd/hosts.tmpl b/data/templates/vyos-hostsd/hosts.tmpl index 8b73c6e51..03662d562 100644 --- a/data/templates/vyos-hostsd/hosts.tmpl +++ b/data/templates/vyos-hostsd/hosts.tmpl @@ -17,8 +17,9 @@ ff02::2 ip6-allrouters {% for tag, taghosts in hosts.items() %} # {{ tag }} {% for host, hostprops in taghosts.items() if hostprops.address is defined %} -{{ "%-15s" | format(hostprops.address) }} {{ host }} {{ hostprops.aliases|join(' ') if hostprops.aliases is defined }} +{% for addr in hostprops.address %} +{{ "%-15s" | format(addr) }} {{ host }} {{ hostprops.aliases|join(' ') if hostprops.aliases is defined }} +{% endfor %} {% endfor %} {% endfor %} {% endif %} - diff --git a/data/templates/zone_policy/nftables.tmpl b/data/templates/zone_policy/nftables.tmpl new file mode 100644 index 000000000..21230c688 --- /dev/null +++ b/data/templates/zone_policy/nftables.tmpl @@ -0,0 +1,97 @@ +#!/usr/sbin/nft -f + +{% if cleanup_commands is defined %} +{% for command in cleanup_commands %} +{{ command }} +{% endfor %} +{% endif %} + +{% if zone is defined %} +table ip filter { +{% for zone_name, zone_conf in zone.items() if zone_conf.ipv4 %} +{% if zone_conf.local_zone is defined %} + chain VZONE_{{ zone_name }}_IN { + iifname lo counter return +{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %} + iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }} + iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } + chain VZONE_{{ zone_name }}_OUT { + oifname lo counter return +{% for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.name is defined %} + oifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }} + oifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } +{% else %} + chain VZONE_{{ zone_name }} { + iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6=False) }} +{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %} +{% if zone[from_zone].local_zone is not defined %} + iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }} + iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endif %} +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } +{% endif %} +{% endfor %} +} + +table ip6 filter { +{% for zone_name, zone_conf in zone.items() if zone_conf.ipv6 %} +{% if zone_conf.local_zone is defined %} + chain VZONE6_{{ zone_name }}_IN { + iifname lo counter return +{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %} + iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }} + iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } + chain VZONE6_{{ zone_name }}_OUT { + oifname lo counter return +{% for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.ipv6_name is defined %} + oifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }} + oifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } +{% else %} + chain VZONE6_{{ zone_name }} { + iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6=True) }} +{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %} +{% if zone[from_zone].local_zone is not defined %} + iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }} + iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endif %} +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } +{% endif %} +{% endfor %} +} + +{% for zone_name, zone_conf in zone.items() %} +{% if zone_conf.ipv4 %} +{% if 'local_zone' in zone_conf %} +insert rule ip filter VYOS_FW_LOCAL counter jump VZONE_{{ zone_name }}_IN +insert rule ip filter VYOS_FW_OUTPUT counter jump VZONE_{{ zone_name }}_OUT +{% else %} +insert rule ip filter VYOS_FW_OUT oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE_{{ zone_name }} +{% endif %} +{% endif %} +{% if zone_conf.ipv6 %} +{% if 'local_zone' in zone_conf %} +insert rule ip6 filter VYOS_FW6_LOCAL counter jump VZONE6_{{ zone_name }}_IN +insert rule ip6 filter VYOS_FW6_OUTPUT counter jump VZONE6_{{ zone_name }}_OUT +{% else %} +insert rule ip6 filter VYOS_FW6_OUT oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE6_{{ zone_name }} +{% endif %} +{% endif %} +{% endfor %} + +{% endif %} diff --git a/debian/control b/debian/control index e407e05b0..3d33a48a6 100644 --- a/debian/control +++ b/debian/control @@ -33,6 +33,7 @@ Architecture: amd64 arm64 Depends: ${python3:Depends}, accel-ppp, + avahi-daemon, beep, bmon, bsdmainutils, @@ -72,6 +73,7 @@ Depends: iw, keepalived (>=2.0.5), lcdproc, + lcdproc-extra-drivers, libatomic1, libcharon-extra-plugins (>=5.9), libcharon-extauth-plugins (>=5.9), @@ -86,7 +88,6 @@ Depends: lldpd, lm-sensors, lsscsi, - mdns-repeater, minisign, modemmanager, mtr-tiny, @@ -105,6 +106,7 @@ Depends: openvpn, openvpn-auth-ldap, openvpn-auth-radius, + openvpn-otp, pciutils, pdns-recursor, pmacct (>= 1.6.0), @@ -182,5 +184,6 @@ Description: VyOS configuration scripts and data for VMware Package: vyos-1x-smoketest Architecture: all Depends: + snmp, vyos-1x Description: VyOS build sanity checking toolkit diff --git a/debian/rules b/debian/rules index c7a7138e1..5a58aeeb6 100755 --- a/debian/rules +++ b/debian/rules @@ -120,6 +120,10 @@ override_dh_auto_install: mkdir -p $(DIR)/$(VYOS_BIN_DIR) cp -r smoketest/bin/* $(DIR)/$(VYOS_BIN_DIR) + # Install udev script + mkdir -p $(DIR)/usr/lib/udev + cp src/helpers/vyos_net_name $(DIR)/usr/lib/udev + ifeq ($(DEB_TARGET_ARCH),amd64) # We only install XDP on amd64 systems mkdir -p $(DIR)/$(VYOS_DATA_DIR)/xdp diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index 20c119e63..63dff43a5 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,6 +1,8 @@ +etc/cron.d etc/cron.hourly etc/dhcp etc/ipsec.d +etc/logrotate.d etc/netplug etc/opennhrp etc/ppp diff --git a/interface-definitions/bcast-relay.xml.in b/interface-definitions/bcast-relay.xml.in index 3f781f07f..aeaa5ab37 100644 --- a/interface-definitions/bcast-relay.xml.in +++ b/interface-definitions/bcast-relay.xml.in @@ -39,15 +39,7 @@ <help>Description</help> </properties> </leafNode> - <leafNode name="interface"> - <properties> - <help>Interface to repeat UDP broadcasts to [REQUIRED]</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <multi/> - </properties> - </leafNode> + #include <include/generic-interface-multi.xml.i> #include <include/port-number.xml.i> </children> </tagNode> diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in index bf672307c..30c7110b8 100644 --- a/interface-definitions/containers.xml.in +++ b/interface-definitions/containers.xml.in @@ -21,6 +21,42 @@ <valueless/> </properties> </leafNode> + <leafNode name="cap-add"> + <properties> + <help>Container capabilities/permissions</help> + <completionHelp> + <list>net-admin net-bind-service net-raw setpcap sys-admin sys-time</list> + </completionHelp> + <valueHelp> + <format>net-admin</format> + <description>Network operations (interface, firewall, routing tables)</description> + </valueHelp> + <valueHelp> + <format>net-bind-service</format> + <description>Bind a socket to privileged ports (port numbers less than 1024)</description> + </valueHelp> + <valueHelp> + <format>net-raw</format> + <description>Permission to create raw network sockets</description> + </valueHelp> + <valueHelp> + <format>setpcap</format> + <description>Capability sets (from bounded or inherited set)</description> + </valueHelp> + <valueHelp> + <format>sys-admin</format> + <description>Administation operations (quotactl, mount, sethostname, setdomainame)</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> + </constraint> + <multi/> + </properties> + </leafNode> #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> <tagNode name="environment"> @@ -141,7 +177,7 @@ </tagNode> <leafNode name="restart"> <properties> - <help>Mount a volume into the container</help> + <help>Restart options for container</help> <completionHelp> <list>no on-failure always</list> </completionHelp> diff --git a/interface-definitions/dhcp-relay.xml.in b/interface-definitions/dhcp-relay.xml.in index 0d485ef80..483e776a7 100644 --- a/interface-definitions/dhcp-relay.xml.in +++ b/interface-definitions/dhcp-relay.xml.in @@ -9,15 +9,7 @@ <priority>910</priority> </properties> <children> - <leafNode name="interface"> - <properties> - <help>DHCP relay interface [REQUIRED]</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -b</script> - </completionHelp> - <multi/> - </properties> - </leafNode> + #include <include/generic-interface-multi-broadcast.xml.i> <node name="relay-options"> <properties> <help>Relay options</help> diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index 47bdc4db1..d1ed579e9 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -254,9 +254,9 @@ <properties> <help>DHCP lease range</help> <constraint> - <regex>[-_a-zA-Z0-9.]+</regex> + <regex>^[-_a-zA-Z0-9.]+$</regex> </constraint> - <constraintErrorMessage>Invalid DHCP lease range name. May only contain letters, numbers and .-_</constraintErrorMessage> + <constraintErrorMessage>Invalid range name, may only be alphanumeric, dot and hyphen</constraintErrorMessage> </properties> <children> <leafNode name="start"> @@ -289,9 +289,9 @@ <properties> <help>Name of static mapping</help> <constraint> - <regex>[-_a-zA-Z0-9.]+</regex> + <regex>^[-_a-zA-Z0-9.]+$</regex> </constraint> - <constraintErrorMessage>Invalid static mapping name. May only contain letters, numbers and .-_</constraintErrorMessage> + <constraintErrorMessage>Invalid static mapping name, may only be alphanumeric, dot and hyphen</constraintErrorMessage> </properties> <children> #include <include/generic-disable-node.xml.i> diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in index 2b1644609..005a55ab3 100644 --- a/interface-definitions/dns-domain-name.xml.in +++ b/interface-definitions/dns-domain-name.xml.in @@ -102,11 +102,11 @@ <constraint> <validator name="ip-address"/> </constraint> + <multi/> </properties> </leafNode> </children> </tagNode> - </children> </node> </children> diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 250642691..64826516e 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -274,6 +274,12 @@ </leafNode> </children> </node> + <leafNode name="ipv6-enable"> + <properties> + <help>Allow explicit IPv6 addresses for Dynamic DNS for this interface</help> + <valueless/> + </properties> + </leafNode> </children> </tagNode> </children> diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index 5b0c87597..4faf604ad 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -105,6 +105,456 @@ </leafNode> </children> </tagNode> + <tagNode name="authoritative-domain"> + <properties> + <help>Domain to host authoritative records for</help> + <valueHelp> + <format>text</format> + <description>An absolute DNS name</description> + </valueHelp> + <constraint> + <regex>^[-_a-zA-Z0-9.]{1,63}$</regex> + </constraint> + </properties> + <children> + <node name="records"> + <properties> + <help>DNS zone records</help> + </properties> + <children> + <tagNode name="a"> + <properties> + <help>"A" record</help> + <valueHelp> + <format>text</format> + <description>A DNS name relative to the root record</description> + </valueHelp> + <valueHelp> + <format>@</format> + <description>Root record</description> + </valueHelp> + <constraint> + <regex>^([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)$</regex> + </constraint> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IPv4 address [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + #include <include/dns/time-to-live.xml.i> + #include <include/generic-disable-node.xml.i> + </children> + </tagNode> + <tagNode name="aaaa"> + <properties> + <help>"AAAA" record</help> + <valueHelp> + <format>text</format> + <description>A DNS name relative to the root record</description> + </valueHelp> + <valueHelp> + <format>@</format> + <description>Root record</description> + </valueHelp> + <constraint> + <regex>^([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)$</regex> + </constraint> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IPv6 address [REQUIRED]</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + #include <include/dns/time-to-live.xml.i> + #include <include/generic-disable-node.xml.i> + </children> + </tagNode> + <tagNode name="cname"> + <properties> + <help>"CNAME" record</help> + <valueHelp> + <format>text</format> + <description>A DNS name relative to the root record</description> + </valueHelp> + <valueHelp> + <format>@</format> + <description>Root record</description> + </valueHelp> + <constraint> + <regex>^([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)$</regex> + </constraint> + </properties> + <children> + <leafNode name="target"> + <properties> + <help>Target DNS name [REQUIRED]</help> + <valueHelp> + <format>name.example.com</format> + <description>An absolute DNS name</description> + </valueHelp> + <constraint> + <regex>^[-_a-zA-Z0-9.]{1,63}(?<!\.)$</regex> + </constraint> + </properties> + </leafNode> + #include <include/dns/time-to-live.xml.i> + #include <include/generic-disable-node.xml.i> + </children> + </tagNode> + <tagNode name="mx"> + <properties> + <help>"MX" record</help> + <valueHelp> + <format>text</format> + <description>A DNS name relative to the root record</description> + </valueHelp> + <valueHelp> + <format>@</format> + <description>Root record</description> + </valueHelp> + <constraint> + <regex>^([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)$</regex> + </constraint> + </properties> + <children> + <tagNode name="server"> + <properties> + <help>Mail server [REQUIRED]</help> + <valueHelp> + <format>name.example.com</format> + <description>An absolute DNS name</description> + </valueHelp> + <constraint> + <regex>^[-_a-zA-Z0-9.]{1,63}(?<!\.)$</regex> + </constraint> + </properties> + <children> + <leafNode name="priority"> + <properties> + <help>Server priority</help> + <valueHelp> + <format>u32:1-999</format> + <description>Server priority (lower numbers are higher priority)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999"/> + </constraint> + </properties> + <defaultValue>10</defaultValue> + </leafNode> + </children> + </tagNode> + #include <include/dns/time-to-live.xml.i> + #include <include/generic-disable-node.xml.i> + </children> + </tagNode> + <tagNode name="ptr"> + <properties> + <help>"PTR" record</help> + <valueHelp> + <format>text</format> + <description>A DNS name relative to the root record</description> + </valueHelp> + <valueHelp> + <format>@</format> + <description>Root record</description> + </valueHelp> + <constraint> + <regex>^([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)$</regex> + </constraint> + </properties> + <children> + <leafNode name="target"> + <properties> + <help>Target DNS name [REQUIRED]</help> + <valueHelp> + <format>name.example.com</format> + <description>An absolute DNS name</description> + </valueHelp> + <constraint> + <regex>^[-_a-zA-Z0-9.]{1,63}(?<!\.)$</regex> + </constraint> + </properties> + </leafNode> + #include <include/dns/time-to-live.xml.i> + #include <include/generic-disable-node.xml.i> + </children> + </tagNode> + <tagNode name="txt"> + <properties> + <help>"TXT" record</help> + <valueHelp> + <format>text</format> + <description>A DNS name relative to the root record</description> + </valueHelp> + <valueHelp> + <format>@</format> + <description>Root record</description> + </valueHelp> + <constraint> + <regex>^([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)$</regex> + </constraint> + </properties> + <children> + <leafNode name="value"> + <properties> + <help>Record contents [REQUIRED]</help> + <valueHelp> + <format>text</format> + <description>Record contents</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + #include <include/dns/time-to-live.xml.i> + #include <include/generic-disable-node.xml.i> + </children> + </tagNode> + <tagNode name="spf"> + <properties> + <help>"SPF" record (type=SPF)</help> + <valueHelp> + <format>text</format> + <description>A DNS name relative to the root record</description> + </valueHelp> + <valueHelp> + <format>@</format> + <description>Root record</description> + </valueHelp> + <constraint> + <regex>^([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)$</regex> + </constraint> + </properties> + <children> + <leafNode name="value"> + <properties> + <help>Record contents [REQUIRED]</help> + <valueHelp> + <format>text</format> + <description>Record contents</description> + </valueHelp> + </properties> + </leafNode> + #include <include/dns/time-to-live.xml.i> + #include <include/generic-disable-node.xml.i> + </children> + </tagNode> + <tagNode name="srv"> + <properties> + <help>"SRV" record</help> + <valueHelp> + <format>text</format> + <description>A DNS name relative to the root record</description> + </valueHelp> + <valueHelp> + <format>@</format> + <description>Root record</description> + </valueHelp> + <constraint> + <regex>^([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)$</regex> + </constraint> + </properties> + <children> + <tagNode name="entry"> + <properties> + <help>Service entry [REQUIRED]</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Entry number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <children> + <leafNode name="hostname"> + <properties> + <help>Server hostname [REQUIRED]</help> + <valueHelp> + <format>name.example.com</format> + <description>An absolute DNS name</description> + </valueHelp> + <constraint> + <regex>^[-_a-zA-Z0-9.]{1,63}(?<!\.)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Port number [REQUIRED]</help> + <valueHelp> + <format>u32:0-65535</format> + <description>TCP/UDP port number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65536"/> + </constraint> + </properties> + </leafNode> + <leafNode name="priority"> + <properties> + <help>Entry priority</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Entry priority (lower numbers are higher priority)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <defaultValue>10</defaultValue> + </leafNode> + <leafNode name="weight"> + <properties> + <help>Entry weight</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Entry weight</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + </children> + </tagNode> + #include <include/dns/time-to-live.xml.i> + #include <include/generic-disable-node.xml.i> + </children> + </tagNode> + <tagNode name="naptr"> + <properties> + <help>"NAPTR" record</help> + <valueHelp> + <format>text</format> + <description>A DNS name relative to the root record</description> + </valueHelp> + <valueHelp> + <format>@</format> + <description>Root record</description> + </valueHelp> + <constraint> + <regex>^([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)$</regex> + </constraint> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>NAPTR rule [REQUIRED]</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Rule number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <children> + <leafNode name="order"> + <properties> + <help>Rule order</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Rule order (lower order is evaluated first)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="preference"> + <properties> + <help>Rule preference</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Rule preference</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="lookup-srv"> + <properties> + <help>"S" flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="lookup-a"> + <properties> + <help>"A" flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="resolve-uri"> + <properties> + <help>"U" flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="protocol-specific"> + <properties> + <help>"P" flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="service"> + <properties> + <help>Service type</help> + <constraint> + <regex>^[a-zA-Z][a-zA-Z0-9]{0,31}(\+[a-zA-Z][a-zA-Z0-9]{0,31})?$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="regexp"> + <properties> + <help>Regular expression</help> + </properties> + </leafNode> + <leafNode name="replacement"> + <properties> + <help>Replacement DNS name</help> + <valueHelp> + <format>name.example.com</format> + <description>An absolute DNS name</description> + </valueHelp> + <constraint> + <regex>^[-_a-zA-Z0-9.]{1,63}(?<!\.)$</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + #include <include/dns/time-to-live.xml.i> + #include <include/generic-disable-node.xml.i> + </children> + </tagNode> + </children> + </node> + #include <include/generic-disable-node.xml.i> + </children> + </tagNode> <leafNode name="ignore-hosts-file"> <properties> <help>Do not use local /etc/hosts file in name resolution</help> @@ -114,7 +564,7 @@ <leafNode name="no-serve-rfc1918"> <properties> <help>Makes the server authoritatively not aware of RFC1918 addresses</help> - <valueless/> + <valueless/> </properties> </leafNode> <leafNode name="allow-from"> diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index f07c619a8..78a48a522 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -1,6 +1,6 @@ <?xml version="1.0"?> <interfaceDefinition> - <node name="nfirewall" owner="${vyos_conf_scripts_dir}/firewall.py"> + <node name="firewall" owner="${vyos_conf_scripts_dir}/firewall.py"> <properties> <priority>199</priority> <help>Firewall</help> @@ -24,6 +24,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>enable</defaultValue> </leafNode> <leafNode name="broadcast-ping"> <properties> @@ -43,6 +44,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <leafNode name="config-trap"> <properties> @@ -62,6 +64,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <node name="group"> <properties> @@ -203,6 +206,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <tagNode name="ipv6-name"> <properties> @@ -214,7 +218,15 @@ #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> - <help>Rule number (1-9999)</help> + <help>Firewall rule number (IPv6)</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/action.xml.i> @@ -225,7 +237,7 @@ </properties> <children> #include <include/firewall/address-ipv6.xml.i> - #include <include/firewall/source-destination-group.xml.i> + #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/firewall/port.xml.i> </children> </node> @@ -235,7 +247,7 @@ </properties> <children> #include <include/firewall/address-ipv6.xml.i> - #include <include/firewall/source-destination-group.xml.i> + #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/firewall/port.xml.i> </children> </node> @@ -292,7 +304,7 @@ <properties> <help>ICMP type-name</help> <completionHelp> - <list>any echo-reply pong destination-unreachable network-unreachable host-unreachable protocol-unreachable port-unreachable fragmentation-needed source-route-failed network-unknown host-unknown network-prohibited host-prohibited TOS-network-unreachable TOS-host-unreachable communication-prohibited host-precedence-violation precedence-cutoff source-quench redirect network-redirect host-redirect TOS-network-redirect TOS host-redirect echo-request ping router-advertisement router-solicitation time-exceeded ttl-exceeded ttl-zero-during-transit ttl-zero-during-reassembly parameter-problem ip-header-bad required-option-missing timestamp-request timestamp-reply address-mask-request address-mask-reply</list> + <list>any echo-reply pong destination-unreachable network-unreachable host-unreachable protocol-unreachable port-unreachable fragmentation-needed source-route-failed network-unknown host-unknown network-prohibited host-prohibited TOS-network-unreachable TOS-host-unreachable communication-prohibited host-precedence-violation precedence-cutoff source-quench redirect network-redirect host-redirect TOS-network-redirect TOS host-redirect echo-request ping router-advertisement router-solicitation time-exceeded ttl-exceeded ttl-zero-during-transit ttl-zero-during-reassembly parameter-problem ip-header-bad required-option-missing timestamp-request timestamp-reply address-mask-request address-mask-reply packet-too-big</list> </completionHelp> <valueHelp> <format>any</format> @@ -454,63 +466,18 @@ <format>address-mask-reply</format> <description>ICMP type/code name</description> </valueHelp> + <valueHelp> + <format>packet-too-big</format> + <description>ICMP type/code name</description> + </valueHelp> <constraint> - <regex>^(any|echo-reply|pong|destination-unreachable|network-unreachable|host-unreachable|protocol-unreachable|port-unreachable|fragmentation-needed|source-route-failed|network-unknown|host-unknown|network-prohibited|host-prohibited|TOS-network-unreachable|TOS-host-unreachable|communication-prohibited|host-precedence-violation|precedence-cutoff|source-quench|redirect|network-redirect|host-redirect|TOS-network-redirect|TOS host-redirect|echo-request|ping|router-advertisement|router-solicitation|time-exceeded|ttl-exceeded|ttl-zero-during-transit|ttl-zero-during-reassembly|parameter-problem|ip-header-bad|required-option-missing|timestamp-request|timestamp-reply|address-mask-request|address-mask-reply)$</regex> + <regex>^(any|echo-reply|pong|destination-unreachable|network-unreachable|host-unreachable|protocol-unreachable|port-unreachable|fragmentation-needed|source-route-failed|network-unknown|host-unknown|network-prohibited|host-prohibited|TOS-network-unreachable|TOS-host-unreachable|communication-prohibited|host-precedence-violation|precedence-cutoff|source-quench|redirect|network-redirect|host-redirect|TOS-network-redirect|TOS host-redirect|echo-request|ping|router-advertisement|router-solicitation|time-exceeded|ttl-exceeded|ttl-zero-during-transit|ttl-zero-during-reassembly|parameter-problem|ip-header-bad|required-option-missing|timestamp-request|timestamp-reply|address-mask-request|address-mask-reply|packet-too-big)$</regex> <validator name="numeric" argument="--range 0-255"/> </constraint> </properties> </leafNode> </children> </node> - <node name="p2p"> - <properties> - <help>P2P application packets</help> - </properties> - <children> - <leafNode name="all"> - <properties> - <help>AppleJuice/BitTorrent/Direct Connect/eDonkey/eMule/Gnutella/KaZaA application packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="applejuice"> - <properties> - <help>AppleJuice application packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="bittorrent"> - <properties> - <help>BitTorrent application packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="directconnect"> - <properties> - <help>Direct Connect application packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="edonkey"> - <properties> - <help>eDonkey/eMule application packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="gnutella"> - <properties> - <help>Gnutella application packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="kazaa"> - <properties> - <help>KaZaA application packets</help> - <valueless/> - </properties> - </leafNode> - </children> - </node> </children> </tagNode> </children> @@ -533,6 +500,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <leafNode name="ipv6-src-route"> <properties> @@ -552,6 +520,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <leafNode name="log-martians"> <properties> @@ -571,6 +540,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>enable</defaultValue> </leafNode> <tagNode name="name"> <properties> @@ -582,7 +552,15 @@ #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> - <help>Rule number (1-9999)</help> + <help>Firewall rule number (IPv4)</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/action.xml.i> @@ -662,6 +640,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <leafNode name="send-redirects"> <properties> @@ -681,6 +660,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>enable</defaultValue> </leafNode> <leafNode name="source-validation"> <properties> @@ -704,6 +684,7 @@ <regex>^(strict|loose|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <node name="state-policy"> <properties> @@ -757,6 +738,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>enable</defaultValue> </leafNode> <leafNode name="twa-hazards-protection"> <properties> @@ -776,6 +758,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> </children> </node> diff --git a/interface-definitions/flow-accounting-conf.xml.in b/interface-definitions/flow-accounting-conf.xml.in index b0f308afd..1b57d706c 100644 --- a/interface-definitions/flow-accounting-conf.xml.in +++ b/interface-definitions/flow-accounting-conf.xml.in @@ -14,23 +14,37 @@ <help>Buffer size</help> <valueHelp> <format>u32</format> - <description>Buffer size in MiB</description> + <description>Buffer size in MiB (default: 10)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-4294967295" /> + <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> + <defaultValue>10</defaultValue> + </leafNode> + <leafNode name="packet-length"> + <properties> + <help>Specifies the maximum number of bytes to capture for each packet</help> + <valueHelp> + <format>u32:128-750</format> + <description>Packet length in bytes (default: 128)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 128-750"/> + </constraint> + </properties> + <defaultValue>128</defaultValue> </leafNode> <leafNode name="enable-egress"> <properties> <help>Enable egress flow accounting</help> - <valueless /> + <valueless/> </properties> </leafNode> <leafNode name="disable-imt"> <properties> <help>Disable in memory table plugin</help> - <valueless /> + <valueless/> </properties> </leafNode> <leafNode name="syslog-facility"> @@ -136,15 +150,7 @@ </constraint> </properties> </leafNode> - <leafNode name="interface"> - <properties> - <help>Interface for flow-accounting [REQUIRED]</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <multi/> - </properties> - </leafNode> + #include <include/generic-interface-multi.xml.i> <node name="netflow"> <properties> <help>NetFlow settings</help> @@ -174,7 +180,7 @@ <description>NetFlow maximum flows</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-4294967295" /> + <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> </leafNode> @@ -186,27 +192,11 @@ <description>Sampling rate (1 in N packets)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-4294967295" /> - </constraint> - </properties> - </leafNode> - <leafNode name="source-ip"> - <properties> - <help>IPv4 or IPv6 source address of NetFlow packets</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 source address of NetFlow packets</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>IPv6 source address of NetFlow packets</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - <validator name="ipv6-address"/> + <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> </leafNode> + #include <include/source-address-ipv4-ipv6.xml.i> <leafNode name="version"> <properties> <help>NetFlow version to export</help> @@ -226,6 +216,7 @@ <description>Internet Protocol Flow Information Export (IPFIX)</description> </valueHelp> </properties> + <defaultValue>9</defaultValue> </leafNode> <tagNode name="server"> <properties> @@ -249,12 +240,13 @@ <help>NetFlow port number</help> <valueHelp> <format>u32:1025-65535</format> - <description>NetFlow port number (default 2055)</description> + <description>NetFlow port number (default: 2055)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1025-65535" /> + <validator name="numeric" argument="--range 1025-65535"/> </constraint> </properties> + <defaultValue>2055</defaultValue> </leafNode> </children> </tagNode> @@ -268,96 +260,104 @@ <help>Expiry scan interval</help> <valueHelp> <format>u32:0-2147483647</format> - <description>Expiry scan interval (default 60)</description> + <description>Expiry scan interval (default: 60)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-2147483647" /> + <validator name="numeric" argument="--range 0-2147483647"/> </constraint> </properties> + <defaultValue>60</defaultValue> </leafNode> <leafNode name="flow-generic"> <properties> <help>Generic flow timeout value</help> <valueHelp> <format>u32:0-2147483647</format> - <description>Generic flow timeout in seconds (default 3600)</description> + <description>Generic flow timeout in seconds (default: 3600)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-2147483647" /> + <validator name="numeric" argument="--range 0-2147483647"/> </constraint> </properties> + <defaultValue>3600</defaultValue> </leafNode> <leafNode name="icmp"> <properties> <help>ICMP timeout value</help> <valueHelp> <format>u32:0-2147483647</format> - <description>ICMP timeout in seconds (default 300)</description> + <description>ICMP timeout in seconds (default: 300)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-2147483647" /> + <validator name="numeric" argument="--range 0-2147483647"/> </constraint> </properties> + <defaultValue>300</defaultValue> </leafNode> <leafNode name="max-active-life"> <properties> <help>Max active timeout value</help> <valueHelp> <format>u32:0-2147483647</format> - <description>Max active timeout in seconds (default 604800)</description> + <description>Max active timeout in seconds (default: 604800)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-2147483647" /> + <validator name="numeric" argument="--range 0-2147483647"/> </constraint> </properties> + <defaultValue>604800</defaultValue> </leafNode> <leafNode name="tcp-fin"> <properties> <help>TCP finish timeout value</help> <valueHelp> <format>u32:0-2147483647</format> - <description>TCP FIN timeout in seconds (default 300)</description> + <description>TCP FIN timeout in seconds (default: 300)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-2147483647" /> + <validator name="numeric" argument="--range 0-2147483647"/> </constraint> </properties> + <defaultValue>300</defaultValue> </leafNode> <leafNode name="tcp-generic"> <properties> <help>TCP generic timeout value</help> <valueHelp> <format>u32:0-2147483647</format> - <description>TCP generic timeout in seconds (default 3600)</description> + <description>TCP generic timeout in seconds (default: 3600)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-2147483647" /> + <validator name="numeric" argument="--range 0-2147483647"/> </constraint> </properties> + <defaultValue>3600</defaultValue> </leafNode> <leafNode name="tcp-rst"> <properties> <help>TCP reset timeout value</help> <valueHelp> <format>u32:0-2147483647</format> - <description>TCP RST timeout in seconds (default 120)</description> + <description>TCP RST timeout in seconds (default: 120)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-2147483647" /> + <validator name="numeric" argument="--range 0-2147483647"/> </constraint> </properties> + <defaultValue>120</defaultValue> </leafNode> <leafNode name="udp"> <properties> <help>UDP timeout value</help> <valueHelp> <format>u32:0-2147483647</format> - <description>UDP timeout in seconds (default 300)</description> + <description>UDP timeout in seconds (default: 300)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-2147483647" /> + <validator name="numeric" argument="--range 0-2147483647"/> </constraint> </properties> + <defaultValue>300</defaultValue> </leafNode> </children> </node> @@ -371,17 +371,16 @@ <leafNode name="agent-address"> <properties> <help>sFlow agent IPv4 address</help> - <valueHelp> - <format>auto</format> - <description>auto select sFlow agent-address (default)</description> - </valueHelp> + <completionHelp> + <list>auto</list> + <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script> + </completionHelp> <valueHelp> <format>ipv4</format> <description>sFlow IPv4 agent address</description> </valueHelp> <constraint> <validator name="ipv4-address"/> - <regex>^auto$</regex> </constraint> </properties> </leafNode> @@ -393,7 +392,7 @@ <description>Sampling rate (1 in N packets)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-4294967295" /> + <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> </leafNode> @@ -419,15 +418,17 @@ <help>sFlow port number</help> <valueHelp> <format>u32:1025-65535</format> - <description>sFlow port number (default 6343)</description> + <description>sFlow port number (default: 6343)</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1025-65535" /> + <validator name="numeric" argument="--range 1025-65535"/> </constraint> </properties> + <defaultValue>6343</defaultValue> </leafNode> </children> </tagNode> + #include <include/source-address-ipv4-ipv6.xml.i> </children> </node> </children> diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in index bb6f71744..6fea2f1f6 100644 --- a/interface-definitions/https.xml.in +++ b/interface-definitions/https.xml.in @@ -101,6 +101,25 @@ <hidden/> </properties> </leafNode> + <leafNode name="socket"> + <properties> + <help>Run server on Unix domain socket</help> + <valueless/> + </properties> + </leafNode> + <node name="cors"> + <properties> + <help>Set CORS options</help> + </properties> + <children> + <leafNode name="allow-origin"> + <properties> + <help>Allow resource request from origin</help> + <multi/> + </properties> + </leafNode> + </children> + </node> </children> </node> <node name="api-restrict"> @@ -121,6 +140,7 @@ <help>TLS certificates</help> </properties> <children> + #include <include/pki/ca-certificate.xml.i> #include <include/pki/certificate.xml.i> <node name="certbot" owner="${vyos_conf_scripts_dir}/le_cert.py"> <properties> @@ -142,6 +162,7 @@ </node> </children> </node> + #include <include/interface/vrf.xml.i> </children> </node> </children> diff --git a/interface-definitions/include/accel-ppp/auth-local-users.xml.i b/interface-definitions/include/accel-ppp/auth-local-users.xml.i index 308d6510d..1b40a9ea7 100644 --- a/interface-definitions/include/accel-ppp/auth-local-users.xml.i +++ b/interface-definitions/include/accel-ppp/auth-local-users.xml.i @@ -18,6 +18,9 @@ <leafNode name="static-ip"> <properties> <help>Static client IP address</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> </properties> <defaultValue>*</defaultValue> </leafNode> diff --git a/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i b/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i index bd3dadf8d..a692f2335 100644 --- a/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i +++ b/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i @@ -27,6 +27,7 @@ <validator name="numeric" argument="--range 48-128"/> </constraint> </properties> + <defaultValue>64</defaultValue> </leafNode> </children> </tagNode> diff --git a/interface-definitions/include/accel-ppp/ppp-options-ipv4.xml.i b/interface-definitions/include/accel-ppp/ppp-options-ipv4.xml.i new file mode 100644 index 000000000..3e065329d --- /dev/null +++ b/interface-definitions/include/accel-ppp/ppp-options-ipv4.xml.i @@ -0,0 +1,23 @@ +<!-- include start from accel-ppp/ppp-options-ipv4.xml.i --> +<leafNode name="ipv4"> + <properties> + <help>IPv4 negotiation algorithm</help> + <constraint> + <regex>^(deny|allow)$</regex> + </constraint> + <constraintErrorMessage>invalid value</constraintErrorMessage> + <valueHelp> + <format>deny</format> + <description>Do not negotiate IPv4</description> + </valueHelp> + <valueHelp> + <format>allow</format> + <description>Negotiate IPv4 only if client requests</description> + </valueHelp> + <completionHelp> + <list>deny allow</list> + </completionHelp> + </properties> + <defaultValue>allow</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/accel-ppp/ppp-options-ipv6.xml.i b/interface-definitions/include/accel-ppp/ppp-options-ipv6.xml.i index cd40a1f96..b9fbac5c6 100644 --- a/interface-definitions/include/accel-ppp/ppp-options-ipv6.xml.i +++ b/interface-definitions/include/accel-ppp/ppp-options-ipv6.xml.i @@ -26,5 +26,6 @@ <list>deny allow prefer require</list> </completionHelp> </properties> + <defaultValue>deny</defaultValue> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/bfd.xml.i b/interface-definitions/include/bfd.xml.i deleted file mode 100644 index 2bc3664e1..000000000 --- a/interface-definitions/include/bfd.xml.i +++ /dev/null @@ -1,8 +0,0 @@ -<!-- include start from bfd.xml.i --> -<leafNode name="bfd"> - <properties> - <help>Enable Bidirectional Forwarding Detection (BFD)</help> - <valueless/> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/bfd/bfd.xml.i b/interface-definitions/include/bfd/bfd.xml.i new file mode 100644 index 000000000..022956d98 --- /dev/null +++ b/interface-definitions/include/bfd/bfd.xml.i @@ -0,0 +1,10 @@ +<!-- include start from bfd/bfd.xml.i --> +<node name="bfd"> + <properties> + <help>Enable Bidirectional Forwarding Detection (BFD)</help> + </properties> + <children> + #include <include/bfd/profile.xml.i> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/bfd-common.xml.i b/interface-definitions/include/bfd/common.xml.i index 1d6ab5d55..e52221441 100644 --- a/interface-definitions/include/bfd-common.xml.i +++ b/interface-definitions/include/bfd/common.xml.i @@ -1,4 +1,4 @@ -<!-- include start from bfd-common.xml.i --> +<!-- include start from bfd/common.xml.i --> <leafNode name="echo-mode"> <properties> <help>Enables the echo transmission mode</help> @@ -15,7 +15,7 @@ <help>Minimum interval of receiving control packets</help> <valueHelp> <format>u32:10-60000</format> - <description>Interval in milliseconds</description> + <description>Interval in milliseconds (default: 300)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 10-60000"/> @@ -28,7 +28,7 @@ <help>Minimum interval of transmitting control packets</help> <valueHelp> <format>u32:10-60000</format> - <description>Interval in milliseconds</description> + <description>Interval in milliseconds (default: 300)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 10-60000"/> @@ -41,7 +41,7 @@ <help>Multiplier to determine packet loss</help> <valueHelp> <format>u32:2-255</format> - <description>Remote transmission interval will be multiplied by this value</description> + <description>Remote transmission interval will be multiplied by this value (default: 3)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 2-255"/> @@ -63,6 +63,12 @@ </leafNode> </children> </node> +<leafNode name="passive"> + <properties> + <help>Do not attempt to start sessions</help> + <valueless/> + </properties> +</leafNode> <leafNode name="shutdown"> <properties> <help>Disable this peer</help> diff --git a/interface-definitions/include/bfd/profile.xml.i b/interface-definitions/include/bfd/profile.xml.i new file mode 100644 index 000000000..5ff057286 --- /dev/null +++ b/interface-definitions/include/bfd/profile.xml.i @@ -0,0 +1,14 @@ +<!-- include start from bfd/profile.xml.i --> +<leafNode name="profile"> + <properties> + <help>Use settings from BFD profile</help> + <completionHelp> + <path>protocols bfd profile</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>BFD profile name</description> + </valueHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/bgp/afi-aggregate-address.xml.i b/interface-definitions/include/bgp/afi-aggregate-address.xml.i index 646751c32..c1b7958da 100644 --- a/interface-definitions/include/bgp/afi-aggregate-address.xml.i +++ b/interface-definitions/include/bgp/afi-aggregate-address.xml.i @@ -5,6 +5,7 @@ <valueless/> </properties> </leafNode> +#include <include/route-map.xml.i> <leafNode name="summary-only"> <properties> <help>Announce the aggregate summary network only</help> diff --git a/interface-definitions/include/bgp/afi-l2vpn-common.xml.i b/interface-definitions/include/bgp/afi-l2vpn-common.xml.i index 8deb189ab..d586635c8 100644 --- a/interface-definitions/include/bgp/afi-l2vpn-common.xml.i +++ b/interface-definitions/include/bgp/afi-l2vpn-common.xml.i @@ -25,7 +25,7 @@ <description>Route target (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description> </valueHelp> <constraint> - <validator name="bgp-route-target" argument="--single"/> + <validator name="bgp-rd-rt" argument="--route-target"/> </constraint> </properties> </leafNode> @@ -37,7 +37,7 @@ <description>Route target (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description> </valueHelp> <constraint> - <validator name="bgp-route-target" argument="--single"/> + <validator name="bgp-rd-rt" argument="--route-target"/> </constraint> </properties> </leafNode> @@ -49,7 +49,7 @@ <description>Route target (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description> </valueHelp> <constraint> - <validator name="bgp-route-target" argument="--single"/> + <validator name="bgp-rd-rt" argument="--route-target"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/bgp/afi-route-target-vpn.xml.i b/interface-definitions/include/bgp/afi-route-target-vpn.xml.i index 1dc184a02..5784f9eac 100644 --- a/interface-definitions/include/bgp/afi-route-target-vpn.xml.i +++ b/interface-definitions/include/bgp/afi-route-target-vpn.xml.i @@ -1,7 +1,7 @@ <!-- include start from bgp/route-target-both.xml.i --> <node name="route-target"> <properties> - <help>Specify route distinguisher</help> + <help>Specify route target list</help> </properties> <children> <node name="vpn"> @@ -17,7 +17,7 @@ <description>Space separated route target list (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description> </valueHelp> <constraint> - <validator name="bgp-route-target" argument="--multi"/> + <validator name="bgp-rd-rt" argument="--route-target-multi"/> </constraint> </properties> </leafNode> @@ -29,7 +29,7 @@ <description>Space separated route target list (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description> </valueHelp> <constraint> - <validator name="bgp-route-target" argument="--multi"/> + <validator name="bgp-rd-rt" argument="--route-target-multi"/> </constraint> </properties> </leafNode> @@ -41,7 +41,7 @@ <description>Space separated route target list (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description> </valueHelp> <constraint> - <validator name="bgp-route-target" argument="--multi"/> + <validator name="bgp-rd-rt" argument="--route-target-multi"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/bgp/afi-common.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i index 62beff40c..f3fc4444c 100644 --- a/interface-definitions/include/bgp/afi-common.xml.i +++ b/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i @@ -1,4 +1,4 @@ -<!-- include start from bgp/afi-common.xml.i --> +<!-- include start from bgp/neighbor-afi-ipv4-ipv6-common.xml.i --> <leafNode name="addpath-tx-all"> <properties> <help>Use addpath to advertise all paths to a neighbor</help> @@ -11,6 +11,61 @@ <valueless/> </properties> </leafNode> +<node name="conditionally-advertise"> + <properties> + <help>Use route-map to conditionally advertise routes</help> + </properties> + <children> + <leafNode name="advertise-map"> + <properties> + <help>Route-map to conditionally advertise routes</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Route map name</description> + </valueHelp> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="exist-map"> + <properties> + <help>Advertise routes only if prefixes in exist-map are installed in BGP table</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Route map name</description> + </valueHelp> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="non-exist-map"> + <properties> + <help>Advertise routes only if prefixes in non-exist-map are not installed in BGP table</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Route map name</description> + </valueHelp> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage> + </properties> + </leafNode> + </children> +</node> #include <include/bgp/afi-allowas-in.xml.i> <leafNode name="as-override"> <properties> diff --git a/interface-definitions/include/bgp/neighbor-afi-ipv4-labeled-unicast.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv4-labeled-unicast.xml.i index 45a440fd8..0eae29f5e 100644 --- a/interface-definitions/include/bgp/neighbor-afi-ipv4-labeled-unicast.xml.i +++ b/interface-definitions/include/bgp/neighbor-afi-ipv4-labeled-unicast.xml.i @@ -13,7 +13,7 @@ </children> </node> #include <include/bgp/afi-ipv4-prefix-list.xml.i> - #include <include/bgp/afi-common.xml.i> + #include <include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i> #include <include/bgp/afi-default-originate.xml.i> </children> </node> diff --git a/interface-definitions/include/bgp/neighbor-afi-ipv4-multicast.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv4-multicast.xml.i index 6526169ca..4bb6df7c3 100644 --- a/interface-definitions/include/bgp/neighbor-afi-ipv4-multicast.xml.i +++ b/interface-definitions/include/bgp/neighbor-afi-ipv4-multicast.xml.i @@ -13,7 +13,7 @@ </children> </node> #include <include/bgp/afi-ipv4-prefix-list.xml.i> - #include <include/bgp/afi-common.xml.i> + #include <include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i> #include <include/bgp/afi-default-originate.xml.i> </children> </node> diff --git a/interface-definitions/include/bgp/neighbor-afi-ipv4-unicast.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv4-unicast.xml.i index b7b7ca5b5..0094ce874 100644 --- a/interface-definitions/include/bgp/neighbor-afi-ipv4-unicast.xml.i +++ b/interface-definitions/include/bgp/neighbor-afi-ipv4-unicast.xml.i @@ -13,7 +13,7 @@ </children> </node> #include <include/bgp/afi-ipv4-prefix-list.xml.i> - #include <include/bgp/afi-common.xml.i> + #include <include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i> #include <include/bgp/afi-default-originate.xml.i> </children> </node> diff --git a/interface-definitions/include/bgp/neighbor-afi-ipv4-vpn.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv4-vpn.xml.i index 838327bc9..220f22fe3 100644 --- a/interface-definitions/include/bgp/neighbor-afi-ipv4-vpn.xml.i +++ b/interface-definitions/include/bgp/neighbor-afi-ipv4-vpn.xml.i @@ -5,7 +5,7 @@ </properties> <children> #include <include/bgp/afi-ipv4-prefix-list.xml.i> - #include <include/bgp/afi-common.xml.i> + #include <include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/bgp/neighbor-afi-ipv6-labeled-unicast.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv6-labeled-unicast.xml.i index f680b7357..995183571 100644 --- a/interface-definitions/include/bgp/neighbor-afi-ipv6-labeled-unicast.xml.i +++ b/interface-definitions/include/bgp/neighbor-afi-ipv6-labeled-unicast.xml.i @@ -14,7 +14,7 @@ </node> #include <include/bgp/afi-ipv6-nexthop-local.xml.i> #include <include/bgp/afi-ipv6-prefix-list.xml.i> - #include <include/bgp/afi-common.xml.i> + #include <include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i> #include <include/bgp/afi-default-originate.xml.i> </children> </node> diff --git a/interface-definitions/include/bgp/neighbor-afi-ipv6-multicast.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv6-multicast.xml.i index 1f8db8361..bb713c313 100644 --- a/interface-definitions/include/bgp/neighbor-afi-ipv6-multicast.xml.i +++ b/interface-definitions/include/bgp/neighbor-afi-ipv6-multicast.xml.i @@ -6,7 +6,7 @@ <children> #include <include/bgp/afi-ipv6-nexthop-local.xml.i> #include <include/bgp/afi-ipv6-prefix-list.xml.i> - #include <include/bgp/afi-common.xml.i> + #include <include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i> #include <include/bgp/afi-default-originate.xml.i> </children> </node> diff --git a/interface-definitions/include/bgp/neighbor-afi-ipv6-unicast.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv6-unicast.xml.i index f6b812c28..26a5e7090 100644 --- a/interface-definitions/include/bgp/neighbor-afi-ipv6-unicast.xml.i +++ b/interface-definitions/include/bgp/neighbor-afi-ipv6-unicast.xml.i @@ -14,7 +14,7 @@ </node> #include <include/bgp/afi-ipv6-nexthop-local.xml.i> #include <include/bgp/afi-ipv6-prefix-list.xml.i> - #include <include/bgp/afi-common.xml.i> + #include <include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i> #include <include/bgp/afi-default-originate.xml.i> </children> </node> diff --git a/interface-definitions/include/bgp/neighbor-afi-ipv6-vpn.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv6-vpn.xml.i index c0df71cf3..5c6811986 100644 --- a/interface-definitions/include/bgp/neighbor-afi-ipv6-vpn.xml.i +++ b/interface-definitions/include/bgp/neighbor-afi-ipv6-vpn.xml.i @@ -6,7 +6,7 @@ <children> #include <include/bgp/afi-ipv6-nexthop-local.xml.i> #include <include/bgp/afi-ipv6-prefix-list.xml.i> - #include <include/bgp/afi-common.xml.i> + #include <include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/bgp/neighbor-bfd.xml.i b/interface-definitions/include/bgp/neighbor-bfd.xml.i index d486bdd8a..fac2a1166 100644 --- a/interface-definitions/include/bgp/neighbor-bfd.xml.i +++ b/interface-definitions/include/bgp/neighbor-bfd.xml.i @@ -4,6 +4,7 @@ <help>Enable Bidirectional Forwarding Detection (BFD) support</help> </properties> <children> + #include <include/bfd/profile.xml.i> <leafNode name="check-control-plane-failure"> <properties> <help>Allow to write CBIT independence in BFD outgoing packets and read both C-BIT value of BFD and lookup BGP peer status</help> diff --git a/interface-definitions/include/bgp/neighbor-description.xml.i b/interface-definitions/include/bgp/neighbor-description.xml.i deleted file mode 100644 index 3095d2560..000000000 --- a/interface-definitions/include/bgp/neighbor-description.xml.i +++ /dev/null @@ -1,7 +0,0 @@ -<!-- include start from bgp/neighbor-description.xml.i --> -<leafNode name="description"> - <properties> - <help>Neighbor specific description</help> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/bgp/neighbor-shutdown.xml.i b/interface-definitions/include/bgp/neighbor-shutdown.xml.i index 6d15899a6..acc7bc5a9 100644 --- a/interface-definitions/include/bgp/neighbor-shutdown.xml.i +++ b/interface-definitions/include/bgp/neighbor-shutdown.xml.i @@ -1,7 +1,7 @@ <!-- include start from bgp/neighbor-shutdown.xml.i --> <leafNode name="shutdown"> <properties> - <help>Administratively shut down this neighbor</help> + <help>Administratively shutdown this neighbor</help> <valueless/> </properties> </leafNode> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index 2b22bac7d..8214d0779 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -960,9 +960,9 @@ </constraint> </properties> </leafNode> + #include <include/generic-description.xml.i> #include <include/bgp/neighbor-bfd.xml.i> #include <include/bgp/neighbor-capability.xml.i> - #include <include/bgp/neighbor-description.xml.i> #include <include/bgp/neighbor-disable-capability-negotiation.xml.i> #include <include/bgp/neighbor-disable-connected-check.xml.i> #include <include/bgp/neighbor-ebgp-multihop.xml.i> @@ -974,6 +974,7 @@ <children> #include <include/bgp/peer-group.xml.i> #include <include/bgp/remote-as.xml.i> + #include <include/source-interface.xml.i> <node name="v6only"> <properties> <help>Enable BGP with v6 link-local only</help> @@ -1180,6 +1181,26 @@ </leafNode> </children> </node> + <node name="conditional-advertisement"> + <properties> + <help>Conditional advertisement settings</help> + </properties> + <children> + <leafNode name="timer"> + <properties> + <help>Set period to rescan BGP table to check if condition is met</help> + <valueHelp> + <format>u32:5-240</format> + <description>Period to rerun the conditional advertisement scanner process (default: 60)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 5-240"/> + </constraint> + </properties> + <defaultValue>60</defaultValue> + </leafNode> + </children> + </node> <node name="dampening"> <properties> <help>Enable route-flap dampening</help> @@ -1252,12 +1273,6 @@ </constraint> </properties> </leafNode> - <leafNode name="no-ipv4-unicast"> - <properties> - <help>Deactivate IPv4 unicast for a peer by default</help> - <valueless/> - </properties> - </leafNode> </children> </node> <leafNode name="deterministic-med"> @@ -1348,6 +1363,12 @@ <valueless/> </properties> </leafNode> + <leafNode name="fast-convergence"> + <properties> + <help>Teardown sessions immediately whenever peer becomes unreachable</help> + <valueless/> + </properties> + </leafNode> <node name="graceful-restart"> <properties> <help>Graceful restart capability parameters</help> @@ -1379,6 +1400,18 @@ <valueless/> </properties> </leafNode> + <leafNode name="minimum-holdtime"> + <properties> + <help>BGP minimum holdtime</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Minimum holdtime in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> <leafNode name="network-import-check"> <properties> <help>Enable IGP route check for network statements</help> @@ -1397,6 +1430,24 @@ <valueless/> </properties> </leafNode> + <leafNode name="reject-as-sets"> + <properties> + <help>Reject routes with AS_SET or AS_CONFED_SET flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="shutdown"> + <properties> + <help>Administrative shutdown of the BGP instance</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="suppress-fib-pending"> + <properties> + <help>Advertise only routes that are programmed in kernel to peers</help> + <valueless/> + </properties> + </leafNode> #include <include/router-id.xml.i> </children> </node> @@ -1418,9 +1469,9 @@ #include <include/bgp/neighbor-afi-l2vpn-evpn.xml.i> </children> </node> + #include <include/generic-description.xml.i> #include <include/bgp/neighbor-bfd.xml.i> #include <include/bgp/neighbor-capability.xml.i> - #include <include/bgp/neighbor-description.xml.i> #include <include/bgp/neighbor-disable-capability-negotiation.xml.i> #include <include/bgp/neighbor-disable-connected-check.xml.i> #include <include/bgp/neighbor-ebgp-multihop.xml.i> @@ -1446,4 +1497,4 @@ #include <include/bgp/timers-keepalive.xml.i> </children> </node> -<!-- include end -->
\ No newline at end of file +<!-- include end --> diff --git a/interface-definitions/include/bgp/route-distinguisher.xml.i b/interface-definitions/include/bgp/route-distinguisher.xml.i index 6d0aa3ef1..8bc5b452e 100644 --- a/interface-definitions/include/bgp/route-distinguisher.xml.i +++ b/interface-definitions/include/bgp/route-distinguisher.xml.i @@ -7,7 +7,7 @@ <description>Route Distinguisher, (x.x.x.x:yyy|xxxx:yyyy)</description> </valueHelp> <constraint> - <regex>^((25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)(\.(25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)){3}|[0-9]{1,10}):[0-9]{1,5}$</regex> + <validator name="bgp-rd-rt" argument="--route-distinguisher"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/dhcp/ntp-server.xml.i b/interface-definitions/include/dhcp/ntp-server.xml.i index 32d8207e5..4d7235aa1 100644 --- a/interface-definitions/include/dhcp/ntp-server.xml.i +++ b/interface-definitions/include/dhcp/ntp-server.xml.i @@ -1,15 +1,15 @@ <!-- include start from dhcp/ntp-server.xml.i --> - <leafNode name="ntp-server"> - <properties> - <help>IP address of NTP server</help> - <valueHelp> - <format>ipv4</format> - <description>NTP server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> +<leafNode name="ntp-server"> + <properties> + <help>IP address of NTP server</help> + <valueHelp> + <format>ipv4</format> + <description>NTP server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> +</leafNode> <!-- include end --> diff --git a/interface-definitions/include/dns/time-to-live.xml.i b/interface-definitions/include/dns/time-to-live.xml.i new file mode 100644 index 000000000..5c1a1472d --- /dev/null +++ b/interface-definitions/include/dns/time-to-live.xml.i @@ -0,0 +1,15 @@ +<!-- include start from dns/time-to-live.xml.i --> +<leafNode name="ttl"> + <properties> + <help>Time-to-live (TTL)</help> + <valueHelp> + <format>u32:0-2147483647</format> + <description>TTL in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647"/> + </constraint> + </properties> + <defaultValue>300</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/firewall/action.xml.i b/interface-definitions/include/firewall/action.xml.i index 230f590cb..4ba93e3aa 100644 --- a/interface-definitions/include/firewall/action.xml.i +++ b/interface-definitions/include/firewall/action.xml.i @@ -3,18 +3,22 @@ <properties> <help>Rule action [REQUIRED]</help> <completionHelp> - <list>permit deny</list> + <list>accept reject drop</list> </completionHelp> <valueHelp> - <format>permit</format> - <description>Permit matching entries</description> + <format>accept</format> + <description>Accept matching entries</description> </valueHelp> <valueHelp> - <format>deny</format> - <description>Deny matching entries</description> + <format>reject</format> + <description>Reject matching entries</description> + </valueHelp> + <valueHelp> + <format>drop</format> + <description>Drop matching entries</description> </valueHelp> <constraint> - <regex>^(permit|deny)$</regex> + <regex>^(accept|reject|drop)$</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/firewall/common-rule.xml.i b/interface-definitions/include/firewall/common-rule.xml.i index a59c0b390..415b6bf00 100644 --- a/interface-definitions/include/firewall/common-rule.xml.i +++ b/interface-definitions/include/firewall/common-rule.xml.i @@ -55,7 +55,7 @@ <help>Maximum number of packets to allow in excess of rate</help> <valueHelp> <format>u32:0-4294967295</format> - <description>burst__change_me</description> + <description>Maximum number of packets to allow in excess of rate</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> @@ -67,7 +67,7 @@ <help>Maximum average matching rate</help> <valueHelp> <format>u32:0-4294967295</format> - <description>rate__change_me</description> + <description>Maximum average matching rate</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> @@ -121,7 +121,6 @@ <validator name="ip-protocol"/> </constraint> </properties> - <defaultValue>all</defaultValue> </leafNode> <node name="recent"> <properties> @@ -285,40 +284,65 @@ <help>Time to match rule</help> </properties> <children> - <leafNode name="monthdays"> - <properties> - <help>Monthdays to match rule on</help> - </properties> - </leafNode> <leafNode name="startdate"> <properties> <help>Date to start matching rule</help> + <valueHelp> + <format>txt</format> + <description>Enter date using following notation - YYYY-MM-DD</description> + </valueHelp> + <constraint> + <regex>^(\d{4}\-\d{2}\-\d{2})$</regex> + </constraint> </properties> </leafNode> <leafNode name="starttime"> <properties> <help>Time of day to start matching rule</help> + <valueHelp> + <format>txt</format> + <description>Enter time using using 24 hour notation - hh:mm:ss</description> + </valueHelp> + <constraint> + <regex>^([0-2][0-9](\:[0-5][0-9]){1,2})$</regex> + </constraint> </properties> </leafNode> <leafNode name="stopdate"> <properties> <help>Date to stop matching rule</help> + <valueHelp> + <format>txt</format> + <description>Enter date using following notation - YYYY-MM-DD</description> + </valueHelp> + <constraint> + <regex>^(\d{4}\-\d{2}\-\d{2})$</regex> + </constraint> </properties> </leafNode> <leafNode name="stoptime"> <properties> <help>Time of day to stop matching rule</help> - </properties> - </leafNode> - <leafNode name="utc"> - <properties> - <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help> - <valueless/> + <valueHelp> + <format>txt</format> + <description>Enter time using using 24 hour notation - hh:mm:ss</description> + </valueHelp> + <constraint> + <regex>^([0-2][0-9](\:[0-5][0-9]){1,2})$</regex> + </constraint> </properties> </leafNode> <leafNode name="weekdays"> <properties> - <help>Weekdays to match rule on</help> + <help>Comma separated weekdays to match rule on</help> + <valueHelp> + <format>txt</format> + <description>Name of day (Monday, Tuesday, Wednesday, Thursdays, Friday, Saturday, Sunday)</description> + </valueHelp> + <valueHelp> + <format>u32:0-6</format> + <description>Day number (0 = Sunday ... 6 = Saturday)</description> + </valueHelp> </properties> </leafNode> </children> diff --git a/interface-definitions/include/firewall/source-destination-group-ipv6.xml.i b/interface-definitions/include/firewall/source-destination-group-ipv6.xml.i new file mode 100644 index 000000000..7815b78d4 --- /dev/null +++ b/interface-definitions/include/firewall/source-destination-group-ipv6.xml.i @@ -0,0 +1,33 @@ +<!-- include start from firewall/source-destination-group-ipv6.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 ipv6-address-group</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="network-group"> + <properties> + <help>Group of networks</help> + <completionHelp> + <path>firewall group ipv6-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/firewall/source-destination-group.xml.i b/interface-definitions/include/firewall/source-destination-group.xml.i index 30226b0d8..9a9bed0fe 100644 --- a/interface-definitions/include/firewall/source-destination-group.xml.i +++ b/interface-definitions/include/firewall/source-destination-group.xml.i @@ -7,16 +7,25 @@ <leafNode name="address-group"> <properties> <help>Group of addresses</help> + <completionHelp> + <path>firewall group address-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> diff --git a/interface-definitions/include/generic-disable-node.xml.i b/interface-definitions/include/generic-disable-node.xml.i index bb4fa5c4b..97a328ecc 100644 --- a/interface-definitions/include/generic-disable-node.xml.i +++ b/interface-definitions/include/generic-disable-node.xml.i @@ -1,7 +1,7 @@ <!-- include start from generic-disable-node.xml.i --> <leafNode name="disable"> <properties> - <help>Temporary disable</help> + <help>Disable instance</help> <valueless/> </properties> </leafNode> diff --git a/interface-definitions/include/generic-interface-broadcast.xml.i b/interface-definitions/include/generic-interface-broadcast.xml.i new file mode 100644 index 000000000..6f76dde1a --- /dev/null +++ b/interface-definitions/include/generic-interface-broadcast.xml.i @@ -0,0 +1,17 @@ +<!-- include start from generic-interface-broadcast.xml.i --> +<leafNode name="interface"> + <properties> + <help>Interface Name to use</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/generic-interface-multi-broadcast.xml.i b/interface-definitions/include/generic-interface-multi-broadcast.xml.i new file mode 100644 index 000000000..00638f3b7 --- /dev/null +++ b/interface-definitions/include/generic-interface-multi-broadcast.xml.i @@ -0,0 +1,18 @@ +<!-- include start from generic-interface-multi-broadcast.xml.i --> +<leafNode name="interface"> + <properties> + <help>Interface Name to use</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/generic-interface-multi.xml.i b/interface-definitions/include/generic-interface-multi.xml.i new file mode 100644 index 000000000..44e87775c --- /dev/null +++ b/interface-definitions/include/generic-interface-multi.xml.i @@ -0,0 +1,18 @@ +<!-- include start from generic-interface-multi.xml.i --> +<leafNode name="interface"> + <properties> + <help>Interface Name to use</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/generic-interface.xml.i b/interface-definitions/include/generic-interface.xml.i new file mode 100644 index 000000000..50af718a5 --- /dev/null +++ b/interface-definitions/include/generic-interface.xml.i @@ -0,0 +1,17 @@ +<!-- include start from generic-interface.xml.i --> +<leafNode name="interface"> + <properties> + <help>Interface Name to use</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/interface/interface-firewall-vif-c.xml.i b/interface-definitions/include/interface/interface-firewall-vif-c.xml.i new file mode 100644 index 000000000..1bc235fcb --- /dev/null +++ b/interface-definitions/include/interface/interface-firewall-vif-c.xml.i @@ -0,0 +1,79 @@ +<!-- include start from interface/interface-firewall-vif-c.xml.i --> +<node name="firewall" owner="${vyos_conf_scripts_dir}/firewall-interface.py $VAR(../../../@).$VAR(../../@).$VAR(../@)"> + <properties> + <priority>615</priority> + <help>Firewall options</help> + </properties> + <children> + <node name="in"> + <properties> + <help>forwarded packets on inbound interface</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Inbound IPv4 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-name"> + <properties> + <help>Inbound IPv6 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <node name="out"> + <properties> + <help>forwarded packets on outbound interface</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Outbound IPv4 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-name"> + <properties> + <help>Outbound IPv6 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <node name="local"> + <properties> + <help>packets destined for this router</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Local IPv4 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-name"> + <properties> + <help>Local IPv6 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/interface/interface-firewall-vif.xml.i b/interface-definitions/include/interface/interface-firewall-vif.xml.i new file mode 100644 index 000000000..a37ac5c4a --- /dev/null +++ b/interface-definitions/include/interface/interface-firewall-vif.xml.i @@ -0,0 +1,79 @@ +<!-- include start from interface/interface-firewall-vif.xml.i --> +<node name="firewall" owner="${vyos_conf_scripts_dir}/firewall-interface.py $VAR(../../@).$VAR(../@)"> + <properties> + <priority>615</priority> + <help>Firewall options</help> + </properties> + <children> + <node name="in"> + <properties> + <help>forwarded packets on inbound interface</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Inbound IPv4 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-name"> + <properties> + <help>Inbound IPv6 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <node name="out"> + <properties> + <help>forwarded packets on outbound interface</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Outbound IPv4 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-name"> + <properties> + <help>Outbound IPv6 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <node name="local"> + <properties> + <help>packets destined for this router</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Local IPv4 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-name"> + <properties> + <help>Local IPv6 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/interface/interface-firewall.xml.i b/interface-definitions/include/interface/interface-firewall.xml.i new file mode 100644 index 000000000..b3f20c3bf --- /dev/null +++ b/interface-definitions/include/interface/interface-firewall.xml.i @@ -0,0 +1,79 @@ +<!-- include start from interface/interface-firewall.xml.i --> +<node name="firewall" owner="${vyos_conf_scripts_dir}/firewall-interface.py $VAR(../@)"> + <properties> + <priority>615</priority> + <help>Firewall options</help> + </properties> + <children> + <node name="in"> + <properties> + <help>forwarded packets on inbound interface</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Inbound IPv4 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-name"> + <properties> + <help>Inbound IPv6 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <node name="out"> + <properties> + <help>forwarded packets on outbound interface</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Outbound IPv4 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-name"> + <properties> + <help>Outbound IPv6 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <node name="local"> + <properties> + <help>packets destined for this router</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Local IPv4 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-name"> + <properties> + <help>Local IPv6 firewall ruleset name for interface</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/interface/interface-policy-vif-c.xml.i b/interface-definitions/include/interface/interface-policy-vif-c.xml.i new file mode 100644 index 000000000..5dad6422b --- /dev/null +++ b/interface-definitions/include/interface/interface-policy-vif-c.xml.i @@ -0,0 +1,26 @@ +<!-- include start from interface/interface-policy-vif-c.xml.i --> +<node name="policy" owner="${vyos_conf_scripts_dir}/policy-route-interface.py $VAR(../../../@).$VAR(../../@).$VAR(../@)"> + <properties> + <priority>620</priority> + <help>Policy route options</help> + </properties> + <children> + <leafNode name="route"> + <properties> + <help>IPv4 policy route ruleset for interface</help> + <completionHelp> + <path>policy route</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-route"> + <properties> + <help>IPv6 policy route ruleset for interface</help> + <completionHelp> + <path>policy ipv6-route</path> + </completionHelp> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/interface/interface-policy-vif.xml.i b/interface-definitions/include/interface/interface-policy-vif.xml.i new file mode 100644 index 000000000..5ee80ae13 --- /dev/null +++ b/interface-definitions/include/interface/interface-policy-vif.xml.i @@ -0,0 +1,26 @@ +<!-- include start from interface/interface-policy-vif.xml.i --> +<node name="policy" owner="${vyos_conf_scripts_dir}/policy-route-interface.py $VAR(../../@).$VAR(../@)"> + <properties> + <priority>620</priority> + <help>Policy route options</help> + </properties> + <children> + <leafNode name="route"> + <properties> + <help>IPv4 policy route ruleset for interface</help> + <completionHelp> + <path>policy route</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-route"> + <properties> + <help>IPv6 policy route ruleset for interface</help> + <completionHelp> + <path>policy ipv6-route</path> + </completionHelp> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/interface/interface-policy.xml.i b/interface-definitions/include/interface/interface-policy.xml.i new file mode 100644 index 000000000..06f025af1 --- /dev/null +++ b/interface-definitions/include/interface/interface-policy.xml.i @@ -0,0 +1,26 @@ +<!-- include start from interface/interface-policy.xml.i --> +<node name="policy" owner="${vyos_conf_scripts_dir}/policy-route-interface.py $VAR(../@)"> + <properties> + <priority>620</priority> + <help>Policy route options</help> + </properties> + <children> + <leafNode name="route"> + <properties> + <help>IPv4 policy route ruleset for interface</help> + <completionHelp> + <path>policy route</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-route"> + <properties> + <help>IPv6 policy route ruleset for interface</help> + <completionHelp> + <path>policy ipv6-route</path> + </completionHelp> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/interface/netns.xml.i b/interface-definitions/include/interface/netns.xml.i new file mode 100644 index 000000000..39f9118fa --- /dev/null +++ b/interface-definitions/include/interface/netns.xml.i @@ -0,0 +1,14 @@ +<!-- include start from interface/netns.xml.i --> +<leafNode name="netns"> + <properties> + <help>Network namespace name</help> + <valueHelp> + <format>text</format> + <description>Network namespace name</description> + </valueHelp> + <completionHelp> + <path>netns name</path> + </completionHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/interface/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i index e7ba6d193..f1a61ff64 100644 --- a/interface-definitions/include/interface/vif-s.xml.i +++ b/interface-definitions/include/interface/vif-s.xml.i @@ -18,6 +18,8 @@ #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> + #include <include/interface/interface-firewall-vif.xml.i> + #include <include/interface/interface-policy-vif.xml.i> <leafNode name="protocol"> <properties> <help>Protocol used for service VLAN (default: 802.1ad)</help> @@ -63,6 +65,8 @@ #include <include/interface/mac.xml.i> #include <include/interface/mtu-68-16000.xml.i> #include <include/interface/vrf.xml.i> + #include <include/interface/interface-firewall-vif-c.xml.i> + #include <include/interface/interface-policy-vif-c.xml.i> </children> </tagNode> #include <include/interface/vrf.xml.i> diff --git a/interface-definitions/include/interface/vif.xml.i b/interface-definitions/include/interface/vif.xml.i index 5644c554f..11ba7e2f8 100644 --- a/interface-definitions/include/interface/vif.xml.i +++ b/interface-definitions/include/interface/vif.xml.i @@ -19,6 +19,8 @@ #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/vrf.xml.i> + #include <include/interface/interface-firewall-vif.xml.i> + #include <include/interface/interface-policy-vif.xml.i> <leafNode name="egress-qos"> <properties> <help>VLAN egress QoS</help> diff --git a/interface-definitions/include/interface/vrf.xml.i b/interface-definitions/include/interface/vrf.xml.i index 5ad978a27..8605f56e8 100644 --- a/interface-definitions/include/interface/vrf.xml.i +++ b/interface-definitions/include/interface/vrf.xml.i @@ -3,7 +3,7 @@ <properties> <help>VRF instance name</help> <valueHelp> - <format>text</format> + <format>txt</format> <description>VRF instance name</description> </valueHelp> <completionHelp> diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i index 84e2f7bb2..8ffa14a19 100644 --- a/interface-definitions/include/isis/protocol-common-config.xml.i +++ b/interface-definitions/include/isis/protocol-common-config.xml.i @@ -648,7 +648,7 @@ </completionHelp> </properties> <children> - #include <include/bfd.xml.i> + #include <include/bfd/bfd.xml.i> <leafNode name="circuit-type"> <properties> <help>Configure circuit type for interface</help> diff --git a/interface-definitions/include/listen-address-ipv4.xml.i b/interface-definitions/include/listen-address-ipv4.xml.i index ee52cebe8..9cca297a0 100644 --- a/interface-definitions/include/listen-address-ipv4.xml.i +++ b/interface-definitions/include/listen-address-ipv4.xml.i @@ -1,13 +1,13 @@ <!-- include start from listen-address-ipv4.xml.i --> <leafNode name="listen-address"> <properties> - <help>Local IPv4 addresses for service to listen on</help> + <help>Local IPv4 addresses to listen on</help> <completionHelp> <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script> </completionHelp> <valueHelp> <format>ipv4</format> - <description>IP address to listen for incoming connections</description> + <description>IPv4 address to listen for incoming connections</description> </valueHelp> <multi/> <constraint> diff --git a/interface-definitions/include/listen-address-vrf.xml.i b/interface-definitions/include/listen-address-vrf.xml.i new file mode 100644 index 000000000..8c2bdce70 --- /dev/null +++ b/interface-definitions/include/listen-address-vrf.xml.i @@ -0,0 +1,25 @@ +<!-- include start from listen-address-vrf.xml.i --> +<tagNode name="listen-address"> + <properties> + <help>Local IP addresses to listen on</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + </completionHelp> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address to listen for incoming connections</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + #include <include/interface/vrf.xml.i> + </children> +</tagNode> +<!-- include end --> diff --git a/interface-definitions/include/listen-address.xml.i b/interface-definitions/include/listen-address.xml.i index 9b86851c7..48003dbf2 100644 --- a/interface-definitions/include/listen-address.xml.i +++ b/interface-definitions/include/listen-address.xml.i @@ -1,13 +1,13 @@ <!-- include start from listen-address.xml.i --> <leafNode name="listen-address"> <properties> - <help>Local IP addresses for service to listen on</help> + <help>Local IP addresses to listen on</help> <completionHelp> <script>${vyos_completion_dir}/list_local_ips.sh --both</script> </completionHelp> <valueHelp> <format>ipv4</format> - <description>IP address to listen for incoming connections</description> + <description>IPv4 address to listen for incoming connections</description> </valueHelp> <valueHelp> <format>ipv6</format> @@ -17,6 +17,7 @@ <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> + <validator name="ipv6-link-local"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/nat-translation-options.xml.i b/interface-definitions/include/nat-translation-options.xml.i index defc8c0d5..df2f76397 100644 --- a/interface-definitions/include/nat-translation-options.xml.i +++ b/interface-definitions/include/nat-translation-options.xml.i @@ -16,7 +16,7 @@ </valueHelp> <valueHelp> <format>random</format> - <description>Random source or destination address allocation for each connection (defaut)</description> + <description>Random source or destination address allocation for each connection (default)</description> </valueHelp> <constraint> <regex>^(persistent|random)$</regex> diff --git a/interface-definitions/include/ospf/auto-cost.xml.i b/interface-definitions/include/ospf/auto-cost.xml.i new file mode 100644 index 000000000..3e6cc8232 --- /dev/null +++ b/interface-definitions/include/ospf/auto-cost.xml.i @@ -0,0 +1,22 @@ +<!-- include start from ospf/auto-cost.xml.i --> +<node name="auto-cost"> + <properties> + <help>Calculate interface cost according to bandwidth</help> + </properties> + <children> + <leafNode name="reference-bandwidth"> + <properties> + <help>Reference bandwidth method to assign cost (default: 100)</help> + <valueHelp> + <format>u32:1-4294967</format> + <description>Reference bandwidth cost in Mbits/sec</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967"/> + </constraint> + </properties> + <defaultValue>100</defaultValue> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/ospf/default-information.xml.i b/interface-definitions/include/ospf/default-information.xml.i new file mode 100644 index 000000000..50cda54a4 --- /dev/null +++ b/interface-definitions/include/ospf/default-information.xml.i @@ -0,0 +1,25 @@ +<!-- include start from ospf/intervals.xml.i --> +<node name="default-information"> + <properties> + <help>Default route advertisment settings</help> + </properties> + <children> + <node name="originate"> + <properties> + <help>Distribute a default route</help> + </properties> + <children> + <leafNode name="always"> + <properties> + <help>Always advertise a default route</help> + <valueless/> + </properties> + </leafNode> + #include <include/ospf/metric.xml.i> + #include <include/ospf/metric-type.xml.i> + #include <include/route-map.xml.i> + </children> + </node> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/ospf/interface-common.xml.i b/interface-definitions/include/ospf/interface-common.xml.i index 4b0aef380..738651594 100644 --- a/interface-definitions/include/ospf/interface-common.xml.i +++ b/interface-definitions/include/ospf/interface-common.xml.i @@ -1,5 +1,5 @@ <!-- include start from ospf/interface-common.xml.i --> -#include <include/bfd.xml.i> +#include <include/bfd/bfd.xml.i> <leafNode name="cost"> <properties> <help>Interface cost</help> diff --git a/interface-definitions/include/ospf/log-adjacency-changes.xml.i b/interface-definitions/include/ospf/log-adjacency-changes.xml.i new file mode 100644 index 000000000..24c6cbe7a --- /dev/null +++ b/interface-definitions/include/ospf/log-adjacency-changes.xml.i @@ -0,0 +1,15 @@ +<!-- include start from ospf/metric-type.xml.i --> +<node name="log-adjacency-changes"> + <properties> + <help>Log adjacency state changes</help> + </properties> + <children> + <leafNode name="detail"> + <properties> + <help>Log all state changes</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/ospf/protocol-common-config.xml.i b/interface-definitions/include/ospf/protocol-common-config.xml.i index 0139296ec..688e78034 100644 --- a/interface-definitions/include/ospf/protocol-common-config.xml.i +++ b/interface-definitions/include/ospf/protocol-common-config.xml.i @@ -275,49 +275,8 @@ </tagNode> </children> </tagNode> -<node name="auto-cost"> - <properties> - <help>Calculate OSPF interface cost according to bandwidth (default: 100)</help> - </properties> - <children> - <leafNode name="reference-bandwidth"> - <properties> - <help>Reference bandwidth method to assign OSPF cost</help> - <valueHelp> - <format>u32:1-4294967</format> - <description>Reference bandwidth cost in Mbits/sec</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-4294967"/> - </constraint> - </properties> - <defaultValue>100</defaultValue> - </leafNode> - </children> -</node> -<node name="default-information"> - <properties> - <help>Default route advertisment settings</help> - </properties> - <children> - <node name="originate"> - <properties> - <help>Distribute a default route</help> - </properties> - <children> - <leafNode name="always"> - <properties> - <help>Always advertise a default route</help> - <valueless/> - </properties> - </leafNode> - #include <include/ospf/metric.xml.i> - #include <include/ospf/metric-type.xml.i> - #include <include/route-map.xml.i> - </children> - </node> - </children> -</node> +#include <include/ospf/auto-cost.xml.i> +#include <include/ospf/default-information.xml.i> <leafNode name="default-metric"> <properties> <help>Metric of redistributed routes</help> @@ -364,6 +323,9 @@ <leafNode name="area"> <properties> <help>Enable OSPF on this interface</help> + <completionHelp> + <path>protocols ospf area</path> + </completionHelp> <valueHelp> <format>u32</format> <description>OSPF area ID as decimal notation</description> @@ -433,21 +395,17 @@ <constraintErrorMessage>Must be broadcast, non-broadcast, point-to-multipoint or point-to-point</constraintErrorMessage> </properties> </leafNode> - </children> -</tagNode> -<node name="log-adjacency-changes"> - <properties> - <help>Log adjacency state changes</help> - </properties> - <children> - <leafNode name="detail"> + <node name="passive"> <properties> - <help>Log all state changes</help> - <valueless/> + <help>Suppress routing updates on an interface</help> </properties> - </leafNode> + <children> + #include <include/generic-disable-node.xml.i> + </children> + </node> </children> -</node> +</tagNode> +#include <include/ospf/log-adjacency-changes.xml.i> <node name="max-metric"> <properties> <help>OSPF maximum and infinite-distance metric</help> @@ -606,26 +564,19 @@ #include <include/router-id.xml.i> </children> </node> -#include <include/routing-passive-interface.xml.i> -<leafNode name="passive-interface-exclude"> +<leafNode name="passive-interface"> <properties> - <help>Interface to exclude when using 'passive-interface default'</help> + <help>Suppress routing updates on an interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> + <list>default</list> </completionHelp> <valueHelp> - <format>txt</format> - <description>Interface to exclude when suppressing routing updates</description> - </valueHelp> - <valueHelp> - <format>vlinkN</format> - <description>Virtual-link interface to exclude when suppressing routing updates</description> + <format>default</format> + <description>Default to suppress routing updates on all interfaces</description> </valueHelp> <constraint> - <validator name="interface-name"/> - <regex>^(vlink[0-9]+)$</regex> + <regex>^(default)$</regex> </constraint> - <multi/> </properties> </leafNode> <node name="redistribute"> diff --git a/interface-definitions/include/ospfv3/no-summary.xml.i b/interface-definitions/include/ospfv3/no-summary.xml.i new file mode 100644 index 000000000..a6afda3e0 --- /dev/null +++ b/interface-definitions/include/ospfv3/no-summary.xml.i @@ -0,0 +1,8 @@ +<!-- include start from ospfv3/no-summary.xml.i --> +<leafNode name="no-summary"> + <properties> + <help>Do not inject inter-area routes into the stub</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/ospfv3/protocol-common-config.xml.i b/interface-definitions/include/ospfv3/protocol-common-config.xml.i new file mode 100644 index 000000000..5d08debda --- /dev/null +++ b/interface-definitions/include/ospfv3/protocol-common-config.xml.i @@ -0,0 +1,252 @@ +<!-- include start from ospfv3/protocol-common-config.xml.i --> +<tagNode name="area"> + <properties> + <help>OSPFv3 Area</help> + <valueHelp> + <format>u32</format> + <description>Area ID as a decimal value</description> + </valueHelp> + <valueHelp> + <format>ipv4</format> + <description>Area ID in IP address forma</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + <validator name="ip-address"/> + </constraint> + </properties> + <children> + <node name="area-type"> + <properties> + <help>OSPFv3 Area type</help> + </properties> + <children> + <node name="nssa"> + <properties> + <help>NSSA OSPFv3 area</help> + </properties> + <children> + <leafNode name="default-information-originate"> + <properties> + <help>Originate Type 7 default into NSSA area</help> + <valueless/> + </properties> + </leafNode> + #include <include/ospfv3/no-summary.xml.i> + </children> + </node> + <node name="stub"> + <properties> + <help>Stub OSPFv3 area</help> + </properties> + <children> + #include <include/ospfv3/no-summary.xml.i> + </children> + </node> + </children> + </node> + <leafNode name="export-list"> + <properties> + <help>Name of export-list</help> + <completionHelp> + <path>policy access-list6</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import-list"> + <properties> + <help>Name of import-list</help> + <completionHelp> + <path>policy access-list6</path> + </completionHelp> + </properties> + </leafNode> + <tagNode name="range"> + <properties> + <help>Specify IPv6 prefix (border routers only)</help> + <valueHelp> + <format>ipv6net</format> + <description>Specify IPv6 prefix (border routers only)</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="advertise"> + <properties> + <help>Advertise this range</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="not-advertise"> + <properties> + <help>Do not advertise this range</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> +</tagNode> +#include <include/ospf/auto-cost.xml.i> +#include <include/ospf/default-information.xml.i> +<node name="distance"> + <properties> + <help>Administrative distance</help> + </properties> + <children> + #include <include/ospf/distance-global.xml.i> + <node name="ospfv3"> + <properties> + <help>OSPFv3 administrative distance</help> + </properties> + <children> + #include <include/ospf/distance-per-protocol.xml.i> + </children> + </node> + </children> +</node> +<tagNode name="interface"> + <properties> + <help>Enable routing on an IPv6 interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface used for routing information exchange</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> + </properties> + <children> + <leafNode name="area"> + <properties> + <help>Enable OSPF on this interface</help> + <completionHelp> + <path>protocols ospfv3 area</path> + </completionHelp> + <valueHelp> + <format>u32</format> + <description>OSPF area ID as decimal notation</description> + </valueHelp> + <valueHelp> + <format>ipv4</format> + <description>OSPF area ID in IP address notation</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + #include <include/ospf/intervals.xml.i> + #include <include/ospf/interface-common.xml.i> + <leafNode name="ifmtu"> + <properties> + <help>Interface MTU</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Interface MTU</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="instance-id"> + <properties> + <help>Instance Id (default: 0)</help> + <valueHelp> + <format>u32:0-255</format> + <description>Instance Id</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="network"> + <properties> + <help>Network type</help> + <completionHelp> + <list>broadcast point-to-point</list> + </completionHelp> + <valueHelp> + <format>broadcast</format> + <description>Broadcast network type</description> + </valueHelp> + <valueHelp> + <format>point-to-point</format> + <description>Point-to-point network type</description> + </valueHelp> + <constraint> + <regex>^(broadcast|point-to-point)$</regex> + </constraint> + <constraintErrorMessage>Must be broadcast or point-to-point</constraintErrorMessage> + </properties> + </leafNode> + #include <include/isis/passive.xml.i> + </children> +</tagNode> +#include <include/ospf/log-adjacency-changes.xml.i> +<node name="parameters"> + <properties> + <help>OSPFv3 specific parameters</help> + </properties> + <children> + #include <include/router-id.xml.i> + </children> +</node> +<node name="redistribute"> + <properties> + <help>Redistribute information from another routing protocol</help> + </properties> + <children> + <node name="bgp"> + <properties> + <help>Redistribute BGP routes</help> + </properties> + <children> + #include <include/route-map.xml.i> + </children> + </node> + <node name="connected"> + <properties> + <help>Redistribute connected routes</help> + </properties> + <children> + #include <include/route-map.xml.i> + </children> + </node> + <node name="kernel"> + <properties> + <help>Redistribute kernel routes</help> + </properties> + <children> + #include <include/route-map.xml.i> + </children> + </node> + <node name="ripng"> + <properties> + <help>Redistribute RIPNG routes</help> + </properties> + <children> + #include <include/route-map.xml.i> + </children> + </node> + <node name="static"> + <properties> + <help>Redistribute static routes</help> + </properties> + <children> + #include <include/route-map.xml.i> + </children> + </node> + </children> +</node> +#include <include/route-map.xml.i> +<!-- include end --> diff --git a/interface-definitions/include/policy/route-common-rule-ipv6.xml.i b/interface-definitions/include/policy/route-common-rule-ipv6.xml.i new file mode 100644 index 000000000..2d6adcd1d --- /dev/null +++ b/interface-definitions/include/policy/route-common-rule-ipv6.xml.i @@ -0,0 +1,569 @@ +<!-- include start from policy/route-common-rule.xml.i --> +#include <include/policy/route-rule-action.xml.i> +#include <include/generic-description.xml.i> +<leafNode name="disable"> + <properties> + <help>Option to disable firewall rule</help> + <valueless/> + </properties> +</leafNode> +<node name="fragment"> + <properties> + <help>IP fragment match</help> + </properties> + <children> + <leafNode name="match-frag"> + <properties> + <help>Second and further fragments of fragmented packets</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="match-non-frag"> + <properties> + <help>Head fragments or unfragmented packets</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<node name="ipsec"> + <properties> + <help>Inbound IPsec packets</help> + </properties> + <children> + <leafNode name="match-ipsec"> + <properties> + <help>Inbound IPsec packets</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="match-none"> + <properties> + <help>Inbound non-IPsec packets</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<node name="limit"> + <properties> + <help>Rate limit using a token bucket filter</help> + </properties> + <children> + <leafNode name="burst"> + <properties> + <help>Maximum number of packets to allow in excess of rate</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Maximum number of packets to allow in excess of rate</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rate"> + <properties> + <help>Maximum average matching rate</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Maximum average matching rate</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<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> +<leafNode name="protocol"> + <properties> + <help>Protocol to match (protocol name, number, or "all")</help> + <completionHelp> + <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script> + </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>0-255</format> + <description>IP protocol number</description> + </valueHelp> + <valueHelp> + <format>!<protocol></format> + <description>IP protocol number</description> + </valueHelp> + <constraint> + <validator name="ip-protocol"/> + </constraint> + </properties> + <defaultValue>all</defaultValue> +</leafNode> +<node name="recent"> + <properties> + <help>Parameters for matching recently seen sources</help> + </properties> + <children> + <leafNode name="count"> + <properties> + <help>Source addresses seen more than N times</help> + <valueHelp> + <format>u32:1-255</format> + <description>Source addresses seen more than N times</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="time"> + <properties> + <help>Source addresses seen in the last N seconds</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Source addresses seen in the last N seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<node name="set"> + <properties> + <help>Packet modifications</help> + </properties> + <children> + <leafNode name="dscp"> + <properties> + <help>Packet Differentiated Services Codepoint (DSCP)</help> + <valueHelp> + <format>u32:0-63</format> + <description>DSCP number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-63"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mark"> + <properties> + <help>Packet marking</help> + <valueHelp> + <format>u32:1-2147483647</format> + <description>Packet marking</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2147483647"/> + </constraint> + </properties> + </leafNode> + <leafNode name="table"> + <properties> + <help>Routing table to forward packet with</help> + <valueHelp> + <format>u32:1-200</format> + <description>Table number</description> + </valueHelp> + <valueHelp> + <format>main</format> + <description>Main table</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-200"/> + <regex>^(main)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="tcp-mss"> + <properties> + <help>TCP Maximum Segment Size</help> + <valueHelp> + <format>u32:500-1460</format> + <description>Explicitly set TCP MSS value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 500-1460"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/firewall/address-ipv6.xml.i> + #include <include/firewall/source-destination-group.xml.i> + <leafNode name="mac-address"> + <properties> + <help>Source MAC address</help> + <valueHelp> + <format><MAC address></format> + <description>MAC address to match</description> + </valueHelp> + <valueHelp> + <format>!<MAC address></format> + <description>Match everything except the specified MAC address</description> + </valueHelp> + </properties> + </leafNode> + #include <include/firewall/port.xml.i> + </children> +</node> +<node name="state"> + <properties> + <help>Session state</help> + </properties> + <children> + <leafNode name="established"> + <properties> + <help>Established state</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable</description> + </valueHelp> + <constraint> + <regex>^(enable|disable)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="invalid"> + <properties> + <help>Invalid state</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable</description> + </valueHelp> + <constraint> + <regex>^(enable|disable)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="new"> + <properties> + <help>New state</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable</description> + </valueHelp> + <constraint> + <regex>^(enable|disable)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="related"> + <properties> + <help>Related state</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable</description> + </valueHelp> + <constraint> + <regex>^(enable|disable)$</regex> + </constraint> + </properties> + </leafNode> + </children> +</node> +<node name="tcp"> + <properties> + <help>TCP flags to match</help> + </properties> + <children> + <leafNode name="flags"> + <properties> + <help>TCP flags to match</help> + <valueHelp> + <format>txt</format> + <description>TCP flags to match</description> + </valueHelp> + <valueHelp> + <format> </format> + <description>\n\n Allowed values for TCP flags : SYN ACK FIN RST URG PSH ALL\n When specifying more than one flag, flags should be comma-separated.\n For example : value of 'SYN,!ACK,!FIN,!RST' will only match packets with\n the SYN flag set, and the ACK, FIN and RST flags unset</description> + </valueHelp> + </properties> + </leafNode> + </children> +</node> +<node name="time"> + <properties> + <help>Time to match rule</help> + </properties> + <children> + <leafNode name="monthdays"> + <properties> + <help>Monthdays to match rule on</help> + </properties> + </leafNode> + <leafNode name="startdate"> + <properties> + <help>Date to start matching rule</help> + </properties> + </leafNode> + <leafNode name="starttime"> + <properties> + <help>Time of day to start matching rule</help> + </properties> + </leafNode> + <leafNode name="stopdate"> + <properties> + <help>Date to stop matching rule</help> + </properties> + </leafNode> + <leafNode name="stoptime"> + <properties> + <help>Time of day to stop matching rule</help> + </properties> + </leafNode> + <leafNode name="utc"> + <properties> + <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="weekdays"> + <properties> + <help>Weekdays to match rule on</help> + </properties> + </leafNode> + </children> +</node> +<node name="icmpv6"> + <properties> + <help>ICMPv6 type and code information</help> + </properties> + <children> + <leafNode name="type"> + <properties> + <help>ICMP type-name</help> + <completionHelp> + <list>any echo-reply pong destination-unreachable network-unreachable host-unreachable protocol-unreachable port-unreachable fragmentation-needed source-route-failed network-unknown host-unknown network-prohibited host-prohibited TOS-network-unreachable TOS-host-unreachable communication-prohibited host-precedence-violation precedence-cutoff source-quench redirect network-redirect host-redirect TOS-network-redirect TOS host-redirect echo-request ping router-advertisement router-solicitation time-exceeded ttl-exceeded ttl-zero-during-transit ttl-zero-during-reassembly parameter-problem ip-header-bad required-option-missing timestamp-request timestamp-reply address-mask-request address-mask-reply packet-too-big</list> + </completionHelp> + <valueHelp> + <format>any</format> + <description>Any ICMP type/code</description> + </valueHelp> + <valueHelp> + <format>echo-reply</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>pong</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>destination-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>network-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>host-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>protocol-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>port-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>fragmentation-needed</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>source-route-failed</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>network-unknown</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>host-unknown</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>network-prohibited</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>host-prohibited</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>TOS-network-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>TOS-host-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>communication-prohibited</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>host-precedence-violation</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>precedence-cutoff</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>source-quench</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>redirect</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>network-redirect</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>host-redirect</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>TOS-network-redirect</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>TOS host-redirect</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>echo-request</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>ping</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>router-advertisement</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>router-solicitation</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>time-exceeded</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>ttl-exceeded</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>ttl-zero-during-transit</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>ttl-zero-during-reassembly</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>parameter-problem</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>ip-header-bad</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>required-option-missing</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>timestamp-request</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>timestamp-reply</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>address-mask-request</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>address-mask-reply</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>packet-too-big</format> + <description>ICMP type/code name</description> + </valueHelp> + <constraint> + <regex>^(any|echo-reply|pong|destination-unreachable|network-unreachable|host-unreachable|protocol-unreachable|port-unreachable|fragmentation-needed|source-route-failed|network-unknown|host-unknown|network-prohibited|host-prohibited|TOS-network-unreachable|TOS-host-unreachable|communication-prohibited|host-precedence-violation|precedence-cutoff|source-quench|redirect|network-redirect|host-redirect|TOS-network-redirect|TOS host-redirect|echo-request|ping|router-advertisement|router-solicitation|time-exceeded|ttl-exceeded|ttl-zero-during-transit|ttl-zero-during-reassembly|parameter-problem|ip-header-bad|required-option-missing|timestamp-request|timestamp-reply|address-mask-request|address-mask-reply|packet-too-big)$</regex> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/policy/route-common-rule.xml.i b/interface-definitions/include/policy/route-common-rule.xml.i new file mode 100644 index 000000000..c4deefd2a --- /dev/null +++ b/interface-definitions/include/policy/route-common-rule.xml.i @@ -0,0 +1,418 @@ +<!-- include start from policy/route-common-rule.xml.i --> +#include <include/policy/route-rule-action.xml.i> +#include <include/generic-description.xml.i> +<leafNode name="disable"> + <properties> + <help>Option to disable firewall rule</help> + <valueless/> + </properties> +</leafNode> +<node name="fragment"> + <properties> + <help>IP fragment match</help> + </properties> + <children> + <leafNode name="match-frag"> + <properties> + <help>Second and further fragments of fragmented packets</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="match-non-frag"> + <properties> + <help>Head fragments or unfragmented packets</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<node name="ipsec"> + <properties> + <help>Inbound IPsec packets</help> + </properties> + <children> + <leafNode name="match-ipsec"> + <properties> + <help>Inbound IPsec packets</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="match-none"> + <properties> + <help>Inbound non-IPsec packets</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<node name="limit"> + <properties> + <help>Rate limit using a token bucket filter</help> + </properties> + <children> + <leafNode name="burst"> + <properties> + <help>Maximum number of packets to allow in excess of rate</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Maximum number of packets to allow in excess of rate</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rate"> + <properties> + <help>Maximum average matching rate</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Maximum average matching rate</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<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> +<leafNode name="protocol"> + <properties> + <help>Protocol to match (protocol name, number, or "all")</help> + <completionHelp> + <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script> + </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>0-255</format> + <description>IP protocol number</description> + </valueHelp> + <valueHelp> + <format>!<protocol></format> + <description>IP protocol number</description> + </valueHelp> + <constraint> + <validator name="ip-protocol"/> + </constraint> + </properties> + <defaultValue>all</defaultValue> +</leafNode> +<node name="recent"> + <properties> + <help>Parameters for matching recently seen sources</help> + </properties> + <children> + <leafNode name="count"> + <properties> + <help>Source addresses seen more than N times</help> + <valueHelp> + <format>u32:1-255</format> + <description>Source addresses seen more than N times</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="time"> + <properties> + <help>Source addresses seen in the last N seconds</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Source addresses seen in the last N seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<node name="set"> + <properties> + <help>Packet modifications</help> + </properties> + <children> + <leafNode name="dscp"> + <properties> + <help>Packet Differentiated Services Codepoint (DSCP)</help> + <valueHelp> + <format>u32:0-63</format> + <description>DSCP number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-63"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mark"> + <properties> + <help>Packet marking</help> + <valueHelp> + <format>u32:1-2147483647</format> + <description>Packet marking</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2147483647"/> + </constraint> + </properties> + </leafNode> + <leafNode name="table"> + <properties> + <help>Routing table to forward packet with</help> + <valueHelp> + <format>u32:1-200</format> + <description>Table number</description> + </valueHelp> + <valueHelp> + <format>main</format> + <description>Main table</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-200"/> + <regex>^(main)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="tcp-mss"> + <properties> + <help>TCP Maximum Segment Size</help> + <valueHelp> + <format>u32:500-1460</format> + <description>Explicitly set TCP MSS value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 500-1460"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/firewall/address.xml.i> + #include <include/firewall/source-destination-group.xml.i> + <leafNode name="mac-address"> + <properties> + <help>Source MAC address</help> + <valueHelp> + <format><MAC address></format> + <description>MAC address to match</description> + </valueHelp> + <valueHelp> + <format>!<MAC address></format> + <description>Match everything except the specified MAC address</description> + </valueHelp> + </properties> + </leafNode> + #include <include/firewall/port.xml.i> + </children> +</node> +<node name="state"> + <properties> + <help>Session state</help> + </properties> + <children> + <leafNode name="established"> + <properties> + <help>Established state</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable</description> + </valueHelp> + <constraint> + <regex>^(enable|disable)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="invalid"> + <properties> + <help>Invalid state</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable</description> + </valueHelp> + <constraint> + <regex>^(enable|disable)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="new"> + <properties> + <help>New state</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable</description> + </valueHelp> + <constraint> + <regex>^(enable|disable)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="related"> + <properties> + <help>Related state</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable</description> + </valueHelp> + <constraint> + <regex>^(enable|disable)$</regex> + </constraint> + </properties> + </leafNode> + </children> +</node> +<node name="tcp"> + <properties> + <help>TCP flags to match</help> + </properties> + <children> + <leafNode name="flags"> + <properties> + <help>TCP flags to match</help> + <valueHelp> + <format>txt</format> + <description>TCP flags to match</description> + </valueHelp> + <valueHelp> + <format> </format> + <description>\n\n Allowed values for TCP flags : SYN ACK FIN RST URG PSH ALL\n When specifying more than one flag, flags should be comma-separated.\n For example : value of 'SYN,!ACK,!FIN,!RST' will only match packets with\n the SYN flag set, and the ACK, FIN and RST flags unset</description> + </valueHelp> + </properties> + </leafNode> + </children> +</node> +<node name="time"> + <properties> + <help>Time to match rule</help> + </properties> + <children> + <leafNode name="monthdays"> + <properties> + <help>Monthdays to match rule on</help> + </properties> + </leafNode> + <leafNode name="startdate"> + <properties> + <help>Date to start matching rule</help> + </properties> + </leafNode> + <leafNode name="starttime"> + <properties> + <help>Time of day to start matching rule</help> + </properties> + </leafNode> + <leafNode name="stopdate"> + <properties> + <help>Date to stop matching rule</help> + </properties> + </leafNode> + <leafNode name="stoptime"> + <properties> + <help>Time of day to stop matching rule</help> + </properties> + </leafNode> + <leafNode name="utc"> + <properties> + <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="weekdays"> + <properties> + <help>Weekdays to match rule on</help> + </properties> + </leafNode> + </children> +</node> +<node name="icmp"> + <properties> + <help>ICMP type and code information</help> + </properties> + <children> + <leafNode name="code"> + <properties> + <help>ICMP code (0-255)</help> + <valueHelp> + <format>u32:0-255</format> + <description>ICMP code (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>ICMP type (0-255)</help> + <valueHelp> + <format>u32:0-255</format> + <description>ICMP type (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + #include <include/firewall/icmp-type-name.xml.i> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/policy/route-rule-action.xml.i b/interface-definitions/include/policy/route-rule-action.xml.i new file mode 100644 index 000000000..9c880579d --- /dev/null +++ b/interface-definitions/include/policy/route-rule-action.xml.i @@ -0,0 +1,17 @@ +<!-- include start from policy/route-rule-action.xml.i --> +<leafNode name="action"> + <properties> + <help>Rule action [REQUIRED]</help> + <completionHelp> + <list>drop</list> + </completionHelp> + <valueHelp> + <format>drop</format> + <description>Drop matching entries</description> + </valueHelp> + <constraint> + <regex>^(drop)$</regex> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/snmp/access-mode.xml.i b/interface-definitions/include/snmp/access-mode.xml.i new file mode 100644 index 000000000..1fce2364e --- /dev/null +++ b/interface-definitions/include/snmp/access-mode.xml.i @@ -0,0 +1,23 @@ +<!-- include start from snmp/access-mode.xml.i --> +<leafNode name="mode"> + <properties> + <help>Define access permission</help> + <completionHelp> + <list>ro rw</list> + </completionHelp> + <valueHelp> + <format>ro</format> + <description>Read-Only (default)</description> + </valueHelp> + <valueHelp> + <format>rw</format> + <description>read write</description> + </valueHelp> + <constraint> + <regex>^(ro|rw)$</regex> + </constraint> + <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> + </properties> + <defaultValue>ro</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/snmp/authentication-type.xml.i b/interface-definitions/include/snmp/authentication-type.xml.i new file mode 100644 index 000000000..2a545864a --- /dev/null +++ b/interface-definitions/include/snmp/authentication-type.xml.i @@ -0,0 +1,22 @@ +<!-- include start from snmp/authentication-type.xml.i --> +<leafNode name="type"> + <properties> + <help>Define used protocol</help> + <completionHelp> + <list>md5 sha</list> + </completionHelp> + <valueHelp> + <format>md5</format> + <description>Message Digest 5 (default)</description> + </valueHelp> + <valueHelp> + <format>sha</format> + <description>Secure Hash Algorithm</description> + </valueHelp> + <constraint> + <regex>^(md5|sha)$</regex> + </constraint> + </properties> + <defaultValue>md5</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/snmp/privacy-type.xml.i b/interface-definitions/include/snmp/privacy-type.xml.i new file mode 100644 index 000000000..47a1e632e --- /dev/null +++ b/interface-definitions/include/snmp/privacy-type.xml.i @@ -0,0 +1,22 @@ +<!-- include start from snmp/privacy-type.xml.i --> +<leafNode name="type"> + <properties> + <help>Defines the protocol for privacy</help> + <completionHelp> + <list>des aes</list> + </completionHelp> + <valueHelp> + <format>des</format> + <description>Data Encryption Standard (default)</description> + </valueHelp> + <valueHelp> + <format>aes</format> + <description>Advanced Encryption Standard</description> + </valueHelp> + <constraint> + <regex>^(des|aes)$</regex> + </constraint> + </properties> + <defaultValue>des</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/snmp/protocol.xml.i b/interface-definitions/include/snmp/protocol.xml.i new file mode 100644 index 000000000..335736724 --- /dev/null +++ b/interface-definitions/include/snmp/protocol.xml.i @@ -0,0 +1,22 @@ +<!-- include start from snmp/protocol.xml.i -->
+<leafNode name="protocol">
+ <properties>
+ <help>Protocol to be used (TCP/UDP)</help>
+ <completionHelp>
+ <list>udp tcp</list>
+ </completionHelp>
+ <valueHelp>
+ <format>udp</format>
+ <description>Listen protocol UDP (default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp</format>
+ <description>Listen protocol TCP</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(udp|tcp)$</regex>
+ </constraint>
+ </properties>
+ <defaultValue>udp</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index 05e0d8461..723041ca5 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -56,6 +56,8 @@ #include <include/interface/disable.xml.i> #include <include/interface/vrf.xml.i> #include <include/interface/mirror.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <leafNode name="hash-policy"> <properties> <help>Bonding transmit hash policy</help> @@ -177,6 +179,13 @@ <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py --bondable</script> </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> <multi/> </properties> </leafNode> @@ -189,6 +198,13 @@ <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py --bondable</script> </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> </properties> </leafNode> #include <include/interface/vif-s.xml.i> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index 144f43f32..0856615be 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -41,6 +41,8 @@ #include <include/interface/disable.xml.i> #include <include/interface/vrf.xml.i> #include <include/interface/mtu-68-16000.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <leafNode name="forwarding-delay"> <properties> <help>Forwarding delay</help> diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in index 2bc88c1a7..3bca8b950 100644 --- a/interface-definitions/interfaces-dummy.xml.in +++ b/interface-definitions/interfaces-dummy.xml.in @@ -19,6 +19,8 @@ #include <include/interface/address-ipv4-ipv6.xml.i> #include <include/interface/description.xml.i> #include <include/interface/disable.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <node name="ip"> <properties> <help>IPv4 routing parameters</help> @@ -27,6 +29,7 @@ #include <include/interface/source-validation.xml.i> </children> </node> + #include <include/interface/netns.xml.i> #include <include/interface/vrf.xml.i> </children> </tagNode> diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in index ceeda12a0..9e113cb71 100644 --- a/interface-definitions/interfaces-ethernet.xml.in +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -31,6 +31,8 @@ </leafNode> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <leafNode name="duplex"> <properties> <help>Duplex mode</help> diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in index 2ca7dd9f6..dd4d324d4 100644 --- a/interface-definitions/interfaces-geneve.xml.in +++ b/interface-definitions/interfaces-geneve.xml.in @@ -23,6 +23,8 @@ #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mtu-1450-16000.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <node name="parameters"> <properties> <help>GENEVE tunnel parameters</help> diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in index 9364c85cd..85d4ab992 100644 --- a/interface-definitions/interfaces-l2tpv3.xml.in +++ b/interface-definitions/interfaces-l2tpv3.xml.in @@ -32,6 +32,8 @@ <defaultValue>5000</defaultValue> </leafNode> #include <include/interface/disable.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <leafNode name="encapsulation"> <properties> <help>Encapsulation type (default: UDP)</help> diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in index 4a566ef8b..d69a093af 100644 --- a/interface-definitions/interfaces-macsec.xml.in +++ b/interface-definitions/interfaces-macsec.xml.in @@ -19,6 +19,8 @@ #include <include/interface/address-ipv4-ipv6.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <node name="security"> <properties> <help>Security/Encryption Settings</help> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index 2ecac78e2..16d91145f 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -34,6 +34,8 @@ </children> </node> #include <include/interface/description.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <leafNode name="device-type"> <properties> <help>OpenVPN interface device-type (default: tun)</help> @@ -633,6 +635,92 @@ </properties> <defaultValue>net30</defaultValue> </leafNode> + <node name="mfa"> + <properties> + <help>multi-factor authentication</help> + </properties> + <children> + <node name="totp"> + <properties> + <help>Time-based one-time passwords</help> + </properties> + <children> + <leafNode name="slop"> + <properties> + <help>Maximum allowed clock slop in seconds (default: 180)</help> + <valueHelp> + <format>1-65535</format> + <description>Seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>180</defaultValue> + </leafNode> + <leafNode name="drift"> + <properties> + <help>Time drift in seconds (default: 0)</help> + <valueHelp> + <format>1-65535</format> + <description>Seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="step"> + <properties> + <help>Step value for totp in seconds (default: 30)</help> + <valueHelp> + <format>1-65535</format> + <description>Seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>30</defaultValue> + </leafNode> + <leafNode name="digits"> + <properties> + <help>Number of digits to use for totp hash (default: 6)</help> + <valueHelp> + <format>1-65535</format> + <description>Seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>6</defaultValue> + </leafNode> + <leafNode name="challenge"> + <properties> + <help>Expect password as result of a challenge response protocol (default: enabled)</help> + <completionHelp> + <list>disable enable</list> + </completionHelp> + <valueHelp> + <format>disable</format> + <description>Disable challenge-response</description> + </valueHelp> + <valueHelp> + <format>enable</format> + <description>Enable chalenge-response (default)</description> + </valueHelp> + <constraint> + <regex>^(disable|enable)$</regex> + </constraint> + </properties> + <defaultValue>enable</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> </children> </node> <leafNode name="shared-secret-key"> @@ -678,7 +766,7 @@ <properties> <help>Specify the minimum required TLS version</help> <completionHelp> - <list>1.0 1.1 1.2</list> + <list>1.0 1.1 1.2 1.3</list> </completionHelp> <valueHelp> <format>1.0</format> @@ -692,8 +780,12 @@ <format>1.2</format> <description>TLS v1.2</description> </valueHelp> + <valueHelp> + <format>1.3</format> + <description>TLS v1.3</description> + </valueHelp> <constraint> - <regex>^(1.0|1.1|1.2)$</regex> + <regex>^(1.0|1.1|1.2|1.3)$</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index 57bb01258..80a890940 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -19,6 +19,8 @@ #include <include/pppoe-access-concentrator.xml.i> #include <include/interface/authentication.xml.i> #include <include/interface/dial-on-demand.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <leafNode name="default-route"> <properties> <help>Default route insertion behaviour (default: auto)</help> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index 366892032..bf7055f8d 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -27,6 +27,8 @@ #include <include/interface/ipv6-options.xml.i> #include <include/source-interface-ethernet.xml.i> #include <include/interface/mac.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <leafNode name="mode"> <properties> <help>Receive mode (default: private)</help> diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in index 7450ef2af..fd69fd177 100644 --- a/interface-definitions/interfaces-tunnel.xml.in +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -30,6 +30,8 @@ #include <include/source-address-ipv4-ipv6.xml.i> #include <include/interface/tunnel-remote.xml.i> #include <include/source-interface.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <leafNode name="6rd-prefix"> <properties> <help>6rd network prefix</help> @@ -54,7 +56,6 @@ </constraint> </properties> </leafNode> - #include <include/dhcp-interface.xml.i> <leafNode name="encapsulation"> <properties> <help>Encapsulation of this tunnel interface</help> @@ -67,39 +68,39 @@ </valueHelp> <valueHelp> <format>gre</format> - <description>Generic Routing Encapsulation</description> + <description>Generic Routing Encapsulation (network layer)</description> </valueHelp> <valueHelp> <format>gretap</format> - <description>Generic Routing Encapsulation (virtual L2 tunnel)</description> + <description>Generic Routing Encapsulation (datalink layer)</description> </valueHelp> <valueHelp> <format>ip6erspan</format> - <description>Encapsulated Remote Switched Port Analyzer over IPv6 network</description> + <description>Encapsulated Remote Switched Port Analyzer over IPv6</description> </valueHelp> <valueHelp> <format>ip6gre</format> - <description>GRE over IPv6 network</description> + <description>GRE over IPv6 (network layer)</description> </valueHelp> <valueHelp> <format>ip6gretap</format> - <description>Generic Routing Encapsulation over IPv6 (virtual L2 tunnel)</description> + <description>GRE over IPv6 (datalink layer)</description> </valueHelp> <valueHelp> <format>ip6ip6</format> - <description>IP6 in IP6 encapsulation</description> + <description>IPv6 in IPv6 encapsulation</description> </valueHelp> <valueHelp> <format>ipip</format> - <description>IP in IP encapsulation</description> + <description>IPv4 in IPv4 encapsulation</description> </valueHelp> <valueHelp> <format>ipip6</format> - <description>IP in IP6 encapsulation</description> + <description>IPv4 in IP6 encapsulation</description> </valueHelp> <valueHelp> <format>sit</format> - <description>Simple Internet Transition encapsulation</description> + <description>Simple Internet Transition (IPv6 in IPv4)</description> </valueHelp> <constraint> <regex>^(erspan|gre|gretap|ip6erspan|ip6gre|ip6gretap|ip6ip6|ipip|ipip6|sit)$</regex> @@ -115,11 +116,11 @@ </completionHelp> <valueHelp> <format>enable</format> - <description>Enable Multicast</description> + <description>Enable multicast</description> </valueHelp> <valueHelp> <format>disable</format> - <description>Disable Multicast (default)</description> + <description>Disable multicast (default)</description> </valueHelp> <constraint> <regex>^(enable|disable)$</regex> @@ -134,22 +135,22 @@ <children> <node name="erspan"> <properties> - <help>ERSPAN Tunnel parameters</help> + <help>ERSPAN tunnel parameters</help> </properties> <children> <leafNode name="direction"> <properties> - <help>Specifies mirrored traffic direction</help> + <help>Mirrored traffic direction</help> <completionHelp> <list>ingress egress</list> </completionHelp> <valueHelp> <format>ingress</format> - <description>Mirror ingress direction</description> + <description>Mirror ingress traffic</description> </valueHelp> <valueHelp> <format>egress</format> - <description>Mirror egress direction</description> + <description>Mirror egress traffic</description> </valueHelp> <constraint> <regex>^(ingress|egress)$</regex> @@ -158,10 +159,10 @@ </leafNode> <leafNode name="hw-id"> <properties> - <help>Unique identifier of ERSPAN engine within a system</help> + <help>Unique identifier of an ERSPAN engine within a system</help> <valueHelp> <format>u32:0-1048575</format> - <description>Unique identifier of ERSPAN engine</description> + <description>Unique identifier of an ERSPAN engine</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-1048575"/> @@ -170,7 +171,7 @@ </leafNode> <leafNode name="index"> <properties> - <help>Specifify ERSPAN version 1 index field</help> + <help>ERSPAN version 1 index field</help> <valueHelp> <format>u32:0-63</format> <description>Platform-depedent field for specifying port number and direction</description> @@ -204,18 +205,18 @@ </node> <node name="ip"> <properties> - <help>IPv4 specific tunnel parameters</help> + <help>IPv4-specific tunnel parameters</help> </properties> <children> <leafNode name="no-pmtu-discovery"> <properties> - <help>Disable Path MTU Discovery on this tunnel</help> + <help>Disable path MTU discovery</help> <valueless/> </properties> </leafNode> <leafNode name="ignore-df"> <properties> - <help>Enable IPv4 DF suppression on this tunnel</help> + <help>Ignore the DF (don't fragment) bit</help> <valueless/> </properties> </leafNode> @@ -229,7 +230,7 @@ </node> <node name="ipv6"> <properties> - <help>IPv6 specific tunnel parameters</help> + <help>IPv6-specific tunnel parameters</help> </properties> <children> <leafNode name="encaplimit"> @@ -240,11 +241,11 @@ </completionHelp> <valueHelp> <format>u32:0-255</format> - <description>Encaplimit (default: 4)</description> + <description>Encapsulation limit (default: 4)</description> </valueHelp> <valueHelp> <format>none</format> - <description>Encaplimit disabled</description> + <description>Disable encapsulation limit</description> </valueHelp> <constraint> <regex>^(none)$</regex> @@ -260,12 +261,12 @@ <help>Hoplimit</help> <valueHelp> <format>u32:0-255</format> - <description>Hoplimit (default 64)</description> + <description>Hop limit (default: 64)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-255"/> </constraint> - <constraintErrorMessage>hoplimit must be between 0-255</constraintErrorMessage> + <constraintErrorMessage>hop limit must be between 0-255</constraintErrorMessage> </properties> <defaultValue>64</defaultValue> </leafNode> diff --git a/interface-definitions/interfaces-vti.xml.in b/interface-definitions/interfaces-vti.xml.in index b12434ae7..f03c7476d 100644 --- a/interface-definitions/interfaces-vti.xml.in +++ b/interface-definitions/interfaces-vti.xml.in @@ -35,6 +35,8 @@ #include <include/interface/ipv6-options.xml.i> #include <include/interface/mtu-68-16000.xml.i> #include <include/interface/vrf.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index 43b73a2e9..4c3c3ac71 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -19,6 +19,18 @@ #include <include/interface/address-ipv4-ipv6.xml.i> #include <include/interface/description.xml.i> #include <include/interface/disable.xml.i> + <leafNode name="external"> + <properties> + <help>Use external control plane</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="gpe"> + <properties> + <help>Enable Generic Protocol extension (VXLAN-GPE)</help> + <valueless/> + </properties> + </leafNode> <leafNode name="group"> <properties> <help>Multicast group address for VXLAN interface</help> @@ -31,14 +43,18 @@ <description>Multicast IPv6 group address</description> </valueHelp> <constraint> - <validator name="ip-address"/> + <validator name="ipv4-multicast"/> + <validator name="ipv6-multicast"/> </constraint> + <constraintErrorMessage>Multicast IPv4/IPv6 address required</constraintErrorMessage> </properties> </leafNode> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mtu-1200-16000.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <leafNode name="mtu"> <defaultValue>1450</defaultValue> </leafNode> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in index 403282e5c..7a7c9c1d9 100644 --- a/interface-definitions/interfaces-wireguard.xml.in +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -22,6 +22,8 @@ #include <include/interface/vrf.xml.i> #include <include/port-number.xml.i> #include <include/interface/mtu-68-16000.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <leafNode name="mtu"> <defaultValue>1420</defaultValue> </leafNode> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index 048c7b475..a2d1439a3 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -17,6 +17,8 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> <node name="capabilities"> <properties> <help>HT and VHT capabilities for your card</help> diff --git a/interface-definitions/interfaces-wwan.xml.in b/interface-definitions/interfaces-wwan.xml.in index 6b6fa1a66..03554feed 100644 --- a/interface-definitions/interfaces-wwan.xml.in +++ b/interface-definitions/interfaces-wwan.xml.in @@ -39,6 +39,8 @@ #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/dial-on-demand.xml.i> + #include <include/interface/interface-firewall.xml.i> + #include <include/interface/interface-policy.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in index 3cf3ba6aa..f79680947 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -18,6 +18,7 @@ <properties> <help>Inbound interface of NAT traffic</help> <completionHelp> + <list>any</list> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> </properties> diff --git a/interface-definitions/nat66.xml.in b/interface-definitions/nat66.xml.in index 077f0d5cf..11d986c96 100644 --- a/interface-definitions/nat66.xml.in +++ b/interface-definitions/nat66.xml.in @@ -3,7 +3,7 @@ <node name="nat66" owner="${vyos_conf_scripts_dir}/nat66.py"> <properties> <help>IPv6-to-IPv6 Network Prefix Translation (NAT66/NPT) Settings</help> - <priority>220</priority> + <priority>500</priority> </properties> <children> <node name="source"> diff --git a/interface-definitions/netns.xml.in b/interface-definitions/netns.xml.in new file mode 100644 index 000000000..80de805fb --- /dev/null +++ b/interface-definitions/netns.xml.in @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="netns" owner="${vyos_conf_scripts_dir}/netns.py"> + <properties> + <help>Network namespace</help> + <priority>299</priority> + </properties> + <children> + <tagNode name="name"> + <properties> + <help>Network namespace name</help> + <constraint> + <regex>^[a-zA-Z0-9-_]{1,100}</regex> + </constraint> + <constraintErrorMessage>Netns name must be alphanumeric and can contain hyphens and underscores.</constraintErrorMessage> + </properties> + <children> + #include <include/interface/description.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/policy-route.xml.in b/interface-definitions/policy-route.xml.in new file mode 100644 index 000000000..ed726d1e4 --- /dev/null +++ b/interface-definitions/policy-route.xml.in @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="policy"> + <children> + <tagNode name="ipv6-route" owner="${vyos_conf_scripts_dir}/policy-route.py"> + <properties> + <help>IPv6 policy route rule set name</help> + <priority>201</priority> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/firewall/name-default-log.xml.i> + <tagNode name="rule"> + <properties> + <help>Rule number (1-9999)</help> + </properties> + <children> + <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/firewall/port.xml.i> + </children> + </node> + <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/firewall/port.xml.i> + </children> + </node> + #include <include/policy/route-common-rule-ipv6.xml.i> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="route" owner="${vyos_conf_scripts_dir}/policy-route.py"> + <properties> + <help>Policy route rule set name</help> + <priority>201</priority> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/firewall/name-default-log.xml.i> + <tagNode name="rule"> + <properties> + <help>Rule number (1-9999)</help> + </properties> + <children> + <node name="destination"> + <properties> + <help>Destination parameters</help> + </properties> + <children> + #include <include/firewall/address.xml.i> + #include <include/firewall/source-destination-group.xml.i> + #include <include/firewall/port.xml.i> + </children> + </node> + <node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/firewall/address.xml.i> + #include <include/firewall/source-destination-group.xml.i> + #include <include/firewall/port.xml.i> + </children> + </node> + #include <include/policy/route-common-rule.xml.i> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index cf65daf00..225f9a6f9 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -597,14 +597,7 @@ </completionHelp> </properties> </leafNode> - <leafNode name="interface"> - <properties> - <help>First hop interface of a route to match</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> - </leafNode> + #include <include/generic-interface.xml.i> <node name="ip"> <properties> <help>IP prefix parameters to match</help> diff --git a/interface-definitions/protocols-bfd.xml.in b/interface-definitions/protocols-bfd.xml.in index cc3c3bf12..a9957d884 100644 --- a/interface-definitions/protocols-bfd.xml.in +++ b/interface-definitions/protocols-bfd.xml.in @@ -26,31 +26,13 @@ </constraint> </properties> <children> - <leafNode name="profile"> - <properties> - <help>Use settings from BFD profile</help> - <completionHelp> - <path>protocols bfd profile</path> - </completionHelp> - <valueHelp> - <format>txt</format> - <description>BFD profile name</description> - </valueHelp> - </properties> - </leafNode> + #include <include/bfd/profile.xml.i> <node name="source"> <properties> <help>Bind listener to specified interface/address, mandatory for IPv6</help> </properties> <children> - <leafNode name="interface"> - <properties> - <help>Local interface to bind our peer listener to</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> - </leafNode> + #include <include/generic-interface.xml.i> <leafNode name="address"> <properties> <help>Local address to bind our peer listener to</help> @@ -73,13 +55,14 @@ </leafNode> </children> </node> - #include <include/bfd-common.xml.i> + #include <include/bfd/common.xml.i> <leafNode name="multihop"> <properties> <help>Allow this BFD peer to not be directly connected</help> <valueless/> </properties> </leafNode> + #include <include/interface/vrf.xml.i> </children> </tagNode> <tagNode name="profile"> @@ -94,7 +77,7 @@ </constraint> </properties> <children> - #include <include/bfd-common.xml.i> + #include <include/bfd/common.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/protocols-mpls.xml.in b/interface-definitions/protocols-mpls.xml.in index e7646e625..be8e30c18 100644 --- a/interface-definitions/protocols-mpls.xml.in +++ b/interface-definitions/protocols-mpls.xml.in @@ -524,15 +524,7 @@ </node> </children> </node> - <leafNode name="interface"> - <properties> - <help>Enable LDP and neighbor discovery on interface</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <multi/> - </properties> - </leafNode> + #include <include/generic-interface-multi.xml.i> </children> </node> <node name="parameters"> @@ -560,15 +552,7 @@ </leafNode> </children> </node> - <leafNode name="interface"> - <properties> - <help>Enable MPLS packet processing on interface</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <multi/> - </properties> - </leafNode> + #include <include/generic-interface-multi.xml.i> </children> </node> </children> diff --git a/interface-definitions/protocols-ospfv3.xml.in b/interface-definitions/protocols-ospfv3.xml.in index 7b42c448d..2b98ffa7b 100644 --- a/interface-definitions/protocols-ospfv3.xml.in +++ b/interface-definitions/protocols-ospfv3.xml.in @@ -8,240 +8,7 @@ <priority>620</priority> </properties> <children> - <tagNode name="area"> - <properties> - <help>OSPFv3 Area</help> - <valueHelp> - <format>u32</format> - <description>Area ID as a decimal value</description> - </valueHelp> - <valueHelp> - <format>ipv4</format> - <description>Area ID in IP address forma</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> - <validator name="ip-address"/> - </constraint> - </properties> - <children> - <node name="area-type"> - <properties> - <help>OSPFv3 Area type</help> - </properties> - <children> - <node name="stub"> - <properties> - <help>Stub OSPFv3 area</help> - </properties> - <children> - <leafNode name="no-summary"> - <properties> - <help>Do not inject inter-area routes into the stub</help> - <valueless/> - </properties> - </leafNode> - </children> - </node> - </children> - </node> - <leafNode name="export-list"> - <properties> - <help>Name of export-list</help> - <completionHelp> - <path>policy access-list6</path> - </completionHelp> - </properties> - </leafNode> - <leafNode name="import-list"> - <properties> - <help>Name of import-list</help> - <completionHelp> - <path>policy access-list6</path> - </completionHelp> - </properties> - </leafNode> - <leafNode name="interface"> - <properties> - <help>Enable routing on an IPv6 interface</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <valueHelp> - <format>txt</format> - <description>Interface used for routing information exchange</description> - </valueHelp> - <constraint> - <validator name="interface-name"/> - </constraint> - <multi/> - </properties> - </leafNode> - <tagNode name="range"> - <properties> - <help>Specify IPv6 prefix (border routers only)</help> - <valueHelp> - <format>ipv6net</format> - <description>Specify IPv6 prefix (border routers only)</description> - </valueHelp> - <constraint> - <validator name="ipv6-prefix"/> - </constraint> - </properties> - <children> - <leafNode name="advertise"> - <properties> - <help>Advertise this range</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="not-advertise"> - <properties> - <help>Do not advertise this range</help> - <valueless/> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> - <node name="distance"> - <properties> - <help>Administrative distance</help> - </properties> - <children> - #include <include/ospf/distance-global.xml.i> - <node name="ospfv3"> - <properties> - <help>OSPFv3 administrative distance</help> - </properties> - <children> - #include <include/ospf/distance-per-protocol.xml.i> - </children> - </node> - </children> - </node> - <tagNode name="interface"> - <properties> - <help>Enable routing on an IPv6 interface</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <valueHelp> - <format>txt</format> - <description>Interface used for routing information exchange</description> - </valueHelp> - <constraint> - <validator name="interface-name"/> - </constraint> - </properties> - <children> - #include <include/ospf/intervals.xml.i> - #include <include/ospf/interface-common.xml.i> - <leafNode name="ifmtu"> - <properties> - <help>Interface MTU</help> - <valueHelp> - <format>u32:1-65535</format> - <description>Interface MTU</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> - <leafNode name="instance-id"> - <properties> - <help>Instance Id (default: 0)</help> - <valueHelp> - <format>u32:0-255</format> - <description>Instance Id</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-255"/> - </constraint> - </properties> - <defaultValue>0</defaultValue> - </leafNode> - <leafNode name="network"> - <properties> - <help>Network type</help> - <completionHelp> - <list>broadcast point-to-point</list> - </completionHelp> - <valueHelp> - <format>broadcast</format> - <description>Broadcast network type</description> - </valueHelp> - <valueHelp> - <format>point-to-point</format> - <description>Point-to-point network type</description> - </valueHelp> - <constraint> - <regex>^(broadcast|point-to-point)$</regex> - </constraint> - <constraintErrorMessage>Must be broadcast or point-to-point</constraintErrorMessage> - </properties> - </leafNode> - #include <include/isis/passive.xml.i> - </children> - </tagNode> - <node name="parameters"> - <properties> - <help>OSPFv3 specific parameters</help> - </properties> - <children> - #include <include/router-id.xml.i> - </children> - </node> - <node name="redistribute"> - <properties> - <help>Redistribute information from another routing protocol</help> - </properties> - <children> - <node name="bgp"> - <properties> - <help>Redistribute BGP routes</help> - </properties> - <children> - #include <include/route-map.xml.i> - </children> - </node> - <node name="connected"> - <properties> - <help>Redistribute connected routes</help> - </properties> - <children> - #include <include/route-map.xml.i> - </children> - </node> - <node name="kernel"> - <properties> - <help>Redistribute kernel routes</help> - </properties> - <children> - #include <include/route-map.xml.i> - </children> - </node> - <node name="ripng"> - <properties> - <help>Redistribute RIPNG routes</help> - </properties> - <children> - #include <include/route-map.xml.i> - </children> - </node> - <node name="static"> - <properties> - <help>Redistribute static routes</help> - </properties> - <children> - #include <include/route-map.xml.i> - </children> - </node> - </children> - </node> - #include <include/route-map.xml.i> + #include <include/ospfv3/protocol-common-config.xml.i> </children> </node> </children> diff --git a/interface-definitions/service_mdns-repeater.xml.in b/interface-definitions/service_mdns-repeater.xml.in index d02dac8a6..9a94f1488 100644 --- a/interface-definitions/service_mdns-repeater.xml.in +++ b/interface-definitions/service_mdns-repeater.xml.in @@ -14,15 +14,7 @@ </properties> <children> #include <include/generic-disable-node.xml.i> - <leafNode name="interface"> - <properties> - <help>Interface to repeat mDNS advertisements [REQUIRED]</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <multi/> - </properties> - </leafNode> + #include <include/generic-interface-multi.xml.i> <leafNode name="vrrp-disable"> <properties> <help>Disables mDNS repeater on VRRP interfaces not in MASTER state</help> diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in index 188aed6c4..97952d882 100644 --- a/interface-definitions/service_pppoe-server.xml.in +++ b/interface-definitions/service_pppoe-server.xml.in @@ -70,19 +70,27 @@ <children> <leafNode name="vlan-id"> <properties> - <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help> + <help>VLAN monitor for the automatic creation of single vlan</help> + <valueHelp> + <format>u32:1-4094</format> + <description>VLAN monitor for the automatic creation of single vlan</description> + </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-4096"/> + <validator name="numeric" argument="--range 1-4094"/> </constraint> - <constraintErrorMessage>VLAN ID needs to be between 1 and 4096</constraintErrorMessage> + <constraintErrorMessage>VLAN ID needs to be between 1 and 4094</constraintErrorMessage> <multi/> </properties> </leafNode> <leafNode name="vlan-range"> <properties> - <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help> + <help>VLAN monitor for the automatic creation of vlans range</help> + <valueHelp> + <format>start-end</format> + <description>VLAN monitor range for the automatic creation of vlans (e.g. 1-4094)</description> + </valueHelp> <constraint> - <regex>(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})-(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})</regex> + <validator name="range" argument="--min=1 --max=4094"/> </constraint> <multi/> </properties> diff --git a/interface-definitions/service_webproxy.xml.in b/interface-definitions/service_webproxy.xml.in index d61a95690..03f504ac7 100644 --- a/interface-definitions/service_webproxy.xml.in +++ b/interface-definitions/service_webproxy.xml.in @@ -16,7 +16,7 @@ <description>Domain to use for urls that do not contain a '.'</description> </valueHelp> <constraint> - <regex>^[\.][a-z0-9-][$]?</regex> + <regex>[.][A-Za-z0-9][-.A-Za-z0-9]*</regex> </constraint> <constraintErrorMessage>Must start append-domain with a '.'</constraintErrorMessage> </properties> diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in index b0b7768d2..67d3aef9a 100644 --- a/interface-definitions/snmp.xml.in +++ b/interface-definitions/snmp.xml.in @@ -20,23 +20,24 @@ <children> <leafNode name="authorization"> <properties> - <help>Authorization type (default: 'ro')</help> + <help>Authorization type</help> <completionHelp> <list>ro rw</list> </completionHelp> <valueHelp> <format>ro</format> - <description>read only</description> + <description>Read-Only (default)</description> </valueHelp> <valueHelp> <format>rw</format> - <description>read write</description> + <description>Read-Write</description> </valueHelp> <constraint> <regex>^(ro|rw)$</regex> </constraint> <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> </properties> + <defaultValue>ro</defaultValue> </leafNode> <leafNode name="client"> <properties> @@ -105,18 +106,9 @@ </constraint> </properties> <children> + #include <include/port-number.xml.i> <leafNode name="port"> - <properties> - <help>Port for SNMP service (default: '161')</help> - <valueHelp> - <format>u32:1-65535</format> - <description>Numeric IP port</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> - </properties> + <defaultValue>161</defaultValue> </leafNode> </children> </tagNode> @@ -131,30 +123,27 @@ </leafNode> <leafNode name="oid-enable"> <properties> - <help>Enable specific oids</help> - <valueHelp> - <format>txt</format> - <description>Enable specific oids</description> - </valueHelp> - <valueHelp> - <format>route-table</format> - <description>Enable route table oids (ipCidrRouteTable inetCidrRouteTable)</description> - </valueHelp> + <help>Enable specific OIDs</help> <completionHelp> <list>route-table</list> </completionHelp> + <valueHelp> + <format>route-table</format> + <description>Enable routing table OIDs (ipCidrRouteTable inetCidrRouteTable)</description> + </valueHelp> <constraint> <regex>^(route-table)$</regex> </constraint> - <constraintErrorMessage>Oid must be 'route-table'</constraintErrorMessage> + <constraintErrorMessage>OID must be 'route-table'</constraintErrorMessage> </properties> </leafNode> + #include <include/snmp/protocol.xml.i> <leafNode name="smux-peer"> <properties> <help>Register a subtree for SMUX-based processing</help> <valueHelp> - <format>oid</format> - <description>Object Identifier</description> + <format>txt</format> + <description>SNMP Object Identifier</description> </valueHelp> <multi/> </properties> @@ -198,18 +187,9 @@ <help>Community used when sending trap information</help> </properties> </leafNode> + #include <include/port-number.xml.i> <leafNode name="port"> - <properties> - <help>Destination port used for trap notification</help> - <valueHelp> - <format>u32:1-65535</format> - <description>Numeric IP port</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> - </properties> + <defaultValue>162</defaultValue> </leafNode> </children> </tagNode> @@ -226,32 +206,14 @@ </constraint> <constraintErrorMessage>ID must contain an even number (from 2 to 36) of hex digits</constraintErrorMessage> </properties> + <defaultValue></defaultValue> </leafNode> <tagNode name="group"> <properties> <help>Specifies the group with name groupname</help> </properties> <children> - <leafNode name="mode"> - <properties> - <help>Define group access permission (default: 'ro')</help> - <completionHelp> - <list>ro rw</list> - </completionHelp> - <valueHelp> - <format>ro</format> - <description>read only</description> - </valueHelp> - <valueHelp> - <format>rw</format> - <description>read write</description> - </valueHelp> - <constraint> - <regex>^(ro|rw)$</regex> - </constraint> - <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> - </properties> - </leafNode> + #include <include/snmp/access-mode.xml.i> <leafNode name="seclevel"> <properties> <help>Security levels</help> @@ -264,7 +226,7 @@ </valueHelp> <valueHelp> <format>auth</format> - <description>Messages are authenticated but not encrypted (authNoPriv)</description> + <description>Messages are authenticated but not encrypted (authNoPriv, default)</description> </valueHelp> <valueHelp> <format>priv</format> @@ -274,6 +236,7 @@ <regex>^(noauth|auth|priv)$</regex> </constraint> </properties> + <defaultValue>auth</defaultValue> </leafNode> <leafNode name="view"> <properties> @@ -325,39 +288,12 @@ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> </leafNode> - <leafNode name="type"> - <properties> - <help>Defines the protocol used for authentication (default: 'md5')</help> - <completionHelp> - <list>md5 sha</list> - </completionHelp> - <valueHelp> - <format>md5</format> - <description>Message Digest 5</description> - </valueHelp> - <valueHelp> - <format>sha</format> - <description>Secure Hash Algorithm</description> - </valueHelp> - <constraint> - <regex>^(md5|sha)$</regex> - </constraint> - </properties> - </leafNode> + #include <include/snmp/authentication-type.xml.i> </children> </node> + #include <include/port-number.xml.i> <leafNode name="port"> - <properties> - <help>Specifies TCP/UDP port of destination SNMP traps/informs (default: '162')</help> - <valueHelp> - <format>u32:1-65535</format> - <description>Numeric IP port</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> - </properties> + <defaultValue>162</defaultValue> </leafNode> <node name="privacy"> <properties> @@ -382,54 +318,18 @@ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> </leafNode> - <leafNode name="type"> - <properties> - <help>Defines the protocol for privacy (default: 'des')</help> - <completionHelp> - <list>des aes</list> - </completionHelp> - <valueHelp> - <format>des</format> - <description>Data Encryption Standard</description> - </valueHelp> - <valueHelp> - <format>aes</format> - <description>Advanced Encryption Standard</description> - </valueHelp> - <constraint> - <regex>^(des|aes)$</regex> - </constraint> - </properties> - </leafNode> + #include <include/snmp/privacy-type.xml.i> </children> </node> - <leafNode name="protocol"> - <properties> - <help>Defines protocol for notification between TCP and UDP</help> - <completionHelp> - <list>tcp udp</list> - </completionHelp> - <valueHelp> - <format>tcp</format> - <description>Use Transmission Control Protocol for notifications</description> - </valueHelp> - <valueHelp> - <format>udp</format> - <description>Use User Datagram Protocol for notifications</description> - </valueHelp> - <constraint> - <regex>^(tcp|udp)$</regex> - </constraint> - </properties> - </leafNode> + #include <include/snmp/protocol.xml.i> <leafNode name="type"> <properties> - <help>Specifies the type of notification between inform and trap (default: 'inform')</help> + <help>Specifies the type of notification between inform and trap</help> <completionHelp> <list>inform trap</list> </completionHelp> <valueHelp> - <format>inform</format> + <format>inform (default)</format> <description>Use INFORM</description> </valueHelp> <valueHelp> @@ -440,6 +340,7 @@ <regex>^(inform|trap)$</regex> </constraint> </properties> + <defaultValue>inform</defaultValue> </leafNode> <leafNode name="user"> <properties> @@ -483,25 +384,7 @@ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> </leafNode> - <leafNode name="type"> - <properties> - <help>Defines the protocol used for authentication (default: 'md5')</help> - <completionHelp> - <list>md5 sha</list> - </completionHelp> - <valueHelp> - <format>md5</format> - <description>Message Digest 5</description> - </valueHelp> - <valueHelp> - <format>sha</format> - <description>Secure Hash Algorithm</description> - </valueHelp> - <constraint> - <regex>^(md5|sha)$</regex> - </constraint> - </properties> - </leafNode> + #include <include/snmp/authentication-type.xml.i> </children> </node> <leafNode name="group"> @@ -512,26 +395,7 @@ </completionHelp> </properties> </leafNode> - <leafNode name="mode"> - <properties> - <help>Define users access permission (default: 'ro')</help> - <completionHelp> - <list>ro rw</list> - </completionHelp> - <valueHelp> - <format>ro</format> - <description>read only</description> - </valueHelp> - <valueHelp> - <format>rw</format> - <description>read write</description> - </valueHelp> - <constraint> - <regex>^(ro|rw)$</regex> - </constraint> - <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> - </properties> - </leafNode> + #include <include/snmp/access-mode.xml.i> <node name="privacy"> <properties> <help>Defines the privacy</help> @@ -555,25 +419,7 @@ <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> </leafNode> - <leafNode name="type"> - <properties> - <help>Defines the protocol for privacy (default: 'des')</help> - <completionHelp> - <list>des aes</list> - </completionHelp> - <valueHelp> - <format>des</format> - <description>Data Encryption Standard</description> - </valueHelp> - <valueHelp> - <format>aes</format> - <description>Advanced Encryption Standard</description> - </valueHelp> - <constraint> - <regex>^(des|aes)$</regex> - </constraint> - </properties> - </leafNode> + #include <include/snmp/privacy-type.xml.i> </children> </node> </children> diff --git a/interface-definitions/system-console.xml.in b/interface-definitions/system-console.xml.in index 88f7f82a9..2897e5e97 100644 --- a/interface-definitions/system-console.xml.in +++ b/interface-definitions/system-console.xml.in @@ -74,6 +74,7 @@ <regex>^(1200|2400|4800|9600|19200|38400|57600|115200)$</regex> </constraint> </properties> + <defaultValue>115200</defaultValue> </leafNode> </children> </tagNode> diff --git a/interface-definitions/system-lcd.xml.in b/interface-definitions/system-lcd.xml.in index 36116ae1b..4c9d5c92e 100644 --- a/interface-definitions/system-lcd.xml.in +++ b/interface-definitions/system-lcd.xml.in @@ -12,7 +12,7 @@ <properties> <help>Model of the display attached to this system [REQUIRED]</help> <completionHelp> - <list>cfa-533 cfa-631 cfa-633 cfa-635 sdec</list> + <list>cfa-533 cfa-631 cfa-633 cfa-635 hd44780 sdec</list> </completionHelp> <valueHelp> <format>cfa-533</format> @@ -31,11 +31,15 @@ <description>Crystalfontz CFA-635</description> </valueHelp> <valueHelp> + <format>hd44780</format> + <description>Hitachi HD44780, Caswell Appliances</description> + </valueHelp> + <valueHelp> <format>sdec</format> <description>Lanner, Watchguard, Nexcom NSA, Sophos UTM appliances</description> </valueHelp> <constraint> - <regex>^(cfa-533|cfa-631|cfa-633|cfa-635|sdec)$</regex> + <regex>^(cfa-533|cfa-631|cfa-633|cfa-635|hd44780|sdec)$</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/system-login-banner.xml.in b/interface-definitions/system-login-banner.xml.in index c4bb14bd6..bdd0ad96a 100644 --- a/interface-definitions/system-login-banner.xml.in +++ b/interface-definitions/system-login-banner.xml.in @@ -15,12 +15,12 @@ <children> <leafNode name="post-login"> <properties> - <help>System loging banner post-login</help> + <help>A system banner after the user logs in </help> </properties> </leafNode> <leafNode name="pre-login"> <properties> - <help>System loging banner pre-login</help> + <help>A system banner before the user logs in</help> </properties> </leafNode> </children> diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in index f4613b8a2..4bfe82268 100644 --- a/interface-definitions/system-login.xml.in +++ b/interface-definitions/system-login.xml.in @@ -44,9 +44,6 @@ <tagNode name="public-keys"> <properties> <help>Remote access public keys</help> - <constraint> - <regex>^[-_a-zA-Z0-9@]+$</regex> - </constraint> <valueHelp> <format>txt</format> <description>Key identifier used by ssh-keygen (usually of form user@host)</description> diff --git a/interface-definitions/system-logs.xml.in b/interface-definitions/system-logs.xml.in new file mode 100644 index 000000000..8b6c7c399 --- /dev/null +++ b/interface-definitions/system-logs.xml.in @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="logs" owner="${vyos_conf_scripts_dir}/system-logs.py"> + <properties> + <help>Logging options</help> + <priority>9999</priority> + </properties> + <children> + <node name="logrotate"> + <properties> + <help>Logrotate options</help> + </properties> + <children> + <node name="atop"> + <properties> + <help>Atop logs options (system resources usage)</help> + </properties> + <children> + <leafNode name="max-size"> + <properties> + <help>Size of a single log file that triggers rotation</help> + <valueHelp> + <format>u32:1-1024</format> + <description>Size in MB (default: 10)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-1024" /> + </constraint> + <constraintErrorMessage>The size must be between 1 and 1024 MB</constraintErrorMessage> + </properties> + <defaultValue>10</defaultValue> + </leafNode> + <leafNode name="rotate"> + <properties> + <help>Count of rotations before old logs will be deleted</help> + <valueHelp> + <format>u32:1-100</format> + <description>Rotations (default: 10)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-100" /> + </constraint> + <constraintErrorMessage>The count must be between 1 and 100</constraintErrorMessage> + </properties> + <defaultValue>10</defaultValue> + </leafNode> + </children> + </node> + <node name="messages"> + <properties> + <help>The /var/log/messages file rotation</help> + </properties> + <children> + <leafNode name="max-size"> + <properties> + <help>Size of a single log file that triggers rotation</help> + <valueHelp> + <format>u32:1-1024</format> + <description>Size in MB (default: 1)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-1024" /> + </constraint> + <constraintErrorMessage>The size must be between 1 and 1024 MB</constraintErrorMessage> + </properties> + <defaultValue>1</defaultValue> + </leafNode> + <leafNode name="rotate"> + <properties> + <help>Count of rotations before old logs will be deleted</help> + <valueHelp> + <format>u32:1-100</format> + <description>Rotations (default: 10)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-100" /> + </constraint> + <constraintErrorMessage>The count must be between 1 and 100</constraintErrorMessage> + </properties> + <defaultValue>10</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-option.xml.in b/interface-definitions/system-option.xml.in index f73c1ee08..75fa67271 100644 --- a/interface-definitions/system-option.xml.in +++ b/interface-definitions/system-option.xml.in @@ -117,6 +117,12 @@ <valueless/> </properties> </leafNode> + <leafNode name="root-partition-auto-resize"> + <properties> + <help>Enable root partition auto-extention on system boot</help> + <valueless/> + </properties> + </leafNode> </children> </node> </children> diff --git a/interface-definitions/tftp-server.xml.in b/interface-definitions/tftp-server.xml.in index 037c097ca..4963eab3c 100644 --- a/interface-definitions/tftp-server.xml.in +++ b/interface-definitions/tftp-server.xml.in @@ -24,7 +24,7 @@ <leafNode name="port"> <defaultValue>69</defaultValue> </leafNode> - #include <include/listen-address.xml.i> + #include <include/listen-address-vrf.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 164ba6618..0c2205410 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -13,13 +13,13 @@ <children> <leafNode name="disable-uniqreqids"> <properties> - <help>Option to disable requirement for unique IDs in the Security Database</help> + <help>Disable requirement for unique IDs in the Security Database</help> <valueless/> </properties> </leafNode> <tagNode name="esp-group"> <properties> - <help>Name of Encapsulating Security Payload (ESP) group</help> + <help>Encapsulated Security Payload (ESP) group name</help> </properties> <children> <leafNode name="compression"> @@ -47,7 +47,7 @@ <help>ESP lifetime</help> <valueHelp> <format>u32:30-86400</format> - <description>ESP lifetime in seconds (default 3600)</description> + <description>ESP lifetime in seconds (default: 3600)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 30-86400"/> @@ -83,7 +83,7 @@ </completionHelp> <valueHelp> <format>enable</format> - <description>Inherit Diffie-Hellman group from IKE group - default</description> + <description>Inherit Diffie-Hellman group from the IKE group (default)</description> </valueHelp> <valueHelp> <format>dh-group1</format> @@ -185,10 +185,10 @@ </leafNode> <tagNode name="proposal"> <properties> - <help>ESP-group proposal [REQUIRED]</help> + <help>ESP group proposal [REQUIRED]</help> <valueHelp> <format>u32:1-65535</format> - <description>ESP-group proposal number</description> + <description>ESP group proposal number</description> </valueHelp> </properties> <children> @@ -200,30 +200,30 @@ </tagNode> <tagNode name="ike-group"> <properties> - <help>Name of Internet Key Exchange (IKE) group</help> + <help>Internet Key Exchange (IKE) group name</help> </properties> <children> <leafNode name="close-action"> <properties> - <help>close-action_help</help> + <help>Action to take if a child SA is unexpectedly closed</help> <completionHelp> <list>none hold clear restart</list> </completionHelp> <valueHelp> <format>none</format> - <description>Set action to none (default)</description> + <description>Do nothing (default)</description> </valueHelp> <valueHelp> <format>hold</format> - <description>Set action to hold</description> + <description>Attempt to re-negotiate when matching traffic is seen</description> </valueHelp> <valueHelp> <format>clear</format> - <description>Set action to clear</description> + <description>Remove the connection immediately</description> </valueHelp> <valueHelp> <format>restart</format> - <description>Set action to restart</description> + <description>Attempt to re-negotiate the connection immediately</description> </valueHelp> <constraint> <regex>^(none|hold|clear|restart)$</regex> @@ -243,15 +243,15 @@ </completionHelp> <valueHelp> <format>hold</format> - <description>Set action to hold (default)</description> + <description>Attempt to re-negotiate the connection when matching traffic is seen (default)</description> </valueHelp> <valueHelp> <format>clear</format> - <description>Set action to clear</description> + <description>Remove the connection immediately</description> </valueHelp> <valueHelp> <format>restart</format> - <description>Set action to restart</description> + <description>Attempt to re-negotiate the connection immediately</description> </valueHelp> <constraint> <regex>^(hold|clear|restart)$</regex> @@ -263,7 +263,7 @@ <help>Keep-alive interval</help> <valueHelp> <format>u32:2-86400</format> - <description>Keep-alive interval in seconds (default 30)</description> + <description>Keep-alive interval in seconds (default: 30)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 2-86400"/> @@ -272,7 +272,7 @@ </leafNode> <leafNode name="timeout"> <properties> - <help>Dead-Peer-Detection keep-alive timeout (IKEv1 only)</help> + <help>Dead Peer Detection keep-alive timeout (IKEv1 only)</help> <valueHelp> <format>u32:2-86400</format> <description>Keep-alive timeout in seconds (default 120)</description> @@ -296,7 +296,7 @@ </valueHelp> <valueHelp> <format>no</format> - <description>Disable remote host re-authenticaton during an IKE rekey. (Default)</description> + <description>Disable remote host re-authenticaton during an IKE rekey. (default)</description> </valueHelp> <constraint> <regex>^(yes|no)$</regex> @@ -305,17 +305,17 @@ </leafNode> <leafNode name="key-exchange"> <properties> - <help>Key Exchange Version</help> + <help>IKE version</help> <completionHelp> <list>ikev1 ikev2</list> </completionHelp> <valueHelp> <format>ikev1</format> - <description>Use IKEv1 for Key Exchange [DEFAULT]</description> + <description>Use IKEv1 for key exchange [DEFAULT]</description> </valueHelp> <valueHelp> <format>ikev2</format> - <description>Use IKEv2 for Key Exchange</description> + <description>Use IKEv2 for key exchange</description> </valueHelp> <constraint> <regex>^(ikev1|ikev2)$</regex> @@ -327,7 +327,7 @@ <help>IKE lifetime</help> <valueHelp> <format>u32:30-86400</format> - <description>IKE lifetime in seconds (default 28800)</description> + <description>IKE lifetime in seconds (default: 28800)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 30-86400"/> @@ -337,7 +337,7 @@ </leafNode> <leafNode name="mobike"> <properties> - <help>Enable MOBIKE Support. MOBIKE is only available for IKEv2.</help> + <help>Enable MOBIKE Support (IKEv2 only)</help> <completionHelp> <list>enable disable</list> </completionHelp> @@ -356,17 +356,17 @@ </leafNode> <leafNode name="mode"> <properties> - <help>IKEv1 Phase 1 Mode Selection</help> + <help>IKEv1 phase 1 mode selection</help> <completionHelp> <list>main aggressive</list> </completionHelp> <valueHelp> <format>main</format> - <description>Use Main mode for Key Exchanges in the IKEv1 Protocol (Recommended Default)</description> + <description>Use the main mode (recommended, default)</description> </valueHelp> <valueHelp> <format>aggressive</format> - <description>Use Aggressive mode for Key Exchanges in the IKEv1 protocol - We do not recommend users to use aggressive mode as it is much more insecure compared to Main mode.</description> + <description>Use the aggressive mode (insecure, not recommended)</description> </valueHelp> <constraint> <regex>^(main|aggressive)$</regex> @@ -375,10 +375,10 @@ </leafNode> <tagNode name="proposal"> <properties> - <help>proposal_help</help> + <help>IKE proposal</help> <valueHelp> <format>u32:1-65535</format> - <description>IKE-group proposal</description> + <description>IKE group proposal</description> </valueHelp> </properties> <children> @@ -490,23 +490,15 @@ </tagNode> <leafNode name="include-ipsec-conf"> <properties> - <help>Sets to include an additional configuration directive file for strongSwan. Use an absolute path to specify the included file</help> + <help>Absolute path to specify a strongSwan config include file</help> </properties> </leafNode> <leafNode name="include-ipsec-secrets"> <properties> - <help>Sets to include an additional secrets file for strongSwan. Use an absolute path to specify the included file.</help> - </properties> - </leafNode> - <leafNode name="interface"> - <properties> - <help>Onterface used for IPsec communication</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <multi/> + <help>Absolute path to a strongSwan secrets include file</help> </properties> </leafNode> + #include <include/generic-interface-multi.xml.i> <node name="log"> <properties> <help>IPsec logging</help> @@ -514,17 +506,17 @@ <children> <leafNode name="level"> <properties> - <help>strongSwan Logger Level</help> + <help>strongSwan logging Level</help> <valueHelp> - <format>u32:0</format> + <format>0</format> <description>Very basic auditing logs e.g. SA up/SA down (default)</description> </valueHelp> <valueHelp> - <format>u32:1</format> + <format>1</format> <description>Generic control flow with errors, a good default to see whats going on</description> </valueHelp> <valueHelp> - <format>u32:2</format> + <format>2</format> <description>More detailed debugging control flow</description> </valueHelp> <constraint> @@ -535,7 +527,7 @@ </leafNode> <leafNode name="subsystem"> <properties> - <help>Subsystem in the daemon the log comes from</help> + <help>Subsystem logging levels</help> <completionHelp> <list>dmn mgr ike chd job cfg knl net asn enc lib esp tls tnc imc imv pts any</list> </completionHelp> @@ -634,7 +626,7 @@ </node> <tagNode name="profile"> <properties> - <help>VPN IPSec Profile</help> + <help>VPN IPSec profile</help> </properties> <children> #include <include/generic-disable-node.xml.i> @@ -651,7 +643,7 @@ </completionHelp> <valueHelp> <format>pre-shared-secret</format> - <description>Use pre shared secret key</description> + <description>Use a pre-shared secret key</description> </valueHelp> </properties> </leafNode> @@ -665,13 +657,13 @@ <children> <leafNode name="tunnel"> <properties> - <help>Tunnel interface associated with this configuration profile</help> + <help>Tunnel interface associated with this profile</help> <completionHelp> <path>interfaces tunnel</path> </completionHelp> <valueHelp> <format>txt</format> - <description>Associated interface to this configuration profile</description> + <description>Associated interface to this profile</description> </valueHelp> <multi/> </properties> @@ -707,15 +699,15 @@ </completionHelp> <valueHelp> <format>eap-tls</format> - <description>Client uses EAP-TLS authentication</description> + <description>Use EAP-TLS authentication</description> </valueHelp> <valueHelp> <format>eap-mschapv2</format> - <description>Client uses EAP-MSCHAPv2 authentication</description> + <description>Use EAP-MSCHAPv2 authentication</description> </valueHelp> <valueHelp> <format>eap-radius</format> - <description>Client uses EAP-RADIUS authentication</description> + <description>Use EAP-RADIUS authentication</description> </valueHelp> <constraint> <regex>^(eap-tls|eap-mschapv2|eap-radius)$</regex> @@ -732,11 +724,11 @@ </completionHelp> <valueHelp> <format>pre-shared-secret</format> - <description>pre-shared-secret_description</description> + <description>Use a pre-shared secret key</description> </valueHelp> <valueHelp> <format>x509</format> - <description>x509_description</description> + <description>Use x.509 certificate</description> </valueHelp> <constraint> <regex>^(pre-shared-secret|x509)$</regex> @@ -762,7 +754,7 @@ </valueHelp> <valueHelp> <format>u32:1-86400</format> - <description>Timeout in seconds (default 28800)</description> + <description>Timeout in seconds (default: 28800)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-86400"/> @@ -772,14 +764,14 @@ </leafNode> <leafNode name="pool"> <properties> - <help>Pool name used for IP address assignments</help> + <help>IP address pool</help> <completionHelp> <path>vpn ipsec remote-access pool</path> <list>dhcp radius</list> </completionHelp> <valueHelp> <format>txt</format> - <description>Name of predefined IP pool</description> + <description>Predefined IP pool name</description> </valueHelp> <valueHelp> <format>dhcp</format> @@ -794,17 +786,17 @@ </leafNode> <leafNode name="unique"> <properties> - <help>Connection uniqueness policy to enforce</help> + <help>Connection uniqueness enforcement policy</help> <completionHelp> <list>never keep replace</list> </completionHelp> <valueHelp> <format>never</format> - <description>Never enforce connection uniqueness policy</description> + <description>Never enforce connection uniqueness</description> </valueHelp> <valueHelp> <format>keep</format> - <description>Rejects new connection attempts if the same user already has an active connection</description> + <description>Reject new connection attempts if the same user already has an active connection</description> </valueHelp> <valueHelp> <format>replace</format> @@ -819,17 +811,10 @@ </tagNode> <node name="dhcp"> <properties> - <help>DHCP pool options for remote-access</help> + <help>DHCP pool options for remote access</help> </properties> <children> - <leafNode name="interface"> - <properties> - <help>Interface with DHCP server to use</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> - </leafNode> + #include <include/generic-interface.xml.i> <leafNode name="server"> <properties> <help>DHCP server address</help> @@ -846,7 +831,7 @@ </node> <tagNode name="pool"> <properties> - <help>IP address pool for remote-access users</help> + <help>IP address pool for remote access users</help> </properties> <children> <leafNode name="exclude"> @@ -943,15 +928,15 @@ </completionHelp> <valueHelp> <format>pre-shared-secret</format> - <description>pre-shared-secret_description</description> + <description>Use pre-shared secret key</description> </valueHelp> <valueHelp> <format>rsa</format> - <description>rsa_description</description> + <description>Use RSA key</description> </valueHelp> <valueHelp> <format>x509</format> - <description>x509_description</description> + <description>Use x.509 certificate</description> </valueHelp> <constraint> <regex>^(pre-shared-secret|rsa|x509)$</regex> @@ -984,11 +969,11 @@ </completionHelp> <valueHelp> <format>initiate</format> - <description>initiate_description</description> + <description>Bring the connection up immediately</description> </valueHelp> <valueHelp> <format>respond</format> - <description>respond_description</description> + <description>Bring the connection up only if traffic is detected</description> </valueHelp> <constraint> <regex>^(initiate|respond)$</regex> @@ -1007,17 +992,17 @@ #include <include/dhcp-interface.xml.i> <leafNode name="force-encapsulation"> <properties> - <help>Force UDP Encapsulation for ESP Payloads</help> + <help>Force UDP Encapsulation for ESP payloads</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> - <description>This endpoint will force UDP encapsulation for this peer</description> + <description>Force UDP encapsulation</description> </valueHelp> <valueHelp> <format>disable</format> - <description>This endpoint will not force UDP encapsulation for this peer</description> + <description>Do not force UDP encapsulation</description> </valueHelp> <constraint> <regex>^(enable|disable)$</regex> @@ -1027,7 +1012,7 @@ #include <include/ipsec/ike-group.xml.i> <leafNode name="ikev2-reauth"> <properties> - <help>Re-authentication of the remote peer during an IKE re-key. IKEv2 option only</help> + <help>Re-authentication of the remote peer during an IKE re-key (IKEv2 only)</help> <completionHelp> <list>yes no inherit</list> </completionHelp> @@ -1041,7 +1026,7 @@ </valueHelp> <valueHelp> <format>inherit</format> - <description>Inherit the reauth configuration form your IKE-group (Default)</description> + <description>Inherit the reauth configuration form your IKE-group (default)</description> </valueHelp> <constraint> <regex>^(yes|no|inherit)$</regex> @@ -1062,9 +1047,21 @@ #include <include/ipsec/esp-group.xml.i> #include <include/ipsec/local-traffic-selector.xml.i> #include <include/ip-protocol.xml.i> + <leafNode name="priority"> + <properties> + <help>Priority for IPSec policy (lowest value more preferable)</help> + <valueHelp> + <format>u32:1-100</format> + <description>Priority for IPSec policy (lowest value more preferable)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-100"/> + </constraint> + </properties> + </leafNode> <node name="remote"> <properties> - <help>Remote parameters for interesting traffic</help> + <help>Match remote addresses</help> </properties> <children> #include <include/port-number.xml.i> diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in index cbd5e38e7..6a88756a7 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn_l2tp.xml.in @@ -34,6 +34,14 @@ <help>Tunnel password used to authenticate the client (LAC)</help> </properties> </leafNode> + <leafNode name="host-name"> + <properties> + <help>Sent to the client (LAC) in the Host-Name attribute</help> + <constraint> + <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]</regex> + </constraint> + </properties> + </leafNode> </children> </node> <leafNode name="ccp-disable"> diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in index 9901a0cdf..fe2fea9f8 100644 --- a/interface-definitions/vpn_sstp.xml.in +++ b/interface-definitions/vpn_sstp.xml.in @@ -43,6 +43,7 @@ </properties> <children> #include <include/accel-ppp/ppp-mppe.xml.i> + #include <include/accel-ppp/ppp-options-ipv4.xml.i> #include <include/accel-ppp/ppp-options-ipv6.xml.i> #include <include/accel-ppp/lcp-echo-interval-failure.xml.i> #include <include/accel-ppp/lcp-echo-timeout.xml.i> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index a82c0b2a6..14c31fa8a 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -60,6 +60,15 @@ #include <include/ospf/protocol-common-config.xml.i> </children> </node> + <node name="ospfv3" owner="${vyos_conf_scripts_dir}/protocols_ospfv3.py $VAR(../../@)"> + <properties> + <help>Open Shortest Path First (OSPF) for IPv6</help> + <priority>621</priority> + </properties> + <children> + #include <include/ospfv3/protocol-common-config.xml.i> + </children> + </node> <node name="static" owner="${vyos_conf_scripts_dir}/protocols_static.py $VAR(../../@)"> <properties> <help>Static route parameters</help> @@ -85,7 +94,7 @@ <constraintErrorMessage>VRF routing table must be in range from 100 to 65535</constraintErrorMessage> </properties> </leafNode> - <leafNode name="vni" owner="${vyos_conf_scripts_dir}/vrf_vni.py $VAR(../@)"> + <leafNode name="vni" owner="${vyos_conf_scripts_dir}/vrf_vni.py"> <properties> <help>Virtual Network Identifier</help> <!-- priority must be after BGP --> diff --git a/interface-definitions/vrrp.xml.in b/interface-definitions/vrrp.xml.in index 44a9a1f54..53d79caac 100644 --- a/interface-definitions/vrrp.xml.in +++ b/interface-definitions/vrrp.xml.in @@ -16,14 +16,7 @@ <help>VRRP group</help> </properties> <children> - <leafNode name="interface"> - <properties> - <help>Network interface</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> - </completionHelp> - </properties> - </leafNode> + #include <include/generic-interface-broadcast.xml.i> <leafNode name="advertise-interval"> <properties> <help>Advertise interval</help> diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in new file mode 100644 index 000000000..dd64c7c16 --- /dev/null +++ b/interface-definitions/zone-policy.xml.in @@ -0,0 +1,143 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="zone-policy" owner="${vyos_conf_scripts_dir}/zone_policy.py"> + <properties> + <help>Configure zone-policy</help> + <priority>250</priority> + </properties> + <children> + <tagNode name="zone"> + <properties> + <help>Zone name</help> + <valueHelp> + <format>txt</format> + <description>Zone name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + <leafNode name="default-action"> + <properties> + <help>Default-action for traffic coming into this zone</help> + <completionHelp> + <list>drop reject</list> + </completionHelp> + <valueHelp> + <format>drop</format> + <description>Drop silently (default)</description> + </valueHelp> + <valueHelp> + <format>reject</format> + <description>Drop and notify source</description> + </valueHelp> + <constraint> + <regex>^(drop|reject)$</regex> + </constraint> + </properties> + </leafNode> + <tagNode name="from"> + <properties> + <help>Zone from which to filter traffic</help> + <completionHelp> + <path>zone-policy zone</path> + </completionHelp> + </properties> + <children> + <node name="firewall"> + <properties> + <help>Firewall options</help> + </properties> + <children> + <leafNode name="ipv6-name"> + <properties> + <help>IPv6 firewall ruleset</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="name"> + <properties> + <help>IPv4 firewall ruleset</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + <leafNode name="interface"> + <properties> + <help>Interface associated with zone</help> + <valueHelp> + <format>txt</format> + <description>Interface associated with zone</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <node name="intra-zone-filtering"> + <properties> + <help>Intra-zone filtering</help> + </properties> + <children> + <leafNode name="action"> + <properties> + <help>Action for intra-zone traffic</help> + <completionHelp> + <list>accept drop</list> + </completionHelp> + <valueHelp> + <format>accept</format> + <description>Accept traffic (default)</description> + </valueHelp> + <valueHelp> + <format>drop</format> + <description>Drop silently</description> + </valueHelp> + <constraint> + <regex>^(accept|drop)$</regex> + </constraint> + </properties> + </leafNode> + <node name="firewall"> + <properties> + <help>Use the specified firewall chain</help> + </properties> + <children> + <leafNode name="ipv6-name"> + <properties> + <help>IPv6 firewall ruleset</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="name"> + <properties> + <help>IPv4 firewall ruleset</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="local-zone"> + <properties> + <help>Zone to be local-zone</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/conntrack-sync.xml.in b/op-mode-definitions/conntrack-sync.xml.in index 41a71b04a..3e29ecd39 100644 --- a/op-mode-definitions/conntrack-sync.xml.in +++ b/op-mode-definitions/conntrack-sync.xml.in @@ -87,6 +87,18 @@ </node> </children> </node> + <leafNode name="statistics"> + <properties> + <help>Show connection syncing statistics</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/conntrack_sync.py --show-statistics</command> + </leafNode> + <leafNode name="status"> + <properties> + <help>Show conntrack-sync status</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/conntrack_sync.py --show-status</command> + </leafNode> </children> </node> </children> diff --git a/op-mode-definitions/disks.xml.in b/op-mode-definitions/disks.xml.in index 2102a2e8e..117ac5065 100644 --- a/op-mode-definitions/disks.xml.in +++ b/op-mode-definitions/disks.xml.in @@ -20,7 +20,7 @@ <script>${vyos_completion_dir}/list_disks.py --exclude ${COMP_WORDS[2]}</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/format_disk.py --target $3 --proto $5</command> + <command>sudo ${vyos_op_scripts_dir}/format_disk.py --target $3 --proto $5</command> </tagNode> </children> </tagNode> diff --git a/op-mode-definitions/firewall.xml.in b/op-mode-definitions/firewall.xml.in new file mode 100644 index 000000000..84df67b3d --- /dev/null +++ b/op-mode-definitions/firewall.xml.in @@ -0,0 +1,178 @@ +<?xml version="1.0"?> +<interfaceDefinition> +<!-- + <node name="clear"> + <children> + <node name="firewall"> + <properties> + <help>Clear firewall statistics</help> + </properties> + <children> + <tagNode name="ipv6-name"> + <properties> + <help>Clear firewall statistics for chain</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear counters for specified chain</help> + </properties> + <command>echo "TODO"</command> + </leafNode> + <tagNode name="rule"> + <properties> + <help>Clear firewall statistics for a rule</help> + <completionHelp> + <path>firewall ipv6-name ${COMP_WORDS[4]} rule</path> + </completionHelp> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear counters for specified rule</help> + </properties> + <command>echo "TODO"</command> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="name"> + <properties> + <help>Clear firewall statistics for chain</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear counters for specified chain</help> + </properties> + <command>echo "TODO"</command> + </leafNode> + <tagNode name="rule"> + <properties> + <help>Clear firewall statistics for a rule</help> + <completionHelp> + <path>firewall name ${COMP_WORDS[4]} rule</path> + </completionHelp> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear counters for specified rule</help> + </properties> + <command>echo "TODO"</command> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +--> +<!-- + <node name="reset"> + <children> + <node name="firewall"> + <properties> + <help>Reset a firewall group</help> + </properties> + <children> + <tagNode name="address-group"> + <properties> + <help>Reset a firewall address group</help> + </properties> + </tagNode> + <tagNode name="network-group"> + <properties> + <help>Reset a firewall network group</help> + </properties> + </tagNode> + <tagNode name="port-group"> + <properties> + <help>Reset a firewall port group</help> + </properties> + </tagNode> + </children> + </node> + </children> + </node> +--> + <node name="show"> + <children> + <node name="firewall"> + <properties> + <help>Show firewall information</help> + </properties> + <children> + <leafNode name="group"> + <properties> + <help>Show firewall group</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show_group --name $4</command> + </leafNode> + <tagNode name="ipv6-name"> + <properties> + <help>Show IPv6 firewall chains</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Show summary of IPv6 firewall rules</help> + <completionHelp> + <path>firewall ipv6-name ${COMP_WORDS[6]} rule</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show --name $4 --rule $6 --ipv6</command> + </tagNode> + </children> + <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show --name $4 --ipv6</command> + </tagNode> + <tagNode name="name"> + <properties> + <help>Show IPv4 firewall chains</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Show summary of IPv4 firewall rules</help> + <completionHelp> + <path>firewall name ${COMP_WORDS[6]} rule</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show --name $4 --rule $6</command> + </tagNode> + </children> + <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show --name $4</command> + </tagNode> + <leafNode name="statistics"> + <properties> + <help>Show statistics of firewall application</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show_statistics</command> + </leafNode> + <leafNode name="summary"> + <properties> + <help>Show summary of firewall application</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show_summary</command> + </leafNode> + </children> + <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show_all</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/force-root-partition-auto-resize.xml.in b/op-mode-definitions/force-root-partition-auto-resize.xml.in new file mode 100644 index 000000000..f84c073b8 --- /dev/null +++ b/op-mode-definitions/force-root-partition-auto-resize.xml.in @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <node name="root-partition-auto-resize"> + <properties> + <help>Resize the VyOS partition</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/force_root-partition-auto-resize.sh</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/generate-ipsec-debug-archive.xml.in b/op-mode-definitions/generate-ipsec-debug-archive.xml.in new file mode 100644 index 000000000..f268d5ae5 --- /dev/null +++ b/op-mode-definitions/generate-ipsec-debug-archive.xml.in @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="ipsec"> + <children> + <node name="debug-archive"> + <properties> + <help>Generate IPSec debug-archive</help> + </properties> + <command>${vyos_op_scripts_dir}/generate_ipsec_debug_archive.sh</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/generate-ipsec-profile.xml.in b/op-mode-definitions/generate-ipsec-profile.xml.in index 8d1051b94..b7203d7d1 100644 --- a/op-mode-definitions/generate-ipsec-profile.xml.in +++ b/op-mode-definitions/generate-ipsec-profile.xml.in @@ -4,7 +4,7 @@ <children> <node name="ipsec"> <properties> - <help>Generate IPsec related configurations</help> + <help>Generate IPsec related configurations and archives</help> </properties> <children> <node name="profile"> diff --git a/op-mode-definitions/include/bgp/afi-common.xml.i b/op-mode-definitions/include/bgp/afi-common.xml.i index 4d5f56656..acf20d950 100644 --- a/op-mode-definitions/include/bgp/afi-common.xml.i +++ b/op-mode-definitions/include/bgp/afi-common.xml.i @@ -61,5 +61,4 @@ </leafNode> </children> </node> -#include <include/vtysh-generic-wide.xml.i> <!-- included end --> diff --git a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i index a51595b7f..084f5da83 100644 --- a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i +++ b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-common.xml.i @@ -230,4 +230,5 @@ </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </tagNode> +#include <include/vtysh-generic-wide.xml.i> <!-- included end --> diff --git a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i index ba6edb256..f6737c8bd 100644 --- a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i +++ b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i @@ -19,5 +19,6 @@ #include <include/bgp/afi-common.xml.i> #include <include/bgp/afi-ipv4-ipv6-common.xml.i> </children> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </node> <!-- included end --> diff --git a/op-mode-definitions/include/bgp/show-bgp-common.xml.i b/op-mode-definitions/include/bgp/show-bgp-common.xml.i index 0664b11fc..e81b26b3e 100644 --- a/op-mode-definitions/include/bgp/show-bgp-common.xml.i +++ b/op-mode-definitions/include/bgp/show-bgp-common.xml.i @@ -22,6 +22,7 @@ #include <include/bgp/afi-ipv4-ipv6-common.xml.i> #include <include/bgp/afi-ipv4-ipv6-vpn.xml.i> </children> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </node> <tagNode name="ipv6"> <properties> @@ -44,6 +45,7 @@ #include <include/bgp/afi-ipv4-ipv6-common.xml.i> #include <include/bgp/afi-ipv4-ipv6-vpn.xml.i> </children> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </node> <node name="l2vpn"> <properties> diff --git a/op-mode-definitions/include/bgp/show-ip-bgp-common.xml.i b/op-mode-definitions/include/bgp/show-ip-bgp-common.xml.i index e599bfb3f..36cc9a3fa 100644 --- a/op-mode-definitions/include/bgp/show-ip-bgp-common.xml.i +++ b/op-mode-definitions/include/bgp/show-ip-bgp-common.xml.i @@ -35,6 +35,7 @@ <properties> <help>Show BGP IPv4 unicast information</help> </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> <children> <leafNode name="cidr-only"> <properties> diff --git a/op-mode-definitions/include/ospfv3/border-routers.xml.i b/op-mode-definitions/include/ospfv3/border-routers.xml.i new file mode 100644 index 000000000..b6fac6785 --- /dev/null +++ b/op-mode-definitions/include/ospfv3/border-routers.xml.i @@ -0,0 +1,20 @@ +<!-- included start from ospfv3/border-routers.xml.i --> +<node name="border-routers"> + <properties> + <help>Show OSPFv3 border-router (ABR and ASBR) information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + </children> +</node> +<tagNode name="border-routers"> + <properties> + <help>Border router ID</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</tagNode> +<!-- included end --> diff --git a/op-mode-definitions/include/ospfv3/database.xml.i b/op-mode-definitions/include/ospfv3/database.xml.i new file mode 100644 index 000000000..e98f9e35b --- /dev/null +++ b/op-mode-definitions/include/ospfv3/database.xml.i @@ -0,0 +1,238 @@ +<!-- included start from ospfv3/database.xml.i --> +<node name="database"> + <properties> + <help>Show OSPFv3 Link state database information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + <tagNode name="adv-router"> + <properties> + <help>Search by Advertising Router ID</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <children> + #include <include/ospfv3/linkstate-id.xml.i> + </children> + </tagNode> + <node name="any"> + <properties> + <help>Search by Any Link state Type</help> + </properties> + <children> + <tagNode name="any"> + <properties> + <help>Search by Link state ID</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <children> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + </children> + </tagNode> + </children> + </node> + <tagNode name="any"> + <properties> + <help>Search by Link state ID</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>vtysh -c "show ipv6 ospf6 database * $6"</command> + <children> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/adv-router-id-node-tag.xml.i> + </children> + </tagNode> + <node name="as-external"> + <properties> + <help>Show AS-External LSAs</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/adv-router.xml.i> + <tagNode name="any"> + <properties> + <help>Search by Advertising Router ID</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>vtysh -c "show ipv6 ospf6 database as-external * $7"</command> + <children> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + </children> + </tagNode> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/self-originated.xml.i> + </children> + </node> + <tagNode name="as-external"> + <properties> + <help>Search by Advertising Router IDs</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <children> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/self-originated.xml.i> + #include <include/ospfv3/adv-router-id-node-tag.xml.i> + </children> + </tagNode> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/self-originated.xml.i> + <node name="group-membership"> + <properties> + <help>Show Group-Membership LSAs</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/adv-router.xml.i> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/linkstate-id-node-tag.xml.i> + #include <include/ospfv3/self-originated.xml.i> + </children> + </node> + <node name="inter-prefix"> + <properties> + <help>Show Inter-Area-Prefix LSAs</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/adv-router.xml.i> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/linkstate-id-node-tag.xml.i> + #include <include/ospfv3/self-originated.xml.i> + </children> + </node> + <node name="inter-router"> + <properties> + <help>Show Inter-Area-Router LSAs</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/adv-router.xml.i> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/linkstate-id-node-tag.xml.i> + #include <include/ospfv3/self-originated.xml.i> + </children> + </node> + <node name="intra-prefix"> + <properties> + <help>Show Intra-Area-Prefix LSAs</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/adv-router.xml.i> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/linkstate-id-node-tag.xml.i> + #include <include/ospfv3/self-originated.xml.i> + </children> + </node> + <node name="link"> + <properties> + <help>Show Link LSAs</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/adv-router.xml.i> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/linkstate-id-node-tag.xml.i> + #include <include/ospfv3/self-originated.xml.i> + </children> + </node> + <node name="network"> + <properties> + <help>Show Network LSAs</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/adv-router.xml.i> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/linkstate-id-node-tag.xml.i> + #include <include/ospfv3/self-originated.xml.i> + </children> + </node> + <node name="node.tag"> + <properties> + <help>Show LSAs</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/adv-router.xml.i> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/linkstate-id-node-tag.xml.i> + #include <include/ospfv3/self-originated.xml.i> + </children> + </node> + <node name="router"> + <properties> + <help>Show router LSAs</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/adv-router.xml.i> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/linkstate-id-node-tag.xml.i> + #include <include/ospfv3/self-originated.xml.i> + </children> + </node> + <node name="type-7"> + <properties> + <help>Show Type-7 LSAs</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/adv-router.xml.i> + #include <include/ospfv3/detail.xml.i> + #include <include/ospfv3/dump.xml.i> + #include <include/ospfv3/internal.xml.i> + #include <include/ospfv3/linkstate-id.xml.i> + #include <include/ospfv3/linkstate-id-node-tag.xml.i> + #include <include/ospfv3/self-originated.xml.i> + </children> + </node> + </children> +</node> +<!-- included end --> diff --git a/op-mode-definitions/include/ospfv3/interface.xml.i b/op-mode-definitions/include/ospfv3/interface.xml.i new file mode 100644 index 000000000..0fb66257d --- /dev/null +++ b/op-mode-definitions/include/ospfv3/interface.xml.i @@ -0,0 +1,75 @@ +<!-- included start from ospfv3/interface.xml.i --> +<node name="interface"> + <properties> + <help>Show OSPFv3 interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + <node name="prefix"> + <properties> + <help>Show connected prefixes to advertise</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + </children> + </node> + <tagNode name="prefix"> + <properties> + <help>Show interface prefix route specific information</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + <node name="match"> + <properties> + <help>Matched interface prefix information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + </children> + </tagNode> + </children> +</node> +<tagNode name="interface"> + <properties> + <help>Specific insterface to examine</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + <node name="prefix"> + <properties> + <help>Show connected prefixes to advertise</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + </children> + </node> + <tagNode name="prefix"> + <properties> + <help>Show interface prefix route specific information</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + <node name="match"> + <properties> + <help>Matched interface prefix information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + </children> + </tagNode> + </children> +</tagNode> +<!-- included end --> diff --git a/op-mode-definitions/include/ospfv3/linkstate.xml.i b/op-mode-definitions/include/ospfv3/linkstate.xml.i new file mode 100644 index 000000000..78ef3efa1 --- /dev/null +++ b/op-mode-definitions/include/ospfv3/linkstate.xml.i @@ -0,0 +1,38 @@ +<!-- included start from ospfv3/linkstate.xml.i --> +<node name="linkstate"> + <properties> + <help>Show OSPFv3 linkstate routing information</help> + </properties> + <children> + #include <include/ospfv3/detail.xml.i> + <tagNode name="network"> + <properties> + <help>Show linkstate Network information</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <children> + <node name="node.tag"> + <properties> + <help>Specify Link state ID as IPv4 address notation</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + </children> + </tagNode> + <tagNode name="router"> + <properties> + <help>Show linkstate Router information</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </tagNode> + </children> +</node> +<!-- included end --> diff --git a/op-mode-definitions/include/ospfv3/neighbor.xml.i b/op-mode-definitions/include/ospfv3/neighbor.xml.i new file mode 100644 index 000000000..37859f815 --- /dev/null +++ b/op-mode-definitions/include/ospfv3/neighbor.xml.i @@ -0,0 +1,17 @@ +<!-- included start from ospfv3/neighbor.xml.i --> +<node name="neighbor"> + <properties> + <help>Show OSPFv3 neighbor information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + <node name="drchoice"> + <properties> + <help>Show neighbor DR choice information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + </children> +</node> +<!-- included end --> diff --git a/op-mode-definitions/include/ospfv3/redistribute.xml.i b/op-mode-definitions/include/ospfv3/redistribute.xml.i new file mode 100644 index 000000000..1c2d6494f --- /dev/null +++ b/op-mode-definitions/include/ospfv3/redistribute.xml.i @@ -0,0 +1,8 @@ +<!-- included start from ospfv3/redistribute.xml.i --> +<node name="redistribute"> + <properties> + <help>Show OSPFv3 redistribute external information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</node> +<!-- included end --> diff --git a/op-mode-definitions/include/ospfv3/route.xml.i b/op-mode-definitions/include/ospfv3/route.xml.i new file mode 100644 index 000000000..9271c9c3a --- /dev/null +++ b/op-mode-definitions/include/ospfv3/route.xml.i @@ -0,0 +1,79 @@ +<!-- included start from ospfv3/route.xml.i --> +<node name="route"> + <properties> + <help>Show OSPFv3 routing table information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + <node name="external-1"> + <properties> + <help>Show Type-1 External route information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + </children> + </node> + <node name="external-2"> + <properties> + <help>Show Type-2 External route information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + </children> + </node> + <node name="inter-area"> + <properties> + <help>Show Inter-Area route information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + </children> + </node> + <node name="intra-area"> + <properties> + <help>Show Intra-Area route information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + </children> + </node> + #include <include/ospfv3/detail.xml.i> + <node name="summary"> + <properties> + <help>Show route table summary</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + </children> +</node> +<tagNode name="route"> + <properties> + <help>Show specified route/prefix information</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + <node name="longer"> + <properties> + <help>Show routes longer than specified prefix</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + <node name="match"> + <properties> + <help>Show routes matching specified prefix</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/ospfv3/detail.xml.i> + </children> + </node> + </children> +</tagNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-bgp.xml.i b/op-mode-definitions/include/show-route-bgp.xml.i new file mode 100644 index 000000000..5c26bf43f --- /dev/null +++ b/op-mode-definitions/include/show-route-bgp.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-bgp.xml.i --> +<leafNode name="bgp"> + <properties> + <help>Border Gateway Protocol (BGP)</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-connected.xml.i b/op-mode-definitions/include/show-route-connected.xml.i new file mode 100644 index 000000000..37364de64 --- /dev/null +++ b/op-mode-definitions/include/show-route-connected.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-connected.xml.i --> +<leafNode name="connected"> + <properties> + <help>Connected routes (directly attached subnet or host)</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-isis.xml.i b/op-mode-definitions/include/show-route-isis.xml.i new file mode 100644 index 000000000..9ff2ccdc5 --- /dev/null +++ b/op-mode-definitions/include/show-route-isis.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-isis.xml.i --> +<leafNode name="isis"> + <properties> + <help>Intermediate System to Intermediate System (IS-IS)</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-kernel.xml.i b/op-mode-definitions/include/show-route-kernel.xml.i new file mode 100644 index 000000000..8c5ac414e --- /dev/null +++ b/op-mode-definitions/include/show-route-kernel.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-kernel.xml.i --> +<leafNode name="kernel"> + <properties> + <help>Kernel routes (not installed via the zebra RIB)</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-ospf.xml.i b/op-mode-definitions/include/show-route-ospf.xml.i new file mode 100644 index 000000000..1122aaba5 --- /dev/null +++ b/op-mode-definitions/include/show-route-ospf.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-ospf.xml.i --> +<leafNode name="ospf"> + <properties> + <help>Open Shortest Path First (OSPFv2)</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-ospfv3.xml.i b/op-mode-definitions/include/show-route-ospfv3.xml.i new file mode 100644 index 000000000..c7a11b7ba --- /dev/null +++ b/op-mode-definitions/include/show-route-ospfv3.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-ospfv3.xml.i --> +<leafNode name="ospfv3"> + <properties> + <help>Open Shortest Path First (IPv6) (OSPFv3)</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-rip.xml.i b/op-mode-definitions/include/show-route-rip.xml.i new file mode 100644 index 000000000..3c2fede28 --- /dev/null +++ b/op-mode-definitions/include/show-route-rip.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-rip.xml.i --> +<leafNode name="rip"> + <properties> + <help>Routing Information Protocol (RIP)</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-ripng.xml.i b/op-mode-definitions/include/show-route-ripng.xml.i new file mode 100644 index 000000000..6e59cb054 --- /dev/null +++ b/op-mode-definitions/include/show-route-ripng.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-ripng.xml.i --> +<leafNode name="ripng"> + <properties> + <help>Routing Information Protocol next-generation (IPv6) (RIPng)</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-static.xml.i b/op-mode-definitions/include/show-route-static.xml.i new file mode 100644 index 000000000..c2e396763 --- /dev/null +++ b/op-mode-definitions/include/show-route-static.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-static.xml.i --> +<leafNode name="static"> + <properties> + <help>Statically configured routes</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-summary.xml.i b/op-mode-definitions/include/show-route-summary.xml.i new file mode 100644 index 000000000..471124562 --- /dev/null +++ b/op-mode-definitions/include/show-route-summary.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-summary.xml.i --> +<leafNode name="summary"> + <properties> + <help>Summary of all routes</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-supernets-only.xml.i b/op-mode-definitions/include/show-route-supernets-only.xml.i new file mode 100644 index 000000000..4d1e7c51f --- /dev/null +++ b/op-mode-definitions/include/show-route-supernets-only.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-supernets-only.xml.i --> +<leafNode name="supernets-only"> + <properties> + <help>Show supernet entries only</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-table.xml.i b/op-mode-definitions/include/show-route-table.xml.i new file mode 100644 index 000000000..c3cf82a86 --- /dev/null +++ b/op-mode-definitions/include/show-route-table.xml.i @@ -0,0 +1,17 @@ +<!-- included start from show-route-table.xml.i --> +<node name="table"> + <properties> + <help>Table to display</help> + </properties> +</node> +<tagNode name="table"> + <properties> + <help>The table number to display</help> + <completionHelp> + <list>all</list> + <path>protocols static table</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</tagNode> +<!-- included end --> diff --git a/op-mode-definitions/include/show-route-tag.xml.i b/op-mode-definitions/include/show-route-tag.xml.i new file mode 100644 index 000000000..8bfa0ae4e --- /dev/null +++ b/op-mode-definitions/include/show-route-tag.xml.i @@ -0,0 +1,16 @@ +<!-- included start from show-route-tag.xml.i --> +<node name="tag"> + <properties> + <help>Show only routes with tag</help> + </properties> +</node> +<tagNode name="tag"> + <properties> + <help>Tag value</help> + <completionHelp> + <list><1-4294967295></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</tagNode> +<!-- included end --> diff --git a/op-mode-definitions/openvpn.xml.in b/op-mode-definitions/openvpn.xml.in index 73cbbe501..301688271 100644 --- a/op-mode-definitions/openvpn.xml.in +++ b/op-mode-definitions/openvpn.xml.in @@ -55,6 +55,41 @@ </properties> <command>${vyos_op_scripts_dir}/show_interfaces.py --intf=$4</command> <children> + <tagNode name="user"> + <properties> + <help>Show OpenVPN interface users</help> + <completionHelp> + <script>sudo ${vyos_completion_dir}/list_openvpn_users.py --interface ${COMP_WORDS[3]}</script> + </completionHelp> + </properties> + <children> + <node name="mfa"> + <properties> + <help>Show multi-factor authentication information</help> + </properties> + <children> + <leafNode name="secret"> + <properties> + <help>Show multi-factor authentication secret</help> + </properties> + <command>${vyos_op_scripts_dir}/show_openvpn_mfa.py --user="$6" --intf="$4" --action=secret</command> + </leafNode> + <leafNode name="uri"> + <properties> + <help>Show multi-factor authentication otpauth uri</help> + </properties> + <command>${vyos_op_scripts_dir}/show_openvpn_mfa.py --user="$6" --intf="$4" --action=uri</command> + </leafNode> + <leafNode name="qrcode"> + <properties> + <help>Show multi-factor authentication QR code</help> + </properties> + <command>${vyos_op_scripts_dir}/show_openvpn_mfa.py --user="$6" --intf="$4" --action=qrcode</command> + </leafNode> + </children> + </node> + </children> + </tagNode> <leafNode name="brief"> <properties> <help>Show summary of specified OpenVPN interface information</help> diff --git a/op-mode-definitions/policy-route.xml.in b/op-mode-definitions/policy-route.xml.in new file mode 100644 index 000000000..c998e5487 --- /dev/null +++ b/op-mode-definitions/policy-route.xml.in @@ -0,0 +1,143 @@ +<?xml version="1.0"?> +<interfaceDefinition> +<!-- + <node name="clear"> + <children> + <node name="policy"> + <properties> + <help>Clear policy statistics</help> + </properties> + <children> + <tagNode name="ipv6-route"> + <properties> + <help>Clear policy statistics for chain</help> + <completionHelp> + <path>policy ipv6-route</path> + </completionHelp> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear counters for specified chain</help> + </properties> + <command>echo "TODO"</command> + </leafNode> + <tagNode name="rule"> + <properties> + <help>Clear policy statistics for a rule</help> + <completionHelp> + <path>policy ipv6-route ${COMP_WORDS[4]} rule</path> + </completionHelp> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear counters for specified rule</help> + </properties> + <command>echo "TODO"</command> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="route"> + <properties> + <help>Clear policy statistics for chain</help> + <completionHelp> + <path>policy route</path> + </completionHelp> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear counters for specified chain</help> + </properties> + <command>echo "TODO"</command> + </leafNode> + <tagNode name="rule"> + <properties> + <help>Clear policy statistics for a rule</help> + <completionHelp> + <path>policy route ${COMP_WORDS[4]} rule</path> + </completionHelp> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear counters for specified rule</help> + </properties> + <command>echo "TODO"</command> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +--> + <node name="show"> + <children> + <node name="policy"> + <properties> + <help>Show policy information</help> + </properties> + <children> + <node name="ipv6-route"> + <properties> + <help>Show IPv6 policy chain</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/policy_route.py --action show_all --ipv6</command> + </node> + <tagNode name="ipv6-route"> + <properties> + <help>Show IPv6 policy chains</help> + <completionHelp> + <path>policy ipv6-route</path> + </completionHelp> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Show summary of IPv6 policy rules</help> + <completionHelp> + <path>policy ipv6-route ${COMP_WORDS[4]} rule</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/policy_route.py --action show --name $4 --rule $6 --ipv6</command> + </tagNode> + </children> + <command>sudo ${vyos_op_scripts_dir}/policy_route.py --action show --name $4 --ipv6</command> + </tagNode> + <node name="route"> + <properties> + <help>Show IPv4 policy chain</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/policy_route.py --action show_all</command> + </node> + <tagNode name="route"> + <properties> + <help>Show IPv4 policy chains</help> + <completionHelp> + <path>policy route</path> + </completionHelp> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Show summary of IPv4 policy rules</help> + <completionHelp> + <path>policy route ${COMP_WORDS[4]} rule</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/policy_route.py --action show --name $4 --rule $6</command> + </tagNode> + </children> + <command>sudo ${vyos_op_scripts_dir}/policy_route.py --action show --name $4</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/restart-frr.xml.in b/op-mode-definitions/restart-frr.xml.in index 475bd1ee8..4e2be1bf2 100644 --- a/op-mode-definitions/restart-frr.xml.in +++ b/op-mode-definitions/restart-frr.xml.in @@ -26,6 +26,12 @@ </properties> <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon isisd</command> </leafNode> + <leafNode name="ldp"> + <properties> + <help>Restart the Label Distribution Protocol (LDP) daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ldpd</command> + </leafNode> <leafNode name="ospf"> <properties> <help>Restart Open Shortest Path First (OSPF) routing daemon</help> diff --git a/op-mode-definitions/show-bfd.xml.in b/op-mode-definitions/show-bfd.xml.in new file mode 100644 index 000000000..39e42e6ec --- /dev/null +++ b/op-mode-definitions/show-bfd.xml.in @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="bfd"> + <properties> + <help>Show Bidirectional Forwarding Detection (BFD)</help> + </properties> + <children> + <node name="peer"> + <properties> + <help>Show all Bidirectional Forwarding Detection (BFD) peer status</help> + </properties> + </node> + <tagNode name="peer"> + <properties> + <help>Show Bidirectional Forwarding Detection (BFD) peer status</help> + <completionHelp> + <script>vtysh -c "show bfd peers" | awk '/[:blank:]*peer/ { printf "%s\n", $2 }'</script> + </completionHelp> + </properties> + <command>vtysh -c "show bfd peers" | sed -n "/peer $4 /,/^$/p"</command> + <children> + <leafNode name="counters"> + <properties> + <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help> + </properties> + <command>vtysh -c "show bfd peers counters" | sed -n "/peer $4 /,/^$/p"</command> + </leafNode> + </children> + </tagNode> + <node name="peers"> + <properties> + <help>Show Bidirectional Forwarding Detection peers</help> + </properties> + <command>vtysh -c "show bfd peers"</command> + <children> + <leafNode name="counters"> + <properties> + <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help> + </properties> + <command>vtysh -c "show bfd peers counters"</command> + </leafNode> + <leafNode name="brief"> + <properties> + <help>Show Bidirectional Forwarding Detection (BFD) peers brief</help> + </properties> + <command>vtysh -c "show bfd peers brief"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-configuration.xml.in b/op-mode-definitions/show-configuration.xml.in index 318942ab0..5a2fdedfa 100644 --- a/op-mode-definitions/show-configuration.xml.in +++ b/op-mode-definitions/show-configuration.xml.in @@ -30,6 +30,21 @@ <!-- no admin check --> <command>${vyos_op_scripts_dir}/show_configuration_files.sh</command> </node> + <node name="json"> + <properties> + <help>Show running configuration in JSON format</help> + </properties> + <!-- no admin check --> + <command>${vyos_op_scripts_dir}/show_configuration_json.py</command> + <children> + <node name="pretty"> + <properties> + <help>Show running configuration in readable JSON format</help> + </properties> + <command>${vyos_op_scripts_dir}/show_configuration_json.py --pretty</command> + </node> + </children> + </node> </children> </node> </children> diff --git a/op-mode-definitions/show-interfaces-geneve.xml.in b/op-mode-definitions/show-interfaces-geneve.xml.in new file mode 100644 index 000000000..a47933315 --- /dev/null +++ b/op-mode-definitions/show-interfaces-geneve.xml.in @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="geneve"> + <properties> + <help>Show specified GENEVE interface information</help> + <completionHelp> + <path>interfaces geneve</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified GENEVE interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="geneve"> + <properties> + <help>Show GENEVE interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=geneve --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed GENEVE interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=geneve --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-route.xml.in b/op-mode-definitions/show-ip-route.xml.in index 0a24bc45a..1e906672d 100644 --- a/op-mode-definitions/show-ip-route.xml.in +++ b/op-mode-definitions/show-ip-route.xml.in @@ -13,12 +13,7 @@ </properties> <command>vtysh -c "show ip route"</command> <children> - <leafNode name="bgp"> - <properties> - <help>Show IP BGP routes</help> - </properties> - <command>vtysh -c "show ip route bgp"</command> - </leafNode> + #include <include/show-route-bgp.xml.i> <node name="cache"> <properties> <help>Show kernel route cache</help> @@ -34,12 +29,7 @@ </properties> <command>ip -s route list cache $5</command> </tagNode> - <leafNode name="connected"> - <properties> - <help>Show IP connected routes</help> - </properties> - <command>vtysh -c "show ip route connected"</command> - </leafNode> + #include <include/show-route-connected.xml.i> <node name="forward"> <properties> <help>Show kernel route table</help> @@ -55,90 +45,36 @@ </properties> <command>ip -s route list $5</command> </tagNode> - <leafNode name="isis"> - <properties> - <help>Show IP IS-IS routes</help> - </properties> - <command>vtysh -c "show ip route isis"</command> - </leafNode> - <leafNode name="kernel"> - <properties> - <help>Show IP kernel routes</help> - </properties> - <command>vtysh -c "show ip route kernel"</command> - </leafNode> - <leafNode name="ospf"> - <properties> - <help>Show IP OSPF routes</help> - </properties> - <command>vtysh -c "show ip route ospf"</command> - </leafNode> - <leafNode name="rip"> - <properties> - <help>Show IP RIP routes</help> - </properties> - <command>vtysh -c "show ip route rip"</command> - </leafNode> - <leafNode name="static"> - <properties> - <help>Show IP static routes</help> - </properties> - <command>vtysh -c "show ip route static"</command> - </leafNode> - <leafNode name="summary"> - <properties> - <help>Show IP routes summary</help> - </properties> - <command>vtysh -c "show ip route summary"</command> - </leafNode> - <leafNode name="supernets-only"> - <properties> - <help>Show IP supernet routes</help> - </properties> - <command>vtysh -c "show ip route supernets-only"</command> - </leafNode> - <node name="table"> - <properties> - <help>Show IP routes in policy table</help> - </properties> - </node> - <tagNode name="table"> - <properties> - <help>Show IP routes in policy table</help> - <completionHelp> - <list><1-200></list> - </completionHelp> - </properties> - <command>vtysh -c "show ip route table $5"</command> - </tagNode> - <node name="tag"> - <properties> - <help>Show only routes with tag</help> - </properties> - </node> - <tagNode name="tag"> - <properties> - <help>Tag value</help> - <completionHelp> - <list><1-4294967295></list> - </completionHelp> - </properties> - <command>vtysh -c "show ip route tag $5"</command> - </tagNode> - <node name="vrf"> - <properties> - <help>Show IP routes in VRF</help> - </properties> - </node> + #include <include/show-route-isis.xml.i> + #include <include/show-route-kernel.xml.i> + #include <include/show-route-ospf.xml.i> + #include <include/show-route-rip.xml.i> + #include <include/show-route-static.xml.i> + #include <include/show-route-summary.xml.i> + #include <include/show-route-supernets-only.xml.i> + #include <include/show-route-table.xml.i> + #include <include/show-route-tag.xml.i> <tagNode name="vrf"> <properties> <help>Show IP routes in VRF</help> <completionHelp> - <list><vrf></list> + <list>all</list> <path>vrf name</path> </completionHelp> </properties> - <command>vtysh -c "show ip route vrf $5"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/show-route-bgp.xml.i> + #include <include/show-route-connected.xml.i> + #include <include/show-route-isis.xml.i> + #include <include/show-route-kernel.xml.i> + #include <include/show-route-ospf.xml.i> + #include <include/show-route-rip.xml.i> + #include <include/show-route-static.xml.i> + #include <include/show-route-summary.xml.i> + #include <include/show-route-supernets-only.xml.i> + #include <include/show-route-tag.xml.i> + </children> </tagNode> </children> </node> @@ -149,7 +85,7 @@ <list><x.x.x.x> <x.x.x.x/x></list> </completionHelp> </properties> - <command>vtysh -c "show ip route $4"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> <children> <leafNode name="longer-prefixes"> <properties> diff --git a/op-mode-definitions/show-ipv6-ospfv3.xml.in b/op-mode-definitions/show-ipv6-ospfv3.xml.in index e6c8a6700..a63465472 100644 --- a/op-mode-definitions/show-ipv6-ospfv3.xml.in +++ b/op-mode-definitions/show-ipv6-ospfv3.xml.in @@ -11,7 +11,7 @@ <properties> <help>Show IPv6 Open Shortest Path First (OSPF)</help> </properties> - <command>vtysh -c "show ipv6 ospf6"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> <children> <node name="area"> <properties> @@ -39,467 +39,74 @@ </tagNode> </children> </tagNode> - <node name="border-routers"> + #include <include/ospfv3/border-routers.xml.i> + #include <include/ospfv3/database.xml.i> + #include <include/ospfv3/interface.xml.i> + #include <include/ospfv3/linkstate.xml.i> + #include <include/ospfv3/neighbor.xml.i> + #include <include/ospfv3/redistribute.xml.i> + #include <include/ospfv3/route.xml.i> + <node name="vrf"> <properties> - <help>Show OSPFv3 border-router (ABR and ASBR) information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/detail.xml.i> - </children> - </node> - <tagNode name="border-routers"> - <properties> - <help>Border router ID</help> + <help>Specify the VRF</help> <completionHelp> - <list><x.x.x.x></list> + <list>all</list> + <path>vrf name</path> </completionHelp> </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - </tagNode> - <node name="database"> - <properties> - <help>Show OSPFv3 Link state database information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - <tagNode name="adv-router"> - <properties> - <help>Search by Advertising Router ID</help> - <completionHelp> - <list><x.x.x.x></list> - </completionHelp> - </properties> - <children> - #include <include/ospfv3/linkstate-id.xml.i> - </children> - </tagNode> - <node name="any"> - <properties> - <help>Search by Any Link state Type</help> - </properties> - <children> - <tagNode name="any"> - <properties> - <help>Search by Link state ID</help> - <completionHelp> - <list><x.x.x.x></list> - </completionHelp> - </properties> - <children> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - </children> - </tagNode> - </children> - </node> - <tagNode name="any"> - <properties> - <help>Search by Link state ID</help> - <completionHelp> - <list><x.x.x.x></list> - </completionHelp> - </properties> - <command>vtysh -c "show ipv6 ospf6 database * $6"</command> - <children> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/adv-router-id-node-tag.xml.i> - </children> - </tagNode> - <node name="as-external"> - <properties> - <help>Show AS-External LSAs</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/adv-router.xml.i> - <tagNode name="any"> - <properties> - <help>Search by Advertising Router ID</help> - <completionHelp> - <list><x.x.x.x></list> - </completionHelp> - </properties> - <command>vtysh -c "show ipv6 ospf6 database as-external * $7"</command> - <children> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - </children> - </tagNode> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/self-originated.xml.i> - </children> - </node> - <tagNode name="as-external"> - <properties> - <help>Search by Advertising Router IDs</help> - <completionHelp> - <list><x.x.x.x></list> - </completionHelp> - </properties> - <children> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/self-originated.xml.i> - #include <include/ospfv3/adv-router-id-node-tag.xml.i> - </children> - </tagNode> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/self-originated.xml.i> - <node name="group-membership"> - <properties> - <help>Show Group-Membership LSAs</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/adv-router.xml.i> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/linkstate-id-node-tag.xml.i> - #include <include/ospfv3/self-originated.xml.i> - </children> - </node> - <node name="inter-prefix"> - <properties> - <help>Show Inter-Area-Prefix LSAs</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/adv-router.xml.i> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/linkstate-id-node-tag.xml.i> - #include <include/ospfv3/self-originated.xml.i> - </children> - </node> - <node name="inter-router"> - <properties> - <help>Show Inter-Area-Router LSAs</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/adv-router.xml.i> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/linkstate-id-node-tag.xml.i> - #include <include/ospfv3/self-originated.xml.i> - </children> - </node> - <node name="intra-prefix"> - <properties> - <help>Show Intra-Area-Prefix LSAs</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/adv-router.xml.i> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/linkstate-id-node-tag.xml.i> - #include <include/ospfv3/self-originated.xml.i> - </children> - </node> - <node name="link"> - <properties> - <help>Show Link LSAs</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/adv-router.xml.i> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/linkstate-id-node-tag.xml.i> - #include <include/ospfv3/self-originated.xml.i> - </children> - </node> - <node name="network"> - <properties> - <help>Show Network LSAs</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/adv-router.xml.i> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/linkstate-id-node-tag.xml.i> - #include <include/ospfv3/self-originated.xml.i> - </children> - </node> - <node name="node.tag"> - <properties> - <help>Show LSAs</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/adv-router.xml.i> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/linkstate-id-node-tag.xml.i> - #include <include/ospfv3/self-originated.xml.i> - </children> - </node> - <node name="router"> - <properties> - <help>Show router LSAs</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/adv-router.xml.i> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/linkstate-id-node-tag.xml.i> - #include <include/ospfv3/self-originated.xml.i> - </children> - </node> - <node name="type-7"> - <properties> - <help>Show Type-7 LSAs</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/adv-router.xml.i> - #include <include/ospfv3/detail.xml.i> - #include <include/ospfv3/dump.xml.i> - #include <include/ospfv3/internal.xml.i> - #include <include/ospfv3/linkstate-id.xml.i> - #include <include/ospfv3/linkstate-id-node-tag.xml.i> - #include <include/ospfv3/self-originated.xml.i> - </children> - </node> - </children> </node> - <node name="interface"> + <tagNode name="vrf"> <properties> - <help>Show OSPFv3 interface information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - <node name="prefix"> - <properties> - <help>Show connected prefixes to advertise</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/detail.xml.i> - </children> - </node> - <tagNode name="prefix"> - <properties> - <help>Show interface prefix route specific information</help> - <completionHelp> - <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> - </completionHelp> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/detail.xml.i> - <node name="match"> - <properties> - <help>Matched interface prefix information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - </node> - </children> - </tagNode> - </children> - </node> - <tagNode name="interface"> - <properties> - <help>Specific insterface to examine</help> + <help>VRF name</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> + <list>all</list> + <path>vrf name</path> </completionHelp> </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> <children> - <node name="prefix"> + <node name="area"> <properties> - <help>Show connected prefixes to advertise</help> + <help>Show Shortest Path First tree information</help> </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/detail.xml.i> - </children> + <command>vtysh -c "show ipv6 ospf6 vrf $5 spf tree"</command> </node> - <tagNode name="prefix"> + <tagNode name="area"> <properties> - <help>Show interface prefix route specific information</help> + <help>Area ID (as an IPv4 notation)</help> <completionHelp> - <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + <path>protocols ospfv3 area</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <command>vtysh -c "show ipv6 ospf6 vrf $5 area $7 spf tree"</command> <children> - #include <include/ospfv3/detail.xml.i> - <node name="match"> + <tagNode name="router"> <properties> - <help>Matched interface prefix information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - </node> - </children> - </tagNode> - </children> - </tagNode> - <node name="linkstate"> - <properties> - <help>Show OSPFv3 linkstate routing information</help> - </properties> - <children> - #include <include/ospfv3/detail.xml.i> - <tagNode name="network"> - <properties> - <help>Show linkstate Network information</help> - <completionHelp> - <list><x.x.x.x></list> - </completionHelp> - </properties> - <children> - <node name="node.tag"> - <properties> - <help>Specify Link state ID as IPv4 address notation</help> + <help> Simulate view point (Router ID)</help> <completionHelp> <list><x.x.x.x></list> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - </node> + <command>vtysh -c "show ipv6 ospf6 vrf $5 simulate spf-tree $9 $6 $7"</command> + </tagNode> </children> </tagNode> - <tagNode name="router"> - <properties> - <help>Show linkstate Router information</help> - <completionHelp> - <list><x.x.x.x></list> - </completionHelp> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - </tagNode> - </children> - </node> - <node name="neighbor"> - <properties> - <help>Show OSPFv3 neighbor information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/detail.xml.i> - <node name="drchoice"> - <properties> - <help>Show neighbor DR choice information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - </node> + #include <include/ospfv3/border-routers.xml.i> + #include <include/ospfv3/database.xml.i> + #include <include/ospfv3/interface.xml.i> + #include <include/ospfv3/linkstate.xml.i> + #include <include/ospfv3/neighbor.xml.i> + #include <include/ospfv3/redistribute.xml.i> + #include <include/ospfv3/route.xml.i> </children> - </node> - <node name="redistribute"> - <properties> - <help>Show OSPFv3 redistribute external information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - </node> - <node name="route"> - <properties> - <help>Show OSPFv3 routing table information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - <node name="external-1"> - <properties> - <help>Show Type-1 External route information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/detail.xml.i> - </children> - </node> - <node name="external-2"> - <properties> - <help>Show Type-2 External route information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/detail.xml.i> - </children> - </node> - <node name="inter-area"> - <properties> - <help>Show Inter-Area route information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/detail.xml.i> - </children> - </node> - <node name="intra-area"> - <properties> - <help>Show Intra-Area route information</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/detail.xml.i> - </children> - </node> - #include <include/ospfv3/detail.xml.i> - <node name="summary"> - <properties> - <help>Show route table summary</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - </node> - </children> - </node> - <tagNode name="route"> + </tagNode> + <leafNode name="vrfs"> <properties> - <help>Show specified route/prefix information</help> - <completionHelp> - <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> - </completionHelp> + <help>Show OSPFv3 VRFs</help> </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - <node name="longer"> - <properties> - <help>Show routes longer than specified prefix</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - </node> - <node name="match"> - <properties> - <help>Show routes matching specified prefix</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> - <children> - #include <include/ospfv3/detail.xml.i> - </children> - </node> - </children> - </tagNode> + </leafNode> </children> </node> </children> diff --git a/op-mode-definitions/show-ipv6-route.xml.in b/op-mode-definitions/show-ipv6-route.xml.in index 8624574ac..2c5024991 100644 --- a/op-mode-definitions/show-ipv6-route.xml.in +++ b/op-mode-definitions/show-ipv6-route.xml.in @@ -13,12 +13,7 @@ </properties> <command>vtysh -c "show ipv6 route"</command> <children> - <node name="bgp"> - <properties> - <help>Show IPv6 BGP routes</help> - </properties> - <command>vtysh -c "show ipv6 route bgp"</command> - </node> + #include <include/show-route-bgp.xml.i> <node name="cache"> <properties> <help>Show kernel IPv6 route cache</help> @@ -34,12 +29,7 @@ </properties> <command>ip -s -f inet6 route list cache $5</command> </tagNode> - <node name="connected"> - <properties> - <help>Show IPv6 connected routes</help> - </properties> - <command>vtysh -c "show ipv6 route connected"</command> - </node> + #include <include/show-route-connected.xml.i> <node name="forward"> <properties> <help>Show kernel IPv6 route table</help> @@ -55,71 +45,36 @@ </properties> <command>ip -s -f inet6 route list $5</command> </tagNode> - <node name="isis"> - <properties> - <help>Show IPv6 IS-IS routes</help> - </properties> - <command>vtysh -c "show ipv6 route isis"</command> - </node> - <node name="kernel"> - <properties> - <help>Show IPv6 Kernel routes</help> - </properties> - <command>vtysh -c "show ipv6 route kernel"</command> - </node> - <node name="ospfv3"> - <properties> - <help>Show IPv6 OSPF routes</help> - </properties> - <command>vtysh -c "show ipv6 route ospf6"</command> - </node> - <node name="ripng"> - <properties> - <help>Show IPv6 RIPNG routes</help> - </properties> - <command>vtysh -c "show ipv6 route ripng"</command> - </node> - <node name="static"> - <properties> - <help>Show IPv6 static routes</help> - </properties> - <command>vtysh -c "show ipv6 route static"</command> - </node> - <node name="summary"> - <properties> - <help>Show IPv6 routes summary</help> - </properties> - <command>vtysh -c "show ipv6 route summary"</command> - </node> - <node name="table"> - <properties> - <help>Show IPv6 routes in policy tables</help> - </properties> - <command>vtysh -c "show ipv6 route table all"</command> - </node> - <tagNode name="table"> - <properties> - <help>Show IPv6 routes in specific policy table</help> - <completionHelp> - <path>protocols static table</path> - </completionHelp> - </properties> - <command>vtysh -c "show ipv6 route table $5"</command> - </tagNode> - <node name="vrf"> - <properties> - <help>Show IPv6 routes in VRFs</help> - </properties> - <command>vtysh -c "show ipv6 route vrf all"</command> - </node> + #include <include/show-route-isis.xml.i> + #include <include/show-route-kernel.xml.i> + #include <include/show-route-ospfv3.xml.i> + #include <include/show-route-ripng.xml.i> + #include <include/show-route-static.xml.i> + #include <include/show-route-summary.xml.i> + #include <include/show-route-table.xml.i> + #include <include/show-route-tag.xml.i> <tagNode name="vrf"> <properties> - <help>Show IPv6 routes in specific VRF</help> + <help>Show IPv6 routes in VRF</help> <completionHelp> + <list>all</list> <path>vrf name</path> </completionHelp> </properties> - <command>vtysh -c "show ipv6 route vrf $5"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/show-route-bgp.xml.i> + #include <include/show-route-connected.xml.i> + #include <include/show-route-isis.xml.i> + #include <include/show-route-kernel.xml.i> + #include <include/show-route-ospfv3.xml.i> + #include <include/show-route-ripng.xml.i> + #include <include/show-route-static.xml.i> + #include <include/show-route-summary.xml.i> + #include <include/show-route-supernets-only.xml.i> + #include <include/show-route-table.xml.i> + #include <include/show-route-tag.xml.i> + </children> </tagNode> </children> </node> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 92c1cf016..4c0a7913b 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -6,7 +6,7 @@ <properties> <help>Show contents of current master log file</help> </properties> - <command>/bin/journalctl</command> + <command>journalctl --no-hostname --boot</command> <children> <leafNode name="all"> <properties> @@ -18,7 +18,7 @@ <properties> <help>Show listing of authorization attempts</help> </properties> - <command>/bin/journalctl --quiet SYSLOG_FACILITY=10 SYSLOG_FACILITY=4</command> + <command>journalctl --no-hostname --boot --quiet SYSLOG_FACILITY=10 SYSLOG_FACILITY=4</command> </leafNode> <leafNode name="cluster"> <properties> @@ -30,14 +30,68 @@ <properties> <help>Show log for Conntrack-sync</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr ) | grep -e conntrackd</command> + <command>journalctl --no-hostname --boot --unit conntrackd.service</command> </leafNode> - <leafNode name="dhcp"> + <node name="dhcp"> <properties> <help>Show log for Dynamic Host Control Protocol (DHCP)</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep dhcpd</command> - </leafNode> + <children> + <node name="server"> + <properties> + <help>Show log for DHCP server</help> + </properties> + <command>journalctl --no-hostname --boot --unit isc-dhcp-server.service</command> + </node> + <node name="client"> + <properties> + <help>Show DHCP client logs</help> + </properties> + <command>journalctl --no-hostname --boot --unit "dhclient@*.service"</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show DHCP client log on specific interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + </properties> + <command>journalctl --no-hostname --boot --unit "dhclient@$6.service"</command> + </tagNode> + </children> + </node> + </children> + </node> + <node name="dhcpv6"> + <properties> + <help>Show log for Dynamic Host Control Protocol IPv6 (DHCPv6)</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Show log for DHCPv6 server</help> + </properties> + <command>journalctl --no-hostname --boot --unit isc-dhcp-server6.service</command> + </node> + <node name="client"> + <properties> + <help>Show DHCPv6 client logs</help> + </properties> + <command>journalctl --no-hostname --boot --unit "dhcp6c@*.service"</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show DHCPv6 client log on specific interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>journalctl --no-hostname --boot --unit "dhcp6c@$6.service"</command> + </tagNode> + </children> + </node> + </children> + </node> <node name="firewall"> <properties> <help>Show log for Firewall</help> @@ -89,7 +143,7 @@ <properties> <help>Show log for HTTPs</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e nginx</command> + <command>journalctl --no-hostname --boot --unit nginx.service</command> </leafNode> <tagNode name="image"> <properties> @@ -119,7 +173,7 @@ <list><NUMBER></list> </completionHelp> </properties> - <command>tail -n "$6" /lib/live/mount/persistence/boot/$4/rw/var/log/messages | ${VYATTA_PAGER:-cat}</command> + <command>tail -n "$6" /lib/live/mount/persistence/boot/$4/rw/var/log/messages | ${VYATTA_PAGER:-cat}</command> </tagNode> </children> </tagNode> @@ -133,7 +187,7 @@ <properties> <help>Show log for LLDP</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e lldpd</command> + <command>journalctl --no-hostname --boot --unit lldpd.service</command> </leafNode> <leafNode name="nat"> <properties> @@ -141,17 +195,28 @@ </properties> <command>egrep -i "kernel:.*\[NAT-[A-Z]{3,}-[0-9]+(-MASQ)?\]" $(find /var/log -maxdepth 1 -type f -name messages\* | sort -t. -k2nr)</command> </leafNode> - <leafNode name="openvpn"> + <node name="openvpn"> <properties> <help>Show log for OpenVPN</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e openvpn</command> - </leafNode> + <command>journalctl --no-hostname --boot --unit openvpn@*.service</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show OpenVPN log on specific interface</help> + <completionHelp> + <path>interfaces openvpn</path> + </completionHelp> + </properties> + <command>journalctl --no-hostname --boot --unit openvpn@$5.service</command> + </tagNode> + </children> + </node> <leafNode name="snmp"> <properties> <help>Show log for Simple Network Monitoring Protocol (SNMP)</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e snmpd</command> + <command>journalctl --no-hostname --boot --unit snmpd.service</command> </leafNode> <tagNode name="tail"> <properties> @@ -195,13 +260,13 @@ <properties> <help>Show log for PPTP</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e accel-pptp -e ppp</command> + <command>journalctl --no-hostname --boot --unit accel-ppp@pptp.service</command> </leafNode> <leafNode name="sstp"> <properties> <help>Show log for SSTP</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e accel-sstp -e ppp</command> + <command>journalctl --no-hostname --boot --unit accel-ppp@sstp.service</command> </leafNode> </children> </node> @@ -209,13 +274,13 @@ <properties> <help>Show log for Virtual Router Redundancy Protocol (VRRP)</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e Keepalived_vrrp</command> + <command>journalctl --no-hostname --boot --unit keepalived.service</command> </leafNode> <leafNode name="webproxy"> <properties> <help>Show log for Webproxy</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "squid"</command> + <command>journalctl --no-hostname --boot --unit squid.service</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-netns.xml.in b/op-mode-definitions/show-netns.xml.in new file mode 100644 index 000000000..8d5072d4e --- /dev/null +++ b/op-mode-definitions/show-netns.xml.in @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="netns"> + <properties> + <help>Show network namespace information</help> + </properties> + <command>ip netns ls</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-protocols.xml.in b/op-mode-definitions/show-protocols.xml.in index d595e2c3c..698001b76 100644 --- a/op-mode-definitions/show-protocols.xml.in +++ b/op-mode-definitions/show-protocols.xml.in @@ -7,50 +7,6 @@ <help>Show protocol specific information</help> </properties> <children> - <node name="bfd"> - <properties> - <help>Show Bidirectional Forwarding Detection (BFD)</help> - </properties> - <children> - <node name="peer"> - <properties> - <help>Show all Bidirectional Forwarding Detection (BFD) peer status</help> - </properties> - <command>vtysh -c "show bfd peers"</command> - <children> - <leafNode name="counters"> - <properties> - <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help> - </properties> - <command>vtysh -c "show bfd peers counters"</command> - </leafNode> - </children> - </node> - <tagNode name="peer"> - <properties> - <help>Show Bidirectional Forwarding Detection (BFD) peer status</help> - <completionHelp> - <script>vtysh -c "show bfd peers" | awk '/[:blank:]*peer/ { printf "%s\n", $2 }'</script> - </completionHelp> - </properties> - <command>vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 'BEGIN { regex = sprintf("(peer %s.*)vrf", BFD_PEER) } { if (match($0, regex, bfd_peer_value)) peer=bfd_peer_value[1] } END { if (peer) system("vtysh -c \"show bfd " peer "\"") }'</command> - <children> - <leafNode name="counters"> - <properties> - <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help> - </properties> - <command>vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 'BEGIN { regex = sprintf("(peer %s.*)vrf", BFD_PEER) } { if (match($0, regex, bfd_peer_value)) peer=bfd_peer_value[1] } END { if (peer) system("vtysh -c \"show bfd " peer " counters\"") }'</command> - </leafNode> - </children> - </tagNode> - <leafNode name="peers"> - <properties> - <help>Show Bidirectional Forwarding Detection (BFD) peers brief</help> - </properties> - <command>vtysh -c "show bfd peers brief"</command> - </leafNode> - </children> - </node> <node name="static"> <properties> <help>Show static protocol parameters</help> diff --git a/op-mode-definitions/show-system.xml.in b/op-mode-definitions/show-system.xml.in index 18a28868d..0f852164e 100644 --- a/op-mode-definitions/show-system.xml.in +++ b/op-mode-definitions/show-system.xml.in @@ -104,7 +104,7 @@ <properties> <help>Show system memory usage</help> </properties> - <command>${vyos_op_scripts_dir}/show_ram.sh</command> + <command>${vyos_op_scripts_dir}/show_ram.py</command> <children> <leafNode name="cache"> <properties> @@ -142,7 +142,7 @@ <properties> <help>Show summary of system processes</help> </properties> - <command>uptime</command> + <command>${vyos_op_scripts_dir}/show_uptime.py</command> </leafNode> <leafNode name="tree"> <properties> diff --git a/op-mode-definitions/zone-policy.xml.in b/op-mode-definitions/zone-policy.xml.in new file mode 100644 index 000000000..c4b02bcee --- /dev/null +++ b/op-mode-definitions/zone-policy.xml.in @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="zone-policy"> + <properties> + <help>Show zone policy information</help> + </properties> + <children> + <tagNode name="zone"> + <properties> + <help>Show summary of zone policy for a specific zone</help> + <completionHelp> + <path>zone-policy zone</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/zone_policy.py --action show --name $4</command> + </tagNode> + </children> + <command>sudo ${vyos_op_scripts_dir}/zone_policy.py --action show</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/base.py b/python/vyos/base.py index 4e23714e5..c78045548 100644 --- a/python/vyos/base.py +++ b/python/vyos/base.py @@ -1,4 +1,4 @@ -# Copyright 2018 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2018-2021 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,6 +13,11 @@ # 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 textwrap import fill class ConfigError(Exception): - pass + def __init__(self, message): + # Reformat the message and trim it to 72 characters in length + message = fill(message, width=72) + # Call the base class constructor with the parameters it needs + super().__init__(message) diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 5c6836e97..d974a7565 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -155,18 +155,15 @@ def get_removed_vlans(conf, dict): D.set_level(conf.get_level()) # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys() - if keys: - dict.update({'vif_remove': [*keys]}) + if keys: dict['vif_remove'] = [*keys] # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys() - if keys: - dict.update({'vif_s_remove': [*keys]}) + if keys: dict['vif_s_remove'] = [*keys] for vif in dict.get('vif_s', {}).keys(): keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys() - if keys: - dict.update({'vif_s': { vif : {'vif_c_remove': [*keys]}}}) + if keys: dict['vif_s'][vif]['vif_c_remove'] = [*keys] return dict @@ -319,6 +316,40 @@ def is_source_interface(conf, interface, intftype=None): old_level = conf.set_level(old_level) return ret_val +def get_dhcp_interfaces(conf, vrf=None): + """ Common helper functions to retrieve all interfaces from current CLI + sessions that have DHCP configured. """ + dhcp_interfaces = [] + dict = conf.get_config_dict(['interfaces'], get_first_key=True) + if not dict: + return dhcp_interfaces + + def check_dhcp(config, ifname): + out = [] + if 'address' in config and 'dhcp' in config['address']: + if 'vrf' in config: + if vrf is config['vrf']: out.append(ifname) + else: out.append(ifname) + return out + + for section, interface in dict.items(): + for ifname, ifconfig in interface.items(): + tmp = check_dhcp(ifconfig, ifname) + dhcp_interfaces.extend(tmp) + # check per VLAN interfaces + for vif, vif_config in ifconfig.get('vif', {}).items(): + tmp = check_dhcp(vif_config, f'{ifname}.{vif}') + dhcp_interfaces.extend(tmp) + # check QinQ VLAN interfaces + for vif_s, vif_s_config in ifconfig.get('vif-s', {}).items(): + tmp = check_dhcp(vif_s_config, f'{ifname}.{vif_s}') + dhcp_interfaces.extend(tmp) + for vif_c, vif_c_config in vif_s_config.get('vif-c', {}).items(): + tmp = check_dhcp(vif_c_config, f'{ifname}.{vif_s}.{vif_c}') + dhcp_interfaces.extend(tmp) + + return dhcp_interfaces + def get_interface_dict(config, base, ifname=''): """ Common utility function to retrieve and mangle the interfaces configuration @@ -368,9 +399,11 @@ def get_interface_dict(config, base, ifname=''): del default_values['dhcpv6_options'] # We have gathered the dict representation of the CLI, but there are - # default options which we need to update into the dictionary - # retrived. - dict = dict_merge(default_values, dict) + # default options which we need to update into the dictionary retrived. + # But we should only add them when interface is not deleted - as this might + # confuse parsers + if 'deleted' not in dict: + dict = dict_merge(default_values, dict) # XXX: T2665: blend in proper DHCPv6-PD default values dict = T2665_set_dhcpv6pd_defaults(dict) @@ -423,9 +456,15 @@ def get_interface_dict(config, base, ifname=''): if not 'dhcpv6_options' in vif_config: del default_vif_values['dhcpv6_options'] - dict['vif'][vif] = dict_merge(default_vif_values, vif_config) - # XXX: T2665: blend in proper DHCPv6-PD default values - dict['vif'][vif] = T2665_set_dhcpv6pd_defaults(dict['vif'][vif]) + # Only add defaults if interface is not about to be deleted - this is + # to keep a cleaner config dict. + if 'deleted' not in dict: + address = leaf_node_changed(config, ['vif', vif, 'address']) + if address: dict['vif'][vif].update({'address_old' : address}) + + dict['vif'][vif] = dict_merge(default_vif_values, dict['vif'][vif]) + # XXX: T2665: blend in proper DHCPv6-PD default values + dict['vif'][vif] = T2665_set_dhcpv6pd_defaults(dict['vif'][vif]) # Check if we are a member of a bridge device bridge = is_member(config, f'{ifname}.{vif}', 'bridge') @@ -441,10 +480,16 @@ def get_interface_dict(config, base, ifname=''): if not 'dhcpv6_options' in vif_s_config: del default_vif_s_values['dhcpv6_options'] - dict['vif_s'][vif_s] = dict_merge(default_vif_s_values, vif_s_config) - # XXX: T2665: blend in proper DHCPv6-PD default values - dict['vif_s'][vif_s] = T2665_set_dhcpv6pd_defaults( - dict['vif_s'][vif_s]) + # Only add defaults if interface is not about to be deleted - this is + # to keep a cleaner config dict. + if 'deleted' not in dict: + address = leaf_node_changed(config, ['vif-s', vif_s, 'address']) + if address: dict['vif_s'][vif_s].update({'address_old' : address}) + + dict['vif_s'][vif_s] = dict_merge(default_vif_s_values, + dict['vif_s'][vif_s]) + # XXX: T2665: blend in proper DHCPv6-PD default values + dict['vif_s'][vif_s] = T2665_set_dhcpv6pd_defaults(dict['vif_s'][vif_s]) # Check if we are a member of a bridge device bridge = is_member(config, f'{ifname}.{vif_s}', 'bridge') @@ -458,11 +503,18 @@ def get_interface_dict(config, base, ifname=''): if not 'dhcpv6_options' in vif_c_config: del default_vif_c_values['dhcpv6_options'] - dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_merge( - default_vif_c_values, vif_c_config) - # XXX: T2665: blend in proper DHCPv6-PD default values - dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_set_dhcpv6pd_defaults( - dict['vif_s'][vif_s]['vif_c'][vif_c]) + # Only add defaults if interface is not about to be deleted - this is + # to keep a cleaner config dict. + if 'deleted' not in dict: + address = leaf_node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c, 'address']) + if address: dict['vif_s'][vif_s]['vif_c'][vif_c].update( + {'address_old' : address}) + + dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_merge( + default_vif_c_values, dict['vif_s'][vif_s]['vif_c'][vif_c]) + # XXX: T2665: blend in proper DHCPv6-PD default values + dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_set_dhcpv6pd_defaults( + dict['vif_s'][vif_s]['vif_c'][vif_c]) # Check if we are a member of a bridge device bridge = is_member(config, f'{ifname}.{vif_s}.{vif_c}', 'bridge') @@ -522,6 +574,11 @@ def get_accel_dict(config, base, chap_secrets): if dict_search('authentication.local_users.username', default_values): del default_values['authentication']['local_users']['username'] + # T2665: defaults include IPv6 client-pool mask per TAG node which need to be + # added to individual local users instead - so we can simply delete them + if dict_search('client_ipv6_pool.prefix.mask', default_values): + del default_values['client_ipv6_pool']['prefix']['mask'] + dict = dict_merge(default_values, dict) # set CPUs cores to process requests @@ -565,4 +622,13 @@ def get_accel_dict(config, base, chap_secrets): dict['authentication']['local_users']['username'][username] = dict_merge( default_values, dict['authentication']['local_users']['username'][username]) + # Add individual IPv6 client-pool default mask if required + if dict_search('client_ipv6_pool.prefix', dict): + # T2665 + default_values = defaults(base + ['client-ipv6-pool', 'prefix']) + + for prefix in dict_search('client_ipv6_pool.prefix', dict): + dict['client_ipv6_pool']['prefix'][prefix] = dict_merge( + default_values, dict['client_ipv6_pool']['prefix'][prefix]) + return dict diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py index 0e41fbe27..4ad7443d7 100644 --- a/python/vyos/configdiff.py +++ b/python/vyos/configdiff.py @@ -17,7 +17,9 @@ from enum import IntFlag, auto from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configdict import list_diff from vyos.util import get_sub_dict, mangle_dict_keys +from vyos.util import dict_search_args from vyos.xml import defaults class ConfigDiffError(Exception): @@ -134,6 +136,34 @@ class ConfigDiff(object): self._key_mangling[1]) return config_dict + def get_child_nodes_diff_str(self, path=[]): + ret = {'add': {}, 'change': {}, 'delete': {}} + + diff = self.get_child_nodes_diff(path, + expand_nodes=Diff.ADD | Diff.DELETE | Diff.MERGE | Diff.STABLE, + no_defaults=True) + + def parse_dict(diff_dict, diff_type, prefix=[]): + for k, v in diff_dict.items(): + if isinstance(v, dict): + parse_dict(v, diff_type, prefix + [k]) + else: + path_str = ' '.join(prefix + [k]) + if diff_type == 'add' or diff_type == 'delete': + if isinstance(v, list): + v = ', '.join(v) + ret[diff_type][path_str] = v + elif diff_type == 'merge': + old_value = dict_search_args(diff['stable'], *prefix, k) + if old_value and old_value != v: + ret['change'][path_str] = [old_value, v] + + parse_dict(diff['merge'], 'merge') + parse_dict(diff['add'], 'add') + parse_dict(diff['delete'], 'delete') + + return ret + def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): """ Args: diff --git a/python/vyos/configquery.py b/python/vyos/configquery.py index 1cdcbcf39..5b097b312 100644 --- a/python/vyos/configquery.py +++ b/python/vyos/configquery.py @@ -18,16 +18,15 @@ A small library that allows querying existence or value(s) of config settings from op mode, and execution of arbitrary op mode commands. ''' -import re -import json -from copy import deepcopy +import os from subprocess import STDOUT -import vyos.util -import vyos.xml +from vyos.util import popen, boot_configuration_complete from vyos.config import Config -from vyos.configtree import ConfigTree -from vyos.configsource import ConfigSourceSession +from vyos.configsource import ConfigSourceSession, ConfigSourceString +from vyos.defaults import directories + +config_file = os.path.join(directories['config'], 'config.boot') class ConfigQueryError(Exception): pass @@ -58,21 +57,21 @@ class CliShellApiConfigQuery(GenericConfigQuery): def exists(self, path: list): cmd = ' '.join(path) - (_, err) = vyos.util.popen(f'cli-shell-api existsActive {cmd}') + (_, err) = popen(f'cli-shell-api existsActive {cmd}') if err: return False return True def value(self, path: list): cmd = ' '.join(path) - (out, err) = vyos.util.popen(f'cli-shell-api returnActiveValue {cmd}') + (out, err) = popen(f'cli-shell-api returnActiveValue {cmd}') if err: raise ConfigQueryError('No value for given path') return out def values(self, path: list): cmd = ' '.join(path) - (out, err) = vyos.util.popen(f'cli-shell-api returnActiveValues {cmd}') + (out, err) = popen(f'cli-shell-api returnActiveValues {cmd}') if err: raise ConfigQueryError('No values for given path') return out @@ -81,25 +80,36 @@ class ConfigTreeQuery(GenericConfigQuery): def __init__(self): super().__init__() - config_source = ConfigSourceSession() - self.configtree = Config(config_source=config_source) + if boot_configuration_complete(): + config_source = ConfigSourceSession() + self.config = Config(config_source=config_source) + else: + try: + with open(config_file) as f: + config_string = f.read() + except OSError as err: + raise ConfigQueryError('No config file available') from err + + config_source = ConfigSourceString(running_config_text=config_string, + session_config_text=config_string) + self.config = Config(config_source=config_source) def exists(self, path: list): - return self.configtree.exists(path) + return self.config.exists(path) def value(self, path: list): - return self.configtree.return_value(path) + return self.config.return_value(path) def values(self, path: list): - return self.configtree.return_values(path) + return self.config.return_values(path) def list_nodes(self, path: list): - return self.configtree.list_nodes(path) + return self.config.list_nodes(path) def get_config_dict(self, path=[], effective=False, key_mangling=None, get_first_key=False, no_multi_convert=False, no_tag_node_value_mangle=False): - return self.configtree.get_config_dict(path, effective=effective, + return self.config.get_config_dict(path, effective=effective, key_mangling=key_mangling, get_first_key=get_first_key, no_multi_convert=no_multi_convert, no_tag_node_value_mangle=no_tag_node_value_mangle) @@ -110,7 +120,7 @@ class VbashOpRun(GenericOpRun): def run(self, path: list, **kwargs): cmd = ' '.join(path) - (out, err) = vyos.util.popen(f'. /opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run; _vyatta_op_run {cmd}', stderr=STDOUT, **kwargs) + (out, err) = popen(f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}', stderr=STDOUT, **kwargs) if err: raise ConfigQueryError(out) return out diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py index b0981d25e..a0f6a46b5 100644 --- a/python/vyos/configsource.py +++ b/python/vyos/configsource.py @@ -19,6 +19,7 @@ import re import subprocess from vyos.configtree import ConfigTree +from vyos.util import boot_configuration_complete class VyOSError(Exception): """ @@ -117,7 +118,7 @@ class ConfigSourceSession(ConfigSource): # Running config can be obtained either from op or conf mode, it always succeeds # once the config system is initialized during boot; # before initialization, set to empty string - if os.path.isfile('/tmp/vyos-config-status'): + if boot_configuration_complete(): try: running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) except VyOSError: diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 8aca76568..365a28feb 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -110,15 +110,12 @@ def verify_tunnel(config): raise ConfigError('Must configure the tunnel encapsulation for '\ '{ifname}!'.format(**config)) - if 'source_address' not in config and 'dhcp_interface' not in config: - raise ConfigError('source-address is mandatory for tunnel') + if 'source_address' not in config and 'source_interface' not in config: + raise ConfigError('source-address or source-interface required for tunnel!') if 'remote' not in config and config['encapsulation'] != 'gre': raise ConfigError('remote ip address is mandatory for tunnel') - if {'source_address', 'dhcp_interface'} <= set(config): - raise ConfigError('Can not use both source-address and dhcp-interface') - if config['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre', 'ip6gretap', 'ip6erspan']: error_ipv6 = 'Encapsulation mode requires IPv6' if 'source_address' in config and not is_ipv6(config['source_address']): diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index dacdbdef2..c77b695bd 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -25,10 +25,12 @@ directories = { "templates": "/usr/share/vyos/templates/", "certbot": "/config/auth/letsencrypt", "api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/", - "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/" - + "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/", + "vyos_udev_dir": "/run/udev/vyos" } +config_status = '/tmp/vyos-config-status' + cfg_group = 'vyattacfg' cfg_vintage = 'vyos' @@ -44,8 +46,9 @@ https_data = { api_data = { 'listen_address' : '127.0.0.1', 'port' : '8080', - 'strict' : 'false', - 'debug' : 'false', + 'socket' : False, + 'strict' : False, + 'debug' : False, 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ] } diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index bc95767b1..e45b0f041 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -45,7 +45,7 @@ class Ethtool: _ring_buffers = { } _ring_buffers_max = { } _driver_name = None - _auto_negotiation = None + _auto_negotiation = False _flow_control = False _flow_control_enabled = None @@ -56,9 +56,6 @@ class Ethtool: link = os.readlink(sysfs_file) self._driver_name = os.path.basename(link) - if not self._driver_name: - raise ValueError(f'Could not determine driver for interface {ifname}!') - # Build a dictinary of supported link-speed and dupley settings. out, err = popen(f'ethtool {ifname}') reading = False @@ -84,10 +81,6 @@ class Ethtool: tmp = line.split()[-1] self._auto_negotiation = bool(tmp == 'on') - if self._auto_negotiation == None: - raise ValueError(f'Could not determine auto-negotiation settings '\ - f'for interface {ifname}!') - # Now populate features dictionaty out, err = popen(f'ethtool --show-features {ifname}') # skip the first line, it only says: "Features for eth0": diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py new file mode 100644 index 000000000..8b7402b7e --- /dev/null +++ b/python/vyos/firewall.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 re + +from vyos.util import cmd +from vyos.util import dict_search_args + +def find_nftables_rule(table, chain, rule_matches=[]): + # Find rule in table/chain that matches all criteria and return the handle + results = cmd(f'sudo nft -a list chain {table} {chain}').split("\n") + for line in results: + if all(rule_match in line for rule_match in rule_matches): + handle_search = re.search('handle (\d+)', line) + if handle_search: + return handle_search[1] + return None + +def remove_nftables_rule(table, chain, handle): + cmd(f'sudo nft delete rule {table} {chain} handle {handle}') + +# Functions below used by template generation + +def nft_action(vyos_action): + if vyos_action == 'accept': + return 'return' + return vyos_action + +def parse_rule(rule_conf, fw_name, rule_id, ip_name): + output = [] + def_suffix = '6' if ip_name == 'ip6' else '' + + if 'state' in rule_conf and rule_conf['state']: + states = ",".join([s for s, v in rule_conf['state'].items() if v == 'enable']) + output.append(f'ct state {{{states}}}') + + if 'protocol' in rule_conf and rule_conf['protocol'] != 'all': + proto = rule_conf['protocol'] + if proto == 'tcp_udp': + proto = '{tcp, udp}' + output.append('meta l4proto ' + proto) + + for side in ['destination', 'source']: + if side in rule_conf: + prefix = side[0] + side_conf = rule_conf[side] + + if 'address' in side_conf: + output.append(f'{ip_name} {prefix}addr {side_conf["address"]}') + + if 'mac_address' in side_conf: + suffix = side_conf["mac_address"] + if suffix[0] == '!': + suffix = f'!= {suffix[1:]}' + output.append(f'ether {prefix}addr {suffix}') + + if 'port' in side_conf: + proto = rule_conf['protocol'] + port = side_conf["port"] + + if isinstance(port, list): + port = ",".join(port) + + if proto == 'tcp_udp': + proto = 'th' + + output.append(f'{proto} {prefix}port {{{port}}}') + + if 'group' in side_conf: + group = side_conf['group'] + if 'address_group' in group: + group_name = group['address_group'] + output.append(f'{ip_name} {prefix}addr $A{def_suffix}_{group_name}') + elif 'network_group' in group: + group_name = group['network_group'] + output.append(f'{ip_name} {prefix}addr $N{def_suffix}_{group_name}') + if 'port_group' in group: + proto = rule_conf['protocol'] + group_name = group['port_group'] + + if proto == 'tcp_udp': + proto = 'th' + + output.append(f'{proto} {prefix}port $P_{group_name}') + + if 'log' in rule_conf and rule_conf['log'] == 'enable': + output.append('log') + + if 'hop_limit' in rule_conf: + operators = {'eq': '==', 'gt': '>', 'lt': '<'} + for op, operator in operators.items(): + if op in rule_conf['hop_limit']: + value = rule_conf['hop_limit'][op] + output.append(f'ip6 hoplimit {operator} {value}') + + for icmp in ['icmp', 'icmpv6']: + if icmp in rule_conf: + if 'type_name' in rule_conf[icmp]: + output.append(icmp + ' type ' + rule_conf[icmp]['type_name']) + else: + if 'code' in rule_conf[icmp]: + output.append(icmp + ' code ' + rule_conf[icmp]['code']) + if 'type' in rule_conf[icmp]: + output.append(icmp + ' type ' + rule_conf[icmp]['type']) + + if 'ipsec' in rule_conf: + if 'match_ipsec' in rule_conf['ipsec']: + output.append('meta ipsec == 1') + if 'match_non_ipsec' in rule_conf['ipsec']: + output.append('meta ipsec == 0') + + if 'fragment' in rule_conf: + # Checking for fragmentation after priority -400 is not possible, + # so we use a priority -450 hook to set a mark + if 'match_frag' in rule_conf['fragment']: + output.append('meta mark 0xffff1') + if 'match_non_frag' in rule_conf['fragment']: + output.append('meta mark != 0xffff1') + + if 'limit' in rule_conf: + if 'rate' in rule_conf['limit']: + output.append(f'limit rate {rule_conf["limit"]["rate"]}/second') + if 'burst' in rule_conf['limit']: + output.append(f'burst {rule_conf["limit"]["burst"]} packets') + + if 'recent' in rule_conf: + count = rule_conf['recent']['count'] + time = rule_conf['recent']['time'] + # output.append(f'meter {fw_name}_{rule_id} {{ ip saddr and 255.255.255.255 limit rate over {count}/{time} burst {count} packets }}') + # Waiting on input from nftables developers due to + # bug with above line and atomic chain flushing. + + if 'time' in rule_conf: + output.append(parse_time(rule_conf['time'])) + + tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') + if tcp_flags: + output.append(parse_tcp_flags(tcp_flags)) + + + output.append('counter') + + if 'set' in rule_conf: + output.append(parse_policy_set(rule_conf['set'], def_suffix)) + + if 'action' in rule_conf: + output.append(nft_action(rule_conf['action'])) + else: + output.append('return') + + output.append(f'comment "{fw_name}-{rule_id}"') + return " ".join(output) + +def parse_tcp_flags(flags): + all_flags = [] + include = [] + for flag in flags.split(","): + if flag[0] == '!': + flag = flag[1:] + else: + include.append(flag) + all_flags.append(flag) + return f'tcp flags & ({"|".join(all_flags)}) == {"|".join(include)}' + +def parse_time(time): + out = [] + if 'startdate' in time: + start = time['startdate'] + if 'T' not in start and 'starttime' in time: + start += f' {time["starttime"]}' + out.append(f'time >= "{start}"') + if 'starttime' in time and 'startdate' not in time: + out.append(f'hour >= "{time["starttime"]}"') + if 'stopdate' in time: + stop = time['stopdate'] + if 'T' not in stop and 'stoptime' in time: + stop += f' {time["stoptime"]}' + out.append(f'time < "{stop}"') + if 'stoptime' in time and 'stopdate' not in time: + out.append(f'hour < "{time["stoptime"]}"') + if 'weekdays' in time: + days = time['weekdays'].split(",") + out_days = [f'"{day}"' for day in days if day[0] != '!'] + out.append(f'day {{{",".join(out_days)}}}') + return " ".join(out) + +def parse_policy_set(set_conf, def_suffix): + out = [] + if 'dscp' in set_conf: + dscp = set_conf['dscp'] + out.append(f'ip{def_suffix} dscp set {dscp}') + if 'mark' in set_conf: + mark = set_conf['mark'] + out.append(f'meta mark set {mark}') + if 'table' in set_conf: + table = set_conf['table'] + if table == 'main': + table = '254' + mark = 0x7FFFFFFF - int(set_conf['table']) + out.append(f'meta mark set {mark}') + if 'tcp_mss' in set_conf: + mss = set_conf['tcp_mss'] + out.append(f'tcp option maxseg size set {mss}') + return " ".join(out) diff --git a/python/vyos/frr.py b/python/vyos/frr.py index df6849472..a8f115d9a 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -84,12 +84,14 @@ if DEBUG: LOG.addHandler(ch2) _frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', - 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd'] + 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd', + 'bfdd'] path_vtysh = '/usr/bin/vtysh' path_frr_reload = '/usr/lib/frr/frr-reload.py' path_config = '/run/frr' +default_add_before = r'(ip prefix-list .*|route-map .*|line vty|end)' class FrrError(Exception): pass @@ -214,13 +216,8 @@ def reload_configuration(config, daemon=None): def save_configuration(): - """Save FRR configuration to /run/frr/config/frr.conf - It save configuration on each commit. T3217 - """ - - cmd(f'{path_vtysh} -n -w') - - return + """ T3217: Save FRR configuration to /run/frr/config/frr.conf """ + return cmd(f'{path_vtysh} -n -w') def execute(command): @@ -448,16 +445,37 @@ class FRRConfig: mark_configuration('\n'.join(self.config)) def commit_configuration(self, daemon=None): - '''Commit the current configuration to FRR - daemon: str with name of the FRR daemon to commit to or - None to use the consolidated config + ''' + Commit the current configuration to FRR daemon: str with name of the + FRR daemon to commit to or None to use the consolidated config. + + Configuration is automatically saved after apply ''' LOG.debug('commit_configuration: Commiting configuration') for i, e in enumerate(self.config): LOG.debug(f'commit_configuration: new_config {i:3} {e}') - reload_configuration('\n'.join(self.config), daemon=daemon) - def modify_section(self, start_pattern, replacement=[], stop_pattern=r'\S+', remove_stop_mark=False, count=0): + # https://github.com/FRRouting/frr/issues/10132 + # https://github.com/FRRouting/frr/issues/10133 + count = 0 + count_max = 5 + while count < count_max: + count += 1 + try: + reload_configuration('\n'.join(self.config), daemon=daemon) + break + except: + # we just need to re-try the commit of the configuration + # for the listed FRR issues above + pass + if count >= count_max: + raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded') + + # Save configuration to /run/frr/config/frr.conf + save_configuration() + + + def modify_section(self, start_pattern, replacement='!', stop_pattern=r'\S+', remove_stop_mark=False, count=0): if isinstance(replacement, str): replacement = replacement.split('\n') elif not isinstance(replacement, list): diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py index 303b6ea47..f31ef51cf 100644 --- a/python/vyos/hostsd_client.py +++ b/python/vyos/hostsd_client.py @@ -79,6 +79,18 @@ class Client(object): msg = {'type': 'forward_zones', 'op': 'get'} return self._communicate(msg) + def add_authoritative_zones(self, data): + msg = {'type': 'authoritative_zones', 'op': 'add', 'data': data} + self._communicate(msg) + + def delete_authoritative_zones(self, data): + msg = {'type': 'authoritative_zones', 'op': 'delete', 'data': data} + self._communicate(msg) + + def get_authoritative_zones(self): + msg = {'type': 'authoritative_zones', 'op': 'get'} + return self._communicate(msg) + def add_search_domains(self, data): msg = {'type': 'search_domains', 'op': 'add', 'data': data} self._communicate(msg) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 2e59a7afc..9d54dc78e 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -80,6 +80,23 @@ class EthernetIf(Interface): super().__init__(ifname, **kargs) self.ethtool = Ethtool(ifname) + def remove(self): + """ + Remove interface from config. Removing the interface deconfigures all + assigned IP addresses. + Example: + >>> from vyos.ifconfig import WWANIf + >>> i = EthernetIf('eth0') + >>> i.remove() + """ + + if self.exists(self.ifname): + # interface is placed in A/D state when removed from config! It + # will remain visible for the operating system. + self.set_admin_state('down') + + super().remove() + def set_flow_control(self, enable): """ Changes the pause parameters of the specified Ethernet device. diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 8857f30e9..91c7f0c33 100755 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -27,8 +27,6 @@ from netifaces import ifaddresses # this is not the same as socket.AF_INET/INET6 from netifaces import AF_INET from netifaces import AF_INET6 -from uuid import uuid3 -from uuid import NAMESPACE_DNS from vyos import ConfigError from vyos.configdict import list_diff @@ -39,6 +37,7 @@ from vyos.util import mac2eui64 from vyos.util import dict_search from vyos.util import read_file from vyos.util import get_interface_config +from vyos.util import get_interface_namespace from vyos.util import is_systemd_service_active from vyos.template import is_ipv4 from vyos.template import is_ipv6 @@ -137,6 +136,9 @@ 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}', @@ -459,11 +461,26 @@ class Interface(Control): >>> Interface('eth0').get_mac() '00:50:ab:cd:ef:00' """ - # calculate a UUID based on the interface name - this is as predictable - # as an interface MAC address and thus can be used in the same way - tmp = uuid3(NAMESPACE_DNS, self.ifname) - # take the last 48 bits from the UUID string - tmp = str(tmp).split('-')[-1] + from hashlib import sha256 + + # Get processor ID number + cpu_id = self._cmd('sudo dmidecode -t 4 | grep ID | head -n1 | sed "s/.*ID://;s/ //g"') + + # XXX: T3894 - it seems not all systems have eth0 - get a list of all + # available Ethernet interfaces on the system (without VLAN subinterfaces) + # and then take the first one. + all_eth_ifs = [x for x in Section.interfaces('ethernet') if '.' not in x] + first_mac = Interface(all_eth_ifs[0]).get_mac() + + sha = sha256() + # Calculate SHA256 sum based on the CPU ID number, eth0 mac address and + # this interface identifier - this is as predictable as an interface + # MAC address and thus can be used in the same way + sha.update(cpu_id.encode()) + sha.update(first_mac.encode()) + sha.update(self.ifname.encode()) + # take the most significant 48 bits from the SHA256 string + tmp = sha.hexdigest()[:12] # Convert pseudo random string into EUI format which now represents a # MAC address tmp = EUI(tmp).value @@ -499,6 +516,35 @@ 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 + 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 + + # 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 + + def set_netns(self, netns): + """ + Add interface from given NETNS. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('dum0').set_netns('foo') + """ + + self.set_interface('netns', netns) + def set_vrf(self, vrf): """ Add/Remove interface from given VRF instance. @@ -531,6 +577,15 @@ class Interface(Control): return None return self.set_interface('arp_cache_tmo', tmo) + def _cleanup_mss_rules(self, table, ifname): + commands = [] + results = self._cmd(f'nft -a list chain {table} VYOS_TCP_MSS').split("\n") + for line in results: + if f'oifname "{ifname}"' in line: + handle_search = re.search('handle (\d+)', line) + if handle_search: + self._cmd(f'nft delete rule {table} VYOS_TCP_MSS handle {handle_search[1]}') + def set_tcp_ipv4_mss(self, mss): """ Set IPv4 TCP MSS value advertised when TCP SYN packets leave this @@ -542,22 +597,14 @@ class Interface(Control): >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_tcp_ipv4_mss(1340) """ - iptables_bin = 'iptables' - base_options = f'-A FORWARD -o {self.ifname} -p tcp -m tcp --tcp-flags SYN,RST SYN' - out = self._cmd(f'{iptables_bin}-save -t mangle') - for line in out.splitlines(): - if line.startswith(base_options): - # remove OLD MSS mangling configuration - line = line.replace('-A FORWARD', '-D FORWARD') - self._cmd(f'{iptables_bin} -t mangle {line}') - - cmd_mss = f'{iptables_bin} -t mangle {base_options} --jump TCPMSS' + 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' if mss == 'clamp-mss-to-pmtu': - self._cmd(f'{cmd_mss} --clamp-mss-to-pmtu') + self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size set rt mtu'") elif int(mss) > 0: - # probably add option to clamp only if bigger: low_mss = str(int(mss) + 1) - self._cmd(f'{cmd_mss} -m tcpmss --mss {low_mss}:65535 --set-mss {mss}') + self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size {low_mss}-65535 tcp option maxseg size set {mss}'") def set_tcp_ipv6_mss(self, mss): """ @@ -570,22 +617,14 @@ class Interface(Control): >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_tcp_mss(1320) """ - iptables_bin = 'ip6tables' - base_options = f'-A FORWARD -o {self.ifname} -p tcp -m tcp --tcp-flags SYN,RST SYN' - out = self._cmd(f'{iptables_bin}-save -t mangle') - for line in out.splitlines(): - if line.startswith(base_options): - # remove OLD MSS mangling configuration - line = line.replace('-A FORWARD', '-D FORWARD') - self._cmd(f'{iptables_bin} -t mangle {line}') - - cmd_mss = f'{iptables_bin} -t mangle {base_options} --jump TCPMSS' + 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' if mss == 'clamp-mss-to-pmtu': - self._cmd(f'{cmd_mss} --clamp-mss-to-pmtu') + self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size set rt mtu'") elif int(mss) > 0: - # probably add option to clamp only if bigger: low_mss = str(int(mss) + 1) - self._cmd(f'{cmd_mss} -m tcpmss --mss {low_mss}:65535 --set-mss {mss}') + self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size {low_mss}-65535 tcp option maxseg size set {mss}'") def set_arp_filter(self, arp_filter): """ @@ -1043,14 +1082,6 @@ class Interface(Control): addr_is_v4 = is_ipv4(addr) - # we can't have both DHCP and static IPv4 addresses assigned - for a in self._addr: - if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or - ( a == 'dhcp' and addr != 'dhcpv6' and addr_is_v4 ) ): - raise ConfigError(( - "Can't configure both static IPv4 and DHCP address " - "on the same interface")) - # add to interface if addr == 'dhcp': self.set_dhcp(True) @@ -1221,7 +1252,7 @@ class Interface(Control): # 'up' check is mandatory b/c even if the interface is A/D, as soon as # the DHCP client is started the interface will be placed in u/u state. # This is not what we intended to do when disabling an interface. - return self._cmd(f'systemctl start dhclient@{ifname}.service') + return self._cmd(f'systemctl restart {systemd_service}') else: # cleanup old config files for file in [config_file, options_file, pid_file, lease_file]: @@ -1238,16 +1269,16 @@ class Interface(Control): ifname = self.ifname config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf' + systemd_service = f'dhcp6c@{ifname}.service' if enable and 'disable' not in self._config: render(config_file, 'dhcp-client/ipv6.tmpl', self._config) - # We must ignore any return codes. This is required to enable DHCPv6-PD - # for interfaces which are yet not up and running. - return self._popen(f'systemctl restart dhcp6c@{ifname}.service') + # We must ignore any return codes. This is required to enable + # DHCPv6-PD for interfaces which are yet not up and running. + return self._popen(f'systemctl restart {systemd_service}') else: - systemd_service = f'dhcp6c@{ifname}.service' if is_systemd_service_active(systemd_service): self._cmd(f'systemctl stop {systemd_service}') if os.path.isfile(config_file): @@ -1266,8 +1297,8 @@ class Interface(Control): source_if = next(iter(self._config['is_mirror_intf'])) config = self._config['is_mirror_intf'][source_if].get('mirror', None) - # Check configuration stored by old perl code before delete T3782 - if not 'redirect' in self._config: + # Check configuration stored by old perl code before delete T3782/T4056 + if not 'redirect' in self._config and not 'traffic_policy' in self._config: # Please do not clear the 'set $? = 0 '. It's meant to force a return of 0 # Remove existing mirroring rules delete_tc_cmd = f'tc qdisc del dev {source_if} handle ffff: ingress 2> /dev/null;' @@ -1348,6 +1379,16 @@ class Interface(Control): if mac: self.set_mac(mac) + # If interface is connected to NETNS we don't have to check all other + # settings like MTU/IPv6/sysctl values, etc. + # 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 + else: + self.del_netns(config.get('netns', '')) + # Update interface description self.set_alias(config.get('description', '')) @@ -1398,7 +1439,7 @@ class Interface(Control): # unbinding will call 'ip link set dev eth0 nomaster' which will # also drop the interface out of a bridge or bond - thus this is # checked before - self.set_vrf(config.get('vrf', None)) + self.set_vrf(config.get('vrf', '')) # Configure MSS value for IPv4 TCP connections tmp = dict_search('ip.adjust_mss', config) diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py index 0e4447b9e..91f667b65 100644 --- a/python/vyos/ifconfig/section.py +++ b/python/vyos/ifconfig/section.py @@ -52,12 +52,12 @@ class Section: name: name of the interface vlan: if vlan is True, do not stop at the vlan number """ - name = name.rstrip('0123456789') - name = name.rstrip('.') - if vlan: - name = name.rstrip('0123456789.') if vrrp: - name = name.rstrip('0123456789v') + name = re.sub(r'\d(\d|v|\.)*$', '', name) + elif vlan: + name = re.sub(r'\d(\d|\.)*$', '', name) + else: + name = re.sub(r'\d+$', '', name) return name @classmethod diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index d73fb47b8..0c5282db4 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -54,18 +54,21 @@ class VXLANIf(Interface): # arguments used by iproute2. For more information please refer to: # - https://man7.org/linux/man-pages/man8/ip-link.8.html mapping = { - 'source_address' : 'local', - 'source_interface' : 'dev', - 'remote' : 'remote', 'group' : 'group', + 'external' : 'external', + 'gpe' : 'gpe', 'parameters.ip.dont_fragment': 'df set', 'parameters.ip.tos' : 'tos', 'parameters.ip.ttl' : 'ttl', 'parameters.ipv6.flowlabel' : 'flowlabel', 'parameters.nolearning' : 'nolearning', + 'remote' : 'remote', + 'source_address' : 'local', + 'source_interface' : 'dev', + 'vni' : 'id', } - cmd = 'ip link add {ifname} type {type} id {vni} dstport {port}' + cmd = 'ip link add {ifname} type {type} dstport {port}' for vyos_key, iproute2_key in mapping.items(): # dict_search will return an empty dict "{}" for valueless nodes like # "parameters.nolearning" - thus we need to test the nodes existence diff --git a/python/vyos/ifconfig/wwan.py b/python/vyos/ifconfig/wwan.py index f18959a60..845c9bef9 100644 --- a/python/vyos/ifconfig/wwan.py +++ b/python/vyos/ifconfig/wwan.py @@ -26,3 +26,20 @@ class WWANIf(Interface): 'eternal': 'wwan[0-9]+$', }, } + + def remove(self): + """ + Remove interface from config. Removing the interface deconfigures all + assigned IP addresses. + Example: + >>> from vyos.ifconfig import WWANIf + >>> i = WWANIf('wwan0') + >>> i.remove() + """ + + if self.exists(self.ifname): + # interface is placed in A/D state when removed from config! It + # will remain visible for the operating system. + self.set_admin_state('down') + + super().remove() diff --git a/python/vyos/range_regex.py b/python/vyos/range_regex.py new file mode 100644 index 000000000..a8190d140 --- /dev/null +++ b/python/vyos/range_regex.py @@ -0,0 +1,142 @@ +'''Copyright (c) 2013, Dmitry Voronin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' +import math + +# coding=utf8 + +# Split range to ranges that has its unique pattern. +# Example for 12-345: +# +# 12- 19: 1[2-9] +# 20- 99: [2-9]\d +# 100-299: [1-2]\d{2} +# 300-339: 3[0-3]\d +# 340-345: 34[0-5] + +def range_to_regex(inpt_range): + if isinstance(inpt_range, str): + range_list = inpt_range.split('-') + # Check input arguments + if len(range_list) == 2: + # The first element in range must be higher then the second + if int(range_list[0]) < int(range_list[1]): + return regex_for_range(int(range_list[0]), int(range_list[1])) + + return None + +def bounded_regex_for_range(min_, max_): + return r'\b({})\b'.format(regex_for_range(min_, max_)) + +def regex_for_range(min_, max_): + """ + > regex_for_range(12, 345) + '1[2-9]|[2-9]\d|[1-2]\d{2}|3[0-3]\d|34[0-5]' + """ + positive_subpatterns = [] + negative_subpatterns = [] + + if min_ < 0: + min__ = 1 + if max_ < 0: + min__ = abs(max_) + max__ = abs(min_) + + negative_subpatterns = split_to_patterns(min__, max__) + min_ = 0 + + if max_ >= 0: + positive_subpatterns = split_to_patterns(min_, max_) + + negative_only_subpatterns = ['-' + val for val in negative_subpatterns if val not in positive_subpatterns] + positive_only_subpatterns = [val for val in positive_subpatterns if val not in negative_subpatterns] + intersected_subpatterns = ['-?' + val for val in negative_subpatterns if val in positive_subpatterns] + + subpatterns = negative_only_subpatterns + intersected_subpatterns + positive_only_subpatterns + return '|'.join(subpatterns) + + +def split_to_patterns(min_, max_): + subpatterns = [] + + start = min_ + for stop in split_to_ranges(min_, max_): + subpatterns.append(range_to_pattern(start, stop)) + start = stop + 1 + + return subpatterns + + +def split_to_ranges(min_, max_): + stops = {max_} + + nines_count = 1 + stop = fill_by_nines(min_, nines_count) + while min_ <= stop < max_: + stops.add(stop) + + nines_count += 1 + stop = fill_by_nines(min_, nines_count) + + zeros_count = 1 + stop = fill_by_zeros(max_ + 1, zeros_count) - 1 + while min_ < stop <= max_: + stops.add(stop) + + zeros_count += 1 + stop = fill_by_zeros(max_ + 1, zeros_count) - 1 + + stops = list(stops) + stops.sort() + + return stops + + +def fill_by_nines(integer, nines_count): + return int(str(integer)[:-nines_count] + '9' * nines_count) + + +def fill_by_zeros(integer, zeros_count): + return integer - integer % 10 ** zeros_count + + +def range_to_pattern(start, stop): + pattern = '' + any_digit_count = 0 + + for start_digit, stop_digit in zip(str(start), str(stop)): + if start_digit == stop_digit: + pattern += start_digit + elif start_digit != '0' or stop_digit != '9': + pattern += '[{}-{}]'.format(start_digit, stop_digit) + else: + any_digit_count += 1 + + if any_digit_count: + pattern += r'\d' + + if any_digit_count > 1: + pattern += '{{{}}}'.format(any_digit_count) + + return pattern
\ No newline at end of file diff --git a/python/vyos/remote.py b/python/vyos/remote.py index e972050b7..aa62ac60d 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -13,38 +13,40 @@ # 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 ftplib import FTP import os import shutil import socket +import ssl import stat import sys import tempfile import urllib.parse -import urllib.request as urlreq -from vyos.template import get_ip -from vyos.template import ip_from_cidr -from vyos.template import is_interface -from vyos.template import is_ipv6 -from vyos.util import cmd +from ftplib import FTP +from ftplib import FTP_TLS + +from paramiko import SSHClient +from paramiko import MissingHostKeyPolicy + +from requests import Session +from requests.adapters import HTTPAdapter +from requests.packages.urllib3 import PoolManager + from vyos.util import ask_yes_no -from vyos.util import print_error -from vyos.util import make_progressbar +from vyos.util import begin +from vyos.util import cmd from vyos.util import make_incremental_progressbar +from vyos.util import make_progressbar +from vyos.util import print_error from vyos.version import get_version -from paramiko import SSHClient -from paramiko import SSHException -from paramiko import MissingHostKeyPolicy -# This is a hardcoded path and no environment variable can change it. -KNOWN_HOSTS_FILE = os.path.expanduser('~/.ssh/known_hosts') + CHUNK_SIZE = 8192 class InteractivePolicy(MissingHostKeyPolicy): """ - Policy for interactively querying the user on whether to proceed with - SSH connections to unknown hosts. + Paramiko policy for interactively querying the user on whether to proceed + with SSH connections to unknown hosts. """ def missing_host_key(self, client, hostname, key): print_error(f"Host '{hostname}' not found in known hosts.") @@ -57,339 +59,261 @@ class InteractivePolicy(MissingHostKeyPolicy): else: raise SSHException(f"Cannot connect to unknown host '{hostname}'.") - -## Helper routines -def get_authentication_variables(default_username=None, default_password=None): +class SourceAdapter(HTTPAdapter): """ - Return the environment variables `$REMOTE_USERNAME` and `$REMOTE_PASSWORD` and - return the defaults provided if environment variables are empty or nonexistent. + urllib3 transport adapter for setting source addresses per session. """ - username, password = os.getenv('REMOTE_USERNAME'), os.getenv('REMOTE_PASSWORD') - # Fall back to defaults if the username variable doesn't exist or is an empty string. - # Note that this is different from `os.getenv('REMOTE_USERNAME', default=default_username)`, - # as we want the username and the password to have the same behaviour. - if not username: - return default_username, default_password - else: - return username, password + def __init__(self, source_pair, *args, **kwargs): + # A source pair is a tuple of a source host string and source port respectively. + # Supply '' and 0 respectively for default values. + self._source_pair = source_pair + super(SourceAdapter, self).__init__(*args, **kwargs) + + def init_poolmanager(self, connections, maxsize, block=False): + self.poolmanager = PoolManager( + num_pools=connections, maxsize=maxsize, + block=block, source_address=self._source_pair) -def get_source_address(source): - """ - Take a string vaguely indicating an origin source (interface, hostname or IP address), - return a tuple in the format `(source_pair, address_family)` where - `source_pair` is `(source_address, source_port)`. - """ - # TODO: Properly distinguish between IPv4 and IPv6. - port = 0 - if is_interface(source): - source = ip_from_cidr(get_ip(source)[0]) - if is_ipv6(source): - return (source, port), socket.AF_INET6 - else: - return (socket.gethostbyname(source), port), socket.AF_INET -def get_port_from_url(url): +def check_storage(path, size): """ - Return the port number from the given `url` named tuple, fall back to - the default if there isn't one. + Check whether `path` has enough storage space for a transfer of `size` bytes. """ - defaults = {"http": 80, "https": 443, "ftp": 21, "tftp": 69,\ - "ssh": 22, "scp": 22, "sftp": 22} - if url.port: - return url.port + path = os.path.abspath(os.path.expanduser(path)) + directory = path if os.path.isdir(path) else (os.path.dirname(os.path.expanduser(path)) or os.getcwd()) + # `size` can be None or 0 to indicate unknown size. + if not size: + print_error('Warning: Cannot determine size of remote file.') + print_error('Bravely continuing regardless.') + return + + if size < 1024 * 1024: + print_error(f'The file is {size / 1024.0:.3f} KiB.') else: - return defaults[url.scheme] - - -## FTP routines -def upload_ftp(local_path, hostname, remote_path,\ - username='anonymous', password='', port=21,\ - source_pair=None, progressbar=False): - size = os.path.getsize(local_path) - with FTP(source_address=source_pair) as conn: - conn.connect(hostname, port) - conn.login(username, password) - with open(local_path, 'rb') as file: - if progressbar and size: + print_error(f'The file is {size / (1024.0 * 1024.0):.3f} MiB.') + + # Will throw `FileNotFoundError' if `directory' is absent. + if size > shutil.disk_usage(directory).free: + raise OSError(f'Not enough disk space available in "{directory}".') + + +class FtpC: + def __init__(self, url, progressbar=False, check_space=False, source_host='', source_port=0): + self.secure = url.scheme == 'ftps' + self.hostname = url.hostname + self.path = url.path + self.username = url.username or os.getenv('REMOTE_USERNAME', 'anonymous') + self.password = url.password or os.getenv('REMOTE_PASSWORD', '') + self.port = url.port or 21 + self.source = (source_host, source_port) + self.progressbar = progressbar + self.check_space = check_space + + def _establish(self): + if self.secure: + return FTP_TLS(source_address=self.source, context=ssl.create_default_context()) + else: + return FTP(source_address=self.source) + + def download(self, location: str): + # Open the file upfront before establishing connection. + with open(location, 'wb') as f, self._establish() as conn: + conn.connect(self.hostname, self.port) + conn.login(self.username, self.password) + # Set secure connection over TLS. + if self.secure: + conn.prot_p() + # Almost all FTP servers support the `SIZE' command. + if self.check_space: + check_storage(path, conn.size(self.path)) + # No progressbar if we can't determine the size or if the file is too small. + if self.progressbar and size and size > CHUNK_SIZE: progress = make_incremental_progressbar(CHUNK_SIZE / size) next(progress) - callback = lambda block: next(progress) + callback = lambda block: begin(f.write(block), next(progress)) else: - callback = None - conn.storbinary(f'STOR {remote_path}', file, CHUNK_SIZE, callback) - -def download_ftp(local_path, hostname, remote_path,\ - username='anonymous', password='', port=21,\ - source_pair=None, progressbar=False): - with FTP(source_address=source_pair) as conn: - conn.connect(hostname, port) - conn.login(username, password) - size = conn.size(remote_path) - with open(local_path, 'wb') as file: - # No progressbar if we can't determine the size. - if progressbar and size: + callback = f.write + conn.retrbinary('RETR ' + self.path, callback, CHUNK_SIZE) + + def upload(self, location: str): + size = os.path.getsize(location) + with open(location, 'rb') as f, self._establish() as conn: + conn.connect(self.hostname, self.port) + conn.login(self.username, self.password) + if self.secure: + conn.prot_p() + if self.progressbar and size and size > CHUNK_SIZE: progress = make_incremental_progressbar(CHUNK_SIZE / size) next(progress) - callback = lambda block: (file.write(block), next(progress)) + callback = lambda block: next(progress) else: - callback = file.write - conn.retrbinary(f'RETR {remote_path}', callback, CHUNK_SIZE) - -def get_ftp_file_size(hostname, remote_path,\ - username='anonymous', password='', port=21,\ - source_pair=None): - with FTP(source_address=source) as conn: - conn.connect(hostname, port) - conn.login(username, password) - size = conn.size(remote_path) - if size: - return size - else: - # SIZE is an extension to the FTP specification, although it's extremely common. - raise ValueError('Failed to receive file size from FTP server. \ - Perhaps the server does not implement the SIZE command?') - - -## SFTP/SCP routines -def transfer_sftp(mode, local_path, hostname, remote_path,\ - username=None, password=None, port=22,\ - source_tuple=None, progressbar=False): - sock = None - if source_tuple: - (source_address, source_port), address_family = source_tuple - sock = socket.socket(address_family, socket.SOCK_STREAM) - sock.bind((source_address, source_port)) - sock.connect((hostname, port)) - callback = make_progressbar() if progressbar else None - with SSHClient() as ssh: + callback = None + conn.storbinary('STOR ' + self.path, f, CHUNK_SIZE, callback) + +class SshC: + known_hosts = os.path.expanduser('~/.ssh/known_hosts') + def __init__(self, url, progressbar=False, check_space=False, source_host='', source_port=0): + self.hostname = url.hostname + self.path = url.path + self.username = url.username or os.getenv('REMOTE_USERNAME') + self.password = url.password or os.getenv('REMOTE_PASSWORD') + self.port = url.port or 22 + self.source = (source_host, source_port) + self.progressbar = progressbar + self.check_space = check_space + + def _establish(self): + ssh = SSHClient() ssh.load_system_host_keys() - if os.path.exists(KNOWN_HOSTS_FILE): - ssh.load_host_keys(KNOWN_HOSTS_FILE) + # Try to load from a user-local known hosts file if one exists. + if os.path.exists(self.known_hosts): + ssh.load_host_keys(self.known_hosts) ssh.set_missing_host_key_policy(InteractivePolicy()) - ssh.connect(hostname, port, username, password, sock=sock) - with ssh.open_sftp() as sftp: - if mode == 'upload': + # `socket.create_connection()` automatically picks a NIC and an IPv4/IPv6 address family + # for us on dual-stack systems. + sock = socket.create_connection((self.hostname, self.port), socket.getdefaulttimeout(), self.source) + ssh.connect(self.hostname, self.port, self.username, self.password, sock=sock) + return ssh + + def download(self, location: str): + callback = make_progressbar() if self.progressbar else None + with self._establish() as ssh, ssh.open_sftp() as sftp: + if self.check_space: + check_storage(location, sftp.stat(self.path).st_size) + sftp.get(self.path, location, callback=callback) + + def upload(self, location: str): + callback = make_progressbar() if self.progressbar else None + with self._establish() as ssh, ssh.open_sftp() as sftp: + try: + # If the remote path is a directory, use the original filename. + if stat.S_ISDIR(sftp.stat(self.path).st_mode): + path = os.path.join(self.path, os.path.basename(location)) + # A file exists at this destination. We're simply going to clobber it. + else: + path = self.path + # This path doesn't point at any existing file. We can freely use this filename. + except IOError: + path = self.path + finally: + sftp.put(location, path, callback=callback) + + +class HttpC: + def __init__(self, url, progressbar=False, check_space=False, source_host='', source_port=0): + self.urlstring = urllib.parse.urlunsplit(url) + self.progressbar = progressbar + self.check_space = check_space + self.source_pair = (source_host, source_port) + self.username = url.username or os.getenv('REMOTE_USERNAME') + self.password = url.password or os.getenv('REMOTE_PASSWORD') + + def _establish(self): + session = Session() + session.mount(self.urlstring, SourceAdapter(self.source_pair)) + session.headers.update({'User-Agent': 'VyOS/' + get_version()}) + if self.username: + session.auth = self.username, self.password + return session + + def download(self, location: str): + with self._establish() as s: + # We ask for uncompressed downloads so that we don't have to deal with decoding. + # Not only would it potentially mess up with the progress bar but + # `shutil.copyfileobj(request.raw, file)` does not handle automatic decoding. + s.headers.update({'Accept-Encoding': 'identity'}) + with s.head(self.urlstring, allow_redirects=True) as r: + # Abort early if the destination is inaccessible. + r.raise_for_status() + # If the request got redirected, keep the last URL we ended up with. + final_urlstring = r.url + if r.history: + print_error('Redirecting to ' + final_urlstring) + # Check for the prospective file size. try: - # If the remote path is a directory, use the original filename. - if stat.S_ISDIR(sftp.stat(remote_path).st_mode): - path = os.path.join(remote_path, os.path.basename(local_path)) - # A file exists at this destination. We're simply going to clobber it. - else: - path = remote_path - # This path doesn't point at any existing file. We can freely use this filename. - except IOError: - path = remote_path - finally: - sftp.put(local_path, path, callback=callback) - elif mode == 'download': - sftp.get(remote_path, local_path, callback=callback) - elif mode == 'size': - return sftp.stat(remote_path).st_size - -def upload_sftp(*args, **kwargs): - transfer_sftp('upload', *args, **kwargs) - -def download_sftp(*args, **kwargs): - transfer_sftp('download', *args, **kwargs) - -def get_sftp_file_size(*args, **kwargs): - return transfer_sftp('size', None, *args, **kwargs) - - -## TFTP routines -def upload_tftp(local_path, hostname, remote_path, port=69, source=None, progressbar=False): - source_option = f'--interface {source}' if source else '' - progress_flag = '--progress-bar' if progressbar else '-s' - with open(local_path, 'rb') as file: - cmd(f'curl {source_option} {progress_flag} -T - tftp://{hostname}:{port}/{remote_path}',\ - stderr=None, input=file.read()).encode() - -def download_tftp(local_path, hostname, remote_path, port=69, source=None, progressbar=False): - source_option = f'--interface {source}' if source else '' - # Not really applicable but we pass it for the sake of uniformity. - progress_flag = '--progress-bar' if progressbar else '-s' - with open(local_path, 'wb') as file: - file.write(cmd(f'curl {source_option} {progress_flag} tftp://{hostname}:{port}/{remote_path}',\ - stderr=None).encode()) - -# get_tftp_file_size() is unimplemented because there is no way to obtain a file's size through TFTP, -# as TFTP does not specify a SIZE command. - - -## HTTP(S) routines -def install_request_opener(urlstring, username, password): - """ - Take `username` and `password` strings and install the appropriate - password manager to `urllib.request.urlopen()` for the given `urlstring`. - """ - manager = urlreq.HTTPPasswordMgrWithDefaultRealm() - manager.add_password(None, urlstring, username, password) - urlreq.install_opener(urlreq.build_opener(urlreq.HTTPBasicAuthHandler(manager))) - -# upload_http() is unimplemented. - -def download_http(local_path, urlstring, username=None, password=None, progressbar=False): - """ - Download the file from from `urlstring` to `local_path`. - Optionally takes `username` and `password` for authentication. - """ - request = urlreq.Request(urlstring, headers={'User-Agent': 'VyOS/' + get_version()}) - if username: - install_request_opener(urlstring, username, password) - with open(local_path, 'wb') as file, urlreq.urlopen(request) as response: - size = response.getheader('Content-Length') - if progressbar and size: - progress = make_incremental_progressbar(CHUNK_SIZE / int(size)) - next(progress) - for chunk in iter(lambda: response.read(CHUNK_SIZE), b''): - file.write(chunk) - next(progress) - next(progress) - # If we can't determine the size or if a progress bar wasn't requested, - # we can let `shutil` take care of the copying. - else: - shutil.copyfileobj(response, file) - -def get_http_file_size(urlstring, username=None, password=None): - """ - Return the size of the file from `urlstring` in terms of number of bytes. - Optionally takes `username` and `password` for authentication. - """ - request = urlreq.Request(urlstring, headers={'User-Agent': 'VyOS/' + get_version()}) - if username: - install_request_opener(urlstring, username, password) - with urlreq.urlopen(request) as response: - size = response.getheader('Content-Length') - if size: - return int(size) - # The server didn't send 'Content-Length' in the response headers. - else: - raise ValueError('Failed to receive file size from HTTP server.') - - -## Dynamic dispatchers -def download(local_path, urlstring, source=None, progressbar=False): + size = int(r.headers['Content-Length']) + # In case the server does not supply the header. + except KeyError: + size = None + if self.check_space: + check_storage(location, size) + with s.get(final_urlstring, stream=True) as r, open(location, 'wb') as f: + if self.progressbar and size: + progress = make_incremental_progressbar(CHUNK_SIZE / size) + next(progress) + for chunk in iter(lambda: begin(next(progress), r.raw.read(CHUNK_SIZE)), b''): + f.write(chunk) + else: + # We'll try to stream the download directly with `copyfileobj()` so that large + # files (like entire VyOS images) don't occupy much memory. + shutil.copyfileobj(r.raw, f) + + def upload(self, location: str): + # Does not yet support progressbars. + with self._establish() as s, open(location, 'rb') as f: + s.post(self.urlstring, data=f, allow_redirects=True) + + +class TftpC: + # We simply allow `curl` to take over because + # 1. TFTP is rather simple. + # 2. Since there's no concept authentication, we don't need to deal with keys/passwords. + # 3. It would be a waste to import, audit and maintain a third-party library for TFTP. + # 4. I'd rather not implement the entire protocol here, no matter how simple it is. + def __init__(self, url, progressbar=False, check_space=False, source_host=None, source_port=0): + source_option = f'--interface {source_host} --local-port {source_port}' if source_host else '' + progress_flag = '--progress-bar' if progressbar else '-s' + self.command = f'curl {source_option} {progress_flag}' + self.urlstring = urllib.parse.urlunsplit(url) + + def download(self, location: str): + with open(location, 'wb') as f: + f.write(cmd(f'{self.command} "{self.urlstring}"').encode()) + + def upload(self, location: str): + with open(location, 'rb') as f: + cmd(f'{self.command} -T - "{self.urlstring}"', input=f.read()) + + +def urlc(urlstring, *args, **kwargs): """ - Dispatch the appropriate download function for the given `urlstring` and save to `local_path`. - Optionally takes a `source` address or interface (not valid for HTTP(S)). - Supports HTTP, HTTPS, FTP, SFTP, SCP (through SFTP) and TFTP. - Reads `$REMOTE_USERNAME` and `$REMOTE_PASSWORD` environment variables. + Dynamically dispatch the appropriate protocol class. """ - url = urllib.parse.urlparse(urlstring) - username, password = get_authentication_variables(url.username, url.password) - port = get_port_from_url(url) - - if url.scheme == 'http' or url.scheme == 'https': - if source: - print_error('Warning: Custom source address not supported for HTTP connections.') - download_http(local_path, urlstring, username, password, progressbar) - elif url.scheme == 'ftp': - source = get_source_address(source)[0] if source else None - username = username if username else 'anonymous' - download_ftp(local_path, url.hostname, url.path, username, password, port, source, progressbar) - elif url.scheme == 'sftp' or url.scheme == 'scp': - source = get_source_address(source) if source else None - download_sftp(local_path, url.hostname, url.path, username, password, port, source, progressbar) - elif url.scheme == 'tftp': - download_tftp(local_path, url.hostname, url.path, port, source, progressbar) - else: - raise ValueError(f'Unsupported URL scheme: {url.scheme}') + url_classes = {'http': HttpC, 'https': HttpC, 'ftp': FtpC, 'ftps': FtpC, \ + 'sftp': SshC, 'ssh': SshC, 'scp': SshC, 'tftp': TftpC} + url = urllib.parse.urlsplit(urlstring) + try: + return url_classes[url.scheme](url, *args, **kwargs) + except KeyError: + raise ValueError(f'Unsupported URL scheme: "{url.scheme}"') -def upload(local_path, urlstring, source=None, progressbar=False): - """ - Dispatch the appropriate upload function for the given URL and upload from local path. - Optionally takes a `source` address. - Supports FTP, SFTP, SCP (through SFTP) and TFTP. - Reads `$REMOTE_USERNAME` and `$REMOTE_PASSWORD` environment variables. - """ - url = urllib.parse.urlparse(urlstring) - username, password = get_authentication_variables(url.username, url.password) - port = get_port_from_url(url) - - if url.scheme == 'ftp': - username = username if username else 'anonymous' - source = get_source_address(source)[0] if source else None - upload_ftp(local_path, url.hostname, url.path, username, password, port, source, progressbar) - elif url.scheme == 'sftp' or url.scheme == 'scp': - source = get_source_address(source) if source else None - upload_sftp(local_path, url.hostname, url.path, username, password, port, source, progressbar) - elif url.scheme == 'tftp': - upload_tftp(local_path, url.hostname, url.path, port, source, progressbar) - else: - raise ValueError(f'Unsupported URL scheme: {url.scheme}') +def download(local_path, urlstring, *args, **kwargs): + urlc(urlstring, *args, **kwargs).download(local_path) -def get_remote_file_size(urlstring, source=None): - """ - Dispatch the appropriate function to return the size of the remote file from `urlstring` - in terms of number of bytes. - Optionally takes a `source` address (not valid for HTTP(S)). - Supports HTTP, HTTPS, FTP and SFTP (through SFTP). - Reads `$REMOTE_USERNAME` and `$REMOTE_PASSWORD` environment variables. - """ - url = urllib.parse.urlparse(urlstring) - username, password = get_authentication_variables(url.username, url.password) - port = get_port_from_url(url) - - if url.scheme == 'http' or url.scheme == 'https': - if source: - print_error('Warning: Custom source address not supported for HTTP connections.') - return get_http_file_size(urlstring, username, password) - elif url.scheme == 'ftp': - source = get_source_address(source)[0] if source else None - username = username if username else 'anonymous' - return get_ftp_file_size(url.hostname, url.path, username, password, port, source) - elif url.scheme == 'sftp' or url.scheme == 'scp': - source = get_source_address(source) if source else None - return get_sftp_file_size(url.hostname, url.path, username, password, port, source) - else: - raise ValueError(f'Unsupported URL scheme: {url.scheme}') +def upload(local_path, urlstring, *args, **kwargs): + urlc(urlstring, *args, **kwargs).upload(local_path) -def get_remote_config(urlstring, source=None): +def get_remote_config(urlstring, source_host='', source_port=0): """ - Download remote (config) file from `urlstring` and return the contents as a string. - Args: - remote file URI: - tftp://<host>[:<port>]/<file> - http[s]://<host>[:<port>]/<file> - [scp|sftp|ftp]://[<user>[:<passwd>]@]<host>[:port]/<file> - source address (optional): - <interface> - <IP address> + Quietly download a file and return it as a string. """ temp = tempfile.NamedTemporaryFile(delete=False).name try: - download(temp, urlstring, source) - with open(temp, 'r') as file: - return file.read() + download(temp, urlstring, False, False, source_host, source_port) + with open(temp, 'r') as f: + return f.read() finally: os.remove(temp) -def friendly_download(local_path, urlstring, source=None): +def friendly_download(local_path, urlstring, source_host='', source_port=0): """ - Download from `urlstring` to `local_path` in an informative way. - Checks the storage space before attempting download. - Intended to be called from interactive, user-facing scripts. + Download with a progress bar, reassuring messages and free space checks. """ - destination_directory = os.path.dirname(local_path) try: - free_space = shutil.disk_usage(destination_directory).free - try: - file_size = get_remote_file_size(urlstring, source) - if file_size < 1024 * 1024: - print_error(f'The file is {file_size / 1024.0:.3f} KiB.') - else: - print_error(f'The file is {file_size / (1024.0 * 1024.0):.3f} MiB.') - if file_size > free_space: - raise OSError(f'Not enough disk space available in "{destination_directory}".') - except ValueError: - # Can't do a storage check in this case, so we bravely continue. - file_size = 0 - print_error('Could not determine the file size in advance.') - else: - print_error('Downloading...') - download(local_path, urlstring, source, progressbar=file_size > 1024 * 1024) + print_error('Downloading...') + download(local_path, urlstring, True, True, source_host, source_port) except KeyboardInterrupt: - print_error('Download aborted by user.') + print_error('\nDownload aborted by user.') sys.exit(1) except: import traceback @@ -401,3 +325,4 @@ def friendly_download(local_path, urlstring, source=None): sys.exit(1) else: print_error('Download complete.') + sys.exit(0) diff --git a/python/vyos/template.py b/python/vyos/template.py index d13915766..2987fcd0e 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -22,6 +22,7 @@ from jinja2 import FileSystemLoader from vyos.defaults import directories from vyos.util import chmod from vyos.util import chown +from vyos.util import dict_search_args from vyos.util import makedir # Holds template filters registered via register_filter() @@ -151,6 +152,16 @@ def bracketize_ipv6(address): return f'[{address}]' return address +@register_filter('dot_colon_to_dash') +def dot_colon_to_dash(text): + """ Replace dot and colon to dash for string + Example: + 192.0.2.1 => 192-0-2-1, 2001:db8::1 => 2001-db8--1 + """ + text = text.replace(":", "-") + text = text.replace(".", "-") + return text + @register_filter('netmask_from_cidr') def netmask_from_cidr(prefix): """ Take CIDR prefix and convert the prefix length to a "subnet mask". @@ -349,7 +360,6 @@ def get_dhcp_router(interface): Returns False of no router is found, returns the IP address as string if a router is found. """ - interface = interface.replace('.', '_') lease_file = f'/var/lib/dhcp/dhclient_{interface}.leases' if not os.path.exists(lease_file): return None @@ -480,3 +490,57 @@ def get_openvpn_ncp_ciphers(ciphers): else: out.append(cipher) return ':'.join(out).upper() + +@register_filter('snmp_auth_oid') +def snmp_auth_oid(type): + if type not in ['md5', 'sha', 'aes', 'des', 'none']: + raise ValueError() + + OIDs = { + 'md5' : '.1.3.6.1.6.3.10.1.1.2', + 'sha' : '.1.3.6.1.6.3.10.1.1.3', + 'aes' : '.1.3.6.1.6.3.10.1.2.4', + 'des' : '.1.3.6.1.6.3.10.1.2.2', + 'none': '.1.3.6.1.6.3.10.1.2.1' + } + return OIDs[type] + +@register_filter('nft_action') +def nft_action(vyos_action): + if vyos_action == 'accept': + return 'return' + return vyos_action + +@register_filter('nft_rule') +def nft_rule(rule_conf, fw_name, rule_id, ip_name='ip'): + from vyos.firewall import parse_rule + return parse_rule(rule_conf, fw_name, rule_id, ip_name) + +@register_filter('nft_state_policy') +def nft_state_policy(conf, state): + out = [f'ct state {state}'] + + if 'log' in conf and 'enable' in conf['log']: + out.append('log') + + out.append('counter') + + if 'action' in conf: + out.append(conf['action']) + + return " ".join(out) + +@register_filter('nft_intra_zone_action') +def nft_intra_zone_action(zone_conf, ipv6=False): + if 'intra_zone_filtering' in zone_conf: + intra_zone = zone_conf['intra_zone_filtering'] + fw_name = 'ipv6_name' if ipv6 else 'name' + + if 'action' in intra_zone: + if intra_zone['action'] == 'accept': + return 'return' + return intra_zone['action'] + elif dict_search_args(intra_zone, 'firewall', fw_name): + name = dict_search_args(intra_zone, 'firewall', fw_name) + return f'jump {name}' + return 'return' diff --git a/python/vyos/util.py b/python/vyos/util.py index 849b27d3b..954c6670d 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -489,6 +489,40 @@ def seconds_to_human(s, separator=""): return result +def bytes_to_human(bytes, initial_exponent=0): + """ Converts a value in bytes to a human-readable size string like 640 KB + + The initial_exponent parameter is the exponent of 2, + e.g. 10 (1024) for kilobytes, 20 (1024 * 1024) for megabytes. + """ + + from math import log2 + + bytes = bytes * (2**initial_exponent) + + # log2 is a float, while range checking requires an int + exponent = int(log2(bytes)) + + if exponent < 10: + value = bytes + suffix = "B" + elif exponent in range(10, 20): + value = bytes / 1024 + suffix = "KB" + elif exponent in range(20, 30): + value = bytes / 1024**2 + suffix = "MB" + elif exponent in range(30, 40): + value = bytes / 1024**3 + suffix = "GB" + else: + value = bytes / 1024**4 + suffix = "TB" + # Add a new case when the first machine with petabyte RAM + # hits the market. + + size_string = "{0:.2f} {1}".format(value, suffix) + return size_string def get_cfg_group_id(): from grp import getgrnam @@ -630,7 +664,6 @@ def ask_yes_no(question, default=False) -> bool: except EOFError: stdout.write("\nPlease respond with yes/y or no/n\n") - def is_admin() -> bool: """Look if current user is in sudo group""" from getpass import getuser @@ -761,6 +794,24 @@ def get_interface_address(interface): tmp = loads(cmd(f'ip -d -j addr show {interface}'))[0] return tmp +def get_interface_namespace(iface): + """ + Returns wich netns the interface belongs to + """ + from json import loads + # Check if netns exist + tmp = loads(cmd(f'ip --json netns ls')) + if len(tmp) == 0: + return None + + for ns in tmp: + namespace = f'{ns["name"]}' + # Search interface in each netns + data = loads(cmd(f'ip netns exec {namespace} ip -j link show')) + for compare in data: + if iface == compare["ifname"]: + return namespace + def get_all_vrfs(): """ Return a dictionary of all system wide known VRF instances """ from json import loads @@ -823,6 +874,20 @@ def make_incremental_progressbar(increment: float): while True: yield +def begin(*args): + """ + Evaluate arguments in order and return the result of the *last* argument. + For combining multiple expressions in one statement. Useful for lambdas. + """ + return args[-1] + +def begin0(*args): + """ + Evaluate arguments in order and return the result of the *first* argument. + For combining multiple expressions in one statement. Useful for lambdas. + """ + return args[0] + def is_systemd_service_active(service): """ Test is a specified systemd service is activated. Returns True if service is active, false otherwise. @@ -869,3 +934,57 @@ def check_port_availability(ipaddress, port, protocol): return True except: return False + +def install_into_config(conf, config_paths, override_prompt=True): + # Allows op-mode scripts to install values if called from an active config session + # config_paths: dict of config paths + # override_prompt: if True, user will be prompted before existing nodes are overwritten + + if not config_paths: + return None + + from vyos.config import Config + + if not Config().in_session(): + print('You are not in configure mode, commands to install manually from configure mode:') + for path in config_paths: + print(f'set {path}') + return None + + count = 0 + + for path in config_paths: + if override_prompt and conf.exists(path) and not conf.is_multi(path): + if not ask_yes_no(f'Config node "{node}" already exists. Do you want to overwrite it?'): + continue + + cmd(f'/opt/vyatta/sbin/my_set {path}') + count += 1 + + if count > 0: + print(f'{count} value(s) installed. Use "compare" to see the pending changes, and "commit" to apply.') + +def is_wwan_connected(interface): + """ Determine if a given WWAN interface, e.g. wwan0 is connected to the + carrier network or not """ + import json + + if not interface.startswith('wwan'): + raise ValueError(f'Specified interface "{interface}" is not a WWAN interface') + + modem = interface.lstrip('wwan') + + tmp = cmd(f'mmcli --modem {modem} --output-json') + tmp = json.loads(tmp) + + # return True/False if interface is in connected state + return dict_search('modem.generic.state', tmp) == 'connected' + +def boot_configuration_complete() -> bool: + """ Check if the boot config loader has completed + """ + from vyos.defaults import config_status + + if os.path.isfile(config_status): + return True + return False diff --git a/smoketest/configs/bgp-big-as-cloud b/smoketest/configs/bgp-big-as-cloud index 694243d1e..10660ec87 100644 --- a/smoketest/configs/bgp-big-as-cloud +++ b/smoketest/configs/bgp-big-as-cloud @@ -1819,6 +1819,12 @@ system { } version 9 } + sflow { + agent-address auto + server 1.2.3.4 { + port 1234 + } + } syslog-facility daemon } host-name vyos diff --git a/smoketest/configs/bgp-dmvpn-hub b/smoketest/configs/bgp-dmvpn-hub new file mode 100644 index 000000000..fc5aadd8f --- /dev/null +++ b/smoketest/configs/bgp-dmvpn-hub @@ -0,0 +1,174 @@ +interfaces { + ethernet eth0 { + address 100.64.10.1/31 + } + ethernet eth1 { + } + loopback lo { + } + tunnel tun0 { + address 192.168.254.62/26 + encapsulation gre + multicast enable + parameters { + ip { + key 1 + } + } + source-address 100.64.10.1 + } +} +protocols { + bgp 65000 { + address-family { + ipv4-unicast { + network 172.20.0.0/16 { + } + } + } + neighbor 192.168.254.1 { + peer-group DMVPN + remote-as 65001 + } + neighbor 192.168.254.2 { + peer-group DMVPN + remote-as 65002 + } + neighbor 192.168.254.3 { + peer-group DMVPN + remote-as 65003 + } + parameters { + default { + no-ipv4-unicast + } + log-neighbor-changes + } + peer-group DMVPN { + address-family { + ipv4-unicast { + } + } + } + timers { + holdtime 30 + keepalive 10 + } + } + nhrp { + tunnel tun0 { + cisco-authentication secret + holding-time 300 + multicast dynamic + redirect + shortcut + } + } + static { + route 0.0.0.0/0 { + next-hop 100.64.10.0 { + } + } + route 172.20.0.0/16 { + blackhole { + distance 200 + } + } + } +} +system { + config-management { + commit-revisions 100 + } + conntrack { + modules { + ftp + h323 + nfs + pptp + sip + sqlnet + tftp + } + } + console { + device ttyS0 { + speed 115200 + } + } + host-name cpe-4 + login { + user vyos { + authentication { + encrypted-password $6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0 + plaintext-password "" + } + } + } + name-server 1.1.1.1 + name-server 8.8.8.8 + name-server 9.9.9.9 + ntp { + server time1.vyos.net { + } + server time2.vyos.net { + } + server time3.vyos.net { + } + } + syslog { + global { + facility all { + level info + } + facility protocols { + level debug + } + } + } +} +vpn { + ipsec { + esp-group ESP-DMVPN { + compression disable + lifetime 1800 + mode transport + pfs dh-group2 + proposal 1 { + encryption aes256 + hash sha1 + } + } + ike-group IKE-DMVPN { + close-action none + ikev2-reauth no + key-exchange ikev1 + lifetime 3600 + proposal 1 { + dh-group 2 + encryption aes256 + hash sha1 + } + } + ipsec-interfaces { + interface eth0 + } + profile NHRPVPN { + authentication { + mode pre-shared-secret + pre-shared-secret VyOS-topsecret + } + bind { + tunnel tun0 + } + esp-group ESP-DMVPN + ike-group IKE-DMVPN + } + } +} + + +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.0-epa3 + diff --git a/smoketest/configs/bgp-dmvpn-spoke b/smoketest/configs/bgp-dmvpn-spoke new file mode 100644 index 000000000..3d7503a9b --- /dev/null +++ b/smoketest/configs/bgp-dmvpn-spoke @@ -0,0 +1,201 @@ +interfaces { + ethernet eth0 { + vif 7 { + description PPPoE-UPLINK + } + } + ethernet eth1 { + address 172.17.1.1/24 + } + loopback lo { + } + pppoe pppoe1 { + authentication { + password cpe-1 + user cpe-1 + } + no-peer-dns + source-interface eth0.7 + } + tunnel tun0 { + address 192.168.254.1/26 + encapsulation gre + multicast enable + parameters { + ip { + key 1 + } + } + source-address 0.0.0.0 + } +} +nat { + source { + rule 10 { + log enable + outbound-interface pppoe1 + source { + address 172.17.0.0/16 + } + translation { + address masquerade + } + } + } +} +protocols { + bgp 65001 { + address-family { + ipv4-unicast { + network 172.17.0.0/16 { + } + } + } + neighbor 192.168.254.62 { + address-family { + ipv4-unicast { + } + } + remote-as 65000 + } + parameters { + default { + no-ipv4-unicast + } + log-neighbor-changes + } + timers { + holdtime 30 + keepalive 10 + } + } + nhrp { + tunnel tun0 { + cisco-authentication secret + holding-time 300 + map 192.168.254.62/26 { + nbma-address 100.64.10.1 + register + } + multicast nhs + redirect + shortcut + } + } + static { + route 172.17.0.0/16 { + blackhole { + distance 200 + } + } + } +} +service { + dhcp-server { + shared-network-name LAN-3 { + subnet 172.17.1.0/24 { + default-router 172.17.1.1 + name-server 172.17.1.1 + range 0 { + start 172.17.1.100 + stop 172.17.1.200 + } + } + } + } +} +system { + config-management { + commit-revisions 100 + } + conntrack { + modules { + ftp + h323 + nfs + pptp + sip + sqlnet + tftp + } + } + console { + device ttyS0 { + speed 115200 + } + } + host-name cpe-1 + login { + user vyos { + authentication { + encrypted-password $6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0 + plaintext-password "" + } + } + } + name-server 1.1.1.1 + name-server 8.8.8.8 + name-server 9.9.9.9 + ntp { + server time1.vyos.net { + } + server time2.vyos.net { + } + server time3.vyos.net { + } + } + syslog { + global { + facility all { + level info + } + facility protocols { + level debug + } + } + } +} +vpn { + ipsec { + esp-group ESP-DMVPN { + compression disable + lifetime 1800 + mode transport + pfs dh-group2 + proposal 1 { + encryption aes256 + hash sha1 + } + } + ike-group IKE-DMVPN { + close-action none + ikev2-reauth no + key-exchange ikev1 + lifetime 3600 + proposal 1 { + dh-group 2 + encryption aes256 + hash sha1 + } + } + ipsec-interfaces { + interface pppoe1 + } + profile NHRPVPN { + authentication { + mode pre-shared-secret + pre-shared-secret VyOS-topsecret + } + bind { + tunnel tun0 + } + esp-group ESP-DMVPN + ike-group IKE-DMVPN + } + } +} + + +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.0-epa3 diff --git a/smoketest/configs/bgp-small-ipv4-unicast b/smoketest/configs/bgp-small-ipv4-unicast new file mode 100644 index 000000000..83f1effd2 --- /dev/null +++ b/smoketest/configs/bgp-small-ipv4-unicast @@ -0,0 +1,77 @@ +interfaces { + ethernet eth0 { + address 192.0.2.1/24 + address 2001:db8::1/64 + } + loopback lo { + } +} +protocols { + bgp 65001 { + address-family { + ipv4-unicast { + network 10.0.150.0/23 { + } + } + ipv6-unicast { + network 2001:db8:200::/40 { + } + } + } + neighbor 192.0.2.10 { + remote-as 65010 + } + neighbor 192.0.2.11 { + remote-as 65011 + } + neighbor 2001:db8::10 { + remote-as 65010 + } + neighbor 2001:db8::11 { + remote-as 65011 + } + parameters { + log-neighbor-changes + } + } +} +service { + ssh { + disable-host-validation + port 22 + } +} +system { + config-management { + commit-revisions 200 + } + console { + device ttyS0 { + speed 115200 + } + } + domain-name vyos.net + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0 + plaintext-password "" + } + } + } + syslog { + global { + facility all { + level notice + } + facility protocols { + level debug + } + } + } +} + +/* Warning: Do not remove the following line. */ +/* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pptp@1:qos@1:quagga@6:snmp@1:ssh@1:system@9:vrrp@2:wanloadbalance@3:webgui@1:webproxy@1:webproxy@2:zone-policy@1" === */ +/* Release version: 1.2.5 */ diff --git a/smoketest/configs/tunnel-broker b/smoketest/configs/tunnel-broker index d4a5c2dfc..9a1e79719 100644 --- a/smoketest/configs/tunnel-broker +++ b/smoketest/configs/tunnel-broker @@ -56,13 +56,13 @@ interfaces { tunnel tun100 { address 172.16.0.1/30 encapsulation gre-bridge - local-ip 192.0.2.0 + local-ip 192.0.2.1 remote-ip 192.0.2.100 } tunnel tun200 { address 172.16.0.5/30 encapsulation gre - local-ip 192.0.2.1 + dhcp-interface eth0 remote-ip 192.0.2.101 } tunnel tun300 { diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 8a84199d9..9de961249 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -171,10 +171,10 @@ class BasicInterfaceTest: def test_add_multiple_ip_addresses(self): # Add address for intf in self._interfaces: + for option in self._options.get(intf, []): + self.cli_set(self._base_path + [intf] + option.split()) for addr in self._test_addr: self.cli_set(self._base_path + [intf, 'address', addr]) - for option in self._options.get(intf, []): - self.cli_set(self._base_path + [intf] + option.split()) self.cli_commit() @@ -286,34 +286,33 @@ class BasicInterfaceTest: base = self._base_path + [interface, 'vif', vlan] for address in self._test_addr: self.cli_set(base + ['address', address]) - self.cli_set(base + ['ingress-qos', '0:1']) - self.cli_set(base + ['egress-qos', '1:6']) self.cli_commit() for intf in self._interfaces: for vlan in self._vlan_range: vif = f'{intf}.{vlan}' - tmp = get_interface_config(f'{vif}') + for address in self._test_addr: + self.assertTrue(is_intf_addr_assigned(vif, address)) - tmp2 = dict_search('linkinfo.info_data.ingress_qos', tmp) - for item in tmp2 if tmp2 else []: - from_key = item['from'] - to_key = item['to'] - self.assertEqual(from_key, 0) - self.assertEqual(to_key, 1) + self.assertEqual(Interface(vif).get_admin_state(), 'up') - tmp2 = dict_search('linkinfo.info_data.egress_qos', tmp) - for item in tmp2 if tmp2 else []: - from_key = item['from'] - to_key = item['to'] - self.assertEqual(from_key, 1) - self.assertEqual(to_key, 6) + # T4064: Delete interface addresses, keep VLAN interface + for interface in self._interfaces: + base = self._base_path + [interface] + for vlan in self._vlan_range: + base = self._base_path + [interface, 'vif', vlan] + self.cli_delete(base + ['address']) + + self.cli_commit() + # Verify no IP address is assigned + for interface in self._interfaces: + for vlan in self._vlan_range: + vif = f'{intf}.{vlan}' for address in self._test_addr: - self.assertTrue(is_intf_addr_assigned(vif, address)) + self.assertFalse(is_intf_addr_assigned(vif, address)) - self.assertEqual(Interface(vif).get_admin_state(), 'up') def test_vif_8021q_mtu_limits(self): # XXX: This testcase is not allowed to run as first testcase, reason @@ -377,8 +376,6 @@ class BasicInterfaceTest: for vlan in self._vlan_range: base = self._base_path + [interface, 'vif', vlan] - for address in self._test_addr: - self.cli_set(base + ['address', address]) self.cli_set(base + ['ingress-qos', '0:1']) self.cli_set(base + ['egress-qos', '1:6']) @@ -403,9 +400,6 @@ class BasicInterfaceTest: self.assertEqual(from_key, 1) self.assertEqual(to_key, 6) - for address in self._test_addr: - self.assertTrue(is_intf_addr_assigned(vif, address)) - self.assertEqual(Interface(vif).get_admin_state(), 'up') new_ingress_qos_from = 1 @@ -416,8 +410,6 @@ class BasicInterfaceTest: base = self._base_path + [interface] for vlan in self._vlan_range: base = self._base_path + [interface, 'vif', vlan] - self.cli_delete(base + ['ingress-qos', '0:1']) - self.cli_delete(base + ['egress-qos', '1:6']) self.cli_set(base + ['ingress-qos', f'{new_ingress_qos_from}:{new_ingress_qos_to}']) self.cli_set(base + ['egress-qos', f'{new_egress_qos_from}:{new_egress_qos_to}']) @@ -518,6 +510,34 @@ class BasicInterfaceTest: tmp = get_interface_config(vif) self.assertEqual(tmp['mtu'], int(self._mtu)) + + # T4064: Delete interface addresses, keep VLAN interface + for interface in self._interfaces: + base = self._base_path + [interface] + for vif_s in self._qinq_range: + for vif_c in self._vlan_range: + self.cli_delete(self._base_path + [interface, 'vif-s', vif_s, 'vif-c', vif_c, 'address']) + + self.cli_commit() + # Verify no IP address is assigned + for interface in self._interfaces: + base = self._base_path + [interface] + for vif_s in self._qinq_range: + for vif_c in self._vlan_range: + vif = f'{interface}.{vif_s}.{vif_c}' + for address in self._test_addr: + self.assertFalse(is_intf_addr_assigned(vif, address)) + + # T3972: remove vif-c interfaces from vif-s + for interface in self._interfaces: + base = self._base_path + [interface] + for vif_s in self._qinq_range: + base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c'] + self.cli_delete(base) + + self.cli_commit() + + def test_vif_s_protocol_change(self): # XXX: This testcase is not allowed to run as first testcase, reason # is the Wireless test will first load the wifi kernel hwsim module @@ -587,11 +607,11 @@ class BasicInterfaceTest: self.cli_commit() for interface in self._interfaces: - base_options = f'-A FORWARD -o {interface} -p tcp -m tcp --tcp-flags SYN,RST SYN' - out = cmd('sudo iptables-save -t mangle') + base_options = f'oifname "{interface}"' + out = cmd('sudo nft list chain raw VYOS_TCP_MSS') for line in out.splitlines(): if line.startswith(base_options): - self.assertIn(f'--set-mss {mss}', line) + self.assertIn(f'tcp option maxseg size set {mss}', line) tmp = read_file(f'/proc/sys/net/ipv4/neigh/{interface}/base_reachable_time_ms') self.assertEqual(tmp, str((int(arp_tmo) * 1000))) # tmo value is in milli seconds @@ -642,11 +662,11 @@ class BasicInterfaceTest: self.cli_commit() for interface in self._interfaces: - base_options = f'-A FORWARD -o {interface} -p tcp -m tcp --tcp-flags SYN,RST SYN' - out = cmd('sudo ip6tables-save -t mangle') + base_options = f'oifname "{interface}"' + out = cmd('sudo nft list chain ip6 raw VYOS_TCP_MSS') for line in out.splitlines(): if line.startswith(base_options): - self.assertIn(f'--set-mss {mss}', line) + self.assertIn(f'tcp option maxseg size set {mss}', line) proc_base = f'/proc/sys/net/ipv6/conf/{interface}' diff --git a/smoketest/scripts/cli/test_configd_init.py b/smoketest/scripts/cli/test_configd_init.py new file mode 100755 index 000000000..5dec89963 --- /dev/null +++ b/smoketest/scripts/cli/test_configd_init.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 unittest +from time import sleep + +from vyos.util import cmd, is_systemd_service_running + +class TestConfigdInit(unittest.TestCase): + def setUp(self): + self.running_state = is_systemd_service_running('vyos-configd.service') + + def test_configd_init(self): + if not self.running_state: + cmd('sudo systemctl start vyos-configd.service') + # allow time for init to succeed/fail + sleep(2) + self.assertTrue(is_systemd_service_running('vyos-configd.service')) + + def tearDown(self): + if not self.running_state: + cmd('sudo systemctl stop vyos-configd.service') + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py index 09ca89721..cc0cdaec0 100644 --- a/smoketest/scripts/cli/test_container.py +++ b/smoketest/scripts/cli/test_container.py @@ -19,7 +19,6 @@ import json from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py new file mode 100755 index 000000000..1520020fd --- /dev/null +++ b/smoketest/scripts/cli/test_firewall.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 unittest + +from glob import glob + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.util import cmd + +sysfs_config = { + 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'default': '0', 'test_value': 'disable'}, + 'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'default': '1', 'test_value': 'enable'}, + 'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route', 'default': '0', 'test_value': 'enable'}, + 'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'}, + 'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'default': '-1', 'test_value': 'enable'}, + 'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians', 'default': '1', 'test_value': 'disable'}, + 'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'}, + 'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects', 'default': '1', 'test_value': 'disable'}, + 'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies', 'default': '1', 'test_value': 'disable'}, + 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337', 'default': '0', 'test_value': 'enable'} +} + +class TestFirewall(VyOSUnitTestSHIM.TestCase): + def setUp(self): + self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '172.16.10.1/24']) + + def tearDown(self): + self.cli_delete(['interfaces', 'ethernet', 'eth0']) + self.cli_commit() + self.cli_delete(['firewall']) + self.cli_commit() + + def test_groups(self): + self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) + self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53']) + self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '123']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'protocol', 'tcp']) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) + + self.cli_commit() + + nftables_search = [ + ['iifname "eth0"', 'jump smoketest'], + ['ip saddr { 172.16.99.0/24 }', 'ip daddr 172.16.10.10', 'tcp dport { 53, 123 }', 'return'], + ] + + nftables_output = cmd('sudo nft list table ip filter') + + 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(matched) + + def test_basic_rules(self): + self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'action', 'reject']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'protocol', 'tcp_udp']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'destination', 'port', '8888']) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) + + self.cli_commit() + + nftables_search = [ + ['iifname "eth0"', 'jump smoketest'], + ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'return'], + ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'reject'], + ['smoketest default-action', 'drop'] + ] + + nftables_output = cmd('sudo nft list table ip filter') + + 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(matched) + + def test_basic_rules_ipv6(self): + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'source', 'address', '2002::1']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'destination', 'address', '2002::1:1']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'action', 'reject']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'protocol', 'tcp_udp']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'destination', 'port', '8888']) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'ipv6-name', 'v6-smoketest']) + + self.cli_commit() + + nftables_search = [ + ['iifname "eth0"', 'jump v6-smoketest'], + ['saddr 2002::1', 'daddr 2002::1:1', 'return'], + ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'reject'], + ['smoketest default-action', 'drop'] + ] + + nftables_output = cmd('sudo nft list table ip6 filter') + + 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(matched) + + def test_sysfs(self): + for name, conf in sysfs_config.items(): + paths = glob(conf['sysfs']) + for path in paths: + with open(path, 'r') as f: + self.assertEqual(f.read().strip(), conf['default'], msg=path) + + self.cli_set(['firewall', name.replace("_", "-"), conf['test_value']]) + + self.cli_commit() + + for name, conf in sysfs_config.items(): + paths = glob(conf['sysfs']) + for path in paths: + with open(path, 'r') as f: + self.assertNotEqual(f.read().strip(), conf['default'], msg=path) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_ha_vrrp.py b/smoketest/scripts/cli/test_ha_vrrp.py index bbf24095b..23a9f7796 100755 --- a/smoketest/scripts/cli/test_ha_vrrp.py +++ b/smoketest/scripts/cli/test_ha_vrrp.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.ifconfig.vrrp import VRRP from vyos.util import cmd @@ -45,7 +44,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): for group in groups: vlan_id = group.lstrip('VLAN') - self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id]) + self.cli_delete(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id]) self.cli_delete(base_path) self.cli_commit() @@ -81,6 +80,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'priority 100', config) # default value self.assertIn(f'advert_int 1', config) # default value self.assertIn(f'preempt_delay 0', config) # default value + self.assertNotIn(f'use_vmac', config) self.assertIn(f' {vip}', config) def test_02_simple_options(self): @@ -108,7 +108,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): # Authentication self.cli_set(group_base + ['authentication', 'type', 'plaintext-password']) - self.cli_set(group_base + ['authentication', 'password', f'vyos-{group}']) + self.cli_set(group_base + ['authentication', 'password', f'{group}']) # commit changes self.cli_commit() @@ -129,7 +129,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' {vip}', config) # Authentication - self.assertIn(f'auth_pass "vyos-{group}"', config) + self.assertIn(f'auth_pass "{group}"', config) self.assertIn(f'auth_type PASS', config) def test_03_sync_group(self): @@ -158,6 +158,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'interface {vrrp_interface}.{vlan_id}', config) self.assertIn(f'virtual_router_id {vlan_id}', config) + self.assertNotIn(f'use_vmac', config) self.assertIn(f' {vip}', config) config = getConfig(f'vrrp_sync_group {sync_group}') @@ -166,4 +167,4 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'{group}', config) if __name__ == '__main__': - unittest.main(verbosity=2, failfast=True) + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_geneve.py b/smoketest/scripts/cli/test_interfaces_geneve.py index 129ee71e5..6233ade6e 100755 --- a/smoketest/scripts/cli/test_interfaces_geneve.py +++ b/smoketest/scripts/cli/test_interfaces_geneve.py @@ -16,7 +16,6 @@ import unittest -from vyos.configsession import ConfigSession from vyos.ifconfig import Interface from vyos.util import get_interface_config diff --git a/smoketest/scripts/cli/test_interfaces_netns.py b/smoketest/scripts/cli/test_interfaces_netns.py new file mode 100755 index 000000000..9975a6b09 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_netns.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 re +import os +import json +import unittest + +from netifaces import interfaces +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Interface +from vyos.ifconfig import Section +from vyos.util import cmd + +base_path = ['netns'] +namespaces = ['mgmt', 'front', 'back', 'ams-ix'] + +class NETNSTest(VyOSUnitTestSHIM.TestCase): + + def setUp(self): + self._interfaces = ['dum10', 'dum12', 'dum50'] + + def test_create_netns(self): + for netns in namespaces: + base = base_path + ['name', netns] + self.cli_set(base) + + # commit changes + self.cli_commit() + + netns_list = cmd('ip netns ls') + + # Verify NETNS configuration + for netns in namespaces: + self.assertTrue(netns in netns_list) + + + def test_netns_assign_interface(self): + netns = 'foo' + self.cli_set(['netns', 'name', netns]) + + # Set + for iface in self._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) + + # Delete + for iface in self._interfaces: + self.cli_delete(['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.assertNotIn(iface, netns_iface_list) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py index 7ce1b9872..f8a6ae986 100755 --- a/smoketest/scripts/cli/test_interfaces_openvpn.py +++ b/smoketest/scripts/cli/test_interfaces_openvpn.py @@ -23,7 +23,6 @@ from netifaces import interfaces from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index 67edce2a0..4f1e1ee99 100755 --- a/smoketest/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -20,7 +20,6 @@ import unittest from psutil import process_iter from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError config_file = '/etc/ppp/peers/{}' diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py index 841527d21..fc2e254d6 100755 --- a/smoketest/scripts/cli/test_interfaces_tunnel.py +++ b/smoketest/scripts/cli/test_interfaces_tunnel.py @@ -156,26 +156,6 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(self._base_path + [interface]) self.cli_commit() - def test_tunnel_verify_local_dhcp(self): - # We can not use source-address and dhcp-interface at the same time - - interface = f'tun1020' - local_if_addr = f'10.0.0.1/24' - - self.cli_set(self._base_path + [interface, 'address', local_if_addr]) - self.cli_set(self._base_path + [interface, 'encapsulation', 'gre']) - self.cli_set(self._base_path + [interface, 'source-address', self.local_v4]) - self.cli_set(self._base_path + [interface, 'remote', remote_ip4]) - self.cli_set(self._base_path + [interface, 'dhcp-interface', 'eth0']) - - # source-address and dhcp-interface can not be used at the same time - with self.assertRaises(ConfigSessionError): - self.cli_commit() - self.cli_delete(self._base_path + [interface, 'dhcp-interface']) - - # Check if commit is ok - self.cli_commit() - def test_tunnel_parameters_gre(self): interface = f'tun1030' gre_key = '10' diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index 7b420cd51..9278adadd 100755 --- a/smoketest/scripts/cli/test_interfaces_vxlan.py +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py @@ -16,7 +16,7 @@ import unittest -from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError from vyos.ifconfig import Interface from vyos.util import get_interface_config @@ -79,6 +79,9 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): label = options['linkinfo']['info_data']['label'] self.assertIn(f'parameters ipv6 flowlabel {label}', self._options[interface]) + if any('external' in s for s in self._options[interface]): + self.assertTrue(options['linkinfo']['info_data']['external']) + self.assertEqual('vxlan', options['linkinfo']['info_kind']) self.assertEqual('set', options['linkinfo']['info_data']['df']) self.assertEqual(f'0x{tos}', options['linkinfo']['info_data']['tos']) @@ -86,5 +89,36 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): self.assertEqual(Interface(interface).get_admin_state(), 'up') ttl += 10 + def test_vxlan_external(self): + interface = 'vxlan0' + source_address = '192.0.2.1' + self.cli_set(self._base_path + [interface, 'external']) + self.cli_set(self._base_path + [interface, 'source-address', source_address]) + + # Both 'VNI' and 'external' can not be specified at the same time. + self.cli_set(self._base_path + [interface, 'vni', '111']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(self._base_path + [interface, 'vni']) + + # Now add some more interfaces - this must fail and a CLI error needs + # to be generated as Linux can only handle one VXLAN tunnel when using + # external mode. + for intf in self._interfaces: + for option in self._options.get(intf, []): + self.cli_set(self._base_path + [intf] + option.split()) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # Remove those test interfaces again + for intf in self._interfaces: + self.cli_delete(self._base_path + [intf]) + + self.cli_commit() + + options = get_interface_config(interface) + self.assertTrue(options['linkinfo']['info_data']['external']) + self.assertEqual('vxlan', options['linkinfo']['info_kind']) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py index 3707eaac3..aaf27a2c4 100755 --- a/smoketest/scripts/cli/test_interfaces_wireguard.py +++ b/smoketest/scripts/cli/test_interfaces_wireguard.py @@ -18,7 +18,6 @@ import os import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError base_path = ['interfaces', 'wireguard'] diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 0706f234e..75c628244 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -20,7 +20,6 @@ import json import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import dict_search diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index 7721105e0..8afe0da26 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -21,7 +21,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import dict_search diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py index deaf23b05..45a4bd61e 100755 --- a/smoketest/scripts/cli/test_pki.py +++ b/smoketest/scripts/cli/test_pki.py @@ -17,7 +17,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError base_path = ['pki'] diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index c2288a86a..5844e1ec1 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd @@ -308,7 +307,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): continue for rule, rule_config in as_path_config['rule'].items(): - tmp = f'bgp as-path access-list {as_path}' + tmp = f'bgp as-path access-list {as_path} seq {rule}' if rule_config['action'] == 'permit': tmp += ' permit' else: diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py new file mode 100755 index 000000000..70a234187 --- /dev/null +++ b/smoketest/scripts/cli/test_policy_route.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.util import cmd + +mark = '100' +table_mark_offset = 0x7fffffff +table_id = '101' + +class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): + def setUp(self): + self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '172.16.10.1/24']) + self.cli_set(['protocols', 'static', 'table', '101', 'route', '0.0.0.0/0', 'interface', 'eth0']) + + def tearDown(self): + self.cli_delete(['interfaces', 'ethernet', 'eth0']) + self.cli_delete(['policy', 'route']) + self.cli_delete(['policy', 'ipv6-route']) + self.cli_commit() + + def test_pbr_mark(self): + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'mark', mark]) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route', 'smoketest']) + + self.cli_commit() + + mark_hex = "{0:#010x}".format(int(mark)) + + nftables_search = [ + ['iifname "eth0"', 'jump VYOS_PBR_smoketest'], + ['ip daddr 172.16.10.10', 'ip saddr 172.16.20.10', 'meta mark set ' + mark_hex], + ] + + nftables_output = cmd('sudo nft list table ip mangle') + + 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(matched) + + def test_pbr_table(self): + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp_udp']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'port', '8888']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'table', table_id]) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route', 'smoketest']) + + self.cli_commit() + + mark_hex = "{0:#010x}".format(table_mark_offset - int(table_id)) + + nftables_search = [ + ['iifname "eth0"', 'jump VYOS_PBR_smoketest'], + ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'meta mark set ' + mark_hex] + ] + + nftables_output = cmd('sudo nft list table ip mangle') + + 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(matched) + + ip_rule_search = [ + ['fwmark ' + hex(table_mark_offset - int(table_id)), 'lookup ' + table_id] + ] + + ip_rule_output = cmd('ip rule show') + + for search in ip_rule_search: + matched = False + for line in ip_rule_output.split("\n"): + if all(item in line for item in search): + matched = True + break + self.assertTrue(matched) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py index a57f8d5f2..fdc254a05 100755 --- a/smoketest/scripts/cli/test_protocols_bfd.py +++ b/smoketest/scripts/cli/test_protocols_bfd.py @@ -17,7 +17,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import process_named_running @@ -25,30 +24,35 @@ PROCESS_NAME = 'bfdd' base_path = ['protocols', 'bfd'] dum_if = 'dum1001' +vrf_name = 'red' peers = { '192.0.2.10' : { 'intv_rx' : '500', 'intv_tx' : '600', 'multihop' : '', 'source_addr': '192.0.2.254', - }, + 'profile' : 'foo-bar-baz', + }, '192.0.2.20' : { 'echo_mode' : '', 'intv_echo' : '100', 'intv_mult' : '100', 'intv_rx' : '222', 'intv_tx' : '333', + 'passive' : '', 'shutdown' : '', + 'profile' : 'foo', 'source_intf': dum_if, - }, - '2001:db8::a' : { + }, + '2001:db8::1000:1' : { 'source_addr': '2001:db8::1', - 'source_intf': dum_if, - }, - '2001:db8::b' : { + 'vrf' : vrf_name, + }, + '2001:db8::2000:1' : { 'source_addr': '2001:db8::1', 'multihop' : '', - }, + 'profile' : 'baz_foo', + }, } profiles = { @@ -60,9 +64,15 @@ profiles = { 'intv_tx' : '333', 'shutdown' : '', }, - 'bar' : { + 'foo-bar-baz' : { + 'intv_mult' : '4', + 'intv_rx' : '400', + 'intv_tx' : '400', + }, + 'baz_foo' : { 'intv_mult' : '102', 'intv_rx' : '444', + 'passive' : '', }, } @@ -74,6 +84,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running(PROCESS_NAME)) def test_bfd_peer(self): + self.cli_set(['vrf', 'name', vrf_name, 'table', '1000']) + for peer, peer_config in peers.items(): if 'echo_mode' in peer_config: self.cli_set(base_path + ['peer', peer, 'echo-mode']) @@ -87,18 +99,22 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['peer', peer, 'interval', 'transmit', peer_config["intv_tx"]]) if 'multihop' in peer_config: self.cli_set(base_path + ['peer', peer, 'multihop']) + if 'passive' in peer_config: + self.cli_set(base_path + ['peer', peer, 'passive']) if 'shutdown' in peer_config: self.cli_set(base_path + ['peer', peer, 'shutdown']) if 'source_addr' in peer_config: self.cli_set(base_path + ['peer', peer, 'source', 'address', peer_config["source_addr"]]) if 'source_intf' in peer_config: self.cli_set(base_path + ['peer', peer, 'source', 'interface', peer_config["source_intf"]]) + if 'vrf' in peer_config: + self.cli_set(base_path + ['peer', peer, 'vrf', peer_config["vrf"]]) # commit changes self.cli_commit() # Verify FRR bgpd configuration - frrconfig = self.getFRRconfig('bfd') + frrconfig = self.getFRRconfig('bfd', daemon=PROCESS_NAME) for peer, peer_config in peers.items(): tmp = f'peer {peer}' if 'multihop' in peer_config: @@ -107,28 +123,33 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): tmp += f' local-address {peer_config["source_addr"]}' if 'source_intf' in peer_config: tmp += f' interface {peer_config["source_intf"]}' + if 'vrf' in peer_config: + tmp += f' vrf {peer_config["vrf"]}' self.assertIn(tmp, frrconfig) - peerconfig = self.getFRRconfig(f' peer {peer}', end='') + peerconfig = self.getFRRconfig(f' peer {peer}', end='', daemon=PROCESS_NAME) if 'echo_mode' in peer_config: self.assertIn(f'echo-mode', peerconfig) if 'intv_echo' in peer_config: - self.assertIn(f'echo-interval {peer_config["intv_echo"]}', peerconfig) + self.assertIn(f'echo receive-interval {peer_config["intv_echo"]}', peerconfig) + self.assertIn(f'echo transmit-interval {peer_config["intv_echo"]}', peerconfig) if 'intv_mult' in peer_config: self.assertIn(f'detect-multiplier {peer_config["intv_mult"]}', peerconfig) if 'intv_rx' in peer_config: self.assertIn(f'receive-interval {peer_config["intv_rx"]}', peerconfig) if 'intv_tx' in peer_config: self.assertIn(f'transmit-interval {peer_config["intv_tx"]}', peerconfig) + if 'passive' in peer_config: + self.assertIn(f'passive-mode', peerconfig) if 'shutdown' in peer_config: self.assertIn(f'shutdown', peerconfig) else: self.assertNotIn(f'shutdown', peerconfig) - def test_bfd_profile(self): - peer = '192.0.2.10' + self.cli_delete(['vrf', 'name', vrf_name]) + def test_bfd_profile(self): for profile, profile_config in profiles.items(): if 'echo_mode' in profile_config: self.cli_set(base_path + ['profile', profile, 'echo-mode']) @@ -140,10 +161,25 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['profile', profile, 'interval', 'receive', profile_config["intv_rx"]]) if 'intv_tx' in profile_config: self.cli_set(base_path + ['profile', profile, 'interval', 'transmit', profile_config["intv_tx"]]) + if 'passive' in profile_config: + self.cli_set(base_path + ['profile', profile, 'passive']) if 'shutdown' in profile_config: self.cli_set(base_path + ['profile', profile, 'shutdown']) - self.cli_set(base_path + ['peer', peer, 'profile', list(profiles)[0]]) + for peer, peer_config in peers.items(): + if 'profile' in peer_config: + self.cli_set(base_path + ['peer', peer, 'profile', peer_config["profile"] + 'wrong']) + if 'source_addr' in peer_config: + self.cli_set(base_path + ['peer', peer, 'source', 'address', peer_config["source_addr"]]) + if 'source_intf' in peer_config: + self.cli_set(base_path + ['peer', peer, 'source', 'interface', peer_config["source_intf"]]) + + # BFD profile does not exist! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + for peer, peer_config in peers.items(): + if 'profile' in peer_config: + self.cli_set(base_path + ['peer', peer, 'profile', peer_config["profile"]]) # commit changes self.cli_commit() @@ -152,19 +188,27 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): for profile, profile_config in profiles.items(): config = self.getFRRconfig(f' profile {profile}', endsection='^ !') if 'echo_mode' in profile_config: - self.assertIn(f'echo-mode', config) + self.assertIn(f' echo-mode', config) if 'intv_echo' in profile_config: - self.assertIn(f'echo-interval {profile_config["intv_echo"]}', config) + self.assertIn(f' echo receive-interval {profile_config["intv_echo"]}', config) + self.assertIn(f' echo transmit-interval {profile_config["intv_echo"]}', config) if 'intv_mult' in profile_config: - self.assertIn(f'detect-multiplier {profile_config["intv_mult"]}', config) + self.assertIn(f' detect-multiplier {profile_config["intv_mult"]}', config) if 'intv_rx' in profile_config: - self.assertIn(f'receive-interval {profile_config["intv_rx"]}', config) + self.assertIn(f' receive-interval {profile_config["intv_rx"]}', config) if 'intv_tx' in profile_config: - self.assertIn(f'transmit-interval {profile_config["intv_tx"]}', config) + self.assertIn(f' transmit-interval {profile_config["intv_tx"]}', config) + if 'passive' in profile_config: + self.assertIn(f' passive-mode', config) if 'shutdown' in profile_config: - self.assertIn(f'shutdown', config) + self.assertIn(f' shutdown', config) else: self.assertNotIn(f'shutdown', config) + for peer, peer_config in peers.items(): + peerconfig = self.getFRRconfig(f' peer {peer}', end='', daemon=PROCESS_NAME) + if 'profile' in peer_config: + self.assertIn(f' profile {peer_config["profile"]}', peerconfig) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 29b5aa9d1..d7230baf4 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -32,9 +32,11 @@ prefix_list_in = 'pfx-foo-in' prefix_list_out = 'pfx-foo-out' prefix_list_in6 = 'pfx-foo-in6' prefix_list_out6 = 'pfx-foo-out6' +bfd_profile = 'foo-bar-baz' neighbor_config = { '192.0.2.1' : { + 'bfd' : '', 'cap_dynamic' : '', 'cap_ext_next' : '', 'remote_as' : '100', @@ -51,23 +53,30 @@ neighbor_config = { 'addpath_all' : '', }, '192.0.2.2' : { + 'bfd_profile' : bfd_profile, 'remote_as' : '200', 'shutdown' : '', 'no_cap_nego' : '', 'port' : '667', 'cap_strict' : '', + 'advertise_map': route_map_in, + 'non_exist_map': route_map_out, 'pfx_list_in' : prefix_list_in, 'pfx_list_out' : prefix_list_out, 'no_send_comm_std' : '', }, '192.0.2.3' : { + 'advertise_map': route_map_in, 'description' : 'foo bar baz', 'remote_as' : '200', 'passive' : '', 'multi_hop' : '5', 'update_src' : 'lo', + 'peer_group' : 'foo', }, '2001:db8::1' : { + 'advertise_map': route_map_in, + 'exist_map' : route_map_out, 'cap_dynamic' : '', 'cap_ext_next' : '', 'remote_as' : '123', @@ -83,6 +92,7 @@ neighbor_config = { 'route_map_out': route_map_out, 'no_send_comm_std' : '', 'addpath_per_as' : '', + 'peer_group' : 'foo-bar', }, '2001:db8::2' : { 'remote_as' : '456', @@ -93,11 +103,15 @@ neighbor_config = { 'pfx_list_in' : prefix_list_in6, 'pfx_list_out' : prefix_list_out6, 'no_send_comm_ext' : '', + 'peer_group' : 'foo-bar_baz', }, } peer_group_config = { 'foo' : { + 'advertise_map': route_map_in, + 'exist_map' : route_map_out, + 'bfd' : '', 'remote_as' : '100', 'passive' : '', 'password' : 'VyOS-Secure123', @@ -105,7 +119,8 @@ peer_group_config = { 'cap_over' : '', 'ttl_security': '5', }, - 'bar' : { + 'foo-bar' : { + 'advertise_map': route_map_in, 'description' : 'foo peer bar group', 'remote_as' : '200', 'shutdown' : '', @@ -115,7 +130,10 @@ peer_group_config = { 'pfx_list_out' : prefix_list_out, 'no_send_comm_ext' : '', }, - 'baz' : { + 'foo-bar_baz' : { + 'advertise_map': route_map_in, + 'non_exist_map': route_map_out, + 'bfd_profile' : bfd_profile, 'cap_dynamic' : '', 'cap_ext_next' : '', 'remote_as' : '200', @@ -128,23 +146,34 @@ peer_group_config = { } class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self.cli_set(['policy', 'route-map', route_map_in, 'rule', '10', 'action', 'permit']) - self.cli_set(['policy', 'route-map', route_map_out, 'rule', '10', 'action', 'permit']) - self.cli_set(['policy', 'prefix-list', prefix_list_in, 'rule', '10', 'action', 'permit']) - self.cli_set(['policy', 'prefix-list', prefix_list_in, 'rule', '10', 'prefix', '192.0.2.0/25']) - self.cli_set(['policy', 'prefix-list', prefix_list_out, 'rule', '10', 'action', 'permit']) - self.cli_set(['policy', 'prefix-list', prefix_list_out, 'rule', '10', 'prefix', '192.0.2.128/25']) - - self.cli_set(['policy', 'prefix-list6', prefix_list_in6, 'rule', '10', 'action', 'permit']) - self.cli_set(['policy', 'prefix-list6', prefix_list_in6, 'rule', '10', 'prefix', '2001:db8:1000::/64']) - self.cli_set(['policy', 'prefix-list6', prefix_list_out6, 'rule', '10', 'action', 'deny']) - self.cli_set(['policy', 'prefix-list6', prefix_list_out6, 'rule', '10', 'prefix', '2001:db8:2000::/64']) + @classmethod + def setUpClass(cls): + super(cls, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + cls.cli_set(cls, ['policy', 'route-map', route_map_in, 'rule', '10', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'route-map', route_map_out, 'rule', '10', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_in, 'rule', '10', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_in, 'rule', '10', 'prefix', '192.0.2.0/25']) + cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_out, 'rule', '10', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_out, 'rule', '10', 'prefix', '192.0.2.128/25']) + + cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_in6, 'rule', '10', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_in6, 'rule', '10', 'prefix', '2001:db8:1000::/64']) + cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out6, 'rule', '10', 'action', 'deny']) + cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out6, 'rule', '10', 'prefix', '2001:db8:2000::/64']) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['policy']) + def setUp(self): self.cli_set(base_path + ['local-as', ASN]) def tearDown(self): - self.cli_delete(['policy']) self.cli_delete(['vrf']) self.cli_delete(base_path) self.cli_commit() @@ -154,6 +183,11 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): def verify_frr_config(self, peer, peer_config, frrconfig): # recurring patterns to verify for both a simple neighbor and a peer-group + if 'bfd' in peer_config: + self.assertIn(f' neighbor {peer} bfd', frrconfig) + if 'bfd_profile' in peer_config: + self.assertIn(f' neighbor {peer} bfd profile {peer_config["bfd_profile"]}', frrconfig) + self.assertIn(f' neighbor {peer} bfd check-control-plane-failure', frrconfig) if 'cap_dynamic' in peer_config: self.assertIn(f' neighbor {peer} capability dynamic', frrconfig) if 'cap_ext_next' in peer_config: @@ -198,7 +232,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' neighbor {peer} addpath-tx-all-paths', frrconfig) if 'addpath_per_as' in peer_config: self.assertIn(f' neighbor {peer} addpath-tx-bestpath-per-AS', frrconfig) - + if 'advertise_map' in peer_config: + base = f' neighbor {peer} advertise-map {peer_config["advertise_map"]}' + if 'exist_map' in peer_config: + base = f'{base} exist-map {peer_config["exist_map"]}' + if 'non_exist_map' in peer_config: + base = f'{base} non-exist-map {peer_config["non_exist_map"]}' + self.assertIn(base, frrconfig) def test_bgp_01_simple(self): router_id = '127.0.0.1' @@ -208,6 +248,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): max_path_v4ibgp = '4' max_path_v6 = '8' max_path_v6ibgp = '16' + cond_adv_timer = '30' + min_hold_time = '2' self.cli_set(base_path + ['parameters', 'router-id', router_id]) self.cli_set(base_path + ['parameters', 'log-neighbor-changes']) @@ -221,8 +263,6 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): # Default local preference (higher = more preferred, default value is 100) self.cli_set(base_path + ['parameters', 'default', 'local-pref', local_pref]) - # Deactivate IPv4 unicast for a peer by default - self.cli_set(base_path + ['parameters', 'default', 'no-ipv4-unicast']) self.cli_set(base_path + ['parameters', 'graceful-restart', 'stalepath-time', stalepath_time]) self.cli_set(base_path + ['parameters', 'graceful-shutdown']) self.cli_set(base_path + ['parameters', 'ebgp-requires-policy']) @@ -231,6 +271,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['parameters', 'bestpath', 'bandwidth', 'default-weight-for-missing']) self.cli_set(base_path + ['parameters', 'bestpath', 'compare-routerid']) + self.cli_set(base_path + ['parameters', 'conditional-advertisement', 'timer', cond_adv_timer]) + self.cli_set(base_path + ['parameters', 'fast-convergence']) + self.cli_set(base_path + ['parameters', 'minimum-holdtime', min_hold_time]) + self.cli_set(base_path + ['parameters', 'reject-as-sets']) + self.cli_set(base_path + ['parameters', 'shutdown']) + self.cli_set(base_path + ['parameters', 'suppress-fib-pending']) + # AFI maximum path support self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'maximum-paths', 'ebgp', max_path_v4]) self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'maximum-paths', 'ibgp', max_path_v4ibgp]) @@ -246,12 +293,17 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' bgp router-id {router_id}', frrconfig) self.assertIn(f' bgp log-neighbor-changes', frrconfig) self.assertIn(f' bgp default local-preference {local_pref}', frrconfig) - self.assertIn(f' no bgp default ipv4-unicast', frrconfig) + self.assertIn(f' bgp conditional-advertisement timer {cond_adv_timer}', frrconfig) + self.assertIn(f' bgp fast-convergence', frrconfig) self.assertIn(f' bgp graceful-restart stalepath-time {stalepath_time}', frrconfig) self.assertIn(f' bgp graceful-shutdown', frrconfig) self.assertIn(f' bgp bestpath as-path multipath-relax', frrconfig) self.assertIn(f' bgp bestpath bandwidth default-weight-for-missing', frrconfig) self.assertIn(f' bgp bestpath compare-routerid', frrconfig) + self.assertIn(f' bgp minimum-holdtime {min_hold_time}', frrconfig) + self.assertIn(f' bgp reject-as-sets', frrconfig) + self.assertIn(f' bgp shutdown', frrconfig) + self.assertIn(f' bgp suppress-fib-pending', frrconfig) self.assertNotIn(f'bgp ebgp-requires-policy', frrconfig) afiv4_config = self.getFRRconfig(' address-family ipv4 unicast') @@ -273,6 +325,11 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): if 'adv_interv' in peer_config: self.cli_set(base_path + ['neighbor', peer, 'advertisement-interval', peer_config["adv_interv"]]) + if 'bfd' in peer_config: + self.cli_set(base_path + ['neighbor', peer, 'bfd']) + if 'bfd_profile' in peer_config: + self.cli_set(base_path + ['neighbor', peer, 'bfd', 'profile', peer_config["bfd_profile"]]) + self.cli_set(base_path + ['neighbor', peer, 'bfd', 'check-control-plane-failure']) if 'cap_dynamic' in peer_config: self.cli_set(base_path + ['neighbor', peer, 'capability', 'dynamic']) if 'cap_ext_next' in peer_config: @@ -322,6 +379,20 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): if 'addpath_per_as' in peer_config: self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'addpath-tx-per-as']) + # Conditional advertisement + if 'advertise_map' in peer_config: + self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'conditionally-advertise', 'advertise-map', peer_config["advertise_map"]]) + # Either exist-map or non-exist-map needs to be specified + if 'exist_map' not in peer_config and 'non_exist_map' not in peer_config: + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'conditionally-advertise', 'exist-map', route_map_in]) + + if 'exist_map' in peer_config: + self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'conditionally-advertise', 'exist-map', peer_config["exist_map"]]) + if 'non_exist_map' in peer_config: + self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'conditionally-advertise', 'non-exist-map', peer_config["non_exist_map"]]) + # commit changes self.cli_commit() @@ -342,6 +413,11 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): def test_bgp_03_peer_groups(self): # Test out individual peer-group configuration items for peer_group, config in peer_group_config.items(): + if 'bfd' in config: + self.cli_set(base_path + ['peer-group', peer_group, 'bfd']) + if 'bfd_profile' in config: + self.cli_set(base_path + ['peer-group', peer_group, 'bfd', 'profile', config["bfd_profile"]]) + self.cli_set(base_path + ['peer-group', peer_group, 'bfd', 'check-control-plane-failure']) if 'cap_dynamic' in config: self.cli_set(base_path + ['peer-group', peer_group, 'capability', 'dynamic']) if 'cap_ext_next' in config: @@ -385,6 +461,24 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): if 'addpath_per_as' in config: self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'addpath-tx-per-as']) + # Conditional advertisement + if 'advertise_map' in config: + self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'conditionally-advertise', 'advertise-map', config["advertise_map"]]) + # Either exist-map or non-exist-map needs to be specified + if 'exist_map' not in config and 'non_exist_map' not in config: + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'conditionally-advertise', 'exist-map', route_map_in]) + + if 'exist_map' in config: + self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'conditionally-advertise', 'exist-map', config["exist_map"]]) + if 'non_exist_map' in config: + self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'conditionally-advertise', 'non-exist-map', config["non_exist_map"]]) + + for peer, peer_config in neighbor_config.items(): + if 'peer_group' in peer_config: + self.cli_set(base_path + ['neighbor', peer, 'peer-group', peer_config['peer_group']]) + # commit changes self.cli_commit() @@ -396,6 +490,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' neighbor {peer_group} peer-group', frrconfig) self.verify_frr_config(peer, peer_config, frrconfig) + for peer, peer_config in neighbor_config.items(): + if 'peer_group' in peer_config: + self.assertIn(f' neighbor {peer} peer-group {peer_config["peer_group"]}', frrconfig) + def test_bgp_04_afi_ipv4(self): networks = { @@ -756,4 +854,4 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' exit-address-family', afi_config) if __name__ == '__main__': - unittest.main(verbosity=2)
\ No newline at end of file + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_igmp-proxy.py b/smoketest/scripts/cli/test_protocols_igmp-proxy.py index 1eaf21722..079b5bee5 100755 --- a/smoketest/scripts/cli/test_protocols_igmp-proxy.py +++ b/smoketest/scripts/cli/test_protocols_igmp-proxy.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import read_file from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py index 8170f2b56..7f51c7178 100755 --- a/smoketest/scripts/cli/test_protocols_isis.py +++ b/smoketest/scripts/cli/test_protocols_isis.py @@ -17,7 +17,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.util import process_named_running @@ -199,18 +198,19 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.assertIn(f' area-password clear {password}', tmp) - def test_isis_06_spf_delay(self): - self.isis_base_config() - + def test_isis_06_spf_delay_bfd(self): network = 'point-to-point' holddown = '10' init_delay = '50' long_delay = '200' short_delay = '100' time_to_learn = '75' + bfd_profile = 'isis-bfd' + self.cli_set(base_path + ['net', net]) for interface in self._interfaces: self.cli_set(base_path + ['interface', interface, 'network', network]) + self.cli_set(base_path + ['interface', interface, 'bfd', 'profile', bfd_profile]) self.cli_set(base_path + ['spf-delay-ietf', 'holddown', holddown]) # verify() - All types of spf-delay must be configured @@ -227,11 +227,6 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(base_path + ['spf-delay-ietf', 'long-delay', long_delay]) - # verify() - All types of spf-delay must be configured - with self.assertRaises(ConfigSessionError): - self.cli_commit() - self.cli_set(base_path + ['spf-delay-ietf', 'short-delay', short_delay]) # verify() - All types of spf-delay must be configured with self.assertRaises(ConfigSessionError): @@ -251,6 +246,8 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.assertIn(f' ip router isis {domain}', tmp) self.assertIn(f' ipv6 router isis {domain}', tmp) self.assertIn(f' isis network {network}', tmp) + self.assertIn(f' isis bfd', tmp) + self.assertIn(f' isis bfd profile {bfd_profile}', tmp) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py new file mode 100755 index 000000000..13d38d01b --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_mpls.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section +from vyos.util import process_named_running + +PROCESS_NAME = 'ldpd' +base_path = ['protocols', 'mpls', 'ldp'] + +peers = { + '192.0.2.10' : { + 'intv_rx' : '500', + 'intv_tx' : '600', + 'multihop' : '', + 'source_addr': '192.0.2.254', + }, + '192.0.2.20' : { + 'echo_mode' : '', + 'intv_echo' : '100', + 'intv_mult' : '100', + 'intv_rx' : '222', + 'intv_tx' : '333', + 'passive' : '', + 'shutdown' : '', + }, + '2001:db8::a' : { + 'source_addr': '2001:db8::1', + }, + '2001:db8::b' : { + 'source_addr': '2001:db8::1', + 'multihop' : '', + }, +} + +profiles = { + 'foo' : { + 'echo_mode' : '', + 'intv_echo' : '100', + 'intv_mult' : '101', + 'intv_rx' : '222', + 'intv_tx' : '333', + 'shutdown' : '', + }, + 'bar' : { + 'intv_mult' : '102', + 'intv_rx' : '444', + 'passive' : '', + }, +} + +class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(cls, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + def test_mpls_basic(self): + self.debug = True + router_id = '1.2.3.4' + transport_ipv4_addr = '5.6.7.8' + interfaces = Section.interfaces('ethernet') + + self.cli_set(base_path + ['router-id', router_id]) + + # At least one LDP interface must be configured + with self.assertRaises(ConfigSessionError): + self.cli_commit() + for interface in interfaces: + self.cli_set(base_path + ['interface', interface]) + + # LDP transport address missing + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['discovery', 'transport-ipv4-address', transport_ipv4_addr]) + + # Commit changes + self.cli_commit() + + # Validate configuration + frrconfig = self.getFRRconfig('mpls ldp', daemon=PROCESS_NAME) + self.assertIn(f'mpls ldp', frrconfig) + self.assertIn(f' router-id {router_id}', frrconfig) + + # Validate AFI IPv4 + afiv4_config = self.getFRRconfig(' address-family ipv4', daemon=PROCESS_NAME) + self.assertIn(f' discovery transport-address {transport_ipv4_addr}', afiv4_config) + for interface in interfaces: + self.assertIn(f' interface {interface}', afiv4_config) + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=True) diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py index aa0ac268d..40b19fec7 100755 --- a/smoketest/scripts/cli/test_protocols_nhrp.py +++ b/smoketest/scripts/cli/test_protocols_nhrp.py @@ -18,6 +18,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.firewall import find_nftables_rule from vyos.util import call, process_named_running, read_file tunnel_path = ['interfaces', 'tunnel'] @@ -91,6 +92,14 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase): for line in opennhrp_lines: self.assertIn(line, tmp_opennhrp_conf) + firewall_matches = [ + 'ip protocol gre', + 'ip saddr 192.0.2.1', + 'ip daddr 224.0.0.0/4', + 'comment "VYOS_NHRP_tun100"' + ] + + self.assertTrue(find_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', firewall_matches) is not None) self.assertTrue(process_named_running('opennhrp')) if __name__ == '__main__': diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 3f13eec80..ee58b0fe2 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -33,14 +33,21 @@ route_map = 'foo-bar-baz10' log = logging.getLogger('TestProtocolsOSPF') class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self.cli_set(['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) - self.cli_set(['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit']) + @classmethod + def setUpClass(cls): + super(cls, cls).setUpClass() + + cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit']) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['policy', 'route-map', route_map]) + super(cls, cls).tearDownClass() def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - self.cli_delete(['policy', 'route-map', route_map]) self.cli_delete(base_path) self.cli_commit() @@ -189,31 +196,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for neighbor in neighbors: self.assertIn(f' neighbor {neighbor} priority {priority} poll-interval {poll_interval}', frrconfig) # default - - def test_ospf_07_passive_interface(self): - self.cli_set(base_path + ['passive-interface', 'default']) - interfaces = Section.interfaces('ethernet') - for interface in interfaces: - self.cli_set(base_path + ['passive-interface-exclude', interface]) - - # commit changes - self.cli_commit() - - # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf') - try: - self.assertIn(f'router ospf', frrconfig) - self.assertIn(f' passive-interface default', frrconfig) # default - for interface in interfaces: - self.assertIn(f' no passive-interface {interface}', frrconfig) # default - except: - log.debug(frrconfig) - log.debug(cmd('sudo dmesg')) - log.debug(cmd('sudo cat /var/log/messages')) - log.debug(cmd('vtysh -c "show run"')) - self.fail('Now we can hopefully see why OSPF fails!') - - def test_ospf_08_redistribute(self): + def test_ospf_07_redistribute(self): metric = '15' metric_type = '1' redistribute = ['bgp', 'connected', 'isis', 'kernel', 'rip', 'static'] @@ -223,9 +206,15 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['redistribute', protocol, 'route-map', route_map]) self.cli_set(base_path + ['redistribute', protocol, 'metric-type', metric_type]) + # enable FRR debugging to find the root cause of failing testcases + cmd('touch /tmp/vyos.frr.debug') + # commit changes self.cli_commit() + # disable FRR debugging + cmd('rm -f /tmp/vyos.frr.debug') + # Verify FRR ospfd configuration frrconfig = self.getFRRconfig('router ospf') try: @@ -234,12 +223,10 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.assertIn(f' redistribute {protocol} metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) except: log.debug(frrconfig) - log.debug(cmd('sudo dmesg')) - log.debug(cmd('sudo cat /var/log/messages')) - log.debug(cmd('vtysh -c "show run"')) + log.debug(cmd('sudo cat /tmp/vyos-configd-script-stdout')) self.fail('Now we can hopefully see why OSPF fails!') - def test_ospf_09_virtual_link(self): + def test_ospf_08_virtual_link(self): networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] area = '10' shortcut = 'enable' @@ -269,22 +256,26 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.assertIn(f' network {network} area {area}', frrconfig) - def test_ospf_10_interface_configuration(self): + def test_ospf_09_interface_configuration(self): interfaces = Section.interfaces('ethernet') password = 'vyos1234' bandwidth = '10000' cost = '150' network = 'point-to-point' priority = '200' + bfd_profile = 'vyos-test' + self.cli_set(base_path + ['passive-interface', 'default']) for interface in interfaces: - self.cli_set(base_path + ['interface', interface, 'authentication', 'plaintext-password', password]) - self.cli_set(base_path + ['interface', interface, 'bandwidth', bandwidth]) - self.cli_set(base_path + ['interface', interface, 'bfd']) - self.cli_set(base_path + ['interface', interface, 'cost', cost]) - self.cli_set(base_path + ['interface', interface, 'mtu-ignore']) - self.cli_set(base_path + ['interface', interface, 'network', network]) - self.cli_set(base_path + ['interface', interface, 'priority', priority]) + base_interface = base_path + ['interface', interface] + self.cli_set(base_interface + ['authentication', 'plaintext-password', password]) + self.cli_set(base_interface + ['bandwidth', bandwidth]) + self.cli_set(base_interface + ['bfd', 'profile', bfd_profile]) + self.cli_set(base_interface + ['cost', cost]) + self.cli_set(base_interface + ['mtu-ignore']) + self.cli_set(base_interface + ['network', network]) + self.cli_set(base_interface + ['priority', priority]) + self.cli_set(base_interface + ['passive', 'disable']) # commit changes self.cli_commit() @@ -294,45 +285,15 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf authentication-key {password}', config) self.assertIn(f' ip ospf bfd', config) + self.assertIn(f' ip ospf bfd profile {bfd_profile}', config) self.assertIn(f' ip ospf cost {cost}', config) self.assertIn(f' ip ospf mtu-ignore', config) self.assertIn(f' ip ospf network {network}', config) self.assertIn(f' ip ospf priority {priority}', config) + self.assertIn(f' no ip ospf passive', config) self.assertIn(f' bandwidth {bandwidth}', config) - - def test_ospf_11_vrfs(self): - # It is safe to assume that when the basic VRF test works, all - # other OSPF related features work, as we entirely inherit the CLI - # templates and Jinja2 FRR template. - table = '1000' - vrf = 'blue' - vrf_base = ['vrf', 'name', vrf] - vrf_iface = 'eth1' - self.cli_set(vrf_base + ['table', table]) - self.cli_set(vrf_base + ['protocols', 'ospf', 'interface', vrf_iface]) - self.cli_set(['interfaces', 'ethernet', vrf_iface, 'vrf', vrf]) - - # Also set a default VRF OSPF config - self.cli_set(base_path) - self.cli_commit() - - # Verify FRR ospfd configuration - frrconfig = self.getFRRconfig('router ospf') - self.assertIn(f'router ospf', frrconfig) - self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig) - self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults - - frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}') - self.assertIn(f'router ospf vrf {vrf}', frrconfig) - self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig) - self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults - - self.cli_delete(['vrf', 'name', vrf]) - self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf']) - - - def test_ospf_12_zebra_route_map(self): + def test_ospf_10_zebra_route_map(self): # Implemented because of T3328 self.cli_set(base_path + ['route-map', route_map]) # commit changes @@ -352,7 +313,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): frrconfig = self.getFRRconfig(zebra_route_map) self.assertNotIn(zebra_route_map, frrconfig) - def test_ospf_13_interface_area(self): + def test_ospf_11_interface_area(self): area = '0' interfaces = Section.interfaces('ethernet') @@ -376,6 +337,37 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf area {area}', config) + def test_ospf_12_vrfs(self): + # It is safe to assume that when the basic VRF test works, all + # other OSPF related features work, as we entirely inherit the CLI + # templates and Jinja2 FRR template. + table = '1000' + vrf = 'blue' + vrf_base = ['vrf', 'name', vrf] + vrf_iface = 'eth1' + self.cli_set(vrf_base + ['table', table]) + self.cli_set(vrf_base + ['protocols', 'ospf', 'interface', vrf_iface]) + self.cli_set(['interfaces', 'ethernet', vrf_iface, 'vrf', vrf]) + + # Also set a default VRF OSPF config + self.cli_set(base_path) + self.cli_commit() + + # Verify FRR ospfd configuration + frrconfig = self.getFRRconfig('router ospf') + self.assertIn(f'router ospf', frrconfig) + self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig) + self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults + + frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}') + self.assertIn(f'router ospf vrf {vrf}', frrconfig) + self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig) + self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults + + # cleanup + self.cli_delete(['vrf', 'name', vrf]) + self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf']) + if __name__ == '__main__': logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_ospfv3.py b/smoketest/scripts/cli/test_protocols_ospfv3.py index 0b4b01993..1327fd910 100755 --- a/smoketest/scripts/cli/test_protocols_ospfv3.py +++ b/smoketest/scripts/cli/test_protocols_ospfv3.py @@ -18,17 +18,31 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.util import process_named_running PROCESS_NAME = 'ospf6d' base_path = ['protocols', 'ospfv3'] +route_map = 'foo-bar-baz-0815' + router_id = '192.0.2.1' default_area = '0' class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(cls, cls).setUpClass() + + cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit']) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['policy', 'route-map', route_map]) + super(cls, cls).tearDownClass() + def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -50,7 +64,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): interfaces = Section.interfaces('ethernet') for interface in interfaces: - self.cli_set(base_path + ['area', default_area, 'interface', interface]) + self.cli_set(base_path + ['interface', interface, 'area', default_area]) # commit changes self.cli_commit() @@ -64,7 +78,8 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.assertIn(f' area {default_area} export-list {acl_name}', frrconfig) for interface in interfaces: - self.assertIn(f' interface {interface} area {default_area}', frrconfig) + if_config = self.getFRRconfig(f'interface {interface}') + self.assertIn(f'ipv6 ospf6 area {default_area}', if_config) self.cli_delete(['policy', 'access-list6', acl_name]) @@ -109,7 +124,9 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): for protocol in redistribute: self.assertIn(f' redistribute {protocol} route-map {route_map}', frrconfig) + def test_ospfv3_04_interfaces(self): + bfd_profile = 'vyos-ipv6' self.cli_set(base_path + ['parameters', 'router-id', router_id]) self.cli_set(base_path + ['area', default_area]) @@ -119,7 +136,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): interfaces = Section.interfaces('ethernet') for interface in interfaces: if_base = base_path + ['interface', interface] - self.cli_set(if_base + ['bfd']) + self.cli_set(if_base + ['bfd', 'profile', bfd_profile]) self.cli_set(if_base + ['cost', cost]) self.cli_set(if_base + ['instance-id', '0']) self.cli_set(if_base + ['mtu-ignore']) @@ -142,6 +159,7 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): if_config = self.getFRRconfig(f'interface {interface}') self.assertIn(f'interface {interface}', if_config) self.assertIn(f' ipv6 ospf6 bfd', if_config) + self.assertIn(f' ipv6 ospf6 bfd profile {bfd_profile}', if_config) self.assertIn(f' ipv6 ospf6 cost {cost}', if_config) self.assertIn(f' ipv6 ospf6 mtu-ignore', if_config) self.assertIn(f' ipv6 ospf6 network point-to-point', if_config) @@ -167,5 +185,97 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): self.assertIn(f' area {area_stub} stub', frrconfig) self.assertIn(f' area {area_stub_nosum} stub no-summary', frrconfig) + + def test_ospfv3_06_area_nssa(self): + area_nssa = '1.1.1.1' + area_nssa_nosum = '2.2.2.2' + area_nssa_default = '3.3.3.3' + + self.cli_set(base_path + ['area', area_nssa, 'area-type', 'nssa']) + self.cli_set(base_path + ['area', area_nssa, 'area-type', 'stub']) + # can only set one area-type per OSPFv3 area + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['area', area_nssa, 'area-type', 'stub']) + + self.cli_set(base_path + ['area', area_nssa_nosum, 'area-type', 'nssa', 'no-summary']) + self.cli_set(base_path + ['area', area_nssa_nosum, 'area-type', 'nssa', 'default-information-originate']) + self.cli_set(base_path + ['area', area_nssa_default, 'area-type', 'nssa', 'default-information-originate']) + + # commit changes + self.cli_commit() + + # Verify FRR ospfd configuration + frrconfig = self.getFRRconfig('router ospf6') + self.assertIn(f'router ospf6', frrconfig) + self.assertIn(f' area {area_nssa} nssa', frrconfig) + self.assertIn(f' area {area_nssa_nosum} nssa default-information-originate no-summary', frrconfig) + self.assertIn(f' area {area_nssa_default} nssa default-information-originate', frrconfig) + + + def test_ospfv3_07_default_originate(self): + seq = '100' + metric = '50' + metric_type = '1' + + self.cli_set(base_path + ['default-information', 'originate', 'metric', metric]) + self.cli_set(base_path + ['default-information', 'originate', 'metric-type', metric_type]) + self.cli_set(base_path + ['default-information', 'originate', 'route-map', route_map]) + + # commit changes + self.cli_commit() + + # Verify FRR ospfd configuration + frrconfig = self.getFRRconfig('router ospf6') + self.assertIn(f'router ospf6', frrconfig) + self.assertIn(f' default-information originate metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) + + # Now set 'always' + self.cli_set(base_path + ['default-information', 'originate', 'always']) + self.cli_commit() + + # Verify FRR ospfd configuration + frrconfig = self.getFRRconfig('router ospf6') + self.assertIn(f' default-information originate always metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig) + + + def test_ospfv3_08_vrfs(self): + # It is safe to assume that when the basic VRF test works, all + # other OSPF related features work, as we entirely inherit the CLI + # templates and Jinja2 FRR template. + table = '1000' + vrf = 'blue' + vrf_base = ['vrf', 'name', vrf] + vrf_iface = 'eth1' + router_id = '1.2.3.4' + router_id_vrf = '1.2.3.5' + + self.cli_set(vrf_base + ['table', table]) + self.cli_set(vrf_base + ['protocols', 'ospfv3', 'interface', vrf_iface, 'bfd']) + self.cli_set(vrf_base + ['protocols', 'ospfv3', 'parameters', 'router-id', router_id_vrf]) + + self.cli_set(['interfaces', 'ethernet', vrf_iface, 'vrf', vrf]) + + # Also set a default VRF OSPF config + self.cli_set(base_path + ['parameters', 'router-id', router_id]) + self.cli_commit() + + # Verify FRR ospfd configuration + frrconfig = self.getFRRconfig('router ospf6') + self.assertIn(f'router ospf6', frrconfig) + self.assertIn(f' ospf6 router-id {router_id}', frrconfig) + + frrconfig = self.getFRRconfig(f'interface {vrf_iface} vrf {vrf}') + self.assertIn(f'interface {vrf_iface} vrf {vrf}', frrconfig) + self.assertIn(f' ipv6 ospf6 bfd', frrconfig) + + frrconfig = self.getFRRconfig(f'router ospf6 vrf {vrf}') + self.assertIn(f'router ospf6 vrf {vrf}', frrconfig) + self.assertIn(f' ospf6 router-id {router_id_vrf}', frrconfig) + + # cleanup + self.cli_delete(['vrf', 'name', vrf]) + self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf']) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=True) diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py index 423cd811a..80d4e79f9 100755 --- a/smoketest/scripts/cli/test_protocols_rip.py +++ b/smoketest/scripts/cli/test_protocols_rip.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.ifconfig import Section from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_protocols_ripng.py b/smoketest/scripts/cli/test_protocols_ripng.py index add92b73d..53336a533 100755 --- a/smoketest/scripts/cli/test_protocols_ripng.py +++ b/smoketest/scripts/cli/test_protocols_ripng.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.ifconfig import Section from vyos.util import process_named_running @@ -55,7 +54,7 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - def test_ripng(self): + def test_ripng_01_parameters(self): metric = '8' interfaces = Section.interfaces('ethernet') aggregates = ['2001:db8:1000::/48', '2001:db8:2000::/48', '2001:db8:3000::/48'] @@ -122,5 +121,25 @@ class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): proto = 'ospf6' self.assertIn(f' redistribute {proto} metric {metric} route-map {route_map}', frrconfig) + def test_ripng_02_zebra_route_map(self): + # Implemented because of T3328 + self.cli_set(base_path + ['route-map', route_map]) + # commit changes + self.cli_commit() + + # Verify FRR configuration + zebra_route_map = f'ipv6 protocol ripng route-map {route_map}' + frrconfig = self.getFRRconfig(zebra_route_map) + self.assertIn(zebra_route_map, frrconfig) + + # Remove the route-map again + self.cli_delete(base_path + ['route-map']) + # commit changes + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig(zebra_route_map) + self.assertNotIn(zebra_route_map, frrconfig) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py index 6d334a9f8..e5e45565b 100755 --- a/smoketest/scripts/cli/test_protocols_rpki.py +++ b/smoketest/scripts/cli/test_protocols_rpki.py @@ -19,7 +19,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import process_named_running @@ -37,8 +36,6 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Nothing RPKI specific should be left over in the config - # - # Disabled until T3266 is resolved # frrconfig = self.getFRRconfig('rpki') # self.assertNotIn('rpki', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py index 0d3228cc7..4c4eb5a7c 100755 --- a/smoketest/scripts/cli/test_protocols_static.py +++ b/smoketest/scripts/cli/test_protocols_static.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.template import is_ipv6 from vyos.util import get_interface_config diff --git a/smoketest/scripts/cli/test_service_bcast-relay.py b/smoketest/scripts/cli/test_service_bcast-relay.py index 58b730ab4..87901869e 100755 --- a/smoketest/scripts/cli/test_service_bcast-relay.py +++ b/smoketest/scripts/cli/test_service_bcast-relay.py @@ -19,7 +19,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from psutil import process_iter -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError base_path = ['service', 'broadcast-relay'] diff --git a/smoketest/scripts/cli/test_service_dhcp-relay.py b/smoketest/scripts/cli/test_service_dhcp-relay.py index db2edba54..bbfd9e032 100755 --- a/smoketest/scripts/cli/test_service_dhcp-relay.py +++ b/smoketest/scripts/cli/test_service_dhcp-relay.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 301f8fa31..14666db15 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import process_named_running from vyos.util import read_file diff --git a/smoketest/scripts/cli/test_service_dhcpv6-relay.py b/smoketest/scripts/cli/test_service_dhcpv6-relay.py index 5a9dd1aa6..fc206435b 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-relay.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-relay.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.template import address_from_cidr diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index 3f9564e59..7177f1505 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.template import inc_ip from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index d8a87ffd4..90d10d40b 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -20,14 +20,17 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import process_named_running +from vyos.util import read_file PROCESS_NAME = 'ddclient' DDCLIENT_CONF = '/run/ddclient/ddclient.conf' + base_path = ['service', 'dns', 'dynamic'] +hostname = 'test.ddns.vyos.io' +interface = 'eth0' def get_config_value(key): tmp = cmd(f'sudo cat {DDCLIENT_CONF}') @@ -36,14 +39,13 @@ def get_config_value(key): return tmp class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): - def tearDown(self): # Delete DDNS configuration self.cli_delete(base_path) self.cli_commit() def test_dyndns_service(self): - ddns = ['interface', 'eth0', 'service'] + ddns = ['interface', interface, 'service'] services = ['cloudflare', 'afraid', 'dyndns', 'zoneedit'] for service in services: @@ -51,7 +53,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): password = 'vyos_pass' zone = 'vyos.io' self.cli_delete(base_path) - self.cli_set(base_path + ddns + [service, 'host-name', 'test.ddns.vyos.io']) + self.cli_set(base_path + ddns + [service, 'host-name', hostname]) self.cli_set(base_path + ddns + [service, 'login', user]) self.cli_set(base_path + ddns + [service, 'password', password]) self.cli_set(base_path + ddns + [service, 'zone', zone]) @@ -94,7 +96,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def test_dyndns_rfc2136(self): # Check if DDNS service can be configured and runs - ddns = ['interface', 'eth0', 'rfc2136', 'vyos'] + ddns = ['interface', interface, 'rfc2136', 'vyos'] ddns_key_file = '/config/auth/my.key' self.cli_set(base_path + ddns + ['key', ddns_key_file]) @@ -122,5 +124,38 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + def test_dyndns_ipv6(self): + ddns = ['interface', interface, 'service', 'dynv6'] + proto = 'dyndns2' + user = 'none' + password = 'paSS_4ord' + srv = 'ddns.vyos.io' + + self.cli_set(base_path + ['interface', interface, 'ipv6-enable']) + self.cli_set(base_path + ddns + ['host-name', hostname]) + self.cli_set(base_path + ddns + ['login', user]) + self.cli_set(base_path + ddns + ['password', password]) + self.cli_set(base_path + ddns + ['protocol', proto]) + self.cli_set(base_path + ddns + ['server', srv]) + + # commit changes + self.cli_commit() + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + protocol = get_config_value('protocol') + login = get_config_value('login') + pwd = get_config_value('password') + server = get_config_value('server') + usev6 = get_config_value('usev6') + + # Check some generating config parameters + self.assertEqual(protocol, proto) + self.assertEqual(login, user) + self.assertEqual(pwd, f"'{password}'") + self.assertEqual(server, srv) + self.assertEqual(usev6, f"if, if={interface}") + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py index 45ca618cb..5929f8cba 100755 --- a/smoketest/scripts/cli/test_service_dns_forwarding.py +++ b/smoketest/scripts/cli/test_service_dns_forwarding.py @@ -19,7 +19,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import read_file from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 3af63636a..8e69efd9c 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -17,7 +17,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.util import run base_path = ['service', 'https'] diff --git a/smoketest/scripts/cli/test_service_mdns-repeater.py b/smoketest/scripts/cli/test_service_mdns-repeater.py index b1092c3e5..f99a98da1 100755 --- a/smoketest/scripts/cli/test_service_mdns-repeater.py +++ b/smoketest/scripts/cli/test_service_mdns-repeater.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.util import process_named_running base_path = ['service', 'mdns', 'repeater'] @@ -42,7 +41,7 @@ class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check for running process - self.assertTrue(process_named_running('mdns-repeater')) + self.assertTrue(process_named_running('avahi-daemon')) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index 26b4626c2..4875fb5d1 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -19,7 +19,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.util import read_file from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py index 008271102..fc24fd54e 100755 --- a/smoketest/scripts/cli/test_service_snmp.py +++ b/smoketest/scripts/cli/test_service_snmp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2021 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 @@ -19,33 +19,66 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.template import is_ipv4 +from vyos.template import address_from_cidr +from vyos.util import call +from vyos.util import DEVNULL from vyos.util import read_file from vyos.util import process_named_running +from vyos.version import get_version_data PROCESS_NAME = 'snmpd' SNMPD_CONF = '/etc/snmp/snmpd.conf' base_path = ['service', 'snmp'] +snmpv3_group = 'default_group' +snmpv3_view = 'default_view' +snmpv3_view_oid = '1' +snmpv3_user = 'vyos' +snmpv3_auth_pw = 'vyos12345678' +snmpv3_priv_pw = 'vyos87654321' +snmpv3_engine_id = '000000000000000000000002' + def get_config_value(key): tmp = read_file(SNMPD_CONF) tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) return tmp[0] class TestSNMPService(VyOSUnitTestSHIM.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): + super(cls, cls).setUpClass() + # ensure we can also run this test on a live system - so lets clean # out the current configuration :) + cls.cli_delete(cls, base_path) + + def tearDown(self): + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + # delete testing SNMP config self.cli_delete(base_path) + self.cli_commit() + + # Check for running process + self.assertFalse(process_named_running(PROCESS_NAME)) def test_snmp_basic(self): + dummy_if = 'dum7312' + dummy_addr = '100.64.0.1/32' + contact = 'maintainers@vyos.io' + location = 'QEMU' + + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', dummy_addr]) + # Check if SNMP can be configured and service runs clients = ['192.0.2.1', '2001:db8::1'] networks = ['192.0.2.128/25', '2001:db8:babe::/48'] - listen = ['127.0.0.1', '::1'] + listen = ['127.0.0.1', '::1', address_from_cidr(dummy_addr)] + port = '5000' for auth in ['ro', 'rw']: community = 'VyOS' + auth @@ -56,10 +89,10 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['community', community, 'network', network]) for addr in listen: - self.cli_set(base_path + ['listen-address', addr]) + self.cli_set(base_path + ['listen-address', addr, 'port', port]) - self.cli_set(base_path + ['contact', 'maintainers@vyos.io']) - self.cli_set(base_path + ['location', 'qemu']) + self.cli_set(base_path + ['contact', contact]) + self.cli_set(base_path + ['location', location]) self.cli_commit() @@ -68,25 +101,35 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): # thus we need to transfor this into a proper list config = get_config_value('agentaddress') expected = 'unix:/run/snmpd.socket' + self.assertIn(expected, config) for addr in listen: if is_ipv4(addr): - expected += ',udp:{}:161'.format(addr) + expected = f'udp:{addr}:{port}' else: - expected += ',udp6:[{}]:161'.format(addr) + expected = f'udp6:[{addr}]:{port}' + self.assertIn(expected, config) + + config = get_config_value('sysDescr') + version_data = get_version_data() + self.assertEqual('VyOS ' + version_data['version'], config) + + config = get_config_value('SysContact') + self.assertEqual(contact, config) - self.assertTrue(expected in config) + config = get_config_value('SysLocation') + self.assertEqual(location, config) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + self.cli_delete(['interfaces', 'dummy', dummy_if]) def test_snmpv3_sha(self): # Check if SNMPv3 can be configured with SHA authentication # and service runs - - self.cli_set(base_path + ['v3', 'engineid', '000000000000000000000002']) + self.cli_set(base_path + ['v3', 'engineid', snmpv3_engine_id]) self.cli_set(base_path + ['v3', 'group', 'default', 'mode', 'ro']) - # check validate() - a view must be created before this can be comitted + # check validate() - a view must be created before this can be committed with self.assertRaises(ConfigSessionError): self.cli_commit() @@ -94,46 +137,52 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['v3', 'group', 'default', 'view', 'default']) # create user - self.cli_set(base_path + ['v3', 'user', 'vyos', 'auth', 'plaintext-password', 'vyos12345678']) - self.cli_set(base_path + ['v3', 'user', 'vyos', 'auth', 'type', 'sha']) - self.cli_set(base_path + ['v3', 'user', 'vyos', 'privacy', 'plaintext-password', 'vyos12345678']) - self.cli_set(base_path + ['v3', 'user', 'vyos', 'privacy', 'type', 'aes']) - self.cli_set(base_path + ['v3', 'user', 'vyos', 'group', 'default']) + self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'auth', 'plaintext-password', snmpv3_auth_pw]) + self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'auth', 'type', 'sha']) + self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'plaintext-password', snmpv3_priv_pw]) + self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'type', 'aes']) + self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'group', 'default']) self.cli_commit() # commit will alter the CLI values - check if they have been updated: hashed_password = '4e52fe55fd011c9c51ae2c65f4b78ca93dcafdfe' - tmp = self._session.show_config(base_path + ['v3', 'user', 'vyos', 'auth', 'encrypted-password']).split()[1] + tmp = self._session.show_config(base_path + ['v3', 'user', snmpv3_user, 'auth', 'encrypted-password']).split()[1] self.assertEqual(tmp, hashed_password) - tmp = self._session.show_config(base_path + ['v3', 'user', 'vyos', 'privacy', 'encrypted-password']).split()[1] + hashed_password = '54705c8de9e81fdf61ad7ac044fa8fe611ddff6b' + tmp = self._session.show_config(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'encrypted-password']).split()[1] self.assertEqual(tmp, hashed_password) # TODO: read in config file and check values - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + # Try SNMPv3 connection + tmp = call(f'snmpwalk -v 3 -u {snmpv3_user} -a SHA -A {snmpv3_auth_pw} -x AES -X {snmpv3_priv_pw} -l authPriv 127.0.0.1', stdout=DEVNULL) + self.assertEqual(tmp, 0) def test_snmpv3_md5(self): # Check if SNMPv3 can be configured with MD5 authentication # and service runs + self.cli_set(base_path + ['v3', 'engineid', snmpv3_engine_id]) - self.cli_set(base_path + ['v3', 'engineid', '000000000000000000000002']) - self.cli_set(base_path + ['v3', 'group', 'default', 'mode', 'ro']) - # check validate() - a view must be created before this can be comitted + # create user + self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'auth', 'plaintext-password', snmpv3_auth_pw]) + self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'auth', 'type', 'md5']) + self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'plaintext-password', snmpv3_priv_pw]) + self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'type', 'des']) + + # check validate() - user requires a group to be created with self.assertRaises(ConfigSessionError): self.cli_commit() + self.cli_set(base_path + ['v3', 'user', 'vyos', 'group', snmpv3_group]) - self.cli_set(base_path + ['v3', 'view', 'default', 'oid', '1']) - self.cli_set(base_path + ['v3', 'group', 'default', 'view', 'default']) + self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'mode', 'ro']) + # check validate() - a view must be created before this can be comitted + with self.assertRaises(ConfigSessionError): + self.cli_commit() - # create user - self.cli_set(base_path + ['v3', 'user', 'vyos', 'auth', 'plaintext-password', 'vyos12345678']) - self.cli_set(base_path + ['v3', 'user', 'vyos', 'auth', 'type', 'md5']) - self.cli_set(base_path + ['v3', 'user', 'vyos', 'privacy', 'plaintext-password', 'vyos12345678']) - self.cli_set(base_path + ['v3', 'user', 'vyos', 'privacy', 'type', 'des']) - self.cli_set(base_path + ['v3', 'user', 'vyos', 'group', 'default']) + self.cli_set(base_path + ['v3', 'view', snmpv3_view, 'oid', snmpv3_view_oid]) + self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'view', snmpv3_view]) self.cli_commit() @@ -142,14 +191,21 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): tmp = self._session.show_config(base_path + ['v3', 'user', 'vyos', 'auth', 'encrypted-password']).split()[1] self.assertEqual(tmp, hashed_password) + hashed_password = 'e11c83f2c510540a3c4de84ee66de440' tmp = self._session.show_config(base_path + ['v3', 'user', 'vyos', 'privacy', 'encrypted-password']).split()[1] self.assertEqual(tmp, hashed_password) - # TODO: read in config file and check values + tmp = read_file(SNMPD_CONF) + # views + self.assertIn(f'view {snmpv3_view} included .{snmpv3_view_oid}', tmp) + # group + self.assertIn(f'group {snmpv3_group} usm {snmpv3_user}', tmp) + # access + self.assertIn(f'access {snmpv3_group} "" usm auth exact {snmpv3_view} none none', tmp) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + # Try SNMPv3 connection + tmp = call(f'snmpwalk -v 3 -u {snmpv3_user} -a MD5 -A {snmpv3_auth_pw} -x DES -X {snmpv3_priv_pw} -l authPriv 127.0.0.1', stdout=DEVNULL) + self.assertEqual(tmp, 0) if __name__ == '__main__': unittest.main(verbosity=2) - diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index ded4d8301..a54c03919 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -20,7 +20,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_service_tftp-server.py b/smoketest/scripts/cli/test_service_tftp-server.py index aed4c6beb..b57c33f26 100755 --- a/smoketest/scripts/cli/test_service_tftp-server.py +++ b/smoketest/scripts/cli/test_service_tftp-server.py @@ -19,8 +19,8 @@ import unittest from psutil import process_iter from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError +from vyos.util import cmd from vyos.util import read_file from vyos.util import process_named_running from vyos.template import is_ipv6 @@ -30,6 +30,7 @@ base_path = ['service', 'tftp-server'] dummy_if_path = ['interfaces', 'dummy', 'dum69'] address_ipv4 = '192.0.2.1' address_ipv6 = '2001:db8::1' +vrf = 'mgmt' class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase): def setUp(self): @@ -98,5 +99,42 @@ class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase): count += 1 self.assertEqual(count, len(address)) + def test_03_tftpd_vrf(self): + directory = '/tmp' + port = '69' # default port + + self.cli_set(base_path + ['allow-upload']) + self.cli_set(base_path + ['directory', directory]) + self.cli_set(base_path + ['listen-address', address_ipv4, 'vrf', vrf]) + + # VRF does yet not exist - an error must be thrown + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(['vrf', 'name', vrf, 'table', '1338']) + self.cli_set(dummy_if_path + ['vrf', vrf]) + + # commit changes + self.cli_commit() + + config = read_file('/etc/default/tftpd0') + # verify listen IP address + self.assertIn(f'{address_ipv4}:{port} -4', config) + # verify directory + self.assertIn(directory, config) + # verify upload + self.assertIn('--create --umask 000', config) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + # Check for process in VRF + tmp = cmd(f'ip vrf pids {vrf}') + self.assertIn(PROCESS_NAME, tmp) + + # delete VRF + self.cli_delete(dummy_if_path + ['vrf']) + self.cli_delete(['vrf', 'name', vrf]) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py index 6780a93f9..8a1a03ce7 100755 --- a/smoketest/scripts/cli/test_service_webproxy.py +++ b/smoketest/scripts/cli/test_service_webproxy.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import process_named_running diff --git a/smoketest/scripts/cli/test_system_acceleration_qat.py b/smoketest/scripts/cli/test_system_acceleration_qat.py index 9584888d6..9e60bb211 100755 --- a/smoketest/scripts/cli/test_system_acceleration_qat.py +++ b/smoketest/scripts/cli/test_system_acceleration_qat.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError base_path = ['system', 'acceleration', 'qat'] diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index a2380981b..95c2a6c55 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -15,11 +15,12 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import re import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession +from vyos.firewall import find_nftables_rule from vyos.util import cmd from vyos.util import read_file @@ -157,8 +158,8 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): 'driver' : ['nf_nat_h323', 'nf_conntrack_h323'], }, 'nfs' : { - 'iptables' : ['-A VYATTA_CT_HELPER -p udp -m udp --dport 111 -j CT --helper rpc', - '-A VYATTA_CT_HELPER -p tcp -m tcp --dport 111 -j CT --helper rpc'], + 'nftables' : ['ct helper set "rpc_tcp"', + 'ct helper set "rpc_udp"'] }, 'pptp' : { 'driver' : ['nf_nat_pptp', 'nf_conntrack_pptp'], @@ -167,9 +168,7 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): 'driver' : ['nf_nat_sip', 'nf_conntrack_sip'], }, 'sqlnet' : { - 'iptables' : ['-A VYATTA_CT_HELPER -p tcp -m tcp --dport 1536 -j CT --helper tns', - '-A VYATTA_CT_HELPER -p tcp -m tcp --dport 1525 -j CT --helper tns', - '-A VYATTA_CT_HELPER -p tcp -m tcp --dport 1521 -j CT --helper tns'], + 'nftables' : ['ct helper set "tns_tcp"'] }, 'tftp' : { 'driver' : ['nf_nat_tftp', 'nf_conntrack_tftp'], @@ -188,10 +187,9 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): if 'driver' in module_options: for driver in module_options['driver']: self.assertTrue(os.path.isdir(f'/sys/module/{driver}')) - if 'iptables' in module_options: - rules = cmd('sudo iptables-save -t raw') - for ruleset in module_options['iptables']: - self.assertIn(ruleset, rules) + if 'nftables' in module_options: + for rule in module_options['nftables']: + self.assertTrue(find_nftables_rule('raw', 'VYOS_CT_HELPER', [rule]) != None) # unload modules for module in modules: @@ -205,10 +203,9 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): if 'driver' in module_options: for driver in module_options['driver']: self.assertFalse(os.path.isdir(f'/sys/module/{driver}')) - if 'iptables' in module_options: - rules = cmd('sudo iptables-save -t raw') - for ruleset in module_options['iptables']: - self.assertNotIn(ruleset, rules) + if 'nftables' in module_options: + for rule in module_options['nftables']: + self.assertTrue(find_nftables_rule('raw', 'VYOS_CT_HELPER', [rule]) == None) def test_conntrack_hash_size(self): hash_size = '65536' diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py new file mode 100755 index 000000000..857df1be6 --- /dev/null +++ b/smoketest/scripts/cli/test_system_flow-accounting.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section +from vyos.util import cmd +from vyos.util import process_named_running +from vyos.util import read_file + +PROCESS_NAME = 'uacctd' +base_path = ['system', 'flow-accounting'] + +uacctd_conf = '/run/pmacct/uacctd.conf' + +class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(cls, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + # after service removal process must no longer run + self.assertFalse(process_named_running(PROCESS_NAME)) + + def test_basic(self): + buffer_size = '5' # MiB + syslog = 'all' + + self.cli_set(base_path + ['buffer-size', buffer_size]) + self.cli_set(base_path + ['syslog-facility', syslog]) + + # You need to configure at least one interface for flow-accounting + with self.assertRaises(ConfigSessionError): + self.cli_commit() + for interface in Section.interfaces('ethernet'): + self.cli_set(base_path + ['interface', interface]) + + # commit changes + self.cli_commit() + + # verify configuration + nftables_output = cmd('sudo nft list chain raw VYOS_CT_PREROUTING_HOOK').splitlines() + for interface in Section.interfaces('ethernet'): + rule_found = False + ifname_search = f'iifname "{interface}"' + + for nftables_line in nftables_output: + if 'FLOW_ACCOUNTING_RULE' in nftables_line and ifname_search in nftables_line: + self.assertIn('group 2', nftables_line) + self.assertIn('snaplen 128', nftables_line) + self.assertIn('queue-threshold 100', nftables_line) + rule_found = True + break + + self.assertTrue(rule_found) + + uacctd = read_file(uacctd_conf) + # circular queue size - buffer_size + tmp = int(buffer_size) *1024 *1024 + self.assertIn(f'plugin_pipe_size: {tmp}', uacctd) + # transfer buffer size - recommended value from pmacct developers 1/1000 of pipe size + tmp = int(buffer_size) *1024 *1024 + # do an integer division + tmp //= 1000 + self.assertIn(f'plugin_buffer_size: {tmp}', uacctd) + + # when 'disable-imt' is not configured on the CLI it must be present + self.assertIn(f'imt_path: /tmp/uacctd.pipe', uacctd) + self.assertIn(f'imt_mem_pools_number: 169', uacctd) + self.assertIn(f'syslog: {syslog}', uacctd) + self.assertIn(f'plugins: memory', uacctd) + + def test_sflow(self): + sampling_rate = '4000' + source_address = '192.0.2.1' + dummy_if = 'dum3841' + agent_address = '192.0.2.2' + + sflow_server = { + '1.2.3.4' : { + }, + '5.6.7.8' : { + 'port' : '6000' + } + } + + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32']) + self.cli_set(base_path + ['disable-imt']) + + # You need to configure at least one interface for flow-accounting + with self.assertRaises(ConfigSessionError): + self.cli_commit() + for interface in Section.interfaces('ethernet'): + self.cli_set(base_path + ['interface', interface]) + + + # You need to configure at least one sFlow or NetFlow protocol, or not + # set "disable-imt" for flow-accounting + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['sflow', 'agent-address', agent_address]) + self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate]) + self.cli_set(base_path + ['sflow', 'source-address', source_address]) + for server, server_config in sflow_server.items(): + self.cli_set(base_path + ['sflow', 'server', server]) + if 'port' in server_config: + self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']]) + + # commit changes + self.cli_commit() + + uacctd = read_file(uacctd_conf) + + # when 'disable-imt' is not configured on the CLI it must be present + self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd) + self.assertNotIn(f'imt_mem_pools_number: 169', uacctd) + self.assertNotIn(f'plugins: memory', uacctd) + + for server, server_config in sflow_server.items(): + if 'port' in server_config: + self.assertIn(f'sfprobe_receiver[sf_{server}]: {server}', uacctd) + else: + self.assertIn(f'sfprobe_receiver[sf_{server}]: {server}:6343', uacctd) + + self.assertIn(f'sfprobe_agentip[sf_{server}]: {agent_address}', uacctd) + self.assertIn(f'sampling_rate[sf_{server}]: {sampling_rate}', uacctd) + self.assertIn(f'sfprobe_source_ip[sf_{server}]: {source_address}', uacctd) + + self.cli_delete(['interfaces', 'dummy', dummy_if]) + + def test_netflow(self): + engine_id = '33' + max_flows = '667' + sampling_rate = '100' + source_address = '192.0.2.1' + dummy_if = 'dum3842' + agent_address = '192.0.2.10' + version = '10' + tmo_expiry = '120' + tmo_flow = '1200' + tmo_icmp = '60' + tmo_max = '50000' + tmo_tcp_fin = '100' + tmo_tcp_generic = '120' + tmo_tcp_rst = '99' + tmo_udp = '10' + + netflow_server = { + '11.22.33.44' : { + }, + '55.66.77.88' : { + 'port' : '6000' + } + } + + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32']) + + for interface in Section.interfaces('ethernet'): + self.cli_set(base_path + ['interface', interface]) + + self.cli_set(base_path + ['netflow', 'engine-id', engine_id]) + self.cli_set(base_path + ['netflow', 'max-flows', max_flows]) + self.cli_set(base_path + ['netflow', 'sampling-rate', sampling_rate]) + self.cli_set(base_path + ['netflow', 'source-address', source_address]) + self.cli_set(base_path + ['netflow', 'version', version]) + + # timeouts + self.cli_set(base_path + ['netflow', 'timeout', 'expiry-interval', tmo_expiry]) + self.cli_set(base_path + ['netflow', 'timeout', 'flow-generic', tmo_flow]) + self.cli_set(base_path + ['netflow', 'timeout', 'icmp', tmo_icmp]) + self.cli_set(base_path + ['netflow', 'timeout', 'max-active-life', tmo_max]) + self.cli_set(base_path + ['netflow', 'timeout', 'tcp-fin', tmo_tcp_fin]) + self.cli_set(base_path + ['netflow', 'timeout', 'tcp-generic', tmo_tcp_generic]) + self.cli_set(base_path + ['netflow', 'timeout', 'tcp-rst', tmo_tcp_rst]) + self.cli_set(base_path + ['netflow', 'timeout', 'udp', tmo_udp]) + + # You need to configure at least one netflow server + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + for server, server_config in netflow_server.items(): + self.cli_set(base_path + ['netflow', 'server', server]) + if 'port' in server_config: + self.cli_set(base_path + ['netflow', 'server', server, 'port', server_config['port']]) + + # commit changes + self.cli_commit() + + uacctd = read_file(uacctd_conf) + + tmp = [] + tmp.append('memory') + for server, server_config in netflow_server.items(): + tmp.append(f'nfprobe[nf_{server}]') + self.assertIn('plugins: ' + ','.join(tmp), uacctd) + + for server, server_config in netflow_server.items(): + self.assertIn(f'nfprobe_engine[nf_{server}]: {engine_id}', uacctd) + self.assertIn(f'nfprobe_maxflows[nf_{server}]: {max_flows}', uacctd) + self.assertIn(f'sampling_rate[nf_{server}]: {sampling_rate}', uacctd) + self.assertIn(f'nfprobe_source_ip[nf_{server}]: {source_address}', uacctd) + self.assertIn(f'nfprobe_version[nf_{server}]: {version}', uacctd) + + if 'port' in server_config: + self.assertIn(f'nfprobe_receiver[nf_{server}]: {server}', uacctd) + else: + self.assertIn(f'nfprobe_receiver[nf_{server}]: {server}:2055', uacctd) + + self.assertIn(f'nfprobe_timeouts[nf_{server}]: expint={tmo_expiry}:general={tmo_flow}:icmp={tmo_icmp}:maxlife={tmo_max}:tcp.fin={tmo_tcp_fin}:tcp={tmo_tcp_generic}:tcp.rst={tmo_tcp_rst}:udp={tmo_udp}', uacctd) + + + self.cli_delete(['interfaces', 'dummy', dummy_if]) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_ip.py b/smoketest/scripts/cli/test_system_ip.py index e98a4e234..83df9d99e 100755 --- a/smoketest/scripts/cli/test_system_ip.py +++ b/smoketest/scripts/cli/test_system_ip.py @@ -17,7 +17,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.util import read_file base_path = ['system', 'ip'] diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py index c9c9e833d..1325d4b39 100755 --- a/smoketest/scripts/cli/test_system_ipv6.py +++ b/smoketest/scripts/cli/test_system_ipv6.py @@ -17,7 +17,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.util import read_file base_path = ['system', 'ipv6'] diff --git a/smoketest/scripts/cli/test_system_lcd.py b/smoketest/scripts/cli/test_system_lcd.py index 7a39e2986..831fba979 100755 --- a/smoketest/scripts/cli/test_system_lcd.py +++ b/smoketest/scripts/cli/test_system_lcd.py @@ -19,7 +19,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from configparser import ConfigParser -from vyos.configsession import ConfigSession from vyos.util import process_named_running config_file = '/run/LCDd/LCDd.conf' diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py index af3a5851c..69a06eeac 100755 --- a/smoketest/scripts/cli/test_system_login.py +++ b/smoketest/scripts/cli/test_system_login.py @@ -24,7 +24,6 @@ from distutils.version import LooseVersion from platform import release as kernel_version from subprocess import Popen, PIPE -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import cmd from vyos.util import read_file @@ -94,7 +93,7 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): def test_system_user_ssh_key(self): ssh_user = 'ssh-test_user' - public_keys = 'vyos' + public_keys = 'vyos_test@domain-foo.com' type = 'ssh-rsa' self.cli_set(base_path + ['user', ssh_user, 'authentication', 'public-keys', public_keys, 'key', ssh_pubkey.replace('\n','')]) diff --git a/smoketest/scripts/cli/test_system_logs.py b/smoketest/scripts/cli/test_system_logs.py new file mode 100755 index 000000000..0c11c4663 --- /dev/null +++ b/smoketest/scripts/cli/test_system_logs.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 re +import unittest +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.util import read_file + +# path to logrotate configs +logrotate_atop_file = '/etc/logrotate.d/vyos-atop' +logrotate_rsyslog_file = '/etc/logrotate.d/vyos-rsyslog' +# default values +default_atop_maxsize = '10M' +default_atop_rotate = '10' +default_rsyslog_size = '1M' +default_rsyslog_rotate = '10' + +base_path = ['system', 'logs'] + + +def logrotate_config_parse(file_path): + # read the file + logrotate_config = read_file(file_path) + # create regex for parsing options + regex_options = re.compile( + r'(^\s+(?P<option_name_script>postrotate|prerotate|firstaction|lastaction|preremove)\n(?P<option_value_script>((?!endscript).)*)\n\s+endscript\n)|(^\s+(?P<option_name>[\S]+)([ \t]+(?P<option_value>\S+))*$)', + re.M | re.S) + # create empty dict for config + logrotate_config_dict = {} + # fill dictionary with actual config + for option in regex_options.finditer(logrotate_config): + option_name = option.group('option_name') + option_value = option.group('option_value') + option_name_script = option.group('option_name_script') + option_value_script = option.group('option_value_script') + if option_name: + logrotate_config_dict[option_name] = option_value + if option_name_script: + logrotate_config_dict[option_name_script] = option_value_script + + # return config dictionary + return (logrotate_config_dict) + + +class TestSystemLogs(VyOSUnitTestSHIM.TestCase): + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + def test_logs_defaults(self): + # test with empty section for default values + self.cli_set(base_path) + self.cli_commit() + + # read the config file and check content + logrotate_config_atop = logrotate_config_parse(logrotate_atop_file) + logrotate_config_rsyslog = logrotate_config_parse( + logrotate_rsyslog_file) + self.assertEqual(logrotate_config_atop['maxsize'], default_atop_maxsize) + self.assertEqual(logrotate_config_atop['rotate'], default_atop_rotate) + self.assertEqual(logrotate_config_rsyslog['size'], default_rsyslog_size) + self.assertEqual(logrotate_config_rsyslog['rotate'], + default_rsyslog_rotate) + + def test_logs_atop_maxsize(self): + # test for maxsize option + self.cli_set(base_path + ['logrotate', 'atop', 'max-size', '50']) + self.cli_commit() + + # read the config file and check content + logrotate_config = logrotate_config_parse(logrotate_atop_file) + self.assertEqual(logrotate_config['maxsize'], '50M') + + def test_logs_atop_rotate(self): + # test for rotate option + self.cli_set(base_path + ['logrotate', 'atop', 'rotate', '50']) + self.cli_commit() + + # read the config file and check content + logrotate_config = logrotate_config_parse(logrotate_atop_file) + self.assertEqual(logrotate_config['rotate'], '50') + + def test_logs_rsyslog_size(self): + # test for size option + self.cli_set(base_path + ['logrotate', 'messages', 'max-size', '50']) + self.cli_commit() + + # read the config file and check content + logrotate_config = logrotate_config_parse(logrotate_rsyslog_file) + self.assertEqual(logrotate_config['size'], '50M') + + def test_logs_rsyslog_rotate(self): + # test for rotate option + self.cli_set(base_path + ['logrotate', 'messages', 'rotate', '50']) + self.cli_commit() + + # read the config file and check content + logrotate_config = logrotate_config_parse(logrotate_rsyslog_file) + self.assertEqual(logrotate_config['rotate'], '50') + + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=True) diff --git a/smoketest/scripts/cli/test_system_nameserver.py b/smoketest/scripts/cli/test_system_nameserver.py index 50dc466c2..58c84988e 100755 --- a/smoketest/scripts/cli/test_system_nameserver.py +++ b/smoketest/scripts/cli/test_system_nameserver.py @@ -19,7 +19,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.util import read_file diff --git a/smoketest/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py index 2b86ebd7c..e8cc64463 100755 --- a/smoketest/scripts/cli/test_system_ntp.py +++ b/smoketest/scripts/cli/test_system_ntp.py @@ -19,7 +19,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.template import address_from_cidr from vyos.template import netmask_from_cidr diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 93569c4ec..1433c7329 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -111,9 +111,22 @@ rgiyCHemtMepq57Pl1Nmj49eEA== """ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self.cli_set(base_path + ['interface', f'{interface}.{vif}']) + @classmethod + def setUpClass(cls): + super(cls, cls).setUpClass() + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + cls.cli_set(cls, base_path + ['interface', f'{interface}.{vif}']) + + @classmethod + def tearDownClass(cls): + super(cls, cls).tearDownClass() + + cls.cli_delete(cls, base_path + ['interface', f'{interface}.{vif}']) + def setUp(self): # Set IKE/ESP Groups self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes128']) self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha1']) @@ -127,7 +140,6 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_delete(tunnel_path) - self.cli_delete(ethernet_path) self.cli_commit() # Check for no longer running process @@ -158,6 +170,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): # Site to site local_address = '192.0.2.10' + priority = '20' peer_base_path = base_path + ['site-to-site', 'peer', peer_ip] self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) @@ -173,6 +186,10 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.11.0/24']) self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'port', '443']) + self.cli_set(peer_base_path + ['tunnel', '2', 'local', 'prefix', '10.1.0.0/16']) + self.cli_set(peer_base_path + ['tunnel', '2', 'remote', 'prefix', '10.2.0.0/16']) + self.cli_set(peer_base_path + ['tunnel', '2', 'priority', priority]) + self.cli_commit() # Verify strongSwan configuration @@ -187,8 +204,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'local_addrs = {local_address} # dhcp:no', f'remote_addrs = {peer_ip}', f'mode = tunnel', + f'peer_{peer_ip.replace(".","-")}_tunnel_1', f'local_ts = 172.16.10.0/24[tcp/443],172.16.11.0/24[tcp/443]', - f'remote_ts = 172.17.10.0/24[tcp/443],172.17.11.0/24[tcp/443]' + f'remote_ts = 172.17.10.0/24[tcp/443],172.17.11.0/24[tcp/443]', + f'mode = tunnel', + f'peer_{peer_ip.replace(".","-")}_tunnel_2', + f'local_ts = 10.1.0.0/16', + f'remote_ts = 10.2.0.0/16', + f'priority = {priority}', + f'mode = tunnel', ] for line in swanctl_conf_lines: self.assertIn(line, swanctl_conf) @@ -307,7 +331,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): swanctl_lines = [ f'proposals = aes128-sha1-modp1024,aes256-sha1-modp1024', f'version = 1', - f'life_time = {ike_lifetime}s', + f'rekey_time = {ike_lifetime}s', f'rekey_time = {esp_lifetime}s', f'esp_proposals = aes128-sha1-modp1024,aes256-sha1-modp1024,3des-md5-modp1024', f'local_ts = dynamic[gre]', diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index cad3b1182..b0e859b5c 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -18,7 +18,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.util import process_named_running OCSERV_CONF = '/run/ocserv/ocserv.conf' diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index f36d16344..5ffa9c086 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -22,7 +22,6 @@ import unittest from netifaces import interfaces from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError from vyos.ifconfig import Interface from vyos.ifconfig import Section @@ -58,7 +57,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: self.assertNotIn(vrf, interfaces()) - def test_vrf_table_id(self): + def test_vrf_vni_and_table_id(self): table = '1000' for vrf in vrfs: base = base_path + ['name', vrf] @@ -70,6 +69,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_set(base + ['table', table]) + self.cli_set(base + ['vni', table]) if vrf == 'green': self.cli_set(base + ['disable']) @@ -101,6 +101,11 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): # ... regex = f'{table}\s+{vrf}\s+#\s+{description}' self.assertTrue(re.findall(regex, iproute2_config)) + + frrconfig = self.getFRRconfig(f'vrf {vrf}') + self.assertIn(f' vni {table}', frrconfig) + + # Increment table ID for the next run table = str(int(table) + 1) def test_vrf_loopback_ips(self): @@ -178,5 +183,42 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): section = Section.section(interface) self.cli_delete(['interfaces', section, interface, 'vrf']) + def test_vrf_static_route(self): + table = '100' + for vrf in vrfs: + next_hop = f'192.0.{table}.1' + prefix = f'10.0.{table}.0/24' + base = base_path + ['name', vrf] + + self.cli_set(base + ['vni', table]) + + # check validate() - a table ID is mandatory + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base + ['table', table]) + self.cli_set(base + ['protocols', 'static', 'route', prefix, 'next-hop', next_hop]) + + table = str(int(table) + 1) + + # commit changes + self.cli_commit() + + # Verify VRF configuration + table = '100' + for vrf in vrfs: + next_hop = f'192.0.{table}.1' + prefix = f'10.0.{table}.0/24' + + self.assertTrue(vrf in interfaces()) + vrf_if = Interface(vrf) + + frrconfig = self.getFRRconfig(f'vrf {vrf}') + self.assertIn(f' vni {table}', frrconfig) + self.assertIn(f' ip route {prefix} {next_hop}', frrconfig) + + # Increment table ID for the next run + table = str(int(table) + 1) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_zone_policy.py b/smoketest/scripts/cli/test_zone_policy.py new file mode 100755 index 000000000..c0af6164b --- /dev/null +++ b/smoketest/scripts/cli/test_zone_policy.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.util import cmd + +class TestZonePolicy(VyOSUnitTestSHIM.TestCase): + def setUp(self): + self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop']) + + def tearDown(self): + self.cli_delete(['zone-policy']) + self.cli_delete(['firewall']) + self.cli_commit() + + def test_basic_zone(self): + self.cli_set(['zone-policy', 'zone', 'smoketest-eth0', 'interface', 'eth0']) + self.cli_set(['zone-policy', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest']) + self.cli_set(['zone-policy', 'zone', 'smoketest-local', 'local-zone']) + self.cli_set(['zone-policy', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest']) + + self.cli_commit() + + nftables_search = [ + ['chain VZONE_smoketest-eth0'], + ['chain VZONE_smoketest-local_IN'], + ['chain VZONE_smoketest-local_OUT'], + ['oifname { "eth0" }', 'jump VZONE_smoketest-eth0'], + ['jump VZONE_smoketest-local_IN'], + ['jump VZONE_smoketest-local_OUT'], + ['iifname { "eth0" }', 'jump smoketest'], + ['oifname { "eth0" }', 'jump smoketest'] + ] + + nftables_output = cmd('sudo nft list table ip filter') + + 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(matched) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/completion/list_openvpn_users.py b/src/completion/list_openvpn_users.py new file mode 100755 index 000000000..c472dbeab --- /dev/null +++ b/src/completion/list_openvpn_users.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2021 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 +import argparse + +from vyos.config import Config +from vyos.util import dict_search + +def get_user_from_interface(interface): + config = Config() + base = ['interfaces', 'openvpn', interface] + openvpn = config.get_config_dict(base, effective=True, key_mangling=('-', '_')) + users = [] + + try: + for user in (dict_search('server.client', openvpn[interface]) or []): + users.append(user.split(',')[0]) + except: + pass + + return users + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-i", "--interface", type=str, help="List users per interface") + args = parser.parse_args() + + users = [] + + users = get_user_from_interface(args.interface) + + print(" ".join(users)) + diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py index 68877f794..c65ef9540 100755 --- a/src/conf_mode/conntrack.py +++ b/src/conf_mode/conntrack.py @@ -15,11 +15,14 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import re from sys import exit from vyos.config import Config from vyos.configdict import dict_merge +from vyos.firewall import find_nftables_rule +from vyos.firewall import remove_nftables_rule from vyos.util import cmd from vyos.util import run from vyos.util import process_named_running @@ -43,8 +46,8 @@ module_map = { 'ko' : ['nf_nat_h323', 'nf_conntrack_h323'], }, 'nfs' : { - 'iptables' : ['VYATTA_CT_HELPER --table raw --proto tcp --dport 111 --jump CT --helper rpc', - 'VYATTA_CT_HELPER --table raw --proto udp --dport 111 --jump CT --helper rpc'], + 'nftables' : ['ct helper set "rpc_tcp" tcp dport "{111}" return', + 'ct helper set "rpc_udp" udp dport "{111}" return'] }, 'pptp' : { 'ko' : ['nf_nat_pptp', 'nf_conntrack_pptp'], @@ -53,9 +56,7 @@ module_map = { 'ko' : ['nf_nat_sip', 'nf_conntrack_sip'], }, 'sqlnet' : { - 'iptables' : ['VYATTA_CT_HELPER --table raw --proto tcp --dport 1521 --jump CT --helper tns', - 'VYATTA_CT_HELPER --table raw --proto tcp --dport 1525 --jump CT --helper tns', - 'VYATTA_CT_HELPER --table raw --proto tcp --dport 1536 --jump CT --helper tns'], + 'nftables' : ['ct helper set "tns_tcp" tcp dport "{1521,1525,1536}" return'] }, 'tftp' : { 'ko' : ['nf_nat_tftp', 'nf_conntrack_tftp'], @@ -93,6 +94,17 @@ def generate(conntrack): return None +def find_nftables_ct_rule(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]) + +def find_remove_rule(rule): + handle = find_nftables_ct_rule(rule) + if handle: + remove_nftables_rule('raw', 'VYOS_CT_HELPER', handle) + def apply(conntrack): # Depending on the enable/disable state of the ALG (Application Layer Gateway) # modules we need to either insmod or rmmod the helpers. @@ -103,20 +115,17 @@ def apply(conntrack): # Only remove the module if it's loaded if os.path.exists(f'/sys/module/{mod}'): cmd(f'rmmod {mod}') - if 'iptables' in module_config: - for rule in module_config['iptables']: - # Only install iptables rule if it does not exist - tmp = run(f'iptables --check {rule}') - if tmp == 0: cmd(f'iptables --delete {rule}') + if 'nftables' in module_config: + for rule in module_config['nftables']: + find_remove_rule(rule) else: if 'ko' in module_config: for mod in module_config['ko']: cmd(f'modprobe {mod}') - if 'iptables' in module_config: - for rule in module_config['iptables']: - # Only install iptables rule if it does not exist - tmp = run(f'iptables --check {rule}') - if tmp > 0: cmd(f'iptables --insert {rule}') + 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 process_named_running('conntrackd'): # Reload conntrack-sync daemon to fetch new sysctl values diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py index 1e0197a13..2e14e0b25 100755 --- a/src/conf_mode/containers.py +++ b/src/conf_mode/containers.py @@ -30,8 +30,6 @@ from vyos.util import cmd from vyos.util import run from vyos.util import read_file from vyos.util import write_file -from vyos.util import is_systemd_service_active -from vyos.util import is_systemd_service_running from vyos.template import inc_ip from vyos.template import is_ipv4 from vyos.template import is_ipv6 @@ -102,7 +100,7 @@ def verify(container): # Check if the specified container network exists network_name = list(container_config['network'])[0] if network_name not in container['network']: - raise ConfigError('Container network "{network_name}" does not exist!') + raise ConfigError(f'Container network "{network_name}" does not exist!') if 'address' in container_config['network'][network_name]: if 'network' not in container_config: @@ -160,7 +158,7 @@ def verify(container): v6_prefix = 0 # If ipv4-prefix not defined for user-defined network if 'prefix' not in network_config: - raise ConfigError(f'prefix for network "{net}" must be defined!') + raise ConfigError(f'prefix for network "{network}" must be defined!') for prefix in network_config['prefix']: if is_ipv4(prefix): v4_prefix += 1 @@ -237,17 +235,6 @@ def apply(container): if os.path.exists(tmp): os.unlink(tmp) - service_name = 'podman.service' - if 'network' in container or 'name' in container: - # Start podman if it's required and not yet running - if not is_systemd_service_active(service_name): - _cmd(f'systemctl start {service_name}') - # Wait for podman to be running - while not is_systemd_service_running(service_name): - sleep(0.250) - else: - _cmd(f'systemctl stop {service_name}') - # Add container if 'name' in container: for name, container_config in container['name'].items(): @@ -271,6 +258,14 @@ def apply(container): tmp = run(f'podman image exists {image}') if tmp != 0: print(os.system(f'podman pull {image}')) + # Add capability options. Should be in uppercase + cap_add = '' + if 'cap_add' in container_config: + for c in container_config['cap_add']: + c = c.upper() + c = c.replace('-', '_') + cap_add += f' --cap-add={c}' + # Check/set environment options "-e foo=bar" env_opt = '' if 'environment' in container_config: @@ -299,7 +294,7 @@ def apply(container): dvol = vol_config['destination'] volume += f' -v {svol}:{dvol}' - container_base_cmd = f'podman run --detach --interactive --tty --replace ' \ + container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \ f'--memory {memory}m --memory-swap 0 --restart {restart} ' \ f'--name {name} {port} {volume} {env_opt}' if 'allow_host_networks' in container_config: @@ -310,7 +305,17 @@ def apply(container): if 'address' in container_config['network'][network]: address = container_config['network'][network]['address'] ipparam = f'--ip {address}' - _cmd(f'{container_base_cmd} --net {network} {ipparam} {image}') + + counter = 0 + while True: + if counter >= 10: + break + try: + _cmd(f'{container_base_cmd} --net {network} {ipparam} {image}') + break + except: + counter = counter +1 + sleep(0.5) return None diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 28f2a4ca5..a8cef5ebf 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -151,9 +151,15 @@ def verify(dhcp): listen_ok = False subnets = [] failover_ok = False + shared_networks = len(dhcp['shared_network_name']) + disabled_shared_networks = 0 + # A shared-network requires a subnet definition for network, network_config in dhcp['shared_network_name'].items(): + if 'disable' in network_config: + disabled_shared_networks += 1 + if 'subnet' not in network_config: raise ConfigError(f'No subnets defined for {network}. At least one\n' \ 'lease subnet must be configured.') @@ -226,7 +232,7 @@ def verify(dhcp): # There must be one subnet connected to a listen interface. # This only counts if the network itself is not disabled! if 'disable' not in network_config: - if is_subnet_connected(subnet, primary=True): + if is_subnet_connected(subnet, primary=False): listen_ok = True # Subnets must be non overlapping @@ -243,6 +249,10 @@ def verify(dhcp): if net.overlaps(net2): raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!') + # Prevent 'disable' for shared-network if only one network is configured + if (shared_networks - disabled_shared_networks) < 1: + raise ConfigError(f'At least one shared network must be active!') + if 'failover' in dhcp: if not failover_ok: raise ConfigError('DHCP failover must be enabled for at least one subnet!') diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 175300bb0..e6a2e4486 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -128,7 +128,7 @@ def verify(dhcpv6): # Subnets must be unique if subnet in subnets: - raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network'])) + raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!') subnets.append(subnet) # DHCPv6 requires at least one configured address range or one static mapping diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index c44e6c974..23a16df63 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -17,6 +17,7 @@ import os from sys import exit +from glob import glob from vyos.config import Config from vyos.configdict import dict_merge @@ -50,10 +51,12 @@ def get_config(config=None): if not conf.exists(base): return None - dns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + dns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. + # options which we need to update into the dictionary retrieved. default_values = defaults(base) + # T2665 due to how defaults under tag nodes work, we must clear these out before we merge + del default_values['authoritative_domain'] dns = dict_merge(default_values, dns) # some additions to the default dictionary @@ -66,20 +69,182 @@ def get_config(config=None): if conf.exists(base_nameservers_dhcp): dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)}) - # Split the source_address property into separate IPv4 and IPv6 lists - # NOTE: In future versions of pdns-recursor (> 4.4.0), this logic can be removed - # as both IPv4 and IPv6 addresses can be specified in a single setting. - source_address_v4 = [] - source_address_v6 = [] - - for source_address in dns['source_address']: - if is_ipv6(source_address): - source_address_v6.append(source_address) - else: - source_address_v4.append(source_address) - - dns.update({'source_address_v4': source_address_v4}) - dns.update({'source_address_v6': source_address_v6}) + if 'authoritative_domain' in dns: + dns['authoritative_zones'] = [] + dns['authoritative_zone_errors'] = [] + for node in dns['authoritative_domain']: + zonedata = dns['authoritative_domain'][node] + if ('disable' in zonedata) or (not 'records' in zonedata): + continue + zone = { + 'name': node, + 'file': "{}/zone.{}.conf".format(pdns_rec_run_dir, node), + 'records': [], + } + + recorddata = zonedata['records'] + + for rtype in [ 'a', 'aaaa', 'cname', 'mx', 'ptr', 'txt', 'spf', 'srv', 'naptr' ]: + if rtype not in recorddata: + continue + for subnode in recorddata[rtype]: + if 'disable' in recorddata[rtype][subnode]: + continue + + rdata = recorddata[rtype][subnode] + + if rtype in [ 'a', 'aaaa' ]: + rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 + rdata = dict_merge(rdefaults, rdata) + + if not 'address' in rdata: + dns['authoritative_zone_errors'].append('{}.{}: at least one address is required'.format(subnode, node)) + continue + + for address in rdata['address']: + zone['records'].append({ + 'name': subnode, + 'type': rtype.upper(), + 'ttl': rdata['ttl'], + 'value': address + }) + elif rtype in ['cname', 'ptr']: + rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 + rdata = dict_merge(rdefaults, rdata) + + if not 'target' in rdata: + dns['authoritative_zone_errors'].append('{}.{}: target is required'.format(subnode, node)) + continue + + zone['records'].append({ + 'name': subnode, + 'type': rtype.upper(), + 'ttl': rdata['ttl'], + 'value': '{}.'.format(rdata['target']) + }) + elif rtype == 'mx': + rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 + del rdefaults['server'] + rdata = dict_merge(rdefaults, rdata) + + if not 'server' in rdata: + dns['authoritative_zone_errors'].append('{}.{}: at least one server is required'.format(subnode, node)) + continue + + for servername in rdata['server']: + serverdata = rdata['server'][servername] + serverdefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'server']) # T2665 + serverdata = dict_merge(serverdefaults, serverdata) + zone['records'].append({ + 'name': subnode, + 'type': rtype.upper(), + 'ttl': rdata['ttl'], + 'value': '{} {}.'.format(serverdata['priority'], servername) + }) + elif rtype == 'txt': + rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 + rdata = dict_merge(rdefaults, rdata) + + if not 'value' in rdata: + dns['authoritative_zone_errors'].append('{}.{}: at least one value is required'.format(subnode, node)) + continue + + for value in rdata['value']: + zone['records'].append({ + 'name': subnode, + 'type': rtype.upper(), + 'ttl': rdata['ttl'], + 'value': "\"{}\"".format(value.replace("\"", "\\\"")) + }) + elif rtype == 'spf': + rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 + rdata = dict_merge(rdefaults, rdata) + + if not 'value' in rdata: + dns['authoritative_zone_errors'].append('{}.{}: value is required'.format(subnode, node)) + continue + + zone['records'].append({ + 'name': subnode, + 'type': rtype.upper(), + 'ttl': rdata['ttl'], + 'value': '"{}"'.format(rdata['value'].replace("\"", "\\\"")) + }) + elif rtype == 'srv': + rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 + del rdefaults['entry'] + rdata = dict_merge(rdefaults, rdata) + + if not 'entry' in rdata: + dns['authoritative_zone_errors'].append('{}.{}: at least one entry is required'.format(subnode, node)) + continue + + for entryno in rdata['entry']: + entrydata = rdata['entry'][entryno] + entrydefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'entry']) # T2665 + entrydata = dict_merge(entrydefaults, entrydata) + + if not 'hostname' in entrydata: + dns['authoritative_zone_errors'].append('{}.{}: hostname is required for entry {}'.format(subnode, node, entryno)) + continue + + if not 'port' in entrydata: + dns['authoritative_zone_errors'].append('{}.{}: port is required for entry {}'.format(subnode, node, entryno)) + continue + + zone['records'].append({ + 'name': subnode, + 'type': rtype.upper(), + 'ttl': rdata['ttl'], + 'value': '{} {} {} {}.'.format(entrydata['priority'], entrydata['weight'], entrydata['port'], entrydata['hostname']) + }) + elif rtype == 'naptr': + rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 + del rdefaults['rule'] + rdata = dict_merge(rdefaults, rdata) + + + if not 'rule' in rdata: + dns['authoritative_zone_errors'].append('{}.{}: at least one rule is required'.format(subnode, node)) + continue + + for ruleno in rdata['rule']: + ruledata = rdata['rule'][ruleno] + ruledefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'rule']) # T2665 + ruledata = dict_merge(ruledefaults, ruledata) + flags = "" + if 'lookup-srv' in ruledata: + flags += "S" + if 'lookup-a' in ruledata: + flags += "A" + if 'resolve-uri' in ruledata: + flags += "U" + if 'protocol-specific' in ruledata: + flags += "P" + + if 'order' in ruledata: + order = ruledata['order'] + else: + order = ruleno + + if 'regexp' in ruledata: + regexp= ruledata['regexp'].replace("\"", "\\\"") + else: + regexp = '' + + if ruledata['replacement']: + replacement = '{}.'.format(ruledata['replacement']) + else: + replacement = '' + + zone['records'].append({ + 'name': subnode, + 'type': rtype.upper(), + 'ttl': rdata['ttl'], + 'value': '{} {} "{}" "{}" "{}" {}'.format(order, ruledata['preference'], flags, ruledata['service'], regexp, replacement) + }) + + dns['authoritative_zones'].append(zone) return dns @@ -101,6 +266,11 @@ def verify(dns): if 'server' not in dns['domain'][domain]: raise ConfigError(f'No server configured for domain {domain}!') + if ('authoritative_zone_errors' in dns) and dns['authoritative_zone_errors']: + for error in dns['authoritative_zone_errors']: + print(error) + raise ConfigError('Invalid authoritative records have been defined') + if 'system' in dns: if not ('system_name_server' in dns or 'system_name_server_dhcp' in dns): print("Warning: No 'system name-server' or 'system " \ @@ -119,6 +289,15 @@ def generate(dns): render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.tmpl', dns, user=pdns_rec_user, group=pdns_rec_group) + for zone_filename in glob(f'{pdns_rec_run_dir}/zone.*.conf'): + os.unlink(zone_filename) + + if 'authoritative_zones' in dns: + for zone in dns['authoritative_zones']: + render(zone['file'], 'dns-forwarding/recursor.zone.conf.tmpl', + zone, user=pdns_rec_user, group=pdns_rec_group) + + # if vyos-hostsd didn't create its files yet, create them (empty) for file in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]: with open(file, 'a'): @@ -134,6 +313,9 @@ def apply(dns): if os.path.isfile(pdns_rec_config_file): os.unlink(pdns_rec_config_file) + + for zone_filename in glob(f'{pdns_rec_run_dir}/zone.*.conf'): + os.unlink(zone_filename) else: ### first apply vyos-hostsd config hc = hostsd_client() @@ -168,6 +350,12 @@ def apply(dns): if 'domain' in dns: hc.add_forward_zones(dns['domain']) + # hostsd generates NTAs for the authoritative zones + # the list and keys() are required as get returns a dict, not list + hc.delete_authoritative_zones(list(hc.get_authoritative_zones())) + if 'authoritative_zones' in dns: + hc.add_authoritative_zones(list(map(lambda zone: zone['name'], dns['authoritative_zones']))) + # call hostsd to generate forward-zones and its lua-config-file hc.apply() diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index c979feca7..a31e5ed75 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -131,9 +131,7 @@ def generate(dyndns): if not dyndns: return None - render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns, - permission=0o600) - + render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns) return None def apply(dyndns): diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py new file mode 100755 index 000000000..3a17dc5a4 --- /dev/null +++ b/src/conf_mode/firewall-interface.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 re + +from sys import argv +from sys import exit + +from vyos.config import Config +from vyos.configdict import leaf_node_changed +from vyos.ifconfig import Section +from vyos.template import render +from vyos.util import cmd +from vyos.util import dict_search_args +from vyos.util import run +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +NFT_CHAINS = { + 'in': 'VYOS_FW_IN', + 'out': 'VYOS_FW_OUT', + 'local': 'VYOS_FW_LOCAL' +} +NFT6_CHAINS = { + 'in': 'VYOS_FW6_IN', + 'out': 'VYOS_FW6_OUT', + 'local': 'VYOS_FW6_LOCAL' +} + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + ifname = argv[1] + ifpath = Section.get_config_path(ifname) + if_firewall_path = f'interfaces {ifpath} firewall' + + if_firewall = conf.get_config_dict(if_firewall_path, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + if_firewall['ifname'] = ifname + if_firewall['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + return if_firewall + +def verify(if_firewall): + # bail out early - looks like removal from running config + if not if_firewall: + return None + + for direction in ['in', 'out', 'local']: + if direction in if_firewall: + if 'name' in if_firewall[direction]: + name = if_firewall[direction]['name'] + + if 'name' not in if_firewall['firewall']: + raise ConfigError('Firewall name not configured') + + if name not in if_firewall['firewall']['name']: + raise ConfigError(f'Invalid firewall name "{name}"') + + if 'ipv6_name' in if_firewall[direction]: + name = if_firewall[direction]['ipv6_name'] + + if 'ipv6_name' not in if_firewall['firewall']: + raise ConfigError('Firewall ipv6-name not configured') + + if name not in if_firewall['firewall']['ipv6_name']: + raise ConfigError(f'Invalid firewall ipv6-name "{name}"') + + return None + +def generate(if_firewall): + return None + +def cleanup_rule(table, chain, ifname, new_name=None): + results = cmd(f'nft -a list chain {table} {chain}').split("\n") + retval = None + for line in results: + if f'ifname "{ifname}"' in line: + if new_name and f'jump {new_name}' in line: + # new_name is used to clear rules for any previously referenced chains + # returns true when rule exists and doesn't need to be created + retval = True + continue + + handle_search = re.search('handle (\d+)', line) + if handle_search: + run(f'nft delete rule {table} {chain} handle {handle_search[1]}') + return retval + +def apply(if_firewall): + ifname = if_firewall['ifname'] + + for direction in ['in', 'out', 'local']: + chain = NFT_CHAINS[direction] + ipv6_chain = NFT6_CHAINS[direction] + if_prefix = 'i' if direction in ['in', 'local'] else 'o' + + name = dict_search_args(if_firewall, direction, 'name') + if name: + rule_exists = cleanup_rule('ip filter', chain, ifname, name) + + if not rule_exists: + run(f'nft insert rule ip filter {chain} {if_prefix}ifname {ifname} counter jump {name}') + else: + cleanup_rule('ip filter', chain, ifname) + + ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') + if ipv6_name: + rule_exists = cleanup_rule('ip6 filter', ipv6_chain, ifname, ipv6_name) + + if not rule_exists: + run(f'nft insert rule ip6 filter {ipv6_chain} {if_prefix}ifname {ifname} counter jump {ipv6_name}') + else: + cleanup_rule('ip6 filter', ipv6_chain, ifname) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 8e6ce5b14..5ac48c9ba 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -16,50 +16,295 @@ import os +from glob import glob +from json import loads from sys import exit from vyos.config import Config from vyos.configdict import dict_merge -from vyos.configdict import node_changed -from vyos.configdict import leaf_node_changed +from vyos.configdiff import get_config_diff, Diff from vyos.template import render -from vyos.util import call +from vyos.util import cmd +from vyos.util import dict_search_args +from vyos.util import process_named_running +from vyos.util import run +from vyos.xml import defaults from vyos import ConfigError from vyos import airbag -from pprint import pprint airbag.enable() +nftables_conf = '/run/nftables.conf' -def get_config(config=None): +sysfs_config = { + 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'}, + 'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'enable': '0', 'disable': '1'}, + 'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route'}, + 'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects'}, + 'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'enable': '0', 'disable': '-1'}, + 'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians'}, + 'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects'}, + 'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects'}, + 'source_validation': {'sysfs': '/proc/sys/net/ipv4/conf/*/rp_filter', 'disable': '0', 'strict': '1', 'loose': '2'}, + 'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies'}, + 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'} +} + +preserve_chains = [ + 'INPUT', + 'FORWARD', + 'OUTPUT', + 'VYOS_FW_IN', + 'VYOS_FW_OUT', + 'VYOS_FW_LOCAL', + 'VYOS_FW_OUTPUT', + 'VYOS_POST_FW', + 'VYOS_FRAG_MARK', + 'VYOS_FW6_IN', + 'VYOS_FW6_OUT', + 'VYOS_FW6_LOCAL', + 'VYOS_FW6_OUTPUT', + 'VYOS_POST_FW6', + 'VYOS_FRAG6_MARK' +] + +valid_groups = [ + 'address_group', + 'network_group', + 'port_group' +] + +snmp_change_type = { + 'unknown': 0, + 'add': 1, + 'delete': 2, + 'change': 3 +} +snmp_event_source = 1 +snmp_trap_mib = 'VYATTA-TRAP-MIB' +snmp_trap_name = 'mgmtEventTrap' +def get_firewall_interfaces(conf): + out = {} + interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + def find_interfaces(iftype_conf, output={}, prefix=''): + for ifname, if_conf in iftype_conf.items(): + if 'firewall' in if_conf: + output[prefix + ifname] = if_conf['firewall'] + for vif in ['vif', 'vif_s', 'vif_c']: + if vif in if_conf: + output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.')) + return output + for iftype, iftype_conf in interfaces.items(): + out.update(find_interfaces(iftype_conf)) + return out + +def get_config(config=None): if config: conf = config else: conf = Config() - base = ['nfirewall'] + base = ['firewall'] + + if not conf.exists(base): + return {} + firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - pprint(firewall) + default_values = defaults(base) + firewall = dict_merge(default_values, firewall) + + firewall['interfaces'] = get_firewall_interfaces(conf) + + if 'config_trap' in firewall and firewall['config_trap'] == 'enable': + diff = get_config_diff(conf) + firewall['trap_diff'] = diff.get_child_nodes_diff_str(base) + firewall['trap_targets'] = conf.get_config_dict(['service', 'snmp', 'trap-target'], + key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) return firewall +def verify_rule(firewall, rule_conf, ipv6): + if 'action' not in rule_conf: + raise ConfigError('Rule action must be defined') + + if 'fragment' in rule_conf: + if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']): + raise ConfigError('Cannot specify both "match-frag" and "match-non-frag"') + + if 'ipsec' in rule_conf: + if {'match_ipsec', 'match_non_ipsec'} <= set(rule_conf['ipsec']): + raise ConfigError('Cannot specify both "match-ipsec" and "match-non-ipsec"') + + if 'recent' in rule_conf: + if not {'count', 'time'} <= set(rule_conf['recent']): + raise ConfigError('Recent "count" and "time" values must be defined') + + for side in ['destination', 'source']: + if side in rule_conf: + side_conf = rule_conf[side] + + if 'group' in side_conf: + if {'address_group', 'network_group'} <= set(side_conf['group']): + raise ConfigError('Only one address-group or network-group can be specified') + + for group in valid_groups: + if group in side_conf['group']: + group_name = side_conf['group'][group] + + fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group + + if not dict_search_args(firewall, 'group', fw_group): + error_group = fw_group.replace("_", "-") + raise ConfigError(f'Group defined in rule but {error_group} is not configured') + + if group_name not in firewall['group'][fw_group]: + error_group = group.replace("_", "-") + raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule') + + if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'): + if 'protocol' not in rule_conf: + raise ConfigError('Protocol must be defined if specifying a port or port-group') + + if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: + raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group') + def verify(firewall): # bail out early - looks like removal from running config if not firewall: return None + if 'config_trap' in firewall and firewall['config_trap'] == 'enable': + if not firewall['trap_targets']: + raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined') + + for name in ['name', 'ipv6_name']: + if name in firewall: + for name_id, name_conf in firewall[name].items(): + if name_id in preserve_chains: + raise ConfigError(f'Firewall name "{name_id}" is reserved for VyOS') + + if 'rule' in name_conf: + for rule_id, rule_conf in name_conf['rule'].items(): + verify_rule(firewall, rule_conf, name == 'ipv6_name') + + for ifname, if_firewall in firewall['interfaces'].items(): + for direction in ['in', 'out', 'local']: + name = dict_search_args(if_firewall, direction, 'name') + ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') + + if name and not dict_search_args(firewall, 'name', name): + raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}') + + if ipv6_name and not dict_search_args(firewall, 'ipv6_name', ipv6_name): + raise ConfigError(f'Firewall ipv6-name "{ipv6_name}" is still referenced on interface {ifname}') + return None +def cleanup_commands(firewall): + commands = [] + for table in ['ip filter', 'ip6 filter']: + json_str = cmd(f'nft -j list table {table}') + obj = loads(json_str) + if 'nftables' not in obj: + continue + for item in obj['nftables']: + if 'chain' in item: + if item['chain']['name'] not in preserve_chains: + chain = item['chain']['name'] + if table == 'ip filter' and dict_search_args(firewall, 'name', chain): + commands.append(f'flush chain {table} {chain}') + elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain): + commands.append(f'flush chain {table} {chain}') + else: + commands.append(f'delete chain {table} {chain}') + return commands + def generate(firewall): - if not firewall: - return None + if not os.path.exists(nftables_conf): + firewall['first_install'] = True + else: + firewall['cleanup_commands'] = cleanup_commands(firewall) + render(nftables_conf, 'firewall/nftables.tmpl', firewall) return None -def apply(firewall): - if not firewall: +def apply_sysfs(firewall): + for name, conf in sysfs_config.items(): + paths = glob(conf['sysfs']) + value = None + + if name in firewall: + conf_value = firewall[name] + + if conf_value in conf: + value = conf[conf_value] + elif conf_value == 'enable': + value = '1' + elif conf_value == 'disable': + value = '0' + + if value: + for path in paths: + with open(path, 'w') as f: + f.write(value) + +def post_apply_trap(firewall): + if 'first_install' in firewall: return None + if 'config_trap' not in firewall or firewall['config_trap'] != 'enable': + return None + + if not process_named_running('snmpd'): + return None + + trap_username = os.getlogin() + + for host, target_conf in firewall['trap_targets'].items(): + community = target_conf['community'] if 'community' in target_conf else 'public' + port = int(target_conf['port']) if 'port' in target_conf else 162 + + base_cmd = f'snmptrap -v2c -c {community} {host}:{port} 0 {snmp_trap_mib}::{snmp_trap_name} ' + + for change_type, changes in firewall['trap_diff'].items(): + for path_str, value in changes.items(): + objects = [ + f'mgmtEventUser s "{trap_username}"', + f'mgmtEventSource i {snmp_event_source}', + f'mgmtEventType i {snmp_change_type[change_type]}' + ] + + if change_type == 'add': + objects.append(f'mgmtEventCurrCfg s "{path_str} {value}"') + elif change_type == 'delete': + objects.append(f'mgmtEventPrevCfg s "{path_str} {value}"') + elif change_type == 'change': + objects.append(f'mgmtEventPrevCfg s "{path_str} {value[0]}"') + objects.append(f'mgmtEventCurrCfg s "{path_str} {value[1]}"') + + cmd(base_cmd + ' '.join(objects)) + +def apply(firewall): + if 'first_install' in firewall: + run('nfct helper add rpc inet tcp') + run('nfct helper add rpc inet udp') + run('nfct helper add tns inet tcp') + + install_result = run(f'nft -f {nftables_conf}') + if install_result == 1: + raise ConfigError('Failed to apply firewall') + + if 'state_policy' in firewall: + for chain in ['INPUT', 'OUTPUT', 'FORWARD']: + cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY') + cmd(f'nft insert rule ip6 filter {chain} jump VYOS_STATE_POLICY6') + + apply_sysfs(firewall) + + post_apply_trap(firewall) + return None if __name__ == '__main__': diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 9cae29481..975f19acf 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2021 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,121 +16,83 @@ import os import re + from sys import exit import ipaddress from ipaddress import ip_address -from jinja2 import FileSystemLoader, Environment +from vyos.config import Config +from vyos.configdict import dict_merge from vyos.ifconfig import Section from vyos.ifconfig import Interface -from vyos.config import Config -from vyos import ConfigError -from vyos.util import cmd from vyos.template import render - +from vyos.util import cmd +from vyos.validate import is_addr_assigned +from vyos.xml import defaults +from vyos import ConfigError from vyos import airbag airbag.enable() -# default values -default_sflow_server_port = 6343 -default_netflow_server_port = 2055 -default_plugin_pipe_size = 10 -default_captured_packet_size = 128 -default_netflow_version = '9' -default_sflow_agentip = 'auto' -uacctd_conf_path = '/etc/pmacct/uacctd.conf' -iptables_nflog_table = 'raw' -iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK' -egress_iptables_nflog_table = 'mangle' -egress_iptables_nflog_chain = 'FORWARD' - -# helper functions -# check if node exists and return True if this is true -def _node_exists(path): - vyos_config = Config() - if vyos_config.exists(path): - return True - -# get sFlow agent-ip if agent-address is "auto" (default behaviour) -def _sflow_default_agentip(config): - # check if any of BGP, OSPF, OSPFv3 protocols are configured and use router-id from there - if config.exists('protocols bgp'): - bgp_router_id = config.return_value("protocols bgp {} parameters router-id".format(config.list_nodes('protocols bgp')[0])) - if bgp_router_id: - return bgp_router_id - if config.return_value('protocols ospf parameters router-id'): - return config.return_value('protocols ospf parameters router-id') - if config.return_value('protocols ospfv3 parameters router-id'): - return config.return_value('protocols ospfv3 parameters router-id') - - # if router-id was not found, use first available ip of any interface - for iface in Section.interfaces(): - for address in Interface(iface).get_addr(): - # return an IP, if this is not loopback - regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') - if regex_filter.search(address): - return regex_filter.search(address).group('ipaddr') - - # return nothing by default - return None - -# get iptables rule dict for chain in table -def _iptables_get_nflog(chain, table): +uacctd_conf_path = '/run/pmacct/uacctd.conf' +nftables_nflog_table = 'raw' +nftables_nflog_chain = 'VYOS_CT_PREROUTING_HOOK' +egress_nftables_nflog_table = 'inet mangle' +egress_nftables_nflog_chain = 'FORWARD' + +# get nftables rule dict for chain in table +def _nftables_get_nflog(chain, table): # define list with rules rules = [] # prepare regex for parsing rules - rule_pattern = "^-A (?P<rule_definition>{0} (\-i|\-o) (?P<interface>[\w\.\*\-]+).*--comment FLOW_ACCOUNTING_RULE.* -j NFLOG.*$)".format(chain) + rule_pattern = '[io]ifname "(?P<interface>[\w\.\*\-]+)".*handle (?P<handle>[\d]+)' rule_re = re.compile(rule_pattern) - for iptables_variant in ['iptables', 'ip6tables']: - # run iptables, save output and split it by lines - iptables_command = f'{iptables_variant} -t {table} -S {chain}' - tmp = cmd(iptables_command, message='Failed to get flows list') - - # parse each line and add information to list - for current_rule in tmp.splitlines(): - current_rule_parsed = rule_re.search(current_rule) - if current_rule_parsed: - rules.append({ 'interface': current_rule_parsed.groupdict()["interface"], 'iptables_variant': iptables_variant, 'table': table, 'rule_definition': current_rule_parsed.groupdict()["rule_definition"] }) + # run nftables, save output and split it by lines + nftables_command = f'nft -a list chain {table} {chain}' + tmp = cmd(nftables_command, message='Failed to get flows list') + # parse each line and add information to list + for current_rule in tmp.splitlines(): + if 'FLOW_ACCOUNTING_RULE' not in current_rule: + continue + current_rule_parsed = rule_re.search(current_rule) + if current_rule_parsed: + groups = current_rule_parsed.groupdict() + rules.append({ 'interface': groups["interface"], 'table': table, 'handle': groups["handle"] }) # return list with rules return rules -# modify iptables rules -def _iptables_config(configured_ifaces, direction): - # define list of iptables commands to modify settings - iptable_commands = [] - iptables_chain = iptables_nflog_chain - iptables_table = iptables_nflog_table +def _nftables_config(configured_ifaces, direction, length=None): + # define list of nftables commands to modify settings + nftable_commands = [] + nftables_chain = nftables_nflog_chain + nftables_table = nftables_nflog_table if direction == "egress": - iptables_chain = egress_iptables_nflog_chain - iptables_table = egress_iptables_nflog_table + nftables_chain = egress_nftables_nflog_chain + nftables_table = egress_nftables_nflog_table # prepare extended list with configured interfaces configured_ifaces_extended = [] for iface in configured_ifaces: - configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'iptables' }) - configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'ip6tables' }) + configured_ifaces_extended.append({ 'iface': iface }) - # get currently configured interfaces with iptables rules - active_nflog_rules = _iptables_get_nflog(iptables_chain, iptables_table) + # get currently configured interfaces with nftables rules + active_nflog_rules = _nftables_get_nflog(nftables_chain, nftables_table) # compare current active list with configured one and delete excessive interfaces, add missed active_nflog_ifaces = [] for rule in active_nflog_rules: - iptables = rule['iptables_variant'] interface = rule['interface'] if interface not in configured_ifaces: table = rule['table'] - rule = rule['rule_definition'] - iptable_commands.append(f'{iptables} -t {table} -D {rule}') + handle = rule['handle'] + nftable_commands.append(f'nft delete rule {table} {nftables_chain} handle {handle}') else: active_nflog_ifaces.append({ 'iface': interface, - 'iptables_variant': iptables, }) # do not create new rules for already configured interfaces @@ -141,244 +103,166 @@ def _iptables_config(configured_ifaces, direction): # create missed rules for iface_extended in configured_ifaces_extended: iface = iface_extended['iface'] - iptables = iface_extended['iptables_variant'] - iptables_op = "-i" - if direction == "egress": - iptables_op = "-o" - - rule_definition = f'{iptables_chain} {iptables_op} {iface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {default_captured_packet_size} --nflog-threshold 100' - iptable_commands.append(f'{iptables} -t {iptables_table} -I {rule_definition}') + iface_prefix = "o" if direction == "egress" else "i" + rule_definition = f'{iface_prefix}ifname "{iface}" counter log group 2 snaplen {length} queue-threshold 100 comment "FLOW_ACCOUNTING_RULE"' + nftable_commands.append(f'nft insert rule {nftables_table} {nftables_chain} {rule_definition}') - # change iptables - for command in iptable_commands: + # change nftables + for command in nftable_commands: cmd(command, raising=ConfigError) -def get_config(): - vc = Config() - vc.set_level('') - # Convert the VyOS config to an abstract internal representation - flow_config = { - 'flow-accounting-configured': vc.exists('system flow-accounting'), - 'buffer-size': vc.return_value('system flow-accounting buffer-size'), - 'enable-egress': _node_exists('system flow-accounting enable-egress'), - 'disable-imt': _node_exists('system flow-accounting disable-imt'), - 'syslog-facility': vc.return_value('system flow-accounting syslog-facility'), - 'interfaces': None, - 'sflow': { - 'configured': vc.exists('system flow-accounting sflow'), - 'agent-address': vc.return_value('system flow-accounting sflow agent-address'), - 'sampling-rate': vc.return_value('system flow-accounting sflow sampling-rate'), - 'servers': None - }, - 'netflow': { - 'configured': vc.exists('system flow-accounting netflow'), - 'engine-id': vc.return_value('system flow-accounting netflow engine-id'), - 'max-flows': vc.return_value('system flow-accounting netflow max-flows'), - 'sampling-rate': vc.return_value('system flow-accounting netflow sampling-rate'), - 'source-ip': vc.return_value('system flow-accounting netflow source-ip'), - 'version': vc.return_value('system flow-accounting netflow version'), - 'timeout': { - 'expint': vc.return_value('system flow-accounting netflow timeout expiry-interval'), - 'general': vc.return_value('system flow-accounting netflow timeout flow-generic'), - 'icmp': vc.return_value('system flow-accounting netflow timeout icmp'), - 'maxlife': vc.return_value('system flow-accounting netflow timeout max-active-life'), - 'tcp.fin': vc.return_value('system flow-accounting netflow timeout tcp-fin'), - 'tcp': vc.return_value('system flow-accounting netflow timeout tcp-generic'), - 'tcp.rst': vc.return_value('system flow-accounting netflow timeout tcp-rst'), - 'udp': vc.return_value('system flow-accounting netflow timeout udp') - }, - 'servers': None - } - } - - # get interfaces list - if vc.exists('system flow-accounting interface'): - flow_config['interfaces'] = vc.return_values('system flow-accounting interface') - - # get sFlow collectors list - if vc.exists('system flow-accounting sflow server'): - flow_config['sflow']['servers'] = [] - sflow_collectors = vc.list_nodes('system flow-accounting sflow server') - for collector in sflow_collectors: - port = default_sflow_server_port - if vc.return_value("system flow-accounting sflow server {} port".format(collector)): - port = vc.return_value("system flow-accounting sflow server {} port".format(collector)) - flow_config['sflow']['servers'].append({ 'address': collector, 'port': port }) - - # get NetFlow collectors list - if vc.exists('system flow-accounting netflow server'): - flow_config['netflow']['servers'] = [] - netflow_collectors = vc.list_nodes('system flow-accounting netflow server') - for collector in netflow_collectors: - port = default_netflow_server_port - if vc.return_value("system flow-accounting netflow server {} port".format(collector)): - port = vc.return_value("system flow-accounting netflow server {} port".format(collector)) - flow_config['netflow']['servers'].append({ 'address': collector, 'port': port }) - - # get sflow agent-id - if flow_config['sflow']['agent-address'] == None or flow_config['sflow']['agent-address'] == 'auto': - flow_config['sflow']['agent-address'] = _sflow_default_agentip(vc) - - # get NetFlow version - if not flow_config['netflow']['version']: - flow_config['netflow']['version'] = default_netflow_version - - # convert NetFlow engine-id format, if this is necessary - if flow_config['netflow']['engine-id'] and flow_config['netflow']['version'] == '5': - regex_filter = re.compile('^\d+$') - if regex_filter.search(flow_config['netflow']['engine-id']): - flow_config['netflow']['engine-id'] = "{}:0".format(flow_config['netflow']['engine-id']) - - # return dict with flow-accounting configuration - return flow_config - -def verify(config): - # Verify that configuration is valid - # skip all checks if flow-accounting was removed - if not config['flow-accounting-configured']: - return True +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['system', 'flow-accounting'] + if not conf.exists(base): + return None + + flow_accounting = conf.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(base) + + # delete individual flow type default - should only be added if user uses + # this feature + for flow_type in ['sflow', 'netflow']: + if flow_type in default_values: + del default_values[flow_type] + flow_accounting = dict_merge(default_values, flow_accounting) + + for flow_type in ['sflow', 'netflow']: + if flow_type in flow_accounting: + default_values = defaults(base + [flow_type]) + # we need to merge individual server configurations + if 'server' in default_values: + del default_values['server'] + flow_accounting[flow_type] = dict_merge(default_values, flow_accounting[flow_type]) + + if 'server' in flow_accounting[flow_type]: + default_values = defaults(base + [flow_type, 'server']) + for server in flow_accounting[flow_type]['server']: + flow_accounting[flow_type]['server'][server] = dict_merge( + default_values,flow_accounting[flow_type]['server'][server]) + + return flow_accounting + +def verify(flow_config): + if not flow_config: + return None # check if at least one collector is enabled - if not (config['sflow']['configured'] or config['netflow']['configured'] or not config['disable-imt']): - raise ConfigError("You need to configure at least one sFlow or NetFlow protocol, or not set \"disable-imt\" for flow-accounting") + if 'sflow' not in flow_config and 'netflow' not in flow_config and 'disable_imt' in flow_config: + raise ConfigError('You need to configure at least sFlow or NetFlow, ' \ + 'or not set "disable-imt" for flow-accounting!') # Check if at least one interface is configured - if not config['interfaces']: - raise ConfigError("You need to configure at least one interface for flow-accounting") + if 'interface' not in flow_config: + raise ConfigError('Flow accounting requires at least one interface to ' \ + 'be configured!') # check that all configured interfaces exists in the system - for iface in config['interfaces']: - if not iface in Section.interfaces(): - # chnged from error to warning to allow adding dynamic interfaces and interface templates - # raise ConfigError("The {} interface is not presented in the system".format(iface)) - print("Warning: the {} interface is not presented in the system".format(iface)) + for interface in flow_config['interface']: + if interface not in Section.interfaces(): + # Changed from error to warning to allow adding dynamic interfaces + # and interface templates + print(f'Warning: Interface "{interface}" is not presented in the system') # check sFlow configuration - if config['sflow']['configured']: - # check if at least one sFlow collector is configured if sFlow configuration is presented - if not config['sflow']['servers']: - raise ConfigError("You need to configure at least one sFlow server") + if 'sflow' in flow_config: + # check if at least one sFlow collector is configured + if 'server' not in flow_config['sflow']: + raise ConfigError('You need to configure at least one sFlow server!') # check that all sFlow collectors use the same IP protocol version sflow_collector_ipver = None - for sflow_collector in config['sflow']['servers']: + for server in flow_config['sflow']['server']: if sflow_collector_ipver: - if sflow_collector_ipver != ip_address(sflow_collector['address']).version: + if sflow_collector_ipver != ip_address(server).version: raise ConfigError("All sFlow servers must use the same IP protocol") else: - sflow_collector_ipver = ip_address(sflow_collector['address']).version - + sflow_collector_ipver = ip_address(server).version # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa - for sflow_collector in config['sflow']['servers']: - if ip_address(sflow_collector['address']).version != ip_address(config['sflow']['agent-address']).version: - raise ConfigError("Different IP address versions cannot be mixed in \"sflow agent-address\" and \"sflow server\". You need to set manually the same IP version for \"agent-address\" as for all sFlow servers") - - # check if configured sFlow agent-id exist in the system - agent_id_presented = None - for iface in Section.interfaces(): - for address in Interface(iface).get_addr(): - # check an IP, if this is not loopback - regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') - if regex_filter.search(address): - if regex_filter.search(address).group('ipaddr') == config['sflow']['agent-address']: - agent_id_presented = True - break - if not agent_id_presented: - raise ConfigError("Your \"sflow agent-address\" does not exist in the system") + for server in flow_config['sflow']['server']: + if 'agent_address' in flow_config['sflow']: + if ip_address(server).version != ip_address(flow_config['sflow']['agent_address']).version: + raise ConfigError('IPv4 and IPv6 addresses can not be mixed in "sflow agent-address" and "sflow '\ + 'server". You need to set the same IP version for both "agent-address" and '\ + 'all sFlow servers') + + if 'agent_address' in flow_config['sflow']: + tmp = flow_config['sflow']['agent_address'] + if not is_addr_assigned(tmp): + print(f'Warning: Configured "sflow agent-address {tmp}" does not exist in the system!') # check NetFlow configuration - if config['netflow']['configured']: + if 'netflow' in flow_config: # check if at least one NetFlow collector is configured if NetFlow configuration is presented - if not config['netflow']['servers']: - raise ConfigError("You need to configure at least one NetFlow server") - - # check if configured netflow source-ip exist in the system - if config['netflow']['source-ip']: - source_ip_presented = None - for iface in Section.interfaces(): - for address in Interface(iface).get_addr(): - # check an IP - regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') - if regex_filter.search(address): - if regex_filter.search(address).group('ipaddr') == config['netflow']['source-ip']: - source_ip_presented = True - break - if not source_ip_presented: - raise ConfigError("Your \"netflow source-ip\" does not exist in the system") - - # check if engine-id compatible with selected protocol version - if config['netflow']['engine-id']: + if 'server' not in flow_config['netflow']: + raise ConfigError('You need to configure at least one NetFlow server!') + + # Check if configured netflow source-address exist in the system + if 'source_address' in flow_config['netflow']: + if not is_addr_assigned(flow_config['netflow']['source_address']): + tmp = flow_config['netflow']['source_address'] + print(f'Warning: Configured "netflow source-address {tmp}" does not exist on the system!') + + # Check if engine-id compatible with selected protocol version + if 'engine_id' in flow_config['netflow']: v5_filter = '^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$' v9v10_filter = '^(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$' - if config['netflow']['version'] == '5': + engine_id = flow_config['netflow']['engine_id'] + version = flow_config['netflow']['version'] + + if flow_config['netflow']['version'] == '5': regex_filter = re.compile(v5_filter) - if not regex_filter.search(config['netflow']['engine-id']): - raise ConfigError("You cannot use NetFlow engine-id {} together with NetFlow protocol version {}".format(config['netflow']['engine-id'], config['netflow']['version'])) + if not regex_filter.search(engine_id): + raise ConfigError(f'You cannot use NetFlow engine-id "{engine_id}" '\ + f'together with NetFlow protocol version "{version}"!') else: regex_filter = re.compile(v9v10_filter) - if not regex_filter.search(config['netflow']['engine-id']): - raise ConfigError("You cannot use NetFlow engine-id {} together with NetFlow protocol version {}".format(config['netflow']['engine-id'], config['netflow']['version'])) + if not regex_filter.search(flow_config['netflow']['engine_id']): + raise ConfigError(f'Can not use NetFlow engine-id "{engine_id}" together '\ + f'with NetFlow protocol version "{version}"!') # return True if all checks were passed return True -def generate(config): - # skip all checks if flow-accounting was removed - if not config['flow-accounting-configured']: - return True +def generate(flow_config): + if not flow_config: + return None - # Calculate all necessary values - if config['buffer-size']: - # circular queue size - config['plugin_pipe_size'] = int(config['buffer-size']) * 1024**2 - else: - config['plugin_pipe_size'] = default_plugin_pipe_size * 1024**2 - # transfer buffer size - # recommended value from pmacct developers 1/1000 of pipe size - config['plugin_buffer_size'] = int(config['plugin_pipe_size'] / 1000) - - # Prepare a timeouts string - timeout_string = '' - for timeout_type, timeout_value in config['netflow']['timeout'].items(): - if timeout_value: - if timeout_string == '': - timeout_string = "{}{}={}".format(timeout_string, timeout_type, timeout_value) - else: - timeout_string = "{}:{}={}".format(timeout_string, timeout_type, timeout_value) - config['netflow']['timeout_string'] = timeout_string + render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', flow_config) - render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', { - 'templatecfg': config, - 'snaplen': default_captured_packet_size, - }) - - -def apply(config): - # define variables - command = None +def apply(flow_config): + action = 'restart' # Check if flow-accounting was removed and define command - if not config['flow-accounting-configured']: - command = 'systemctl stop uacctd.service' - else: - command = 'systemctl restart uacctd.service' + if not flow_config: + _nftables_config([], 'ingress') + _nftables_config([], 'egress') - # run command to start or stop flow-accounting - cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting') + # Stop flow-accounting daemon and remove configuration file + cmd('systemctl stop uacctd.service') + if os.path.exists(uacctd_conf_path): + os.unlink(uacctd_conf_path) + return - # configure iptables rules for defined interfaces - if config['interfaces']: - _iptables_config(config['interfaces'], 'ingress') + # Start/reload flow-accounting daemon + cmd(f'systemctl restart uacctd.service') + + # configure nftables rules for defined interfaces + if 'interface' in flow_config: + _nftables_config(flow_config['interface'], 'ingress', flow_config['packet_length']) # configure egress the same way if configured otherwise remove it - if config['enable-egress']: - _iptables_config(config['interfaces'], 'egress') + if 'enable_egress' in flow_config: + _nftables_config(flow_config['interface'], 'egress', flow_config['packet_length']) else: - _iptables_config([], 'egress') - else: - _iptables_config([], 'ingress') - _iptables_config([], 'egress') + _nftables_config([], 'egress') if __name__ == '__main__': try: diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index a7135911d..87bad0dc6 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -79,7 +79,7 @@ def get_config(config=None): # system static-host-mapping for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']): hosts['static_host_mapping'][hn] = {} - hosts['static_host_mapping'][hn]['address'] = conf.return_value(['system', 'static-host-mapping', 'host-name', hn, 'inet']) + hosts['static_host_mapping'][hn]['address'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'inet']) hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias']) return hosts diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 7e4b117c8..b5f5e919f 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2021 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 @@ -13,25 +13,26 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# import sys import os import json -import time + +from time import sleep from copy import deepcopy import vyos.defaults + from vyos.config import Config -from vyos import ConfigError +from vyos.template import render from vyos.util import cmd from vyos.util import call - +from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = '/etc/vyos/http-api.conf' +api_conf_file = '/etc/vyos/http-api.conf' +systemd_service = '/run/systemd/system/vyos-http-api.service' vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode'] @@ -49,21 +50,35 @@ def get_config(config=None): else: conf = Config() - if not conf.exists('service https api'): + base = ['service', 'https', 'api'] + if not conf.exists(base): return None - else: - conf.set_level('service https api') + # Do we run inside a VRF context? + vrf_path = ['service', 'https', 'vrf'] + if conf.exists(vrf_path): + http_api['vrf'] = conf.return_value(vrf_path) + + conf.set_level('service https api') if conf.exists('strict'): - http_api['strict'] = 'true' + http_api['strict'] = True if conf.exists('debug'): - http_api['debug'] = 'true' + http_api['debug'] = True + + if conf.exists('socket'): + http_api['socket'] = True if conf.exists('port'): port = conf.return_value('port') http_api['port'] = port + if conf.exists('cors'): + http_api['cors'] = {} + if conf.exists('cors allow-origin'): + origins = conf.return_values('cors allow-origin') + http_api['cors']['origins'] = origins[:] + if conf.exists('keys'): for name in conf.list_nodes('keys id'): if conf.exists('keys id {0} key'.format(name)): @@ -83,24 +98,31 @@ def verify(http_api): def generate(http_api): if http_api is None: + if os.path.exists(systemd_service): + os.unlink(systemd_service) return None if not os.path.exists('/etc/vyos'): os.mkdir('/etc/vyos') - with open(config_file, 'w') as f: + with open(api_conf_file, 'w') as f: json.dump(http_api, f, indent=2) + render(systemd_service, 'https/vyos-http-api.service.tmpl', http_api) return None def apply(http_api): + # Reload systemd manager configuration + call('systemctl daemon-reload') + service_name = 'vyos-http-api.service' + if http_api is not None: - call('systemctl restart vyos-http-api.service') + call(f'systemctl restart {service_name}') else: - call('systemctl stop vyos-http-api.service') + call(f'systemctl stop {service_name}') # Let uvicorn settle before restarting Nginx - time.sleep(2) + sleep(1) cmd(f'{vyos_conf_scripts_dir}/https.py', raising=ConfigError) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index be4380462..37fa36797 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -23,16 +23,19 @@ import vyos.defaults import vyos.certbot_util from vyos.config import Config +from vyos.configverify import verify_vrf from vyos import ConfigError from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call +from vyos.util import write_file from vyos import airbag airbag.enable() config_file = '/etc/nginx/sites-available/default' +systemd_override = r'/etc/systemd/system/nginx.service.d/override.conf' cert_dir = '/etc/ssl/certs' key_dir = '/etc/ssl/private' certbot_dir = vyos.defaults.directories['certbot'] @@ -58,10 +61,11 @@ def get_config(config=None): else: conf = Config() - if not conf.exists('service https'): + base = ['service', 'https'] + if not conf.exists(base): return None - https = conf.get_config_dict('service https', get_first_key=True) + https = conf.get_config_dict(base, get_first_key=True) if https: https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), @@ -102,6 +106,8 @@ def verify(https): if not domains_found: raise ConfigError("At least one 'virtual-host <id> server-name' " "matching the 'certbot domain-name' is required.") + + verify_vrf(https) return None def generate(https): @@ -139,15 +145,17 @@ def generate(https): cert_path = os.path.join(cert_dir, f'{cert_name}.pem') key_path = os.path.join(key_dir, f'{cert_name}.pem') - with open(cert_path, 'w') as f: - f.write(wrap_certificate(pki_cert['certificate'])) + server_cert = str(wrap_certificate(pki_cert['certificate'])) + if 'ca-certificate' in cert_dict: + ca_cert = cert_dict['ca-certificate'] + server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate'])) - with open(key_path, 'w') as f: - f.write(wrap_private_key(pki_cert['private']['key'])) + write_file(cert_path, server_cert) + write_file(key_path, wrap_private_key(pki_cert['private']['key'])) vyos_cert_data = { - "crt": cert_path, - "key": key_path + 'crt': cert_path, + 'key': key_path } for block in server_block_list: @@ -184,6 +192,8 @@ def generate(https): vhosts = https.get('api-restrict', {}).get('virtual-host', []) if vhosts: api_data['vhost'] = vhosts[:] + if 'socket' in list(api_settings): + api_data['socket'] = True if api_data: vhost_list = api_data.get('vhost', []) @@ -205,10 +215,12 @@ def generate(https): } render(config_file, 'https/nginx.default.tmpl', data) - + render(systemd_override, 'https/override.conf.tmpl', https) return None def apply(https): + # Reload systemd manager configuration + call('systemctl daemon-reload') if https is not None: call('systemctl restart nginx.service') else: diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 02b7f83bf..3b8fae710 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -16,6 +16,7 @@ import os import re +import tempfile from cryptography.hazmat.primitives.asymmetric import ec from glob import glob @@ -26,6 +27,7 @@ from ipaddress import IPv6Address from ipaddress import IPv6Network from ipaddress import summarize_address_range from netifaces import interfaces +from secrets import SystemRandom from shutil import rmtree from vyos.config import Config @@ -48,6 +50,7 @@ from vyos.util import chown from vyos.util import dict_search from vyos.util import dict_search_args from vyos.util import makedir +from vyos.util import read_file from vyos.util import write_file from vyos.validate import is_addr_assigned @@ -60,6 +63,10 @@ group = 'openvpn' cfg_dir = '/run/openvpn' cfg_file = '/run/openvpn/{ifname}.conf' +otp_path = '/config/auth/openvpn' +otp_file = '/config/auth/openvpn/{ifname}-otp-secrets' +secret_chars = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567') +service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf' def get_config(config=None): """ @@ -80,7 +87,20 @@ def get_config(config=None): if 'deleted' not in openvpn: openvpn['pki'] = tmp_pki + # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict' + # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there. + tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True) + + # We have to cleanup the config dict, as default values could enable features + # which are not explicitly enabled on the CLI. Example: server mfa totp + # originate comes with defaults, which will enable the + # totp plugin, even when not set via CLI so we + # need to check this first and drop those keys + if dict_search('server.mfa.totp', tmp) == None: + del openvpn['server']['mfa'] + openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn) + return openvpn def is_ec_private_key(pki, cert_name): @@ -134,7 +154,7 @@ def verify_pki(openvpn): if tls['certificate'] not in pki['certificate']: raise ConfigError(f'Invalid certificate on openvpn interface {interface}') - if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected'): + if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected') is not None: raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}') if mode == 'server' and 'dh_params' not in tls and not is_ec_private_key(pki, tls['certificate']): @@ -169,6 +189,10 @@ def verify_pki(openvpn): def verify(openvpn): if 'deleted' in openvpn: + # remove totp secrets file if totp is not configured + if os.path.isfile(otp_file.format(**openvpn)): + os.remove(otp_file.format(**openvpn)) + verify_bridge_delete(openvpn) return None @@ -309,10 +333,10 @@ def verify(openvpn): if 'is_bridge_member' not in openvpn: raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode') - - for client in (dict_search('client', openvpn) or []): - if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1: - raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP') + if hasattr(dict_search('server.client', openvpn), '__iter__'): + for client_k, client_v in dict_search('server.client', openvpn).items(): + if (client_v.get('ip') and len(client_v['ip']) > 1) or (client_v.get('ipv6_ip') and len(client_v['ipv6_ip']) > 1): + raise ConfigError(f'Server client "{client_k}": cannot specify more than 1 IPv4 and 1 IPv6 IP') if dict_search('server.client_ip_pool', openvpn): if not (dict_search('server.client_ip_pool.start', openvpn) and dict_search('server.client_ip_pool.stop', openvpn)): @@ -360,6 +384,29 @@ def verify(openvpn): if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet: print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.') + # add mfa users to the file the mfa plugin uses + if dict_search('server.mfa.totp', openvpn): + user_data = '' + if not os.path.isfile(otp_file.format(**openvpn)): + write_file(otp_file.format(**openvpn), user_data, + user=user, group=group, mode=0o644) + + ovpn_users = read_file(otp_file.format(**openvpn)) + for client in (dict_search('server.client', openvpn) or []): + exists = None + for ovpn_user in ovpn_users.split('\n'): + if re.search('^' + client + ' ', ovpn_user): + user_data += f'{ovpn_user}\n' + exists = 'true' + + if not exists: + random = SystemRandom() + totp_secret = ''.join(random.choice(secret_chars) for _ in range(16)) + user_data += f'{client} otp totp:sha1:base32:{totp_secret}::xxx *\n' + + write_file(otp_file.format(**openvpn), user_data, + user=user, group=group, mode=0o644) + else: # checks for both client and site-to-site go here if dict_search('server.reject_unconfigured_clients', openvpn): @@ -525,6 +572,7 @@ def generate_pki_files(openvpn): def generate(openvpn): interface = openvpn['ifname'] directory = os.path.dirname(cfg_file.format(**openvpn)) + plugin_dir = '/usr/lib/openvpn' # create base config directory on demand makedir(directory, user, group) # enforce proper permissions on /run/openvpn @@ -536,6 +584,11 @@ def generate(openvpn): if os.path.isdir(ccd_dir): rmtree(ccd_dir, ignore_errors=True) + # Remove systemd directories with overrides + service_dir = os.path.dirname(service_file.format(**openvpn)) + if os.path.isdir(service_dir): + rmtree(service_dir, ignore_errors=True) + if 'deleted' in openvpn or 'disable' in openvpn: return None @@ -571,14 +624,20 @@ def generate(openvpn): render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn, formater=lambda _: _.replace(""", '"'), user=user, group=group) + # Render 20-override.conf for OpenVPN service + render(service_file.format(**openvpn), 'openvpn/service-override.conf.tmpl', openvpn, + formater=lambda _: _.replace(""", '"'), user=user, group=group) + # Reload systemd services config to apply an override + call(f'systemctl daemon-reload') + return None def apply(openvpn): interface = openvpn['ifname'] - call(f'systemctl stop openvpn@{interface}.service') # Do some cleanup when OpenVPN is disabled/deleted if 'deleted' in openvpn or 'disable' in openvpn: + call(f'systemctl stop openvpn@{interface}.service') for cleanup_file in glob(f'/run/openvpn/{interface}.*'): if os.path.isfile(cleanup_file): os.unlink(cleanup_file) @@ -590,7 +649,7 @@ def apply(openvpn): # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process - call(f'systemctl start openvpn@{interface}.service') + call(f'systemctl reload-or-restart openvpn@{interface}.service') o = VTunIf(**openvpn) o.update(openvpn) diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index ef385d2e7..30f57ec0c 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -98,7 +98,7 @@ def verify(tunnel): # If tunnel source address any and key not set if tunnel['encapsulation'] in ['gre'] and \ - tunnel['source_address'] == '0.0.0.0' and \ + dict_search('source_address', tunnel) == '0.0.0.0' and \ dict_search('parameters.ip.key', tunnel) == None: raise ConfigError('Tunnel parameters ip key must be set!') @@ -107,19 +107,22 @@ def verify(tunnel): # Check pairs tunnel source-address/encapsulation/key with exists tunnels. # Prevent the same key for 2 tunnels with same source-address/encap. T2920 for tunnel_if in Section.interfaces('tunnel'): + # It makes no sense to run the test for re-used GRE keys on our + # own interface we are currently working on + if tunnel['ifname'] == tunnel_if: + continue tunnel_cfg = get_interface_config(tunnel_if) - exist_encap = tunnel_cfg['linkinfo']['info_kind'] - exist_source_address = tunnel_cfg['address'] - exist_key = tunnel_cfg['linkinfo']['info_data']['ikey'] - new_source_address = tunnel['source_address'] + # no match on encapsulation - bail out + if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: + continue + new_source_address = dict_search('source_address', tunnel) # Convert tunnel key to ip key, format "ip -j link show" # 1 => 0.0.0.1, 999 => 0.0.3.231 - orig_new_key = int(tunnel['parameters']['ip']['key']) - new_key = IPv4Address(orig_new_key) + orig_new_key = dict_search('parameters.ip.key', tunnel) + new_key = IPv4Address(int(orig_new_key)) new_key = str(new_key) - if tunnel['encapsulation'] == exist_encap and \ - new_source_address == exist_source_address and \ - new_key == exist_key: + if dict_search('address', tunnel_cfg) == new_source_address and \ + dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key: raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \ f'is already used for tunnel "{tunnel_if}"!') diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 804f2d14f..1f097c4e3 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -44,6 +44,20 @@ def get_config(config=None): base = ['interfaces', 'vxlan'] vxlan = get_interface_dict(conf, base) + # We need to verify that no other VXLAN tunnel is configured when external + # mode is in use - Linux Kernel limitation + conf.set_level(base) + vxlan['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + # This if-clause is just to be sure - it will always evaluate to true + ifname = vxlan['ifname'] + if ifname in vxlan['other_tunnels']: + del vxlan['other_tunnels'][ifname] + if len(vxlan['other_tunnels']) == 0: + del vxlan['other_tunnels'] + return vxlan def verify(vxlan): @@ -63,8 +77,21 @@ def verify(vxlan): if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan): raise ConfigError('Group, remote or source-address must be configured') - if 'vni' not in vxlan: - raise ConfigError('Must configure VNI for VXLAN') + if 'vni' not in vxlan and 'external' not in vxlan: + raise ConfigError( + 'Must either configure VXLAN "vni" or use "external" CLI option!') + + if {'external', 'vni'} <= set(vxlan): + raise ConfigError('Can not specify both "external" and "VNI"!') + + if {'external', 'other_tunnels'} <= set(vxlan): + other_tunnels = ', '.join(vxlan['other_tunnels']) + raise ConfigError(f'Only one VXLAN tunnel is supported when "external" '\ + f'CLI option is used. Additional tunnels: {other_tunnels}') + + if 'gpe' in vxlan and 'external' not in vxlan: + raise ConfigError(f'VXLAN-GPE is only supported when "external" '\ + f'CLI option is used.') if 'source_interface' in vxlan: # VXLAN adds at least an overhead of 50 byte - we need to check the diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 7b3de6e8a..af35b5f03 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -82,11 +82,12 @@ def get_config(config=None): tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) if not (dict_search('security.wpa.passphrase', tmp) or dict_search('security.wpa.radius', tmp)): - del wifi['security']['wpa'] + if 'deleted' not in wifi: + del wifi['security']['wpa'] # defaults include RADIUS server specifics per TAG node which need to be # added to individual RADIUS servers instead - so we can simply delete them - if dict_search('security.wpa.radius.server.port', wifi): + if dict_search('security.wpa.radius.server.port', wifi) != None: del wifi['security']['wpa']['radius']['server']['port'] if not len(wifi['security']['wpa']['radius']['server']): del wifi['security']['wpa']['radius'] diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index faa5eb628..a4b033374 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -17,6 +17,7 @@ import os from sys import exit +from time import sleep from vyos.config import Config from vyos.configdict import get_interface_dict @@ -25,11 +26,18 @@ from vyos.configverify import verify_interface_exists from vyos.configverify import verify_vrf from vyos.ifconfig import WWANIf from vyos.util import cmd +from vyos.util import call from vyos.util import dict_search +from vyos.util import DEVNULL +from vyos.util import is_systemd_service_active +from vyos.util import write_file from vyos import ConfigError from vyos import airbag airbag.enable() +service_name = 'ModemManager.service' +cron_script = '/etc/cron.d/wwan' + def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -42,6 +50,20 @@ def get_config(config=None): base = ['interfaces', 'wwan'] wwan = get_interface_dict(conf, base) + # We need to know the amount of other WWAN interfaces as ModemManager needs + # to be started or stopped. + conf.set_level(base) + wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + # This if-clause is just to be sure - it will always evaluate to true + ifname = wwan['ifname'] + if ifname in wwan['other_interfaces']: + del wwan['other_interfaces'][ifname] + if len(wwan['other_interfaces']) == 0: + del wwan['other_interfaces'] + return wwan def verify(wwan): @@ -59,9 +81,26 @@ def verify(wwan): return None def generate(wwan): + if 'deleted' in wwan: + return None + + if not os.path.exists(cron_script): + write_file(cron_script, '*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py') return None def apply(wwan): + if not is_systemd_service_active(service_name): + cmd(f'systemctl start {service_name}') + + counter = 100 + # Wait until a modem is detected and then we can continue + while counter > 0: + counter -= 1 + tmp = cmd('mmcli -L') + if tmp != 'No modems were found': + break + sleep(0.250) + # we only need the modem number. wwan0 -> 0, wwan1 -> 1 modem = wwan['ifname'].lstrip('wwan') base_cmd = f'mmcli --modem {modem}' @@ -71,6 +110,15 @@ def apply(wwan): w = WWANIf(wwan['ifname']) if 'deleted' in wwan or 'disable' in wwan: w.remove() + + # There are no other WWAN interfaces - stop the daemon + if 'other_interfaces' not in wwan: + cmd(f'systemctl stop {service_name}') + # Clean CRON helper script which is used for to re-connect when + # RF signal is lost + if os.path.exists(cron_script): + os.unlink(cron_script) + return None ip_type = 'ipv4' @@ -88,9 +136,12 @@ def apply(wwan): options += ',user={user},password={password}'.format(**wwan['authentication']) command = f'{base_cmd} --simple-connect="{options}"' - cmd(command) + call(command, stdout=DEVNULL) w.update(wwan) + if 'other_interfaces' not in wwan and 'deleted' in wwan: + cmd(f'systemctl start {service_name}') + return None if __name__ == '__main__': diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 59939d0fb..96f8f6fb6 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -42,7 +42,7 @@ if LooseVersion(kernel_version()) > LooseVersion('5.1'): else: k_mod = ['nft_nat', 'nft_chain_nat_ipv4'] -iptables_nat_config = '/tmp/vyos-nat-rules.nft' +nftables_nat_config = '/tmp/vyos-nat-rules.nft' def get_handler(json, chain, target): """ Get nftable rule handler number of given chain/target combination. @@ -93,7 +93,6 @@ def get_config(config=None): nat[direction]['rule'][rule] = dict_merge(default_values, nat[direction]['rule'][rule]) - # read in current nftable (once) for further processing tmp = cmd('nft -j list table raw') nftable_json = json.loads(tmp) @@ -106,9 +105,9 @@ def get_config(config=None): nat['helper_functions'] = 'remove' # Retrieve current table handler positions - nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER') + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER') nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') - nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_HELPER') nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') nat['deleted'] = '' return nat @@ -119,10 +118,10 @@ def get_config(config=None): nat['helper_functions'] = 'add' # Retrieve current table handler positions - nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE') - nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK') - nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE') - nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK') + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_IGNORE') + nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_PREROUTING_HOOK') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_IGNORE') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_OUTPUT_HOOK') return nat @@ -180,14 +179,14 @@ def verify(nat): return None def generate(nat): - render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, + render(nftables_nat_config, 'firewall/nftables-nat.tmpl', nat, permission=0o755) return None def apply(nat): - cmd(f'{iptables_nat_config}') - if os.path.isfile(iptables_nat_config): - os.unlink(iptables_nat_config) + cmd(f'{nftables_nat_config}') + if os.path.isfile(nftables_nat_config): + os.unlink(nftables_nat_config) return None diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index f8bc073bb..8bf2e8073 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -35,7 +35,7 @@ airbag.enable() k_mod = ['nft_nat', 'nft_chain_nat'] -iptables_nat_config = '/tmp/vyos-nat66-rules.nft' +nftables_nat66_config = '/tmp/vyos-nat66-rules.nft' ndppd_config = '/run/ndppd/ndppd.conf' def get_handler(json, chain, target): @@ -79,9 +79,9 @@ def get_config(config=None): if not conf.exists(base): nat['helper_functions'] = 'remove' - nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER') + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER') nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') - nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_HELPER') nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') nat['deleted'] = '' return nat @@ -92,10 +92,10 @@ def get_config(config=None): nat['helper_functions'] = 'add' # Retrieve current table handler positions - nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE') - nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK') - nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE') - nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK') + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_IGNORE') + nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_PREROUTING_HOOK') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_IGNORE') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_OUTPUT_HOOK') else: nat['helper_functions'] = 'has' @@ -117,7 +117,7 @@ def verify(nat): raise ConfigError(f'{err_msg} outbound-interface not specified') if config['outbound_interface'] not in interfaces(): - print(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') + raise ConfigError(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') addr = dict_search('translation.address', config) if addr != None: @@ -145,22 +145,22 @@ def verify(nat): return None def generate(nat): - render(iptables_nat_config, 'firewall/nftables-nat66.tmpl', nat, permission=0o755) + render(nftables_nat66_config, 'firewall/nftables-nat66.tmpl', nat, permission=0o755) render(ndppd_config, 'ndppd/ndppd.conf.tmpl', nat, permission=0o755) return None def apply(nat): if not nat: return None - cmd(f'{iptables_nat_config}') + cmd(f'{nftables_nat66_config}') if 'deleted' in nat or not dict_search('source.rule', nat): cmd('systemctl stop ndppd') if os.path.isfile(ndppd_config): os.unlink(ndppd_config) else: cmd('systemctl restart ndppd') - if os.path.isfile(iptables_nat_config): - os.unlink(iptables_nat_config) + if os.path.isfile(nftables_nat66_config): + os.unlink(nftables_nat66_config) return None diff --git a/src/conf_mode/netns.py b/src/conf_mode/netns.py new file mode 100755 index 000000000..0924eb616 --- /dev/null +++ b/src/conf_mode/netns.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 sys import exit +from tempfile import NamedTemporaryFile + +from vyos.config import Config +from vyos.configdict import node_changed +from vyos.ifconfig import Interface +from vyos.util import call +from vyos.util import dict_search +from vyos.util import get_interface_config +from vyos import ConfigError +from vyos import airbag +airbag.enable() + + +def netns_interfaces(c, match): + """ + get NETNS bound interfaces + """ + matched = [] + old_level = c.get_level() + c.set_level(['interfaces']) + section = c.get_config_dict([], get_first_key=True) + for type in section: + interfaces = section[type] + for name in interfaces: + interface = interfaces[name] + if 'netns' in interface: + v = interface.get('netns', '') + if v == match: + matched.append(name) + + c.set_level(old_level) + return matched + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['netns'] + netns = conf.get_config_dict(base, get_first_key=True, + no_tag_node_value_mangle=True) + + # determine which NETNS has been removed + for name in node_changed(conf, base + ['name']): + if 'netns_remove' not in netns: + netns.update({'netns_remove' : {}}) + + netns['netns_remove'][name] = {} + # get NETNS bound interfaces + interfaces = netns_interfaces(conf, name) + if interfaces: netns['netns_remove'][name]['interface'] = interfaces + + return netns + +def verify(netns): + # ensure NETNS is not assigned to any interface + if 'netns_remove' in netns: + for name, config in netns['netns_remove'].items(): + if 'interface' in config: + raise ConfigError(f'Can not remove NETNS "{name}", it still has '\ + f'member interfaces!') + + if 'name' in netns: + for name, config in netns['name'].items(): + print(name) + + return None + + +def generate(netns): + if not netns: + return None + + return None + + +def apply(netns): + + for tmp in (dict_search('netns_remove', netns) or []): + if os.path.isfile(f'/run/netns/{tmp}'): + call(f'ip netns del {tmp}') + + if 'name' in netns: + for name, config in netns['name'].items(): + if not os.path.isfile(f'/run/netns/{name}'): + call(f'ip netns add {name}') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py new file mode 100755 index 000000000..e81135a74 --- /dev/null +++ b/src/conf_mode/policy-route-interface.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 re + +from sys import argv +from sys import exit + +from vyos.config import Config +from vyos.ifconfig import Section +from vyos.template import render +from vyos.util import cmd +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + ifname = argv[1] + ifpath = Section.get_config_path(ifname) + if_policy_path = f'interfaces {ifpath} policy' + + if_policy = conf.get_config_dict(if_policy_path, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + if_policy['ifname'] = ifname + if_policy['policy'] = conf.get_config_dict(['policy'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + return if_policy + +def verify(if_policy): + # bail out early - looks like removal from running config + if not if_policy: + return None + + for route in ['route', 'ipv6_route']: + if route in if_policy: + if route not in if_policy['policy']: + raise ConfigError('Policy route not configured') + + route_name = if_policy[route] + + if route_name not in if_policy['policy'][route]: + raise ConfigError(f'Invalid policy route name "{name}"') + + return None + +def generate(if_policy): + return None + +def cleanup_rule(table, chain, ifname, new_name=None): + results = cmd(f'nft -a list chain {table} {chain}').split("\n") + retval = None + for line in results: + if f'oifname "{ifname}"' in line: + if new_name and f'jump {new_name}' in line: + # new_name is used to clear rules for any previously referenced chains + # returns true when rule exists and doesn't need to be created + retval = True + continue + + handle_search = re.search('handle (\d+)', line) + if handle_search: + cmd(f'nft delete rule {table} {chain} handle {handle_search[1]}') + return retval + +def apply(if_policy): + ifname = if_policy['ifname'] + + route_chain = 'VYOS_PBR_PREROUTING' + ipv6_route_chain = 'VYOS_PBR6_PREROUTING' + + if 'route' in if_policy: + name = 'VYOS_PBR_' + if_policy['route'] + rule_exists = cleanup_rule('ip mangle', route_chain, ifname, name) + + if not rule_exists: + cmd(f'nft insert rule ip mangle {route_chain} iifname {ifname} counter jump {name}') + else: + cleanup_rule('ip mangle', route_chain, ifname) + + if 'ipv6_route' in if_policy: + name = 'VYOS_PBR6_' + if_policy['ipv6_route'] + rule_exists = cleanup_rule('ip6 mangle', ipv6_route_chain, ifname, name) + + if not rule_exists: + cmd(f'nft insert rule ip6 mangle {ipv6_route_chain} iifname {ifname} counter jump {name}') + else: + cleanup_rule('ip6 mangle', ipv6_route_chain, ifname) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py new file mode 100755 index 000000000..d098be68d --- /dev/null +++ b/src/conf_mode/policy-route.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 json import loads +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import cmd +from vyos.util import dict_search_args +from vyos.util import run +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +mark_offset = 0x7FFFFFFF +nftables_conf = '/run/nftables_policy.conf' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['policy'] + + if not conf.exists(base + ['route']) and not conf.exists(base + ['ipv6-route']): + return None + + policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + return policy + +def verify(policy): + # bail out early - looks like removal from running config + if not policy: + return None + + for route in ['route', 'ipv6_route']: + if route in policy: + for name, pol_conf in policy[route].items(): + if 'rule' in pol_conf: + for rule_id, rule_conf in pol_conf.items(): + icmp = 'icmp' if route == 'route' else 'icmpv6' + if icmp in rule_conf: + icmp_defined = False + if 'type_name' in rule_conf[icmp]: + icmp_defined = True + if 'code' in rule_conf[icmp] or 'type' in rule_conf[icmp]: + raise ConfigError(f'{name} rule {rule_id}: Cannot use ICMP type/code with ICMP type-name') + if 'code' in rule_conf[icmp]: + icmp_defined = True + if 'type' not in rule_conf[icmp]: + raise ConfigError(f'{name} rule {rule_id}: ICMP code can only be defined if ICMP type is defined') + if 'type' in rule_conf[icmp]: + icmp_defined = True + + if icmp_defined and 'protocol' not in rule_conf or rule_conf['protocol'] != icmp: + raise ConfigError(f'{name} rule {rule_id}: ICMP type/code or type-name can only be defined if protocol is ICMP') + if 'set' in rule_conf: + if 'tcp_mss' in rule_conf['set']: + tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') + if not tcp_flags or 'SYN' not in tcp_flags.split(","): + raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS') + if 'tcp' in rule_conf: + if 'flags' in rule_conf['tcp']: + if 'protocol' not in rule_conf or rule_conf['protocol'] != 'tcp': + raise ConfigError(f'{name} rule {rule_id}: TCP flags can only be set if protocol is set to TCP') + + + return None + +def generate(policy): + if not policy: + if os.path.exists(nftables_conf): + os.unlink(nftables_conf) + return None + + if not os.path.exists(nftables_conf): + policy['first_install'] = True + + render(nftables_conf, 'firewall/nftables-policy.tmpl', policy) + return None + +def apply_table_marks(policy): + for route in ['route', 'ipv6_route']: + if route in policy: + for name, pol_conf in policy[route].items(): + if 'rule' in pol_conf: + for rule_id, rule_conf in pol_conf['rule'].items(): + set_table = dict_search_args(rule_conf, 'set', 'table') + if set_table: + if set_table == 'main': + set_table = '254' + table_mark = mark_offset - int(set_table) + cmd(f'ip rule add fwmark {table_mark} table {set_table}') + +def cleanup_table_marks(): + json_rules = cmd('ip -j -N rule list') + rules = loads(json_rules) + for rule in rules: + if 'fwmark' not in rule or 'table' not in rule: + continue + fwmark = rule['fwmark'] + table = int(rule['table']) + if fwmark[:2] == '0x': + fwmark = int(fwmark, 16) + if (int(fwmark) == (mark_offset - table)): + cmd(f'ip rule del fwmark {fwmark} table {table}') + +def apply(policy): + if not policy or 'first_install' not in policy: + run(f'nft flush table ip mangle') + run(f'nft flush table ip6 mangle') + + if not policy: + cleanup_table_marks() + return None + + install_result = run(f'nft -f {nftables_conf}') + if install_result == 1: + raise ConfigError('Failed to apply policy based routing') + + if 'first_install' not in policy: + cleanup_table_marks() + + apply_table_marks(policy) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index 1a03d520b..e251396c7 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -171,9 +171,7 @@ def verify(policy): def generate(policy): if not policy: - policy['new_frr_config'] = '' return None - policy['new_frr_config'] = render_to_string('frr/policy.frr.tmpl', policy) return None @@ -190,8 +188,9 @@ def apply(policy): frr_cfg.modify_section(r'^bgp community-list .*') frr_cfg.modify_section(r'^bgp extcommunity-list .*') frr_cfg.modify_section(r'^bgp large-community-list .*') - frr_cfg.modify_section(r'^route-map .*') - frr_cfg.add_before('^line vty', policy['new_frr_config']) + frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', remove_stop_mark=True) + if 'new_frr_config' in policy: + frr_cfg.add_before(frr.default_add_before, policy['new_frr_config']) frr_cfg.commit_configuration(bgp_daemon) # The route-map used for the FIB (zebra) is part of the zebra daemon @@ -200,19 +199,11 @@ def apply(policy): frr_cfg.modify_section(r'^ipv6 access-list .*') frr_cfg.modify_section(r'^ip prefix-list .*') frr_cfg.modify_section(r'^ipv6 prefix-list .*') - frr_cfg.modify_section(r'^route-map .*') - frr_cfg.add_before('^line vty', policy['new_frr_config']) + frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', remove_stop_mark=True) + if 'new_frr_config' in policy: + frr_cfg.add_before(frr.default_add_before, policy['new_frr_config']) frr_cfg.commit_configuration(zebra_daemon) - # If FRR config is blank, rerun the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if policy['new_frr_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(zebra_daemon) - - # Save configuration to /run/frr/config/frr.conf - frr.save_configuration() - return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 348bae59f..4ebc0989c 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -16,10 +16,9 @@ import os -from sys import exit - from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_vrf from vyos.template import is_ipv6 from vyos.template import render_to_string from vyos.validate import is_ipv6_link_local @@ -35,8 +34,9 @@ def get_config(config=None): else: conf = Config() base = ['protocols', 'bfd'] - bfd = conf.get_config_dict(base, get_first_key=True) - + bfd = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) # Bail out early if configuration tree does not exist if not conf.exists(base): return bfd @@ -79,28 +79,37 @@ def verify(bfd): # multihop and echo-mode cannot be used together if 'echo_mode' in peer_config: - raise ConfigError('Multihop and echo-mode cannot be used together') + raise ConfigError('BFD multihop and echo-mode cannot be used together') # multihop doesn't accept interface names if 'source' in peer_config and 'interface' in peer_config['source']: - raise ConfigError('Multihop and source interface cannot be used together') + raise ConfigError('BFD multihop and source interface cannot be used together') + + if 'profile' in peer_config: + profile_name = peer_config['profile'] + if 'profile' not in bfd or profile_name not in bfd['profile']: + raise ConfigError(f'BFD profile "{profile_name}" does not exist!') + + if 'vrf' in peer_config: + verify_vrf(peer_config) return None def generate(bfd): if not bfd: - bfd['new_frr_config'] = '' return None - - bfd['new_frr_config'] = render_to_string('frr/bfd.frr.tmpl', bfd) + bfd['new_frr_config'] = render_to_string('frr/bfdd.frr.tmpl', bfd) def apply(bfd): + bfd_daemon = 'bfdd' + # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration() - frr_cfg.modify_section('^bfd', '') - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bfd['new_frr_config']) - frr_cfg.commit_configuration() + frr_cfg.load_configuration(bfd_daemon) + frr_cfg.modify_section('^bfd', stop_pattern='^exit', remove_stop_mark=True) + if 'new_frr_config' in bfd: + frr_cfg.add_before(frr.default_add_before, bfd['new_frr_config']) + frr_cfg.commit_configuration(bfd_daemon) return None diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 68284e0f9..d8704727c 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -183,6 +183,28 @@ def verify(bgp): raise ConfigError(f'Neighbor "{peer}" cannot have both ipv6-unicast and ipv6-labeled-unicast configured at the same time!') afi_config = peer_config['address_family'][afi] + + if 'conditionally_advertise' in afi_config: + if 'advertise_map' not in afi_config['conditionally_advertise']: + raise ConfigError('Must speficy advertise-map when conditionally-advertise is in use!') + # Verify advertise-map (which is a route-map) exists + verify_route_map(afi_config['conditionally_advertise']['advertise_map'], bgp) + + if ('exist_map' not in afi_config['conditionally_advertise'] and + 'non_exist_map' not in afi_config['conditionally_advertise']): + raise ConfigError('Must either speficy exist-map or non-exist-map when ' \ + 'conditionally-advertise is in use!') + + if {'exist_map', 'non_exist_map'} <= set(afi_config['conditionally_advertise']): + raise ConfigError('Can not specify both exist-map and non-exist-map for ' \ + 'conditionally-advertise!') + + if 'exist_map' in afi_config['conditionally_advertise']: + verify_route_map(afi_config['conditionally_advertise']['exist_map'], bgp) + + if 'non_exist_map' in afi_config['conditionally_advertise']: + verify_route_map(afi_config['conditionally_advertise']['non_exist_map'], bgp) + # Validate if configured Prefix list exists if 'prefix_list' in afi_config: for tmp in ['import', 'export']: @@ -255,21 +277,11 @@ def verify(bgp): tmp = dict_search(f'route_map.vpn.{export_import}', afi_config) if tmp: verify_route_map(tmp, bgp) - if afi in ['l2vpn_evpn'] and 'vrf' not in bgp: - # Some L2VPN EVPN AFI options are only supported under VRF - if 'vni' in afi_config: - for vni, vni_config in afi_config['vni'].items(): - if 'rd' in vni_config: - raise ConfigError('VNI route-distinguisher is only supported under EVPN VRF') - if 'route_target' in vni_config: - raise ConfigError('VNI route-target is only supported under EVPN VRF') return None def generate(bgp): if not bgp or 'deleted' in bgp: - bgp['frr_bgpd_config'] = '' - bgp['frr_zebra_config'] = '' return None bgp['protocol'] = 'bgp' # required for frr/vrf.route-map.frr.tmpl @@ -287,8 +299,9 @@ def apply(bgp): # The route-map used for the FIB (zebra) is part of the zebra daemon frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'(\s+)?ip protocol bgp route-map [-a-zA-Z0-9.]+$', '', '(\s|!)') - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['frr_zebra_config']) + frr_cfg.modify_section(r'(\s+)?ip protocol bgp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') + if 'frr_zebra_config' in bgp: + frr_cfg.add_before(frr.default_add_before, bgp['frr_zebra_config']) frr_cfg.commit_configuration(zebra_daemon) # Generate empty helper string which can be ammended to FRR commands, it @@ -298,13 +311,11 @@ def apply(bgp): vrf = ' vrf ' + bgp['vrf'] frr_cfg.load_configuration(bgp_daemon) - frr_cfg.modify_section(f'^router bgp \d+{vrf}$', '') - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['frr_bgpd_config']) + frr_cfg.modify_section(f'^router bgp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True) + if 'frr_bgpd_config' in bgp: + frr_cfg.add_before(frr.default_add_before, bgp['frr_bgpd_config']) frr_cfg.commit_configuration(bgp_daemon) - # Save configuration to /run/frr/config/frr.conf - frr.save_configuration() - return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 4505e2496..9b4b215de 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -56,10 +56,10 @@ def get_config(config=None): # instead of the VRF instance. if vrf: isis['vrf'] = vrf - # As we no re-use this Python handler for both VRF and non VRF instances for - # IS-IS we need to find out if any interfaces changed so properly adjust - # the FRR configuration and not by acctident change interfaces from a - # different VRF. + # FRR has VRF support for different routing daemons. As interfaces belong + # to VRFs - or the global VRF, we need to check for changed interfaces so + # that they will be properly rendered for the FRR config. Also this eases + # removal of interfaces from the running configuration. interfaces_removed = node_changed(conf, base + ['interface']) if interfaces_removed: isis['interface_removed'] = list(interfaces_removed) @@ -196,8 +196,6 @@ def verify(isis): def generate(isis): if not isis or 'deleted' in isis: - isis['frr_isisd_config'] = '' - isis['frr_zebra_config'] = '' return None isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.tmpl @@ -214,8 +212,9 @@ def apply(isis): # The route-map used for the FIB (zebra) is part of the zebra daemon frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+$', '', '(\s|!)') - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_zebra_config']) + frr_cfg.modify_section('(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') + if 'frr_zebra_config' in isis: + frr_cfg.add_before(frr.default_add_before, isis['frr_zebra_config']) frr_cfg.commit_configuration(zebra_daemon) # Generate empty helper string which can be ammended to FRR commands, it @@ -225,19 +224,18 @@ def apply(isis): vrf = ' vrf ' + isis['vrf'] frr_cfg.load_configuration(isis_daemon) - frr_cfg.modify_section(f'^router isis VyOS{vrf}$', '') + frr_cfg.modify_section(f'^router isis VyOS{vrf}', stop_pattern='^exit', remove_stop_mark=True) for key in ['interface', 'interface_removed']: if key not in isis: continue for interface in isis[key]: - frr_cfg.modify_section(f'^interface {interface}{vrf}$', '') + frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True) - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_isisd_config']) - frr_cfg.commit_configuration(isis_daemon) + if 'frr_isisd_config' in isis: + frr_cfg.add_before(frr.default_add_before, isis['frr_isisd_config']) - # Save configuration to /run/frr/config/frr.conf - frr.save_configuration() + frr_cfg.commit_configuration(isis_daemon) return None diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 3b27608da..0b0c7d07b 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -66,36 +66,24 @@ def verify(mpls): def generate(mpls): # If there's no MPLS config generated, create dictionary key with no value. - if not mpls: - mpls['new_frr_config'] = '' + if not mpls or 'deleted' in mpls: return None - mpls['new_frr_config'] = render_to_string('frr/ldpd.frr.tmpl', mpls) + mpls['frr_ldpd_config'] = render_to_string('frr/ldpd.frr.tmpl', mpls) return None def apply(mpls): - # Define dictionary that will load FRR config - frr_cfg = {} + ldpd_damon = 'ldpd' + # Save original configuration prior to starting any commit actions - frr_cfg['original_config'] = frr.get_configuration(daemon='ldpd') - frr_cfg['modified_config'] = frr.replace_section(frr_cfg['original_config'], mpls['new_frr_config'], from_re='mpls.*') - - # If FRR config is blank, rerun the blank commit three times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if mpls['new_frr_config'] == '': - for x in range(3): - frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd') - elif not 'ldp' in mpls: - for x in range(3): - frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd') - else: - # FRR mark configuration will test for syntax errors and throws an - # exception if any syntax errors is detected - frr.mark_configuration(frr_cfg['modified_config']) + frr_cfg = frr.FRRConfig() + + frr_cfg.load_configuration(ldpd_damon) + frr_cfg.modify_section(f'^mpls ldp', stop_pattern='^exit', remove_stop_mark=True) - # Commit resulting configuration to FRR, this will throw CommitError - # on failure - frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd') + if 'frr_ldpd_config' in mpls: + frr_cfg.add_before(frr.default_add_before, mpls['frr_ldpd_config']) + frr_cfg.commit_configuration(ldpd_damon) # Set number of entries in the platform label tables labels = '0' @@ -122,7 +110,7 @@ def apply(mpls): system_interfaces = [] # Populate system interfaces list with local MPLS capable interfaces for interface in glob('/proc/sys/net/mpls/conf/*'): - system_interfaces.append(os.path.basename(interface)) + system_interfaces.append(os.path.basename(interface)) # This is where the comparison is done on if an interface needs to be enabled/disabled. for system_interface in system_interfaces: interface_state = read_file(f'/proc/sys/net/mpls/conf/{system_interface}/input') @@ -138,7 +126,7 @@ def apply(mpls): system_interfaces = [] # If MPLS interfaces are not configured, set MPLS processing disabled for interface in glob('/proc/sys/net/mpls/conf/*'): - system_interfaces.append(os.path.basename(interface)) + system_interfaces.append(os.path.basename(interface)) for system_interface in system_interfaces: system_interface = system_interface.replace('.', '/') call(f'sysctl -wq net.mpls.conf.{system_interface}.input=0') diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py index 12dacdba0..7eeb5cd30 100755 --- a/src/conf_mode/protocols_nhrp.py +++ b/src/conf_mode/protocols_nhrp.py @@ -16,6 +16,8 @@ from vyos.config import Config from vyos.configdict import node_changed +from vyos.firewall import find_nftables_rule +from vyos.firewall import remove_nftables_rule from vyos.template import render from vyos.util import process_named_running from vyos.util import run @@ -88,24 +90,19 @@ def generate(nhrp): def apply(nhrp): if 'tunnel' in nhrp: for tunnel, tunnel_conf in nhrp['tunnel'].items(): - if 'source_address' in tunnel_conf: - chain = f'VYOS_NHRP_{tunnel}_OUT_HOOK' - source_address = tunnel_conf['source_address'] + if 'source_address' in nhrp['if_tunnel'][tunnel]: + comment = f'VYOS_NHRP_{tunnel}' + source_address = nhrp['if_tunnel'][tunnel]['source_address'] - chain_exists = run(f'sudo iptables --check {chain} -j RETURN') == 0 - if not chain_exists: - run(f'sudo iptables --new {chain}') - run(f'sudo iptables --append {chain} -p gre -s {source_address} -d 224.0.0.0/4 -j DROP') - run(f'sudo iptables --append {chain} -j RETURN') - run(f'sudo iptables --insert OUTPUT 2 -j {chain}') + rule_handle = find_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', ['ip protocol gre', f'ip saddr {source_address}', 'ip daddr 224.0.0.0/4']) + if not rule_handle: + run(f'sudo nft insert rule ip filter VYOS_FW_OUTPUT ip protocol gre ip saddr {source_address} ip daddr 224.0.0.0/4 counter drop comment "{comment}"') for tunnel in nhrp['del_tunnels']: - chain = f'VYOS_NHRP_{tunnel}_OUT_HOOK' - chain_exists = run(f'sudo iptables --check {chain} -j RETURN') == 0 - if chain_exists: - run(f'sudo iptables --delete OUTPUT -j {chain}') - run(f'sudo iptables --flush {chain}') - run(f'sudo iptables --delete-chain {chain}') + comment = f'VYOS_NHRP_{tunnel}' + rule_handle = find_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', [f'comment "{comment}"']) + if rule_handle: + remove_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', rule_handle) action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop' run(f'systemctl {action} opennhrp') diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 6ccda2e5a..4895cde6f 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -56,10 +56,10 @@ def get_config(config=None): # instead of the VRF instance. if vrf: ospf['vrf'] = vrf - # As we no re-use this Python handler for both VRF and non VRF instances for - # OSPF we need to find out if any interfaces changed so properly adjust - # the FRR configuration and not by acctident change interfaces from a - # different VRF. + # FRR has VRF support for different routing daemons. As interfaces belong + # to VRFs - or the global VRF, we need to check for changed interfaces so + # that they will be properly rendered for the FRR config. Also this eases + # removal of interfaces from the running configuration. interfaces_removed = node_changed(conf, base + ['interface']) if interfaces_removed: ospf['interface_removed'] = list(interfaces_removed) @@ -177,11 +177,11 @@ def verify(ospf): raise ConfigError('Can not use OSPF interface area and area ' \ 'network configuration at the same time!') - if 'vrf' in ospf: # If interface specific options are set, we must ensure that the # interface is bound to our requesting VRF. Due to the VyOS # priorities the interface is bound to the VRF after creation of # the VRF itself, and before any routing protocol is configured. + if 'vrf' in ospf: vrf = ospf['vrf'] tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: @@ -191,8 +191,6 @@ def verify(ospf): def generate(ospf): if not ospf or 'deleted' in ospf: - ospf['frr_ospfd_config'] = '' - ospf['frr_zebra_config'] = '' return None ospf['protocol'] = 'ospf' # required for frr/vrf.route-map.frr.tmpl @@ -209,8 +207,9 @@ def apply(ospf): # The route-map used for the FIB (zebra) is part of the zebra daemon frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'(\s+)?ip protocol ospf route-map [-a-zA-Z0-9.]+$', '', '(\s|!)') - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['frr_zebra_config']) + frr_cfg.modify_section('(\s+)?ip protocol ospf route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') + if 'frr_zebra_config' in ospf: + frr_cfg.add_before(frr.default_add_before, ospf['frr_zebra_config']) frr_cfg.commit_configuration(zebra_daemon) # Generate empty helper string which can be ammended to FRR commands, it @@ -220,20 +219,18 @@ def apply(ospf): vrf = ' vrf ' + ospf['vrf'] frr_cfg.load_configuration(ospf_daemon) - frr_cfg.modify_section(f'^router ospf{vrf}$', '') + frr_cfg.modify_section(f'^router ospf{vrf}', stop_pattern='^exit', remove_stop_mark=True) for key in ['interface', 'interface_removed']: if key not in ospf: continue for interface in ospf[key]: - frr_cfg.modify_section(f'^interface {interface}{vrf}$', '') + frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True) - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['frr_ospfd_config']) + if 'frr_ospfd_config' in ospf: + frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config']) frr_cfg.commit_configuration(ospf_daemon) - # Save configuration to /run/frr/config/frr.conf - frr.save_configuration() - return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index 536ffa690..f8e733ba5 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -17,32 +17,80 @@ import os from sys import exit +from sys import argv from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configdict import node_changed from vyos.configverify import verify_common_route_maps +from vyos.configverify import verify_route_map +from vyos.configverify import verify_interface_exists from vyos.template import render_to_string from vyos.ifconfig import Interface +from vyos.util import dict_search +from vyos.util import get_interface_config from vyos.xml import defaults from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() -frr_daemon = 'ospf6d' - def get_config(config=None): if config: conf = config else: conf = Config() - base = ['protocols', 'ospfv3'] + + vrf = None + if len(argv) > 1: + vrf = argv[1] + + base_path = ['protocols', 'ospfv3'] + + # eqivalent of the C foo ? 'a' : 'b' statement + base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospfv3'] or base_path ospfv3 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # Assign the name of our VRF context. This MUST be done before the return + # statement below, else on deletion we will delete the default instance + # instead of the VRF instance. + if vrf: ospfv3['vrf'] = vrf + + # FRR has VRF support for different routing daemons. As interfaces belong + # to VRFs - or the global VRF, we need to check for changed interfaces so + # that they will be properly rendered for the FRR config. Also this eases + # removal of interfaces from the running configuration. + interfaces_removed = node_changed(conf, base + ['interface']) + if interfaces_removed: + ospfv3['interface_removed'] = list(interfaces_removed) + # Bail out early if configuration tree does not exist if not conf.exists(base): + ospfv3.update({'deleted' : ''}) return ospfv3 + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + # XXX: Note that we can not call defaults(base), as defaults does not work + # on an instance of a tag node. As we use the exact same CLI definition for + # both the non-vrf and vrf version this is absolutely safe! + default_values = defaults(base_path) + + # We have to cleanup the default dict, as default values could enable features + # which are not explicitly enabled on the CLI. Example: default-information + # originate comes with a default metric-type of 2, which will enable the + # entire default-information originate tree, even when not set via CLI so we + # need to check this first and probably drop that key. + if dict_search('default_information.originate', ospfv3) is None: + del default_values['default_information'] + + # XXX: T2665: we currently have no nice way for defaults under tag nodes, + # clean them out and add them manually :( + del default_values['interface'] + + # merge in remaining default values + ospfv3 = dict_merge(default_values, ospfv3) + # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify(). # @@ -60,34 +108,68 @@ def verify(ospfv3): verify_common_route_maps(ospfv3) + # As we can have a default-information route-map, we need to validate it! + route_map_name = dict_search('default_information.originate.route_map', ospfv3) + if route_map_name: verify_route_map(route_map_name, ospfv3) + + if 'area' in ospfv3: + for area, area_config in ospfv3['area'].items(): + if 'area_type' in area_config: + if len(area_config['area_type']) > 1: + raise ConfigError(f'Can only configure one area-type for OSPFv3 area "{area}"!') + if 'interface' in ospfv3: - for ifname, if_config in ospfv3['interface'].items(): - if 'ifmtu' in if_config: - mtu = Interface(ifname).get_mtu() - if int(if_config['ifmtu']) > int(mtu): + for interface, interface_config in ospfv3['interface'].items(): + verify_interface_exists(interface) + if 'ifmtu' in interface_config: + mtu = Interface(interface).get_mtu() + if int(interface_config['ifmtu']) > int(mtu): raise ConfigError(f'OSPFv3 ifmtu can not exceed physical MTU of "{mtu}"') + # If interface specific options are set, we must ensure that the + # interface is bound to our requesting VRF. Due to the VyOS + # priorities the interface is bound to the VRF after creation of + # the VRF itself, and before any routing protocol is configured. + if 'vrf' in ospfv3: + vrf = ospfv3['vrf'] + tmp = get_interface_config(interface) + if 'master' not in tmp or tmp['master'] != vrf: + raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!') + return None def generate(ospfv3): - if not ospfv3: - ospfv3['new_frr_config'] = '' + if not ospfv3 or 'deleted' in ospfv3: return None ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.tmpl', ospfv3) return None def apply(ospfv3): + ospf6_daemon = 'ospf6d' + # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section(r'^interface \S+', '') - frr_cfg.modify_section('^router ospf6$', '') - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospfv3['new_frr_config']) - frr_cfg.commit_configuration(frr_daemon) - - # Save configuration to /run/frr/config/frr.conf - frr.save_configuration() + + # Generate empty helper string which can be ammended to FRR commands, it + # will be either empty (default VRF) or contain the "vrf <name" statement + vrf = '' + if 'vrf' in ospfv3: + vrf = ' vrf ' + ospfv3['vrf'] + + frr_cfg.load_configuration(ospf6_daemon) + frr_cfg.modify_section(f'^router ospf6{vrf}', stop_pattern='^exit', remove_stop_mark=True) + + for key in ['interface', 'interface_removed']: + if key not in ospfv3: + continue + for interface in ospfv3[key]: + frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True) + + if 'new_frr_config' in ospfv3: + frr_cfg.add_before(frr.default_add_before, ospfv3['new_frr_config']) + + frr_cfg.commit_configuration(ospf6_daemon) return None diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index e56eb1f56..300f56489 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -20,6 +20,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configdict import node_changed from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list @@ -39,8 +40,17 @@ def get_config(config=None): base = ['protocols', 'rip'] rip = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # FRR has VRF support for different routing daemons. As interfaces belong + # to VRFs - or the global VRF, we need to check for changed interfaces so + # that they will be properly rendered for the FRR config. Also this eases + # removal of interfaces from the running configuration. + interfaces_removed = node_changed(conf, base + ['interface']) + if interfaces_removed: + rip['interface_removed'] = list(interfaces_removed) + # Bail out early if configuration tree does not exist if not conf.exists(base): + rip.update({'deleted' : ''}) return rip # We have gathered the dict representation of the CLI, but there are default @@ -89,12 +99,10 @@ def verify(rip): f'with "split-horizon disable" for "{interface}"!') def generate(rip): - if not rip: - rip['new_frr_config'] = '' + if not rip or 'deleted' in rip: return None - rip['new_frr_config'] = render_to_string('frr/rip.frr.tmpl', rip) - + rip['new_frr_config'] = render_to_string('frr/ripd.frr.tmpl', rip) return None def apply(rip): @@ -106,19 +114,22 @@ def apply(rip): # The route-map used for the FIB (zebra) is part of the zebra daemon frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'^ip protocol rip route-map [-a-zA-Z0-9.]+$', '') + frr_cfg.modify_section('^ip protocol rip route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') frr_cfg.commit_configuration(zebra_daemon) frr_cfg.load_configuration(rip_daemon) - frr_cfg.modify_section(r'key chain \S+', '') - frr_cfg.modify_section(r'interface \S+', '') - frr_cfg.modify_section('^router rip$', '') + frr_cfg.modify_section('^key chain \S+', stop_pattern='^exit', remove_stop_mark=True) + frr_cfg.modify_section('^router rip', stop_pattern='^exit', remove_stop_mark=True) - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rip['new_frr_config']) - frr_cfg.commit_configuration(rip_daemon) + for key in ['interface', 'interface_removed']: + if key not in rip: + continue + for interface in rip[key]: + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - # Save configuration to /run/frr/config/frr.conf - frr.save_configuration() + if 'new_frr_config' in rip: + frr_cfg.add_before(frr.default_add_before, rip['new_frr_config']) + frr_cfg.commit_configuration(rip_daemon) return None diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py index aaec5dacb..d9b8c0b30 100755 --- a/src/conf_mode/protocols_ripng.py +++ b/src/conf_mode/protocols_ripng.py @@ -31,8 +31,6 @@ from vyos import frr from vyos import airbag airbag.enable() -frr_daemon = 'ripngd' - def get_config(config=None): if config: conf = config @@ -95,21 +93,28 @@ def generate(ripng): ripng['new_frr_config'] = '' return None - ripng['new_frr_config'] = render_to_string('frr/ripng.frr.tmpl', ripng) + ripng['new_frr_config'] = render_to_string('frr/ripngd.frr.tmpl', ripng) return None def apply(ripng): + ripng_daemon = 'ripngd' + zebra_daemon = 'zebra' + # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section(r'key chain \S+', '') - frr_cfg.modify_section(r'interface \S+', '') - frr_cfg.modify_section('router ripng', '') - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ripng['new_frr_config']) - frr_cfg.commit_configuration(frr_daemon) - - # Save configuration to /run/frr/config/frr.conf - frr.save_configuration() + + # The route-map used for the FIB (zebra) is part of the zebra daemon + frr_cfg.load_configuration(zebra_daemon) + frr_cfg.modify_section('^ipv6 protocol ripng route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') + frr_cfg.commit_configuration(zebra_daemon) + + frr_cfg.load_configuration(ripng_daemon) + frr_cfg.modify_section('key chain \S+', stop_pattern='^exit', remove_stop_mark=True) + frr_cfg.modify_section('interface \S+', stop_pattern='^exit', remove_stop_mark=True) + frr_cfg.modify_section('^router ripng', stop_pattern='^exit', remove_stop_mark=True) + if 'new_frr_config' in ripng: + frr_cfg.add_before(frr.default_add_before, ripng['new_frr_config']) + frr_cfg.commit_configuration(ripng_daemon) return None diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py index 947c8ab7a..51ad0d315 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -28,8 +28,6 @@ from vyos import frr from vyos import airbag airbag.enable() -frr_daemon = 'bgpd' - def get_config(config=None): if config: conf = config @@ -38,7 +36,9 @@ def get_config(config=None): base = ['protocols', 'rpki'] rpki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # Bail out early if configuration tree does not exist if not conf.exists(base): + rpki.update({'deleted' : ''}) return rpki # We have gathered the dict representation of the CLI, but there are default @@ -79,17 +79,22 @@ def verify(rpki): return None def generate(rpki): + if not rpki: + return rpki['new_frr_config'] = render_to_string('frr/rpki.frr.tmpl', rpki) return None def apply(rpki): + bgp_daemon = 'bgpd' + # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section('rpki', '') - frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rpki['new_frr_config']) - frr_cfg.commit_configuration(frr_daemon) + frr_cfg.load_configuration(bgp_daemon) + frr_cfg.modify_section('^rpki', stop_pattern='^exit', remove_stop_mark=True) + if 'new_frr_config' in rpki: + frr_cfg.add_before(frr.default_add_before, rpki['new_frr_config']) + frr_cfg.commit_configuration(bgp_daemon) return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 338247e30..c1e427b16 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -21,6 +21,7 @@ from sys import argv from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configdict import get_dhcp_interfaces from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_vrf from vyos.template import render_to_string @@ -56,6 +57,10 @@ def get_config(config=None): # Merge policy dict into "regular" config dict static = dict_merge(tmp, static) + # T3680 - get a list of all interfaces currently configured to use DHCP + tmp = get_dhcp_interfaces(conf, vrf) + if tmp: static['dhcp'] = tmp + return static def verify(static): @@ -80,7 +85,9 @@ def verify(static): return None def generate(static): - static['new_frr_config'] = render_to_string('frr/static.frr.tmpl', static) + if not static: + return None + static['new_frr_config'] = render_to_string('frr/staticd.frr.tmpl', static) return None def apply(static): @@ -92,24 +99,21 @@ def apply(static): # The route-map used for the FIB (zebra) is part of the zebra daemon frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'^ip protocol static route-map [-a-zA-Z0-9.]+$', '') + frr_cfg.modify_section(r'^ip protocol static route-map [-a-zA-Z0-9.]+', '') frr_cfg.commit_configuration(zebra_daemon) - frr_cfg.load_configuration(static_daemon) if 'vrf' in static: vrf = static['vrf'] - frr_cfg.modify_section(f'^vrf {vrf}$', '') + frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit', remove_stop_mark=True) else: - frr_cfg.modify_section(r'^ip route .*', '') - frr_cfg.modify_section(r'^ipv6 route .*', '') + frr_cfg.modify_section(r'^ip route .*') + frr_cfg.modify_section(r'^ipv6 route .*') - frr_cfg.add_before(r'(interface .*|line vty)', static['new_frr_config']) + if 'new_frr_config' in static: + frr_cfg.add_before(frr.default_add_before, static['new_frr_config']) frr_cfg.commit_configuration(static_daemon) - # Save configuration to /run/frr/config/frr.conf - frr.save_configuration() - return None if __name__ == '__main__': diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py index c920920ed..d31a0c49e 100755 --- a/src/conf_mode/service_mdns-repeater.py +++ b/src/conf_mode/service_mdns-repeater.py @@ -28,7 +28,7 @@ from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = r'/etc/default/mdns-repeater' +config_file = '/run/avahi-daemon/avahi-daemon.conf' vrrp_running_file = '/run/mdns_vrrp_active' def get_config(config=None): @@ -92,12 +92,12 @@ def generate(mdns): if len(mdns['interface']) < 2: return None - render(config_file, 'mdns-repeater/mdns-repeater.tmpl', mdns) + render(config_file, 'mdns-repeater/avahi-daemon.tmpl', mdns) return None def apply(mdns): if not mdns or 'disable' in mdns: - call('systemctl stop mdns-repeater.service') + call('systemctl stop avahi-daemon.service') if os.path.exists(config_file): os.unlink(config_file) @@ -106,16 +106,16 @@ def apply(mdns): else: if 'vrrp_disable' not in mdns and os.path.exists(vrrp_running_file): os.unlink(vrrp_running_file) - + if mdns['vrrp_exists'] and 'vrrp_disable' in mdns: if not os.path.exists(vrrp_running_file): os.mknod(vrrp_running_file) # vrrp script looks for this file to update mdns repeater if len(mdns['interface']) < 2: - call('systemctl stop mdns-repeater.service') + call('systemctl stop avahi-daemon.service') return None - call('systemctl restart mdns-repeater.service') + call('systemctl restart avahi-daemon.service') return None diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 9fbd531da..1f31d132d 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -24,8 +24,11 @@ from vyos.configverify import verify_accel_ppp_base_service from vyos.template import render from vyos.util import call from vyos.util import dict_search +from vyos.util import get_interface_config from vyos import ConfigError from vyos import airbag +from vyos.range_regex import range_to_regex + airbag.enable() pppoe_conf = r'/run/accel-pppd/pppoe.conf' @@ -56,6 +59,11 @@ def verify(pppoe): if 'interface' not in pppoe: raise ConfigError('At least one listen interface must be defined!') + # Check is interface exists in the system + for iface in pppoe['interface']: + if not get_interface_config(iface): + raise ConfigError(f'Interface {iface} does not exist!') + # local ippool and gateway settings config checks if not (dict_search('client_ip_pool.subnet', pppoe) or (dict_search('client_ip_pool.start', pppoe) and @@ -73,6 +81,13 @@ def generate(pppoe): if not pppoe: return None + # Generate special regex for dynamic interfaces + for iface in pppoe['interface']: + if 'vlan_range' in pppoe['interface'][iface]: + pppoe['interface'][iface]['regex'] = [] + for vlan_range in pppoe['interface'][iface]['vlan_range']: + pppoe['interface'][iface]['regex'].append(range_to_regex(vlan_range)) + render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe) if dict_search('authentication.mode', pppoe) == 'local': diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 23e45a5b7..8ce48780b 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -19,70 +19,49 @@ import os from sys import exit from vyos.config import Config +from vyos.configdict import dict_merge from vyos.configverify import verify_vrf -from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random +from vyos.snmpv3_hashgen import plaintext_to_md5 +from vyos.snmpv3_hashgen import plaintext_to_sha1 +from vyos.snmpv3_hashgen import random from vyos.template import render -from vyos.template import is_ipv4 -from vyos.util import call, chmod_755 +from vyos.util import call +from vyos.util import chmod_755 +from vyos.util import dict_search from vyos.validate import is_addr_assigned from vyos.version import get_version_data -from vyos import ConfigError, airbag +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag airbag.enable() config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' config_file_access = r'/usr/share/snmp/snmpd.conf' config_file_user = r'/var/lib/snmp/snmpd.conf' -default_script_dir = r'/config/user-data/' systemd_override = r'/etc/systemd/system/snmpd.service.d/override.conf' +systemd_service = 'snmpd.service' -# SNMP OIDs used to mark auth/priv type -OIDs = { - 'md5' : '.1.3.6.1.6.3.10.1.1.2', - 'sha' : '.1.3.6.1.6.3.10.1.1.3', - 'aes' : '.1.3.6.1.6.3.10.1.2.4', - 'des' : '.1.3.6.1.6.3.10.1.2.2', - 'none': '.1.3.6.1.6.3.10.1.2.1' -} - -default_config_data = { - 'listen_on': [], - 'listen_address': [], - 'ipv6_enabled': 'True', - 'communities': [], - 'smux_peers': [], - 'location' : '', - 'description' : '', - 'contact' : '', - 'route_table': 'False', - 'trap_source': '', - 'trap_targets': [], - 'vyos_user': '', - 'vyos_user_pass': '', - 'version': '', - 'v3_enabled': 'False', - 'v3_engineid': '', - 'v3_groups': [], - 'v3_traps': [], - 'v3_users': [], - 'v3_views': [], - 'script_ext': [] -} - -def rmfile(file): - if os.path.isfile(file): - os.unlink(file) - -def get_config(): - snmp = default_config_data - conf = Config() - if not conf.exists('service snmp'): - return None +def get_config(config=None): + if config: + conf = config else: - if conf.exists('system ipv6 disable'): - snmp['ipv6_enabled'] = False + conf = Config() + base = ['service', 'snmp'] + + snmp = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + if not conf.exists(base): + snmp.update({'deleted' : ''}) + + if conf.exists(['service', 'lldp', 'snmp', 'enable']): + snmp.update({'lldp_snmp' : ''}) - conf.set_level('service snmp') + if conf.exists(['system', 'ipv6', 'disable']): + snmp.update({'ipv6_disabled' : ''}) + + if 'deleted' in snmp: + return snmp version_data = get_version_data() snmp['version'] = version_data['version'] @@ -91,461 +70,207 @@ def get_config(): snmp['vyos_user'] = 'vyos' + random(8) snmp['vyos_user_pass'] = random(16) - if conf.exists('community'): - for name in conf.list_nodes('community'): - community = { - 'name': name, - 'authorization': 'ro', - 'network_v4': [], - 'network_v6': [], - 'has_source' : False - } - - if conf.exists('community {0} authorization'.format(name)): - community['authorization'] = conf.return_value('community {0} authorization'.format(name)) - - # Subnet of SNMP client(s) allowed to contact system - if conf.exists('community {0} network'.format(name)): - for addr in conf.return_values('community {0} network'.format(name)): - if is_ipv4(addr): - community['network_v4'].append(addr) - else: - community['network_v6'].append(addr) - - # IP address of SNMP client allowed to contact system - if conf.exists('community {0} client'.format(name)): - for addr in conf.return_values('community {0} client'.format(name)): - if is_ipv4(addr): - community['network_v4'].append(addr) - else: - community['network_v6'].append(addr) - - if (len(community['network_v4']) > 0) or (len(community['network_v6']) > 0): - community['has_source'] = True - - snmp['communities'].append(community) - - if conf.exists('contact'): - snmp['contact'] = conf.return_value('contact') - - if conf.exists('description'): - snmp['description'] = conf.return_value('description') - - if conf.exists('listen-address'): - for addr in conf.list_nodes('listen-address'): - port = '161' - if conf.exists('listen-address {0} port'.format(addr)): - port = conf.return_value('listen-address {0} port'.format(addr)) - - snmp['listen_address'].append((addr, port)) + # 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(base) + + # We can not merge defaults for tagNodes - those need to be blended in + # per tagNode instance + if 'listen_address' in default_values: + del default_values['listen_address'] + if 'community' in default_values: + del default_values['community'] + if 'trap_target' in default_values: + del default_values['trap_target'] + if 'v3' in default_values: + del default_values['v3'] + snmp = dict_merge(default_values, snmp) + + if 'listen_address' in snmp: + default_values = defaults(base + ['listen-address']) + for address in snmp['listen_address']: + snmp['listen_address'][address] = dict_merge( + default_values, snmp['listen_address'][address]) # Always listen on localhost if an explicit address has been configured # This is a safety measure to not end up with invalid listen addresses # that are not configured on this system. See https://phabricator.vyos.net/T850 - if not '127.0.0.1' in conf.list_nodes('listen-address'): - snmp['listen_address'].append(('127.0.0.1', '161')) - - if not '::1' in conf.list_nodes('listen-address'): - snmp['listen_address'].append(('::1', '161')) - - if conf.exists('location'): - snmp['location'] = conf.return_value('location') - - if conf.exists('smux-peer'): - snmp['smux_peers'] = conf.return_values('smux-peer') - - if conf.exists('trap-source'): - snmp['trap_source'] = conf.return_value('trap-source') - - if conf.exists('trap-target'): - for target in conf.list_nodes('trap-target'): - trap_tgt = { - 'target': target, - 'community': '', - 'port': '' - } - - if conf.exists('trap-target {0} community'.format(target)): - trap_tgt['community'] = conf.return_value('trap-target {0} community'.format(target)) - - if conf.exists('trap-target {0} port'.format(target)): - trap_tgt['port'] = conf.return_value('trap-target {0} port'.format(target)) - - snmp['trap_targets'].append(trap_tgt) - - if conf.exists('script-extensions'): - for extname in conf.list_nodes('script-extensions extension-name'): - conf_script = conf.return_value('script-extensions extension-name {} script'.format(extname)) - # if script has not absolute path, use pre configured path - if "/" not in conf_script: - conf_script = default_script_dir + conf_script - - extension = { - 'name': extname, - 'script' : conf_script - } - - snmp['script_ext'].append(extension) - - if conf.exists('oid-enable route-table'): - snmp['route_table'] = True - - if conf.exists('vrf'): - # Append key to dict but don't place it in the default dictionary. - # This is required to make the override.conf.tmpl work until we - # migrate to get_config_dict(). - snmp['vrf'] = conf.return_value('vrf') - - - ######################################################################### - # ____ _ _ __ __ ____ _____ # - # / ___|| \ | | \/ | _ \ __ _|___ / # - # \___ \| \| | |\/| | |_) | \ \ / / |_ \ # - # ___) | |\ | | | | __/ \ V / ___) | # - # |____/|_| \_|_| |_|_| \_/ |____/ # - # # - # now take care about the fancy SNMP v3 stuff, or bail out eraly # - ######################################################################### - if not conf.exists('v3'): - return snmp - else: - snmp['v3_enabled'] = True - - # 'set service snmp v3 engineid' - if conf.exists('v3 engineid'): - snmp['v3_engineid'] = conf.return_value('v3 engineid') - - # 'set service snmp v3 group' - if conf.exists('v3 group'): - for group in conf.list_nodes('v3 group'): - v3_group = { - 'name': group, - 'mode': 'ro', - 'seclevel': 'auth', - 'view': '' - } - - if conf.exists('v3 group {0} mode'.format(group)): - v3_group['mode'] = conf.return_value('v3 group {0} mode'.format(group)) - - if conf.exists('v3 group {0} seclevel'.format(group)): - v3_group['seclevel'] = conf.return_value('v3 group {0} seclevel'.format(group)) - - if conf.exists('v3 group {0} view'.format(group)): - v3_group['view'] = conf.return_value('v3 group {0} view'.format(group)) - - snmp['v3_groups'].append(v3_group) - - # 'set service snmp v3 trap-target' - if conf.exists('v3 trap-target'): - for trap in conf.list_nodes('v3 trap-target'): - trap_cfg = { - 'ipAddr': trap, - 'secName': '', - 'authProtocol': 'md5', - 'authPassword': '', - 'authMasterKey': '', - 'privProtocol': 'des', - 'privPassword': '', - 'privMasterKey': '', - 'ipProto': 'udp', - 'ipPort': '162', - 'type': '', - 'secLevel': 'noAuthNoPriv' - } - - if conf.exists('v3 trap-target {0} user'.format(trap)): - # Set the securityName used for authenticated SNMPv3 messages. - trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap)) - - if conf.exists('v3 trap-target {0} auth type'.format(trap)): - # Set the authentication protocol (MD5 or SHA) used for authenticated SNMPv3 messages - # cmdline option '-a' - trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap)) - - if conf.exists('v3 trap-target {0} auth plaintext-password'.format(trap)): - # Set the authentication pass phrase used for authenticated SNMPv3 messages. - # cmdline option '-A' - trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-password'.format(trap)) - - if conf.exists('v3 trap-target {0} auth encrypted-password'.format(trap)): - # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys. - # cmdline option '-3m' - trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-password'.format(trap)) - - if conf.exists('v3 trap-target {0} privacy type'.format(trap)): - # Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages. - # cmdline option '-x' - trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap)) - - if conf.exists('v3 trap-target {0} privacy plaintext-password'.format(trap)): - # Set the privacy pass phrase used for encrypted SNMPv3 messages. - # cmdline option '-X' - trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-password'.format(trap)) - - if conf.exists('v3 trap-target {0} privacy encrypted-password'.format(trap)): - # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys. - # cmdline option '-3M' - trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-password'.format(trap)) - - if conf.exists('v3 trap-target {0} protocol'.format(trap)): - trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap)) - - if conf.exists('v3 trap-target {0} port'.format(trap)): - trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap)) - - if conf.exists('v3 trap-target {0} type'.format(trap)): - trap_cfg['type'] = conf.return_value('v3 trap-target {0} type'.format(trap)) - - # Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv). - # Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv. - if trap_cfg['authPassword'] or trap_cfg['authMasterKey']: - if trap_cfg['privProtocol'] or trap_cfg['privPassword']: - trap_cfg['secLevel'] = 'authPriv' - else: - trap_cfg['secLevel'] = 'authNoPriv' - - snmp['v3_traps'].append(trap_cfg) - - # 'set service snmp v3 user' - if conf.exists('v3 user'): - for user in conf.list_nodes('v3 user'): - user_cfg = { - 'name': user, - 'authMasterKey': '', - 'authPassword': '', - 'authProtocol': 'md5', - 'authOID': 'none', - 'group': '', - 'mode': 'ro', - 'privMasterKey': '', - 'privPassword': '', - 'privOID': '', - 'privProtocol': 'des' - } - - # v3 user {0} auth - if conf.exists('v3 user {0} auth encrypted-password'.format(user)): - user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-password'.format(user)) - - if conf.exists('v3 user {0} auth plaintext-password'.format(user)): - user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-password'.format(user)) - - # load default value - type = user_cfg['authProtocol'] - if conf.exists('v3 user {0} auth type'.format(user)): - type = conf.return_value('v3 user {0} auth type'.format(user)) - - # (re-)update with either default value or value from CLI - user_cfg['authProtocol'] = type - user_cfg['authOID'] = OIDs[type] - - # v3 user {0} group - if conf.exists('v3 user {0} group'.format(user)): - user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user)) - - # v3 user {0} mode - if conf.exists('v3 user {0} mode'.format(user)): - user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user)) - - # v3 user {0} privacy - if conf.exists('v3 user {0} privacy encrypted-password'.format(user)): - user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-password'.format(user)) - - if conf.exists('v3 user {0} privacy plaintext-password'.format(user)): - user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-password'.format(user)) - - # load default value - type = user_cfg['privProtocol'] - if conf.exists('v3 user {0} privacy type'.format(user)): - type = conf.return_value('v3 user {0} privacy type'.format(user)) - - # (re-)update with either default value or value from CLI - user_cfg['privProtocol'] = type - user_cfg['privOID'] = OIDs[type] - - snmp['v3_users'].append(user_cfg) - - # 'set service snmp v3 view' - if conf.exists('v3 view'): - for view in conf.list_nodes('v3 view'): - view_cfg = { - 'name': view, - 'oids': [] - } - - if conf.exists('v3 view {0} oid'.format(view)): - for oid in conf.list_nodes('v3 view {0} oid'.format(view)): - oid_cfg = { - 'oid': oid - } - view_cfg['oids'].append(oid_cfg) - snmp['v3_views'].append(view_cfg) + if '127.0.0.1' not in snmp['listen_address']: + tmp = {'127.0.0.1': {'port': '161'}} + snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) + + if '::1' not in snmp['listen_address']: + if 'ipv6_disabled' not in snmp: + tmp = {'::1': {'port': '161'}} + snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) + + if 'community' in snmp: + default_values = defaults(base + ['community']) + for community in snmp['community']: + snmp['community'][community] = dict_merge( + default_values, snmp['community'][community]) + + if 'trap_target' in snmp: + default_values = defaults(base + ['trap-target']) + for trap in snmp['trap_target']: + snmp['trap_target'][trap] = dict_merge( + default_values, snmp['trap_target'][trap]) + + if 'v3' in snmp: + default_values = defaults(base + ['v3']) + # tagNodes need to be merged in individually later on + for tmp in ['user', 'group', 'trap_target']: + del default_values[tmp] + snmp['v3'] = dict_merge(default_values, snmp['v3']) + + for user_group in ['user', 'group']: + if user_group in snmp['v3']: + default_values = defaults(base + ['v3', user_group]) + for tmp in snmp['v3'][user_group]: + snmp['v3'][user_group][tmp] = dict_merge( + default_values, snmp['v3'][user_group][tmp]) + + if 'trap_target' in snmp['v3']: + default_values = defaults(base + ['v3', 'trap-target']) + for trap in snmp['v3']['trap_target']: + snmp['v3']['trap_target'][trap] = dict_merge( + default_values, snmp['v3']['trap_target'][trap]) return snmp def verify(snmp): - if snmp is None: - # we can not delete SNMP when LLDP is configured with SNMP - conf = Config() - if conf.exists('service lldp snmp enable'): - raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!') - + if not snmp: return None + if {'deleted', 'lldp_snmp'} <= set(snmp): + raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!') + ### check if the configured script actually exist - if snmp['script_ext']: - for ext in snmp['script_ext']: - if not os.path.isfile(ext['script']): - print ("WARNING: script: {} doesn't exist".format(ext['script'])) + if 'script_extensions' in snmp and 'extension_name' in snmp['script_extensions']: + for extension, extension_opt in snmp['script_extensions']['extension_name'].items(): + if 'script' not in extension_opt: + raise ConfigError(f'Script extension "{extension}" requires an actual script to be configured!') + + tmp = extension_opt['script'] + if not os.path.isfile(tmp): + print(f'WARNING: script "{tmp}" does not exist!') else: - chmod_755(ext['script']) - - for listen in snmp['listen_address']: - addr = listen[0] - port = listen[1] - - if is_ipv4(addr): - # example: udp:127.0.0.1:161 - listen = 'udp:' + addr + ':' + port - elif snmp['ipv6_enabled']: - # example: udp6:[::1]:161 - listen = 'udp6:' + '[' + addr + ']' + ':' + port - - # We only wan't to configure addresses that exist on the system. - # Hint the user if they don't exist - if is_addr_assigned(addr): - snmp['listen_on'].append(listen) - else: - print('WARNING: SNMP listen address {0} not configured!'.format(addr)) + chmod_755(extension_opt['script']) + + if 'listen_address' in snmp: + for address in snmp['listen_address']: + # We only wan't to configure addresses that exist on the system. + # Hint the user if they don't exist + if not is_addr_assigned(address): + print(f'WARNING: SNMP listen address "{address}" not configured!') + + if 'trap_target' in snmp: + for trap, trap_config in snmp['trap_target'].items(): + if 'community' not in trap_config: + raise ConfigError(f'Trap target "{trap}" requires a community to be set!') verify_vrf(snmp) # bail out early if SNMP v3 is not configured - if not snmp['v3_enabled']: + if 'v3' not in snmp: return None - if 'v3_groups' in snmp.keys(): - for group in snmp['v3_groups']: - # - # A view must exist prior to mapping it into a group - # - if 'view' in group.keys(): - error = True - if 'v3_views' in snmp.keys(): - for view in snmp['v3_views']: - if view['name'] == group['view']: - error = False - if error: - raise ConfigError('You must create view "{0}" first'.format(group['view'])) - else: - raise ConfigError('"view" must be specified') - - if not 'mode' in group.keys(): - raise ConfigError('"mode" must be specified') - - if not 'seclevel' in group.keys(): - raise ConfigError('"seclevel" must be specified') - - if 'v3_traps' in snmp.keys(): - for trap in snmp['v3_traps']: - if trap['authPassword'] and trap['authMasterKey']: - raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap auth') - - if trap['authPassword'] == '' and trap['authMasterKey'] == '': - raise ConfigError('Must specify encrypted-password or plaintext-key for trap auth') - - if trap['privPassword'] and trap['privMasterKey']: - raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap privacy') + if 'user' in snmp['v3']: + for user, user_config in snmp['v3']['user'].items(): + if 'group' not in user_config: + raise ConfigError(f'Group membership required for user "{user}"!') - if trap['privPassword'] == '' and trap['privMasterKey'] == '': - raise ConfigError('Must specify encrypted-password or plaintext-key for trap privacy') + if 'plaintext_password' not in user_config['auth'] and 'encrypted_password' not in user_config['auth']: + raise ConfigError(f'Must specify authentication encrypted-password or plaintext-password for user "{user}"!') - if not 'type' in trap.keys(): - raise ConfigError('v3 trap: "type" must be specified') + if 'plaintext_password' not in user_config['privacy'] and 'encrypted_password' not in user_config['privacy']: + raise ConfigError(f'Must specify privacy encrypted-password or plaintext-password for user "{user}"!') - if not 'authPassword' and 'authMasterKey' in trap.keys(): - raise ConfigError('v3 trap: "auth" must be specified') + if 'group' in snmp['v3']: + for group, group_config in snmp['v3']['group'].items(): + if 'seclevel' not in group_config: + raise ConfigError(f'Must configure "seclevel" for group "{group}"!') + if 'view' not in group_config: + raise ConfigError(f'Must configure "view" for group "{group}"!') - if not 'authProtocol' in trap.keys(): - raise ConfigError('v3 trap: "protocol" must be specified') + # Check if 'view' exists + view = group_config['view'] + if 'view' not in snmp['v3'] or view not in snmp['v3']['view']: + raise ConfigError(f'You must create view "{view}" first!') - if not 'privPassword' and 'privMasterKey' in trap.keys(): - raise ConfigError('v3 trap: "user" must be specified') + if 'view' in snmp['v3']: + for view, view_config in snmp['v3']['view'].items(): + if 'oid' not in view_config: + raise ConfigError(f'Must configure an "oid" for view "{view}"!') - if 'v3_users' in snmp.keys(): - for user in snmp['v3_users']: - # - # Group must exist prior to mapping it into a group - # seclevel will be extracted from group - # - if user['group']: - error = True - if 'v3_groups' in snmp.keys(): - for group in snmp['v3_groups']: - if group['name'] == user['group']: - seclevel = group['seclevel'] - error = False + if 'trap_target' in snmp['v3']: + for trap, trap_config in snmp['v3']['trap_target'].items(): + if 'plaintext_password' not in trap_config['auth'] and 'encrypted_password' not in trap_config['auth']: + raise ConfigError(f'Must specify one of authentication encrypted-password or plaintext-password for trap "{trap}"!') - if error: - raise ConfigError('You must create group "{0}" first'.format(user['group'])) + if {'plaintext_password', 'encrypted_password'} <= set(trap_config['auth']): + raise ConfigError(f'Can not specify both authentication encrypted-password and plaintext-password for trap "{trap}"!') - # Depending on the configured security level the user has to provide additional info - if (not user['authPassword'] and not user['authMasterKey']): - raise ConfigError('Must specify encrypted-password or plaintext-key for user auth') + if 'plaintext_password' not in trap_config['privacy'] and 'encrypted_password' not in trap_config['privacy']: + raise ConfigError(f'Must specify one of privacy encrypted-password or plaintext-password for trap "{trap}"!') - if user['privPassword'] == '' and user['privMasterKey'] == '': - raise ConfigError('Must specify encrypted-password or plaintext-key for user privacy') + if {'plaintext_password', 'encrypted_password'} <= set(trap_config['privacy']): + raise ConfigError(f'Can not specify both privacy encrypted-password and plaintext-password for trap "{trap}"!') - if user['mode'] == '': - raise ConfigError('Must specify user mode ro/rw') - - if 'v3_views' in snmp.keys(): - for view in snmp['v3_views']: - if not view['oids']: - raise ConfigError('Must configure an oid') + if 'type' not in trap_config: + raise ConfigError('SNMP v3 trap "type" must be specified!') return None def generate(snmp): + # # As we are manipulating the snmpd user database we have to stop it first! # This is even save if service is going to be removed - call('systemctl stop snmpd.service') - config_files = [config_file_client, config_file_daemon, config_file_access, - config_file_user, systemd_override] + call(f'systemctl stop {systemd_service}') + # Clean config files + config_files = [config_file_client, config_file_daemon, + config_file_access, config_file_user, systemd_override] for file in config_files: - rmfile(file) + if os.path.isfile(file): + os.unlink(file) if not snmp: return None - if 'v3_users' in snmp.keys(): + if 'v3' in snmp: # net-snmp is now regenerating the configuration file in the background # thus we need to re-open and re-read the file as the content changed. # After that we can no read the encrypted password from the config and # replace the CLI plaintext password with its encrypted version. - os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos" + os.environ['vyos_libexec_dir'] = '/usr/libexec/vyos' - for user in snmp['v3_users']: - if user['authProtocol'] == 'sha': - hash = plaintext_to_sha1 - else: - hash = plaintext_to_md5 + if 'user' in snmp['v3']: + for user, user_config in snmp['v3']['user'].items(): + if dict_search('auth.type', user_config) == 'sha': + hash = plaintext_to_sha1 + else: + hash = plaintext_to_md5 + + if dict_search('auth.plaintext_password', user_config) is not None: + tmp = hash(dict_search('auth.plaintext_password', user_config), + dict_search('v3.engineid', snmp)) + + snmp['v3']['user'][user]['auth']['encrypted_password'] = tmp + del snmp['v3']['user'][user]['auth']['plaintext_password'] - if user['authPassword']: - user['authMasterKey'] = hash(user['authPassword'], snmp['v3_engineid']) - user['authPassword'] = '' + call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" auth encrypted-password "{tmp}" > /dev/null') + call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" auth plaintext-password > /dev/null') - call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" auth encrypted-password "{authMasterKey}" > /dev/null'.format(**user)) - call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" auth plaintext-password > /dev/null'.format(**user)) + if dict_search('privacy.plaintext_password', user_config) is not None: + tmp = hash(dict_search('privacy.plaintext_password', user_config), + dict_search('v3.engineid', snmp)) - if user['privPassword']: - user['privMasterKey'] = hash(user['privPassword'], snmp['v3_engineid']) - user['privPassword'] = '' + snmp['v3']['user'][user]['privacy']['encrypted_password'] = tmp + del snmp['v3']['user'][user]['privacy']['plaintext_password'] - call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" privacy encrypted-password "{privMasterKey}" > /dev/null'.format(**user)) - call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" privacy plaintext-password > /dev/null'.format(**user)) + call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" privacy encrypted-password "{tmp}" > /dev/null') + call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" privacy plaintext-password > /dev/null') # Write client config file render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp) @@ -568,7 +293,7 @@ def apply(snmp): return None # start SNMP daemon - call('systemctl restart snmpd.service') + call(f'systemctl restart {systemd_service}') # Enable AgentX in FRR call('vtysh -c "configure terminal" -c "agentx" >/dev/null') diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py index a960a4da3..a521c9834 100755 --- a/src/conf_mode/system-login-banner.py +++ b/src/conf_mode/system-login-banner.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -15,34 +15,33 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from sys import exit +from copy import deepcopy + from vyos.config import Config +from vyos.util import write_file from vyos import ConfigError - from vyos import airbag airbag.enable() -motd=""" -The programs included with the Debian/VyOS GNU/Linux system are free software; -the exact distribution terms for each program are described in the -individual files in /usr/share/doc/*/copyright. - -Debian/VyOS GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent -permitted by applicable law. - -""" +try: + with open('/usr/share/vyos/default_motd') as f: + motd = f.read() +except: + # Use an empty banner if the default banner file cannot be read + motd = "\n" PRELOGIN_FILE = r'/etc/issue' PRELOGIN_NET_FILE = r'/etc/issue.net' POSTLOGIN_FILE = r'/etc/motd' default_config_data = { - 'issue': 'Welcome to VyOS - \\n \\l\n', - 'issue_net': 'Welcome to VyOS\n', + 'issue': 'Welcome to VyOS - \\n \\l\n\n', + 'issue_net': '', 'motd': motd } def get_config(config=None): - banner = default_config_data + banner = deepcopy(default_config_data) if config: conf = config else: @@ -91,14 +90,9 @@ def generate(banner): pass def apply(banner): - with open(PRELOGIN_FILE, 'w') as f: - f.write(banner['issue']) - - with open(PRELOGIN_NET_FILE, 'w') as f: - f.write(banner['issue_net']) - - with open(POSTLOGIN_FILE, 'w') as f: - f.write(banner['motd']) + write_file(PRELOGIN_FILE, banner['issue']) + write_file(PRELOGIN_NET_FILE, banner['issue_net']) + write_file(POSTLOGIN_FILE, banner['motd']) return None diff --git a/src/conf_mode/system-logs.py b/src/conf_mode/system-logs.py new file mode 100755 index 000000000..e6296656d --- /dev/null +++ b/src/conf_mode/system-logs.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +from sys import exit + +from vyos import ConfigError +from vyos import airbag +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.logger import syslog +from vyos.template import render +from vyos.util import dict_search +from vyos.xml import defaults +airbag.enable() + +# path to logrotate configs +logrotate_atop_file = '/etc/logrotate.d/vyos-atop' +logrotate_rsyslog_file = '/etc/logrotate.d/vyos-rsyslog' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['system', 'logs'] + default_values = defaults(base) + logs_config = conf.get_config_dict(base, + key_mangling=('-', '_'), + get_first_key=True) + logs_config = dict_merge(default_values, logs_config) + + return logs_config + + +def verify(logs_config): + # Nothing to verify here + pass + + +def generate(logs_config): + # get configuration for logrotate atop + logrotate_atop = dict_search('logrotate.atop', logs_config) + # generate new config file for atop + syslog.debug('Adding logrotate config for atop') + render(logrotate_atop_file, 'logs/logrotate/vyos-atop.tmpl', logrotate_atop) + + # get configuration for logrotate rsyslog + logrotate_rsyslog = dict_search('logrotate.messages', logs_config) + # generate new config file for rsyslog + syslog.debug('Adding logrotate config for rsyslog') + render(logrotate_rsyslog_file, 'logs/logrotate/vyos-rsyslog.tmpl', + logrotate_rsyslog) + + +def apply(logs_config): + # No further actions needed + pass + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py index 55cf6b142..b1c63e316 100755 --- a/src/conf_mode/system-option.py +++ b/src/conf_mode/system-option.py @@ -126,6 +126,12 @@ def apply(options): if 'keyboard_layout' in options: cmd('loadkeys {keyboard_layout}'.format(**options)) + # Enable/diable root-partition-auto-resize SystemD service + if 'root_partition_auto_resize' in options: + cmd('systemctl enable root-partition-auto-resize.service') + else: + cmd('systemctl disable root-partition-auto-resize.service') + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index 33a546bd3..19b252513 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -18,9 +18,14 @@ import os import re from vyos.config import Config -from vyos.util import call, read_file, write_file +from vyos.configdict import dict_merge +from vyos.util import call +from vyos.util import read_file +from vyos.util import write_file from vyos.template import render -from vyos import ConfigError, airbag +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag airbag.enable() by_bus_dir = '/dev/serial/by-bus' @@ -36,21 +41,27 @@ def get_config(config=None): console = conf.get_config_dict(base, get_first_key=True) # bail out early if no serial console is configured - if 'device' not in console.keys(): + if 'device' not in console: return console # convert CLI values to system values - for device in console['device'].keys(): - # no speed setting has been configured - use default value - if not 'speed' in console['device'][device].keys(): - tmp = { 'speed': '' } - if device.startswith('hvc'): - tmp['speed'] = 38400 - else: - tmp['speed'] = 115200 + default_values = defaults(base + ['device']) + for device, device_config in console['device'].items(): + if 'speed' not in device_config and device.startswith('hvc'): + # XEN console has a different default console speed + console['device'][device]['speed'] = 38400 + else: + # Merge in XML defaults - the proper way to do it + console['device'][device] = dict_merge(default_values, + console['device'][device]) + + return console - console['device'][device].update(tmp) +def verify(console): + if not console or 'device' not in console: + return None + for device in console['device']: if device.startswith('usb'): # It is much easiert to work with the native ttyUSBn name when using # getty, but that name may change across reboots - depending on the @@ -58,13 +69,13 @@ def get_config(config=None): # to its dynamic device file - and create a new dict entry for it. by_bus_device = f'{by_bus_dir}/{device}' if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device): - tmp = os.path.basename(os.readlink(by_bus_device)) - # updating the dict must come as last step in the loop! - console['device'][tmp] = console['device'].pop(device) + device = os.path.basename(os.readlink(by_bus_device)) - return console + # If the device name still starts with usbXXX no matching tty was found + # and it can not be used as a serial interface + if device.startswith('usb'): + raise ConfigError(f'Device {device} does not support beeing used as tty') -def verify(console): return None def generate(console): @@ -76,20 +87,29 @@ def generate(console): call(f'systemctl stop {basename}') os.unlink(os.path.join(root, basename)) - if not console: + if not console or 'device' not in console: return None - for device in console['device'].keys(): + for device, device_config in console['device'].items(): + if device.startswith('usb'): + # It is much easiert to work with the native ttyUSBn name when using + # getty, but that name may change across reboots - depending on the + # amount of connected devices. We will resolve the fixed device name + # to its dynamic device file - and create a new dict entry for it. + by_bus_device = f'{by_bus_dir}/{device}' + if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device): + device = os.path.basename(os.readlink(by_bus_device)) + config_file = base_dir + f'/serial-getty@{device}.service' getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service' - render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device]) + render(config_file, 'getty/serial-getty.service.tmpl', device_config) os.symlink(config_file, getty_wants_symlink) # GRUB # For existing serial line change speed (if necessary) # Only applys to ttyS0 - if 'ttyS0' not in console['device'].keys(): + if 'ttyS0' not in console['device']: return None speed = console['device']['ttyS0']['speed'] @@ -98,7 +118,6 @@ def generate(console): return None lines = read_file(grub_config).split('\n') - p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$') write = False newlines = [] @@ -122,9 +141,8 @@ def generate(console): return None def apply(console): - # reset screen blanking + # Reset screen blanking call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux </dev/tty1 >/dev/tty1 2>&1') - # Reload systemd manager configuration call('systemctl daemon-reload') @@ -136,11 +154,11 @@ def apply(console): call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1') # Start getty process on configured serial interfaces - for device in console['device'].keys(): + for device in console['device']: # Only start console if it exists on the running system. If a user # detaches a USB serial console and reboots - it should not fail! if os.path.exists(f'/dev/{device}'): - call(f'systemctl start serial-getty@{device}.service') + call(f'systemctl restart serial-getty@{device}.service') return None diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index 2409eec1f..ef726670c 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -24,6 +24,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_vrf from vyos.template import render from vyos.template import is_ipv4 from vyos.util import call @@ -65,10 +66,11 @@ def verify(tftpd): if 'listen_address' not in tftpd: raise ConfigError('TFTP server listen address must be configured!') - for address in tftpd['listen_address']: + for address, address_config in tftpd['listen_address'].items(): if not is_addr_assigned(address): print(f'WARNING: TFTP server listen address "{address}" not ' \ 'assigned to any interface!') + verify_vrf(address_config) return None @@ -83,7 +85,7 @@ def generate(tftpd): return None idx = 0 - for address in tftpd['listen_address']: + for address, address_config in tftpd['listen_address'].items(): config = deepcopy(tftpd) port = tftpd['port'] if is_ipv4(address): @@ -91,6 +93,9 @@ def generate(tftpd): else: config['listen_address'] = f'[{address}]:{port} -6' + if 'vrf' in address_config: + config['vrf'] = address_config['vrf'] + file = config_file + str(idx) render(file, 'tftp-server/default.tmpl', config) idx = idx + 1 diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 9c52f77ca..818e8fa0b 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -290,6 +290,8 @@ def get_config(config=None): # LNS secret if conf.exists(['lns', 'shared-secret']): l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret']) + if conf.exists(['lns', 'host-name']): + l2tp['lns_host_name'] = conf.return_value(['lns', 'host-name']) if conf.exists(['ccp-disable']): l2tp['ccp_disable'] = True diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index f6db196dc..51ea1f223 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -23,9 +23,11 @@ from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call +from vyos.util import is_systemd_service_running from vyos.xml import defaults from vyos import ConfigError from crypt import crypt, mksalt, METHOD_SHA512 +from time import sleep from vyos import airbag airbag.enable() @@ -172,6 +174,16 @@ def apply(ocserv): os.unlink(file) else: call('systemctl restart ocserv.service') + counter = 0 + while True: + # exit early when service runs + if is_systemd_service_running("ocserv.service"): + break + sleep(0.250) + if counter > 5: + raise ConfigError('openconnect failed to start, check the logs for details') + break + counter += 1 if __name__ == '__main__': diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index d1a71a5ad..68980e5ab 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -26,6 +26,7 @@ from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call from vyos.util import dict_search +from vyos.util import write_file from vyos import ConfigError from vyos import airbag airbag.enable() @@ -34,6 +35,10 @@ cfg_dir = '/run/accel-pppd' sstp_conf = '/run/accel-pppd/sstp.conf' sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets' +cert_file_path = os.path.join(cfg_dir, 'sstp-cert.pem') +cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key') +ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem') + def get_config(config=None): if config: conf = config @@ -58,7 +63,7 @@ def verify(sstp): verify_accel_ppp_base_service(sstp) - if not sstp['client_ip_pool']: + if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp: raise ConfigError('Client IP subnet required') # @@ -72,22 +77,32 @@ def verify(sstp): ssl = sstp['ssl'] + # CA if 'ca_certificate' not in ssl: raise ConfigError('SSL CA certificate missing on SSTP config') + ca_name = ssl['ca_certificate'] + + if ca_name not in sstp['pki']['ca']: + raise ConfigError('Invalid CA certificate on SSTP config') + + if 'certificate' not in sstp['pki']['ca'][ca_name]: + raise ConfigError('Missing certificate data for CA certificate on SSTP config') + + # Certificate if 'certificate' not in ssl: raise ConfigError('SSL certificate missing on SSTP config') cert_name = ssl['certificate'] - if ssl['ca_certificate'] not in sstp['pki']['ca']: - raise ConfigError('Invalid CA certificate on SSTP config') - if cert_name not in sstp['pki']['certificate']: raise ConfigError('Invalid certificate on SSTP config') pki_cert = sstp['pki']['certificate'][cert_name] + if 'certificate' not in pki_cert: + raise ConfigError('Missing certificate data for certificate on SSTP config') + if 'private' not in pki_cert or 'key' not in pki_cert['private']: raise ConfigError('Missing private key for certificate on SSTP config') @@ -98,27 +113,18 @@ def generate(sstp): if not sstp: return None - cert_file_path = os.path.join(cfg_dir, 'sstp-cert.pem') - cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key') - ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem') + # accel-cmd reload doesn't work so any change results in a restart of the daemon + render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp) cert_name = sstp['ssl']['certificate'] pki_cert = sstp['pki']['certificate'][cert_name] - with open(cert_file_path, 'w') as f: - f.write(wrap_certificate(pki_cert['certificate'])) - - with open(cert_key_path, 'w') as f: - f.write(wrap_private_key(pki_cert['private']['key'])) - ca_cert_name = sstp['ssl']['ca_certificate'] pki_ca = sstp['pki']['ca'][ca_cert_name] - with open(ca_cert_file_path, 'w') as f: - f.write(wrap_certificate(pki_ca['certificate'])) - - # accel-cmd reload doesn't work so any change results in a restart of the daemon - render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp) + write_file(cert_file_path, wrap_certificate(pki_cert['certificate'])) + write_file(cert_key_path, wrap_private_key(pki_cert['private']['key'])) + write_file(ca_cert_file_path, wrap_certificate(pki_ca['certificate'])) if dict_search('authentication.mode', sstp) == 'local': render(sstp_chap_secrets, 'accel-ppp/chap-secrets.config_dict.tmpl', diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 919083ac4..38c0c4463 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -18,7 +18,6 @@ import os from sys import exit from json import loads -from tempfile import NamedTemporaryFile from vyos.config import Config from vyos.configdict import node_changed @@ -31,10 +30,12 @@ from vyos.util import get_interface_config from vyos.util import popen from vyos.util import run from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() -config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' +config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf' +nft_vrf_config = '/tmp/nftables-vrf-zones' def list_rules(): command = 'ip -j -4 rule show' @@ -128,8 +129,8 @@ def verify(vrf): def generate(vrf): render(config_file, 'vrf/vrf.conf.tmpl', vrf) # Render nftables zones config - vrf['nft_vrf_zones'] = NamedTemporaryFile().name - render(vrf['nft_vrf_zones'], 'firewall/nftables-vrf-zones.tmpl', vrf) + + render(nft_vrf_config, 'firewall/nftables-vrf-zones.tmpl', vrf) return None @@ -165,8 +166,9 @@ def apply(vrf): _, err = popen('nft list table inet vrf_zones') # If not, create a table if err: - cmd(f'nft -f {vrf["nft_vrf_zones"]}') - os.unlink(vrf['nft_vrf_zones']) + if os.path.exists(nft_vrf_config): + cmd(f'nft -f {nft_vrf_config}') + os.unlink(nft_vrf_config) for name, config in vrf['name'].items(): table = config['table'] diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py index 87ee8f2d1..1a7bd1f09 100755 --- a/src/conf_mode/vrf_vni.py +++ b/src/conf_mode/vrf_vni.py @@ -32,37 +32,26 @@ def get_config(config=None): else: conf = Config() - # This script only works with a passed VRF name - if len(argv) < 1: - raise NotImplementedError - vrf = argv[1] + base = ['vrf'] + vrf = conf.get_config_dict(base, get_first_key=True) + return vrf - # "assemble" dict - easier here then use a full blown get_config_dict() - # on a single leafNode - vni = { 'vrf' : vrf } - tmp = conf.return_value(['vrf', 'name', vrf, 'vni']) - if tmp: vni.update({ 'vni' : tmp }) - - return vni - -def verify(vni): +def verify(vrf): return None -def generate(vni): - vni['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vni) +def generate(vrf): + vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vrf) return None -def apply(vni): +def apply(vrf): # add configuration to FRR frr_cfg = frr.FRRConfig() frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '') - frr_cfg.add_before(r'(interface .*|line vty)', vni['new_frr_config']) + frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True) + if 'new_frr_config' in vrf: + frr_cfg.add_before(frr.default_add_before, vrf['new_frr_config']) frr_cfg.commit_configuration(frr_daemon) - # Save configuration to /run/frr/config/frr.conf - frr.save_configuration() - return None if __name__ == '__main__': diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index e8f1c1f99..c72efc61f 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -28,6 +28,7 @@ from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.util import call +from vyos.util import is_systemd_service_running from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -139,7 +140,12 @@ def apply(vrrp): call(f'systemctl stop {service_name}') return None - call(f'systemctl restart {service_name}') + # XXX: T3944 - reload keepalived configuration if service is already running + # to not cause any service disruption when applying changes. + if is_systemd_service_running(service_name): + call(f'systemctl reload {service_name}') + else: + call(f'systemctl restart {service_name}') return None if __name__ == '__main__': diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py new file mode 100755 index 000000000..2535ea33b --- /dev/null +++ b/src/conf_mode/zone_policy.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 json import loads +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import cmd +from vyos.util import dict_search_args +from vyos.util import run +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +nftables_conf = '/run/nftables_zone.conf' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['zone-policy'] + zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + if zone_policy: + zone_policy['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + return zone_policy + +def verify(zone_policy): + # bail out early - looks like removal from running config + if not zone_policy: + return None + + local_zone = False + interfaces = [] + + if 'zone' in zone_policy: + for zone, zone_conf in zone_policy['zone'].items(): + if 'local_zone' not in zone_conf and 'interface' not in zone_conf: + raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone') + + if 'local_zone' in zone_conf: + if local_zone: + raise ConfigError('There cannot be multiple local zones') + if 'interface' in zone_conf: + raise ConfigError('Local zone cannot have interfaces assigned') + if 'intra_zone_filtering' in zone_conf: + raise ConfigError('Local zone cannot use intra-zone-filtering') + local_zone = True + + if 'interface' in zone_conf: + found_duplicates = [intf for intf in zone_conf['interface'] if intf in interfaces] + + if found_duplicates: + raise ConfigError(f'Interfaces cannot be assigned to multiple zones') + + interfaces += zone_conf['interface'] + + if 'intra_zone_filtering' in zone_conf: + intra_zone = zone_conf['intra_zone_filtering'] + + if len(intra_zone) > 1: + raise ConfigError('Only one intra-zone-filtering action must be specified') + + if 'firewall' in intra_zone: + v4_name = dict_search_args(intra_zone, 'firewall', 'name') + if v4_name and not dict_search_args(zone_policy, 'firewall', 'name', v4_name): + raise ConfigError(f'Firewall name "{v4_name}" does not exist') + + v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6-name') + if v6_name and not dict_search_args(zone_policy, 'firewall', 'ipv6-name', v6_name): + raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') + + if not v4_name and not v6_name: + raise ConfigError('No firewall names specified for intra-zone-filtering') + + if 'from' in zone_conf: + for from_zone, from_conf in zone_conf['from'].items(): + v4_name = dict_search_args(from_conf, 'firewall', 'name') + if v4_name: + if 'name' not in zone_policy['firewall']: + raise ConfigError(f'Firewall name "{v4_name}" does not exist') + + if not dict_search_args(zone_policy, 'firewall', 'name', v4_name): + raise ConfigError(f'Firewall name "{v4_name}" does not exist') + + v6_name = dict_search_args(from_conf, 'firewall', 'v6_name') + if v6_name: + if 'ipv6_name' not in zone_policy['firewall']: + raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') + + if not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name): + raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') + + return None + +def has_ipv4_fw(zone_conf): + if 'from' not in zone_conf: + return False + zone_from = zone_conf['from'] + return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'name')]) + +def has_ipv6_fw(zone_conf): + if 'from' not in zone_conf: + return False + zone_from = zone_conf['from'] + return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'ipv6_name')]) + +def get_local_from(zone_policy, local_zone_name): + # Get all zone firewall names from the local zone + out = {} + for zone, zone_conf in zone_policy['zone'].items(): + if zone == local_zone_name: + continue + if 'from' not in zone_conf: + continue + if local_zone_name in zone_conf['from']: + out[zone] = zone_conf['from'][local_zone_name] + return out + +def cleanup_commands(): + commands = [] + for table in ['ip filter', 'ip6 filter']: + json_str = cmd(f'nft -j list table {table}') + obj = loads(json_str) + if 'nftables' not in obj: + continue + for item in obj['nftables']: + if 'rule' in item: + chain = item['rule']['chain'] + handle = item['rule']['handle'] + if 'expr' not in item['rule']: + continue + for expr in item['rule']['expr']: + target = dict_search_args(expr, 'jump', 'target') + if target and target.startswith("VZONE"): + commands.append(f'delete rule {table} {chain} handle {handle}') + for item in obj['nftables']: + if 'chain' in item: + if item['chain']['name'].startswith("VZONE"): + chain = item['chain']['name'] + commands.append(f'delete chain {table} {chain}') + return commands + +def generate(zone_policy): + data = zone_policy or {} + + if os.path.exists(nftables_conf): # Check to see if we've run before + data['cleanup_commands'] = cleanup_commands() + + if 'zone' in data: + for zone, zone_conf in data['zone'].items(): + zone_conf['ipv4'] = has_ipv4_fw(zone_conf) + zone_conf['ipv6'] = has_ipv6_fw(zone_conf) + + if 'local_zone' in zone_conf: + zone_conf['from_local'] = get_local_from(data, zone) + + render(nftables_conf, 'zone_policy/nftables.tmpl', data) + return None + +def apply(zone_policy): + install_result = run(f'nft -f {nftables_conf}') + if install_result == 1: + raise ConfigError('Failed to apply zone-policy') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/etc/cron.d/check-wwan b/src/etc/cron.d/check-wwan new file mode 100644 index 000000000..28190776f --- /dev/null +++ b/src/etc/cron.d/check-wwan @@ -0,0 +1 @@ +*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient index f737148dc..ae6bf9f16 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient +++ b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient @@ -23,10 +23,12 @@ if [ -z ${CONTROLLED_STOP} ] ; then if ([ $dhclient -ne $current_dhclient ] && [ $dhclient -ne $master_dhclient ]); then # get path to PID-file of dhclient process local dhclient_pidfile=`ps --no-headers --format args --pid $dhclient | awk 'match(\$0, ".*-pf (/.*pid) .*", PF) { print PF[1] }'` + # get path to lease-file of dhclient process + local dhclient_leasefile=`ps --no-headers --format args --pid $dhclient | awk 'match(\$0, ".*-lf (/\\\S*leases) .*", LF) { print LF[1] }'` # stop dhclient with native command - this will run dhclient-script with correct reason unlike simple kill - logmsg info "Stopping dhclient with PID: ${dhclient}, PID file: $dhclient_pidfile" + logmsg info "Stopping dhclient with PID: ${dhclient}, PID file: ${dhclient_pidfile}, Leases file: ${dhclient_leasefile}" if [[ -e $dhclient_pidfile ]]; then - dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile + dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile -lf $dhclient_leasefile else logmsg error "PID file $dhclient_pidfile does not exists, killing dhclient with SIGTERM signal" kill -s 15 ${dhclient} diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf index 24090e2a8..b1902b585 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf +++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf @@ -1,44 +1,48 @@ -# modified make_resolv_conf () for VyOS -make_resolv_conf() { - hostsd_client="/usr/bin/vyos-hostsd-client" - hostsd_changes= +# modified make_resolv_conf() for VyOS +# should be used only if vyos-hostsd is running - if [ -n "$new_domain_name" ]; then - logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" - $hostsd_client --delete-search-domains --tag "dhcp-$interface" - logmsg info "Adding domain name \"$new_domain_name\" as search domain with tag \"dhcp-$interface\" via vyos-hostsd-client" - $hostsd_client --add-search-domains "$new_domain_name" --tag "dhcp-$interface" - hostsd_changes=y - fi +if /usr/bin/systemctl -q is-active vyos-hostsd; then + make_resolv_conf() { + hostsd_client="/usr/bin/vyos-hostsd-client" + hostsd_changes= - if [ -n "$new_dhcp6_domain_search" ]; then - logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" - $hostsd_client --delete-search-domains --tag "dhcpv6-$interface" - logmsg info "Adding search domain \"$new_dhcp6_domain_search\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" - $hostsd_client --add-search-domains "$new_dhcp6_domain_search" --tag "dhcpv6-$interface" - hostsd_changes=y - fi + if [ -n "$new_domain_name" ]; then + logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-search-domains --tag "dhcp-$interface" + logmsg info "Adding domain name \"$new_domain_name\" as search domain with tag \"dhcp-$interface\" via vyos-hostsd-client" + $hostsd_client --add-search-domains "$new_domain_name" --tag "dhcp-$interface" + hostsd_changes=y + fi - if [ -n "$new_domain_name_servers" ]; then - logmsg info "Deleting nameservers with tag \"dhcp-$interface\" via vyos-hostsd-client" - $hostsd_client --delete-name-servers --tag "dhcp-$interface" - logmsg info "Adding nameservers \"$new_domain_name_servers\" with tag \"dhcp-$interface\" via vyos-hostsd-client" - $hostsd_client --add-name-servers $new_domain_name_servers --tag "dhcp-$interface" - hostsd_changes=y - fi + if [ -n "$new_dhcp6_domain_search" ]; then + logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-search-domains --tag "dhcpv6-$interface" + logmsg info "Adding search domain \"$new_dhcp6_domain_search\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --add-search-domains "$new_dhcp6_domain_search" --tag "dhcpv6-$interface" + hostsd_changes=y + fi - if [ -n "$new_dhcp6_name_servers" ]; then - logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client" - $hostsd_client --delete-name-servers --tag "dhcpv6-$interface" - logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" - $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface" - hostsd_changes=y - fi + if [ -n "$new_domain_name_servers" ]; then + logmsg info "Deleting nameservers with tag \"dhcp-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-name-servers --tag "dhcp-$interface" + logmsg info "Adding nameservers \"$new_domain_name_servers\" with tag \"dhcp-$interface\" via vyos-hostsd-client" + $hostsd_client --add-name-servers $new_domain_name_servers --tag "dhcp-$interface" + hostsd_changes=y + fi - if [ $hostsd_changes ]; then - logmsg info "Applying changes via vyos-hostsd-client" - $hostsd_client --apply - else - logmsg info "No changes to apply via vyos-hostsd-client" - fi -} + if [ -n "$new_dhcp6_name_servers" ]; then + logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-name-servers --tag "dhcpv6-$interface" + logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface" + hostsd_changes=y + fi + + if [ $hostsd_changes ]; then + logmsg info "Applying changes via vyos-hostsd-client" + $hostsd_client --apply + else + logmsg info "No changes to apply via vyos-hostsd-client" + fi + } +fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index 694d53b6b..ad6a1d5eb 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -4,21 +4,32 @@ # NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state hostsd_client="/usr/bin/vyos-hostsd-client" hostsd_changes= +# check vyos-hostsd status +/usr/bin/systemctl -q is-active vyos-hostsd +hostsd_status=$? if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then - # delete search domains and nameservers via vyos-hostsd - logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" - $hostsd_client --delete-search-domains --tag "dhcp-$interface" - logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client" - $hostsd_client --delete-name-servers --tag "dhcp-${interface}" - hostsd_changes=y + if [[ $hostsd_status -eq 0 ]]; then + # delete search domains and nameservers via vyos-hostsd + logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-search-domains --tag "dhcp-$interface" + logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client" + $hostsd_client --delete-name-servers --tag "dhcp-${interface}" + hostsd_changes=y + fi if_metric="$IF_METRIC" - # try to delete default ip route + # try to delete default ip route for router in $old_routers; do - logmsg info "Deleting default route: via $router dev ${interface} ${if_metric:+metric $if_metric}" - ip -4 route del default via $router dev ${interface} ${if_metric:+metric $if_metric} + # check if we are bound to a VRF + local vrf_name=$(basename /sys/class/net/${interface}/upper_* | sed -e 's/upper_//') + if [ "$vrf_name" != "*" ]; then + vrf="vrf $vrf_name" + fi + + logmsg info "Deleting default route: via $router dev ${interface} ${if_metric:+metric $if_metric} ${vrf}" + ip -4 route del default via $router dev ${interface} ${if_metric:+metric $if_metric} ${vrf} if_metric=$((if_metric+1)) done @@ -86,12 +97,14 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then fi if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then - # delete search domains and nameservers via vyos-hostsd - logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" - $hostsd_client --delete-search-domains --tag "dhcpv6-$interface" - logmsg info "Deleting nameservers with tag \"dhcpv6-${interface}\" via vyos-hostsd-client" - $hostsd_client --delete-name-servers --tag "dhcpv6-${interface}" - hostsd_changes=y + if [[ $hostsd_status -eq 0 ]]; then + # delete search domains and nameservers via vyos-hostsd + logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-search-domains --tag "dhcpv6-$interface" + logmsg info "Deleting nameservers with tag \"dhcpv6-${interface}\" via vyos-hostsd-client" + $hostsd_client --delete-name-servers --tag "dhcpv6-${interface}" + hostsd_changes=y + fi fi if [ $hostsd_changes ]; then diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook index a7a9a2ce6..61a89e62a 100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook @@ -35,19 +35,14 @@ fi python3 - <<PYEND import os import re + from vyos.util import call from vyos.util import cmd +from vyos.util import read_file +from vyos.util import write_file SWANCTL_CONF="/etc/swanctl/swanctl.conf" -def getlines(file): - with open(file, 'r') as f: - return f.readlines() - -def writelines(file, lines): - with open(file, 'w') as f: - f.writelines(lines) - def ipsec_down(ip_address): # This prevents the need to restart ipsec and kill all active connections, only the stale connection is closed status = cmd('sudo ipsec statusall') @@ -66,23 +61,26 @@ if __name__ == '__main__': new_ip = os.getenv('new_ip_address') old_ip = os.getenv('old_ip_address') - conf_lines = getlines(SWANCTL_CONF) - found = False - to_match = f'# dhcp:{interface}' + if os.path.exists(SWANCTL_CONF): + conf_lines = read_file(SWANCTL_CONF) + found = False + to_match = f'# dhcp:{interface}' + + for i, line in enumerate(conf_lines): + if line.find(to_match) > 0: + conf_lines[i] = line.replace(old_ip, new_ip) + found = True - for i, line in enumerate(conf_lines): - if line.find(to_match) > 0: - conf_lines[i] = line.replace(old_ip, new_ip) - found = True + for i, line in enumerate(secrets_lines): + if line.find(to_match) > 0: + secrets_lines[i] = line.replace(old_ip, new_ip) - for i, line in enumerate(secrets_lines): - if line.find(to_match) > 0: - secrets_lines[i] = line.replace(old_ip, new_ip) + if found: + write_file(SWANCTL_CONF, conf_lines) + ipsec_down(old_ip) + call('sudo ipsec rereadall') + call('sudo ipsec reload') + call('sudo swanctl -q') - if found: - writelines(SWANCTL_CONF, conf_lines) - ipsec_down(old_ip) - call('sudo ipsec rereadall') - call('sudo ipsec reload') - call('sudo swanctl -q') + exit(0) PYEND
\ No newline at end of file diff --git a/src/etc/logrotate.d/vyos-atop b/src/etc/logrotate.d/vyos-atop new file mode 100644 index 000000000..0c8359c7b --- /dev/null +++ b/src/etc/logrotate.d/vyos-atop @@ -0,0 +1,20 @@ +/var/log/atop/atop.log { + daily + dateext + dateformat _%Y-%m-%d_%H-%M-%S + maxsize 10M + missingok + nocompress + nocreate + nomail + rotate 10 + prerotate + # stop the service + systemctl stop atop.service + endscript + postrotate + # start atop service again + systemctl start atop.service + endscript +} + diff --git a/src/etc/systemd/system/atop.service.d/10-override.conf b/src/etc/systemd/system/atop.service.d/10-override.conf new file mode 100644 index 000000000..10df15862 --- /dev/null +++ b/src/etc/systemd/system/atop.service.d/10-override.conf @@ -0,0 +1,6 @@ +[Service] +ExecStartPre= +ExecStart= +ExecStart=/bin/sh -c 'exec /usr/bin/atop ${LOGOPTS} -w "${LOGPATH}/atop.log" ${LOGINTERVAL}' +ExecStartPost= + diff --git a/src/etc/systemd/system/avahi-daemon.service.d/override.conf b/src/etc/systemd/system/avahi-daemon.service.d/override.conf new file mode 100644 index 000000000..a9d2085f7 --- /dev/null +++ b/src/etc/systemd/system/avahi-daemon.service.d/override.conf @@ -0,0 +1,8 @@ +[Unit] +After= +After=vyos-router.service +ConditionPathExists=/run/avahi-daemon/avahi-daemon.conf + +[Service] +ExecStart= +ExecStart=/usr/sbin/avahi-daemon --syslog --file /run/avahi-daemon/avahi-daemon.conf
\ No newline at end of file diff --git a/src/etc/systemd/system/openvpn@.service.d/override.conf b/src/etc/systemd/system/openvpn@.service.d/10-override.conf index 03fe6b587..775a2d7ba 100644 --- a/src/etc/systemd/system/openvpn@.service.d/override.conf +++ b/src/etc/systemd/system/openvpn@.service.d/10-override.conf @@ -7,6 +7,7 @@ WorkingDirectory= WorkingDirectory=/run/openvpn ExecStart= ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid +ExecReload=/bin/kill -HUP $MAINPID User=openvpn Group=openvpn AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE diff --git a/src/etc/systemd/system/uacctd.service.d/override.conf b/src/etc/systemd/system/uacctd.service.d/override.conf new file mode 100644 index 000000000..38bcce515 --- /dev/null +++ b/src/etc/systemd/system/uacctd.service.d/override.conf @@ -0,0 +1,14 @@ +[Unit] +After= +After=vyos-router.service +ConditionPathExists= +ConditionPathExists=/run/pmacct/uacctd.conf + +[Service] +EnvironmentFile= +ExecStart= +ExecStart=/usr/sbin/uacctd -f /run/pmacct/uacctd.conf +WorkingDirectory= +WorkingDirectory=/run/pmacct +PIDFile= +PIDFile=/run/pmacct/uacctd.pid diff --git a/src/etc/udev/rules.d/62-temporary-interface-rename.rules b/src/etc/udev/rules.d/62-temporary-interface-rename.rules new file mode 100644 index 000000000..4a579dcab --- /dev/null +++ b/src/etc/udev/rules.d/62-temporary-interface-rename.rules @@ -0,0 +1 @@ +SUBSYSTEM=="net", ACTION=="add", KERNEL=="eth*", DRIVERS=="?*", NAME="e$env{IFINDEX}" diff --git a/src/etc/udev/rules.d/65-vyatta-net.rules b/src/etc/udev/rules.d/65-vyatta-net.rules deleted file mode 100644 index 2b48c1213..000000000 --- a/src/etc/udev/rules.d/65-vyatta-net.rules +++ /dev/null @@ -1,26 +0,0 @@ -# These rules use vyatta_net_name to persistently name network interfaces -# per "hwid" association in the Vyatta configuration file. - -ACTION!="add", GOTO="vyatta_net_end" -SUBSYSTEM!="net", GOTO="vyatta_net_end" - -# ignore the interface if a name has already been set -NAME=="?*", GOTO="vyatta_net_end" - -# Do name change for ethernet and wireless devices only -KERNEL!="eth*|wlan*", GOTO="vyatta_net_end" - -# ignore "secondary" monitor interfaces of mac80211 drivers -KERNEL=="wlan*", ATTRS{type}=="803", GOTO="vyatta_net_end" - -# If using VyOS predefined names -ENV{VYOS_IFNAME}!="eth*", GOTO="end_vyos_predef_names" - -DRIVERS=="?*", PROGRAM="vyatta_net_name %k $attr{address} $env{VYOS_IFNAME}", NAME="%c", GOTO="vyatta_net_end" - -LABEL="end_vyos_predef_names" - -# ignore interfaces without a driver link like bridges and VLANs -DRIVERS=="?*", PROGRAM="vyatta_net_name %k $attr{address}", NAME="%c" - -LABEL="vyatta_net_end" diff --git a/src/etc/udev/rules.d/65-vyos-net.rules b/src/etc/udev/rules.d/65-vyos-net.rules new file mode 100644 index 000000000..32ae352de --- /dev/null +++ b/src/etc/udev/rules.d/65-vyos-net.rules @@ -0,0 +1,23 @@ +# These rules use vyos_net_name to persistently name network interfaces +# per "hwid" association in the VyOS configuration file. + +ACTION!="add", GOTO="vyos_net_end" +SUBSYSTEM!="net", GOTO="vyos_net_end" + +# Do name change for ethernet and wireless devices only +KERNEL!="eth*|wlan*|e*", GOTO="vyos_net_end" + +# ignore "secondary" monitor interfaces of mac80211 drivers +KERNEL=="wlan*", ATTRS{type}=="803", GOTO="vyos_net_end" + +# If using VyOS predefined names +ENV{VYOS_IFNAME}!="eth*", GOTO="end_vyos_predef_names" + +DRIVERS=="?*", PROGRAM="vyos_net_name %k $attr{address} $env{VYOS_IFNAME}", NAME="%c", GOTO="vyos_net_end" + +LABEL="end_vyos_predef_names" + +# ignore interfaces without a driver link like bridges and VLANs +DRIVERS=="?*", PROGRAM="vyos_net_name %k $attr{address}", NAME="%c" + +LABEL="vyos_net_end" diff --git a/src/etc/udev/rules.d/90-vyos-serial.rules b/src/etc/udev/rules.d/90-vyos-serial.rules index 872fd4fea..30c1d3170 100644 --- a/src/etc/udev/rules.d/90-vyos-serial.rules +++ b/src/etc/udev/rules.d/90-vyos-serial.rules @@ -22,7 +22,7 @@ IMPORT{builtin}="path_id", IMPORT{builtin}="usb_id" # (tr -d -) does the replacement # - Replace the first group after ":" to represent the bus relation (sed -e 0,/:/s//b/) indicated by "b" # - Replace the next group after ":" to represent the port relation (sed -e 0,/:/s//p/) indicated by "p" -ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", PROGRAM="/bin/sh -c 'echo $env{ID_PATH:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result" -ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", PROGRAM="/bin/sh -c 'echo $env{ID_PATH:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result" +ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", PROGRAM="/bin/sh -c 'echo $env{ID_PATH} | cut -d- -f3- | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result" +ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", PROGRAM="/bin/sh -c 'echo $env{ID_PATH} | cut -d- -f3- | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result" LABEL="serial_end" diff --git a/src/helpers/strip-private.py b/src/helpers/strip-private.py index c74a379aa..e4e1fe11d 100755 --- a/src/helpers/strip-private.py +++ b/src/helpers/strip-private.py @@ -106,6 +106,7 @@ if __name__ == "__main__": stripping_rules = [ # Strip passwords (True, re.compile(r'password \S+'), 'password xxxxxx'), + (True, re.compile(r'cisco-authentication \S+'), 'cisco-authentication xxxxxx'), # Strip public key information (True, re.compile(r'public-keys \S+'), 'public-keys xxxx@xxx.xxx'), (True, re.compile(r'type \'ssh-(rsa|dss)\''), 'type ssh-xxx'), diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py index c5bf22f10..b9cc87bfa 100755 --- a/src/helpers/vyos-boot-config-loader.py +++ b/src/helpers/vyos-boot-config-loader.py @@ -23,12 +23,12 @@ import grp import traceback from datetime import datetime -from vyos.defaults import directories +from vyos.defaults import directories, config_status from vyos.configsession import ConfigSession, ConfigSessionError from vyos.configtree import ConfigTree from vyos.util import cmd -STATUS_FILE = '/tmp/vyos-config-status' +STATUS_FILE = config_status TRACE_FILE = '/tmp/boot-config-trace' CFG_GROUP = 'vyattacfg' diff --git a/src/helpers/vyos-check-wwan.py b/src/helpers/vyos-check-wwan.py new file mode 100755 index 000000000..2ff9a574f --- /dev/null +++ b/src/helpers/vyos-check-wwan.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +from vyos.configquery import VbashOpRun +from vyos.configquery import ConfigTreeQuery + +from vyos.util import is_wwan_connected + +conf = ConfigTreeQuery() +dict = conf.get_config_dict(['interfaces', 'wwan'], key_mangling=('-', '_'), + get_first_key=True) + +for interface, interface_config in dict.items(): + if not is_wwan_connected(interface): + if 'disable' in interface_config: + # do not restart this interface as it's disabled by the user + continue + + op = VbashOpRun() + op.run(['connect', 'interface', interface]) + +exit(0) diff --git a/src/helpers/vyos-interface-rescan.py b/src/helpers/vyos-interface-rescan.py new file mode 100755 index 000000000..1ac1810e0 --- /dev/null +++ b/src/helpers/vyos-interface-rescan.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 stat +import argparse +import logging +import netaddr + +from vyos.configtree import ConfigTree +from vyos.defaults import directories +from vyos.util import get_cfg_group_id + +debug = False + +vyos_udev_dir = directories['vyos_udev_dir'] +vyos_log_dir = directories['log'] +log_file = os.path.splitext(os.path.basename(__file__))[0] +vyos_log_file = os.path.join(vyos_log_dir, log_file) + +logger = logging.getLogger(__name__) +handler = logging.FileHandler(vyos_log_file, mode='a') +formatter = logging.Formatter('%(levelname)s: %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) + +passlist = { + '02:07:01' : 'Interlan', + '02:60:60' : '3Com', + '02:60:8c' : '3Com', + '02:a0:c9' : 'Intel', + '02:aa:3c' : 'Olivetti', + '02:cf:1f' : 'CMC', + '02:e0:3b' : 'Prominet', + '02:e6:d3' : 'BTI', + '52:54:00' : 'Realtek', + '52:54:4c' : 'Novell 2000', + '52:54:ab' : 'Realtec', + 'e2:0c:0f' : 'Kingston Technologies' +} + +def is_multicast(addr: netaddr.eui.EUI) -> bool: + return bool(addr.words[0] & 0b1) + +def is_locally_administered(addr: netaddr.eui.EUI) -> bool: + return bool(addr.words[0] & 0b10) + +def is_on_passlist(hwid: str) -> bool: + top = hwid.rsplit(':', 3)[0] + if top in list(passlist): + return True + return False + +def is_persistent(hwid: str) -> bool: + addr = netaddr.EUI(hwid) + if is_multicast(addr): + return False + if is_locally_administered(addr) and not is_on_passlist(hwid): + return False + return True + +def get_wireless_physical_device(intf: str) -> str: + if 'wlan' not in intf: + return '' + try: + tmp = os.readlink(f'/sys/class/net/{intf}/phy80211') + except OSError: + logger.critical(f"Failed to read '/sys/class/net/{intf}/phy80211'") + return '' + phy = os.path.basename(tmp) + logger.info(f"wireless phy is {phy}") + return phy + +def get_interface_type(intf: str) -> str: + if 'eth' in intf: + intf_type = 'ethernet' + elif 'wlan' in intf: + intf_type = 'wireless' + else: + logger.critical('Unrecognized interface type!') + intf_type = '' + return intf_type + +def get_new_interfaces() -> dict: + """ Read any new interface data left in /run/udev/vyos by vyos_net_name + """ + interfaces = {} + + for intf in os.listdir(vyos_udev_dir): + path = os.path.join(vyos_udev_dir, intf) + try: + with open(path) as f: + hwid = f.read().rstrip() + except OSError as e: + logger.error(f"OSError {e}") + continue + interfaces[intf] = hwid + + # reverse sort to simplify insertion in config + interfaces = {key: value for key, value in sorted(interfaces.items(), + reverse=True)} + return interfaces + +def filter_interfaces(intfs: dict) -> dict: + """ Ignore no longer existing interfaces or non-persistent mac addresses + """ + filtered = {} + + for intf, hwid in intfs.items(): + if not os.path.isdir(os.path.join('/sys/class/net', intf)): + continue + if not is_persistent(hwid): + continue + filtered[intf] = hwid + + return filtered + +def interface_rescan(config_path: str): + """ Read new data and update config file + """ + interfaces = get_new_interfaces() + + logger.debug(f"interfaces from udev: {interfaces}") + + interfaces = filter_interfaces(interfaces) + + logger.debug(f"filtered interfaces: {interfaces}") + + try: + with open(config_path) as f: + config_file = f.read() + except OSError as e: + logger.critical(f"OSError {e}") + exit(1) + + config = ConfigTree(config_file) + + for intf, hwid in interfaces.items(): + logger.info(f"Writing '{intf}' '{hwid}' to config file") + intf_type = get_interface_type(intf) + if not intf_type: + continue + if not config.exists(['interfaces', intf_type]): + config.set(['interfaces', intf_type]) + config.set_tag(['interfaces', intf_type]) + config.set(['interfaces', intf_type, intf, 'hw-id'], value=hwid) + + if intf_type == 'wireless': + phy = get_wireless_physical_device(intf) + if not phy: + continue + config.set(['interfaces', intf_type, intf, 'physical-device'], + value=phy) + + try: + with open(config_path, 'w') as f: + f.write(config.to_string()) + except OSError as e: + logger.critical(f"OSError {e}") + +def main(): + global debug + + argparser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter) + argparser.add_argument('configfile', type=str) + argparser.add_argument('--debug', action='store_true') + args = argparser.parse_args() + + if args.debug: + debug = True + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + configfile = args.configfile + + # preserve vyattacfg group write access to running config + os.setgid(get_cfg_group_id()) + os.umask(0o002) + + # log file perms are not automatic; this could be cleaner by moving to a + # logging config file + os.chown(vyos_log_file, 0, get_cfg_group_id()) + os.chmod(vyos_log_file, + stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH) + + interface_rescan(configfile) + +if __name__ == '__main__': + main() diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name new file mode 100755 index 000000000..afeef8f2d --- /dev/null +++ b/src/helpers/vyos_net_name @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 re +import time +import logging +import threading +from sys import argv + +from vyos.configtree import ConfigTree +from vyos.defaults import directories +from vyos.util import cmd, boot_configuration_complete + +vyos_udev_dir = directories['vyos_udev_dir'] +vyos_log_dir = '/run/udev/log' +vyos_log_file = os.path.join(vyos_log_dir, 'vyos-net-name') + +config_path = '/opt/vyatta/etc/config/config.boot' + +lock = threading.Lock() + +try: + os.mkdir(vyos_log_dir) +except FileExistsError: + pass + +logging.basicConfig(filename=vyos_log_file, level=logging.DEBUG) + +def is_available(intfs: dict, intf_name: str) -> bool: + """ Check if interface name is already assigned + """ + if intf_name in list(intfs.values()): + return False + return True + +def find_available(intfs: dict, prefix: str) -> str: + """ Find lowest indexed iterface name that is not assigned + """ + index_list = [int(x.replace(prefix, '')) for x in list(intfs.values()) if prefix in x] + index_list.sort() + # find 'holes' in list, if any + missing = sorted(set(range(index_list[0], index_list[-1])) - set(index_list)) + if missing: + return f'{prefix}{missing[0]}' + + return f'{prefix}{len(index_list)}' + +def mod_ifname(ifname: str) -> str: + """ Check interface with names eX and return ifname on the next format eth{ifindex} - 2 + """ + if re.match("^e[0-9]+$", ifname): + intf = ifname.split("e") + if intf[1]: + if int(intf[1]) >= 2: + return "eth" + str(int(intf[1]) - 2) + else: + return "eth" + str(intf[1]) + + return ifname + +def get_biosdevname(ifname: str) -> str: + """ Use legacy vyatta-biosdevname to query for name + + This is carried over for compatability only, and will likely be dropped + going forward. + XXX: This throws an error, and likely has for a long time, unnoticed + since vyatta_net_name redirected stderr to /dev/null. + """ + intf = mod_ifname(ifname) + + if 'eth' not in intf: + return intf + if os.path.isdir('/proc/xen'): + return intf + + time.sleep(1) + + try: + biosname = cmd(f'/sbin/biosdevname --policy all_ethN -i {ifname}') + except Exception as e: + logging.error(f'biosdevname error: {e}') + biosname = '' + + return intf if biosname == '' else biosname + +def leave_rescan_hint(intf_name: str, hwid: str): + """Write interface information reported by udev + + This script is called while the root mount is still read-only. Leave + information in /run/udev: file name, the interface; contents, the + hardware id. + """ + try: + os.mkdir(vyos_udev_dir) + except FileExistsError: + pass + except Exception as e: + logging.critical(f"Error creating rescan hint directory: {e}") + exit(1) + + try: + with open(os.path.join(vyos_udev_dir, intf_name), 'w') as f: + f.write(hwid) + except OSError as e: + logging.critical(f"OSError {e}") + +def get_configfile_interfaces() -> dict: + """Read existing interfaces from config file + """ + interfaces: dict = {} + + if not os.path.isfile(config_path): + # If the case, then we are running off of livecd; return empty + return interfaces + + try: + with open(config_path) as f: + config_file = f.read() + except OSError as e: + logging.critical(f"OSError {e}") + exit(1) + + try: + config = ConfigTree(config_file) + except Exception: + logging.debug(f"updating component version string syntax") + try: + # this will update the component version string in place, for + # updates 1.2 --> 1.3/1.4 + os.system(f'/usr/libexec/vyos/run-config-migration.py {config_path} --virtual --set-vintage=vyos') + with open(config_path) as f: + config_file = f.read() + config = ConfigTree(config_file) + except Exception as e: + logging.critical(f"ConfigTree error: {e}") + + base = ['interfaces', 'ethernet'] + if config.exists(base): + eth_intfs = config.list_nodes(base) + for intf in eth_intfs: + path = base + [intf, 'hw-id'] + if not config.exists(path): + logging.warning(f"no 'hw-id' entry for {intf}") + continue + hwid = config.return_value(path) + if hwid in list(interfaces): + logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}") + continue + interfaces[hwid] = intf + + base = ['interfaces', 'wireless'] + if config.exists(base): + wlan_intfs = config.list_nodes(base) + for intf in wlan_intfs: + path = base + [intf, 'hw-id'] + if not config.exists(path): + logging.warning(f"no 'hw-id' entry for {intf}") + continue + hwid = config.return_value(path) + if hwid in list(interfaces): + logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}") + continue + interfaces[hwid] = intf + + logging.debug(f"config file entries: {interfaces}") + + return interfaces + +def add_assigned_interfaces(intfs: dict): + """Add interfaces found by previous invocation of udev rule + """ + if not os.path.isdir(vyos_udev_dir): + return + + for intf in os.listdir(vyos_udev_dir): + path = os.path.join(vyos_udev_dir, intf) + try: + with open(path) as f: + hwid = f.read().rstrip() + except OSError as e: + logging.error(f"OSError {e}") + continue + intfs[hwid] = intf + +def on_boot_event(intf_name: str, hwid: str, predefined: str = '') -> str: + """Called on boot by vyos-router: 'coldplug' in vyatta_net_name + """ + logging.info(f"lookup {intf_name}, {hwid}") + interfaces = get_configfile_interfaces() + logging.debug(f"config file interfaces are {interfaces}") + + if hwid in list(interfaces): + logging.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'") + return interfaces[hwid] + + add_assigned_interfaces(interfaces) + logging.debug(f"adding assigned interfaces: {interfaces}") + + if predefined: + newname = predefined + logging.info(f"predefined interface name for '{intf_name}' is '{newname}'") + else: + newname = get_biosdevname(intf_name) + logging.info(f"biosdevname returned '{newname}' for '{intf_name}'") + + if not is_available(interfaces, newname): + prefix = re.sub(r'\d+$', '', newname) + newname = find_available(interfaces, prefix) + + logging.info(f"new name for '{intf_name}' is '{newname}'") + + leave_rescan_hint(newname, hwid) + + return newname + +def hotplug_event(): + # Not yet implemented, since interface-rescan will only be run on boot. + pass + +if len(argv) > 3: + predef_name = argv[3] +else: + predef_name = '' + +lock.acquire() +if not boot_configuration_complete(): + res = on_boot_event(argv[1], argv[2], predefined=predef_name) + logging.debug(f"on boot, returned name is {res}") + print(res) +else: + logging.debug("boot configuration complete") +lock.release() + diff --git a/src/migration-scripts/bgp/1-to-2 b/src/migration-scripts/bgp/1-to-2 new file mode 100755 index 000000000..4c6d5ceb8 --- /dev/null +++ b/src/migration-scripts/bgp/1-to-2 @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# T3741: no-ipv4-unicast is now enabled by default + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.template import is_ipv4 + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['protocols', 'bgp'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +# This is now a default option - simply delete it. +# As it was configured explicitly - we can also bail out early as we need to +# do nothing! +if config.exists(base + ['parameters', 'default', 'no-ipv4-unicast']): + config.delete(base + ['parameters', 'default', 'no-ipv4-unicast']) + + # Check if the "default" node is now empty, if so - remove it + if len(config.list_nodes(base + ['parameters', 'default'])) == 0: + config.delete(base + ['parameters', 'default']) + + # Check if the "default" node is now empty, if so - remove it + if len(config.list_nodes(base + ['parameters'])) == 0: + config.delete(base + ['parameters']) + + exit(0) + +# As we now install a new default option into BGP we need to migrate all +# existing BGP neighbors and restore the old behavior +if config.exists(base + ['neighbor']): + for neighbor in config.list_nodes(base + ['neighbor']): + peer_group = base + ['neighbor', neighbor, 'peer-group'] + if config.exists(peer_group): + peer_group_name = config.return_value(peer_group) + # peer group enables old behavior for neighbor - bail out + if config.exists(base + ['peer-group', peer_group_name, 'address-family', 'ipv4-unicast']): + continue + + afi_ipv4 = base + ['neighbor', neighbor, 'address-family', 'ipv4-unicast'] + if not config.exists(afi_ipv4): + config.set(afi_ipv4) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2 index 8c4f4b5c7..ba10c26f2 100755 --- a/src/migration-scripts/dns-forwarding/1-to-2 +++ b/src/migration-scripts/dns-forwarding/1-to-2 @@ -67,8 +67,14 @@ if config.exists(base + ['listen-on']): # retrieve corresponding interface addresses in CIDR format # those need to be converted in pure IP addresses without network information path = ['interfaces', section, intf, 'address'] - for addr in config.return_values(path): - listen_addr.append( ip_interface(addr).ip ) + try: + for addr in config.return_values(path): + listen_addr.append( ip_interface(addr).ip ) + except: + # Some interface types do not use "address" option (e.g. OpenVPN) + # and may not even have a fixed address + print("Could not retrieve the address of the interface {} from the config".format(intf)) + print("You will need to update your DNS forwarding configuration manually") for addr in listen_addr: config.set(base + ['listen-address'], value=addr, replace=False) diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7 new file mode 100755 index 000000000..4a4097d56 --- /dev/null +++ b/src/migration-scripts/firewall/6-to-7 @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# T2199: Remove unavailable nodes due to XML/Python implementation using nftables +# monthdays: nftables does not have a monthdays equivalent +# utc: nftables userspace uses localtime and calculates the UTC offset automatically + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.ifconfig import Section + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['firewall'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['name']): + for name in config.list_nodes(base + ['name']): + if config.exists(base + ['name', name, 'rule']): + for rule in config.list_nodes(base + ['name', name, 'rule']): + rule_time = base + ['name', name, 'rule', rule, 'time'] + + if config.exists(rule_time + ['monthdays']): + config.delete(rule_time + ['monthdays']) + + if config.exists(rule_time + ['utc']): + config.delete(rule_time + ['utc']) + +if config.exists(base + ['ipv6-name']): + for name in config.list_nodes(base + ['ipv6-name']): + if config.exists(base + ['ipv6-name', name, 'rule']): + for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): + rule_time = base + ['ipv6-name', name, 'rule', rule, 'time'] + + if config.exists(rule_time + ['monthdays']): + config.delete(rule_time + ['monthdays']) + + if config.exists(rule_time + ['utc']): + config.delete(rule_time + ['utc']) + +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/flow-accounting/0-to-1 b/src/migration-scripts/flow-accounting/0-to-1 new file mode 100755 index 000000000..72cce77b0 --- /dev/null +++ b/src/migration-scripts/flow-accounting/0-to-1 @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# T4099: flow-accounting: sync "source-ip" and "source-address" between netflow +# and sflow ion CLI +# T4105: flow-accounting: drop "sflow agent-address auto" + +from sys import argv +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['system', 'flow-accounting'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +# T4099 +tmp = base + ['netflow', 'source-ip'] +if config.exists(tmp): + config.rename(tmp, 'source-address') + +# T4105 +tmp = base + ['sflow', 'agent-address'] +if config.exists(tmp): + value = config.return_value(tmp) + if value == 'auto': + # delete the "auto" + config.delete(tmp) + + # 1) check if BGP router-id is set + # 2) check if OSPF router-id is set + # 3) check if OSPFv3 router-id is set + router_id = None + for protocol in ['bgp', 'ospf', 'ospfv3']: + if config.exists(['protocols', protocol, 'parameters', 'router-id']): + router_id = config.return_value(['protocols', protocol, 'parameters', 'router-id']) + break + if router_id: + config.set(tmp, value=router_id) + +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/interfaces/21-to-22 b/src/migration-scripts/interfaces/21-to-22 index 06e07572f..098102102 100755 --- a/src/migration-scripts/interfaces/21-to-22 +++ b/src/migration-scripts/interfaces/21-to-22 @@ -15,131 +15,32 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from sys import argv -from sys import exit from vyos.configtree import ConfigTree -def migrate_ospf(config, path, interface): - path = path + ['ospf'] - if config.exists(path): - new_base = ['protocols', 'ospf', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) +if (len(argv) < 1): + print("Must specify file name!") + exit(1) - # if "ip ospf" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() -def migrate_ospfv3(config, path, interface): - path = path + ['ospfv3'] - if config.exists(path): - new_base = ['protocols', 'ospfv3', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) +config = ConfigTree(config_file) +base = ['interfaces', 'tunnel'] - # if "ipv6 ospfv3" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) +if not config.exists(base): + exit(0) -def migrate_rip(config, path, interface): - path = path + ['rip'] +for interface in config.list_nodes(base): + path = base + [interface, 'dhcp-interface'] if config.exists(path): - new_base = ['protocols', 'rip', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) + tmp = config.return_value(path) config.delete(path) - - # if "ip rip" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - -def migrate_ripng(config, path, interface): - path = path + ['ripng'] - if config.exists(path): - new_base = ['protocols', 'ripng', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ipv6 ripng" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - -if __name__ == '__main__': - if (len(argv) < 1): - print("Must specify file name!") - exit(1) - - file_name = argv[1] - with open(file_name, 'r') as f: - config_file = f.read() - - config = ConfigTree(config_file) - - # - # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0" - # - for type in config.list_nodes(['interfaces']): - for interface in config.list_nodes(['interfaces', type]): - ip_base = ['interfaces', type, interface, 'ip'] - ipv6_base = ['interfaces', type, interface, 'ipv6'] - migrate_rip(config, ip_base, interface) - migrate_ripng(config, ipv6_base, interface) - migrate_ospf(config, ip_base, interface) - migrate_ospfv3(config, ipv6_base, interface) - - vif_path = ['interfaces', type, interface, 'vif'] - if config.exists(vif_path): - for vif in config.list_nodes(vif_path): - vif_ip_base = vif_path + [vif, 'ip'] - vif_ipv6_base = vif_path + [vif, 'ipv6'] - ifname = f'{interface}.{vif}' - - migrate_rip(config, vif_ip_base, ifname) - migrate_ripng(config, vif_ipv6_base, ifname) - migrate_ospf(config, vif_ip_base, ifname) - migrate_ospfv3(config, vif_ipv6_base, ifname) - - - vif_s_path = ['interfaces', type, interface, 'vif-s'] - if config.exists(vif_s_path): - for vif_s in config.list_nodes(vif_s_path): - vif_s_ip_base = vif_s_path + [vif_s, 'ip'] - vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6'] - - # vif-c interfaces MUST be migrated before their parent vif-s - # interface as the migrate_*() functions delete the path! - vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] - if config.exists(vif_c_path): - for vif_c in config.list_nodes(vif_c_path): - vif_c_ip_base = vif_c_path + [vif_c, 'ip'] - vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6'] - ifname = f'{interface}.{vif_s}.{vif_c}' - - migrate_rip(config, vif_c_ip_base, ifname) - migrate_ripng(config, vif_c_ipv6_base, ifname) - migrate_ospf(config, vif_c_ip_base, ifname) - migrate_ospfv3(config, vif_c_ipv6_base, ifname) - - - ifname = f'{interface}.{vif_s}' - migrate_rip(config, vif_s_ip_base, ifname) - migrate_ripng(config, vif_s_ipv6_base, ifname) - migrate_ospf(config, vif_s_ip_base, ifname) - migrate_ospfv3(config, vif_s_ipv6_base, ifname) - - 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) + config.set(base + [interface, 'source-interface'], value=tmp) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/interfaces/22-to-23 b/src/migration-scripts/interfaces/22-to-23 index d1ec2ad3e..06e07572f 100755 --- a/src/migration-scripts/interfaces/22-to-23 +++ b/src/migration-scripts/interfaces/22-to-23 @@ -14,47 +14,132 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported -# having a VTI interface in the CLI but no IPSec configuration - drop VTI -# configuration if this is the case for VyOS 1.4 - -import sys +from sys import argv +from sys import exit from vyos.configtree import ConfigTree +def migrate_ospf(config, path, interface): + path = path + ['ospf'] + if config.exists(path): + new_base = ['protocols', 'ospf', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ip ospf" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_ospfv3(config, path, interface): + path = path + ['ospfv3'] + if config.exists(path): + new_base = ['protocols', 'ospfv3', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ipv6 ospfv3" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_rip(config, path, interface): + path = path + ['rip'] + if config.exists(path): + new_base = ['protocols', 'rip', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ip rip" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_ripng(config, path, interface): + path = path + ['ripng'] + if config.exists(path): + new_base = ['protocols', 'ripng', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ipv6 ripng" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + if __name__ == '__main__': - if (len(sys.argv) < 1): + if (len(argv) < 1): print("Must specify file name!") - sys.exit(1) - - file_name = sys.argv[1] + exit(1) + file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) - base = ['interfaces', 'vti'] - if not config.exists(base): - # Nothing to do - sys.exit(0) - - ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer'] - for interface in config.list_nodes(base): - found = False - if config.exists(ipsec_base): - for peer in config.list_nodes(ipsec_base): - if config.exists(ipsec_base + [peer, 'vti', 'bind']): - tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind']) - if tmp == interface: - # Interface was found and we no longer need to search - # for it in our IPSec peers - found = True - break - if not found: - config.delete(base + [interface]) + + # + # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0" + # + for type in config.list_nodes(['interfaces']): + for interface in config.list_nodes(['interfaces', type]): + ip_base = ['interfaces', type, interface, 'ip'] + ipv6_base = ['interfaces', type, interface, 'ipv6'] + migrate_rip(config, ip_base, interface) + migrate_ripng(config, ipv6_base, interface) + migrate_ospf(config, ip_base, interface) + migrate_ospfv3(config, ipv6_base, interface) + + vif_path = ['interfaces', type, interface, 'vif'] + if config.exists(vif_path): + for vif in config.list_nodes(vif_path): + vif_ip_base = vif_path + [vif, 'ip'] + vif_ipv6_base = vif_path + [vif, 'ipv6'] + ifname = f'{interface}.{vif}' + + migrate_rip(config, vif_ip_base, ifname) + migrate_ripng(config, vif_ipv6_base, ifname) + migrate_ospf(config, vif_ip_base, ifname) + migrate_ospfv3(config, vif_ipv6_base, ifname) + + + vif_s_path = ['interfaces', type, interface, 'vif-s'] + if config.exists(vif_s_path): + for vif_s in config.list_nodes(vif_s_path): + vif_s_ip_base = vif_s_path + [vif_s, 'ip'] + vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6'] + + # vif-c interfaces MUST be migrated before their parent vif-s + # interface as the migrate_*() functions delete the path! + vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] + if config.exists(vif_c_path): + for vif_c in config.list_nodes(vif_c_path): + vif_c_ip_base = vif_c_path + [vif_c, 'ip'] + vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6'] + ifname = f'{interface}.{vif_s}.{vif_c}' + + migrate_rip(config, vif_c_ip_base, ifname) + migrate_ripng(config, vif_c_ipv6_base, ifname) + migrate_ospf(config, vif_c_ip_base, ifname) + migrate_ospfv3(config, vif_c_ipv6_base, ifname) + + + ifname = f'{interface}.{vif_s}' + migrate_rip(config, vif_s_ip_base, ifname) + migrate_ripng(config, vif_s_ipv6_base, ifname) + migrate_ospf(config, vif_s_ip_base, ifname) + migrate_ospfv3(config, vif_s_ipv6_base, ifname) 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)) - sys.exit(1) + exit(1) diff --git a/src/migration-scripts/interfaces/23-to-24 b/src/migration-scripts/interfaces/23-to-24 index 93ce9215f..d1ec2ad3e 100755 --- a/src/migration-scripts/interfaces/23-to-24 +++ b/src/migration-scripts/interfaces/23-to-24 @@ -14,356 +14,47 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# Migrate Wireguard to store keys in CLI -# Migrate EAPoL to PKI configuration +# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported +# having a VTI interface in the CLI but no IPSec configuration - drop VTI +# configuration if this is the case for VyOS 1.4 -import os import sys from vyos.configtree import ConfigTree -from vyos.pki import load_certificate -from vyos.pki import load_crl -from vyos.pki import load_dh_parameters -from vyos.pki import load_private_key -from vyos.pki import encode_certificate -from vyos.pki import encode_dh_parameters -from vyos.pki import encode_private_key -from vyos.util import run -def wrapped_pem_to_config_value(pem): - out = [] - for line in pem.strip().split("\n"): - if not line or line.startswith("-----") or line[0] == '#': - continue - out.append(line) - return "".join(out) +if __name__ == '__main__': + if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) -def read_file_for_pki(config_auth_path): - full_path = os.path.join(AUTH_DIR, config_auth_path) - output = None + file_name = sys.argv[1] - if os.path.isfile(full_path): - if not os.access(full_path, os.R_OK): - run(f'sudo chmod 644 {full_path}') + with open(file_name, 'r') as f: + config_file = f.read() - with open(full_path, 'r') as f: - output = f.read() + config = ConfigTree(config_file) + base = ['interfaces', 'vti'] + if not config.exists(base): + # Nothing to do + sys.exit(0) - return output - -if (len(sys.argv) < 1): - print("Must specify file name!") - sys.exit(1) - -file_name = sys.argv[1] - -with open(file_name, 'r') as f: - config_file = f.read() - -config = ConfigTree(config_file) - -AUTH_DIR = '/config/auth' -pki_base = ['pki'] - -# OpenVPN -base = ['interfaces', 'openvpn'] - -if config.exists(base): - for interface in config.list_nodes(base): - x509_base = base + [interface, 'tls'] - pki_name = f'openvpn_{interface}' - - if config.exists(base + [interface, 'shared-secret-key-file']): - if not config.exists(pki_base + ['openvpn', 'shared-secret']): - config.set(pki_base + ['openvpn', 'shared-secret']) - config.set_tag(pki_base + ['openvpn', 'shared-secret']) - - key_file = config.return_value(base + [interface, 'shared-secret-key-file']) - key = read_file_for_pki(key_file) - key_pki_name = f'{pki_name}_shared' - - if key: - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') - config.set(base + [interface, 'shared-secret-key'], value=key_pki_name) - else: - print(f'Failed to migrate shared-secret-key on openvpn interface {interface}') - - config.delete(base + [interface, 'shared-secret-key-file']) - - if not config.exists(base + [interface, 'tls']): - continue - - if config.exists(base + [interface, 'tls', 'auth-file']): - if not config.exists(pki_base + ['openvpn', 'shared-secret']): - config.set(pki_base + ['openvpn', 'shared-secret']) - config.set_tag(pki_base + ['openvpn', 'shared-secret']) - - key_file = config.return_value(base + [interface, 'tls', 'auth-file']) - key = read_file_for_pki(key_file) - key_pki_name = f'{pki_name}_auth' - - if key: - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') - config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name) - else: - print(f'Failed to migrate auth-key on openvpn interface {interface}') - - config.delete(base + [interface, 'tls', 'auth-file']) - - if config.exists(base + [interface, 'tls', 'crypt-file']): - if not config.exists(pki_base + ['openvpn', 'shared-secret']): - config.set(pki_base + ['openvpn', 'shared-secret']) - config.set_tag(pki_base + ['openvpn', 'shared-secret']) - - key_file = config.return_value(base + [interface, 'tls', 'crypt-file']) - key = read_file_for_pki(key_file) - key_pki_name = f'{pki_name}_crypt' - - if key: - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') - config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name) - else: - print(f'Failed to migrate crypt-key on openvpn interface {interface}') - - config.delete(base + [interface, 'tls', 'crypt-file']) - - if config.exists(x509_base + ['ca-cert-file']): - if not config.exists(pki_base + ['ca']): - config.set(pki_base + ['ca']) - config.set_tag(pki_base + ['ca']) - - cert_file = config.return_value(x509_base + ['ca-cert-file']) - cert_path = os.path.join(AUTH_DIR, cert_file) - cert = None - - if os.path.isfile(cert_path): - if not os.access(cert_path, os.R_OK): - run(f'sudo chmod 644 {cert_path}') - - with open(cert_path, 'r') as f: - cert_data = f.read() - cert = load_certificate(cert_data, wrap_tags=False) - - if cert: - cert_pem = encode_certificate(cert) - config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['ca-certificate'], value=pki_name) - else: - print(f'Failed to migrate CA certificate on openvpn interface {interface}') - - config.delete(x509_base + ['ca-cert-file']) - - if config.exists(x509_base + ['crl-file']): - if not config.exists(pki_base + ['ca']): - config.set(pki_base + ['ca']) - config.set_tag(pki_base + ['ca']) - - crl_file = config.return_value(x509_base + ['crl-file']) - crl_path = os.path.join(AUTH_DIR, crl_file) - crl = None - - if os.path.isfile(crl_path): - if not os.access(crl_path, os.R_OK): - run(f'sudo chmod 644 {crl_path}') - - with open(crl_path, 'r') as f: - crl_data = f.read() - crl = load_crl(crl_data, wrap_tags=False) - - if crl: - crl_pem = encode_certificate(crl) - config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) - else: - print(f'Failed to migrate CRL on openvpn interface {interface}') - - config.delete(x509_base + ['crl-file']) - - if config.exists(x509_base + ['cert-file']): - if not config.exists(pki_base + ['certificate']): - config.set(pki_base + ['certificate']) - config.set_tag(pki_base + ['certificate']) - - cert_file = config.return_value(x509_base + ['cert-file']) - cert_path = os.path.join(AUTH_DIR, cert_file) - cert = None - - if os.path.isfile(cert_path): - if not os.access(cert_path, os.R_OK): - run(f'sudo chmod 644 {cert_path}') - - with open(cert_path, 'r') as f: - cert_data = f.read() - cert = load_certificate(cert_data, wrap_tags=False) - - if cert: - cert_pem = encode_certificate(cert) - config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['certificate'], value=pki_name) - else: - print(f'Failed to migrate certificate on openvpn interface {interface}') - - config.delete(x509_base + ['cert-file']) - - if config.exists(x509_base + ['key-file']): - key_file = config.return_value(x509_base + ['key-file']) - key_path = os.path.join(AUTH_DIR, key_file) - key = None - - if os.path.isfile(key_path): - if not os.access(key_path, os.R_OK): - run(f'sudo chmod 644 {key_path}') - - with open(key_path, 'r') as f: - key_data = f.read() - key = load_private_key(key_data, passphrase=None, wrap_tags=False) - - if key: - key_pem = encode_private_key(key, passphrase=None) - config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) - else: - print(f'Failed to migrate private key on openvpn interface {interface}') - - config.delete(x509_base + ['key-file']) - - if config.exists(x509_base + ['dh-file']): - if not config.exists(pki_base + ['dh']): - config.set(pki_base + ['dh']) - config.set_tag(pki_base + ['dh']) - - dh_file = config.return_value(x509_base + ['dh-file']) - dh_path = os.path.join(AUTH_DIR, dh_file) - dh = None - - if os.path.isfile(dh_path): - if not os.access(dh_path, os.R_OK): - run(f'sudo chmod 644 {dh_path}') - - with open(dh_path, 'r') as f: - dh_data = f.read() - dh = load_dh_parameters(dh_data, wrap_tags=False) - - if dh: - dh_pem = encode_dh_parameters(dh) - config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem)) - config.set(x509_base + ['dh-params'], value=pki_name) - else: - print(f'Failed to migrate DH parameters on openvpn interface {interface}') - - config.delete(x509_base + ['dh-file']) - -# Wireguard -base = ['interfaces', 'wireguard'] - -if config.exists(base): - for interface in config.list_nodes(base): - private_key_path = base + [interface, 'private-key'] - - key_file = 'default' - if config.exists(private_key_path): - key_file = config.return_value(private_key_path) - - full_key_path = f'/config/auth/wireguard/{key_file}/private.key' - - if not os.path.exists(full_key_path): - print(f'Could not find wireguard private key for migration on interface "{interface}"') - continue - - with open(full_key_path, 'r') as f: - key_data = f.read().strip() - config.set(private_key_path, value=key_data) - - for peer in config.list_nodes(base + [interface, 'peer']): - config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key') - -# Ethernet EAPoL -base = ['interfaces', 'ethernet'] - -if config.exists(base): + ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer'] for interface in config.list_nodes(base): - if not config.exists(base + [interface, 'eapol']): - continue - - x509_base = base + [interface, 'eapol'] - pki_name = f'eapol_{interface}' - - if config.exists(x509_base + ['ca-cert-file']): - if not config.exists(pki_base + ['ca']): - config.set(pki_base + ['ca']) - config.set_tag(pki_base + ['ca']) - - cert_file = config.return_value(x509_base + ['ca-cert-file']) - cert_path = os.path.join(AUTH_DIR, cert_file) - cert = None - - if os.path.isfile(cert_path): - if not os.access(cert_path, os.R_OK): - run(f'sudo chmod 644 {cert_path}') - - with open(cert_path, 'r') as f: - cert_data = f.read() - cert = load_certificate(cert_data, wrap_tags=False) - - if cert: - cert_pem = encode_certificate(cert) - config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['ca-certificate'], value=pki_name) - else: - print(f'Failed to migrate CA certificate on eapol config for interface {interface}') - - config.delete(x509_base + ['ca-cert-file']) - - if config.exists(x509_base + ['cert-file']): - if not config.exists(pki_base + ['certificate']): - config.set(pki_base + ['certificate']) - config.set_tag(pki_base + ['certificate']) - - cert_file = config.return_value(x509_base + ['cert-file']) - cert_path = os.path.join(AUTH_DIR, cert_file) - cert = None - - if os.path.isfile(cert_path): - if not os.access(cert_path, os.R_OK): - run(f'sudo chmod 644 {cert_path}') - - with open(cert_path, 'r') as f: - cert_data = f.read() - cert = load_certificate(cert_data, wrap_tags=False) - - if cert: - cert_pem = encode_certificate(cert) - config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['certificate'], value=pki_name) - else: - print(f'Failed to migrate certificate on eapol config for interface {interface}') - - config.delete(x509_base + ['cert-file']) - - if config.exists(x509_base + ['key-file']): - key_file = config.return_value(x509_base + ['key-file']) - key_path = os.path.join(AUTH_DIR, key_file) - key = None - - if os.path.isfile(key_path): - if not os.access(key_path, os.R_OK): - run(f'sudo chmod 644 {key_path}') - - with open(key_path, 'r') as f: - key_data = f.read() - key = load_private_key(key_data, passphrase=None, wrap_tags=False) - - if key: - key_pem = encode_private_key(key, passphrase=None) - config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) - else: - print(f'Failed to migrate private key on eapol config for interface {interface}') - - config.delete(x509_base + ['key-file']) - -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)) - sys.exit(1) + found = False + if config.exists(ipsec_base): + for peer in config.list_nodes(ipsec_base): + if config.exists(ipsec_base + [peer, 'vti', 'bind']): + tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind']) + if tmp == interface: + # Interface was found and we no longer need to search + # for it in our IPSec peers + found = True + break + if not found: + config.delete(base + [interface]) + + 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)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/24-to-25 b/src/migration-scripts/interfaces/24-to-25 new file mode 100755 index 000000000..93ce9215f --- /dev/null +++ b/src/migration-scripts/interfaces/24-to-25 @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# Migrate Wireguard to store keys in CLI +# Migrate EAPoL to PKI configuration + +import os +import sys +from vyos.configtree import ConfigTree +from vyos.pki import load_certificate +from vyos.pki import load_crl +from vyos.pki import load_dh_parameters +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_dh_parameters +from vyos.pki import encode_private_key +from vyos.util import run + +def wrapped_pem_to_config_value(pem): + out = [] + for line in pem.strip().split("\n"): + if not line or line.startswith("-----") or line[0] == '#': + continue + out.append(line) + return "".join(out) + +def read_file_for_pki(config_auth_path): + full_path = os.path.join(AUTH_DIR, config_auth_path) + output = None + + if os.path.isfile(full_path): + if not os.access(full_path, os.R_OK): + run(f'sudo chmod 644 {full_path}') + + with open(full_path, 'r') as f: + output = f.read() + + return output + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +AUTH_DIR = '/config/auth' +pki_base = ['pki'] + +# OpenVPN +base = ['interfaces', 'openvpn'] + +if config.exists(base): + for interface in config.list_nodes(base): + x509_base = base + [interface, 'tls'] + pki_name = f'openvpn_{interface}' + + if config.exists(base + [interface, 'shared-secret-key-file']): + if not config.exists(pki_base + ['openvpn', 'shared-secret']): + config.set(pki_base + ['openvpn', 'shared-secret']) + config.set_tag(pki_base + ['openvpn', 'shared-secret']) + + key_file = config.return_value(base + [interface, 'shared-secret-key-file']) + key = read_file_for_pki(key_file) + key_pki_name = f'{pki_name}_shared' + + if key: + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') + config.set(base + [interface, 'shared-secret-key'], value=key_pki_name) + else: + print(f'Failed to migrate shared-secret-key on openvpn interface {interface}') + + config.delete(base + [interface, 'shared-secret-key-file']) + + if not config.exists(base + [interface, 'tls']): + continue + + if config.exists(base + [interface, 'tls', 'auth-file']): + if not config.exists(pki_base + ['openvpn', 'shared-secret']): + config.set(pki_base + ['openvpn', 'shared-secret']) + config.set_tag(pki_base + ['openvpn', 'shared-secret']) + + key_file = config.return_value(base + [interface, 'tls', 'auth-file']) + key = read_file_for_pki(key_file) + key_pki_name = f'{pki_name}_auth' + + if key: + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') + config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name) + else: + print(f'Failed to migrate auth-key on openvpn interface {interface}') + + config.delete(base + [interface, 'tls', 'auth-file']) + + if config.exists(base + [interface, 'tls', 'crypt-file']): + if not config.exists(pki_base + ['openvpn', 'shared-secret']): + config.set(pki_base + ['openvpn', 'shared-secret']) + config.set_tag(pki_base + ['openvpn', 'shared-secret']) + + key_file = config.return_value(base + [interface, 'tls', 'crypt-file']) + key = read_file_for_pki(key_file) + key_pki_name = f'{pki_name}_crypt' + + if key: + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') + config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name) + else: + print(f'Failed to migrate crypt-key on openvpn interface {interface}') + + config.delete(base + [interface, 'tls', 'crypt-file']) + + if config.exists(x509_base + ['ca-cert-file']): + if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on openvpn interface {interface}') + + config.delete(x509_base + ['ca-cert-file']) + + if config.exists(x509_base + ['crl-file']): + if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + crl_file = config.return_value(x509_base + ['crl-file']) + crl_path = os.path.join(AUTH_DIR, crl_file) + crl = None + + if os.path.isfile(crl_path): + if not os.access(crl_path, os.R_OK): + run(f'sudo chmod 644 {crl_path}') + + with open(crl_path, 'r') as f: + crl_data = f.read() + crl = load_crl(crl_data, wrap_tags=False) + + if crl: + crl_pem = encode_certificate(crl) + config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) + else: + print(f'Failed to migrate CRL on openvpn interface {interface}') + + config.delete(x509_base + ['crl-file']) + + if config.exists(x509_base + ['cert-file']): + if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + + cert_file = config.return_value(x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on openvpn interface {interface}') + + config.delete(x509_base + ['cert-file']) + + if config.exists(x509_base + ['key-file']): + key_file = config.return_value(x509_base + ['key-file']) + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=None, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=None) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + else: + print(f'Failed to migrate private key on openvpn interface {interface}') + + config.delete(x509_base + ['key-file']) + + if config.exists(x509_base + ['dh-file']): + if not config.exists(pki_base + ['dh']): + config.set(pki_base + ['dh']) + config.set_tag(pki_base + ['dh']) + + dh_file = config.return_value(x509_base + ['dh-file']) + dh_path = os.path.join(AUTH_DIR, dh_file) + dh = None + + if os.path.isfile(dh_path): + if not os.access(dh_path, os.R_OK): + run(f'sudo chmod 644 {dh_path}') + + with open(dh_path, 'r') as f: + dh_data = f.read() + dh = load_dh_parameters(dh_data, wrap_tags=False) + + if dh: + dh_pem = encode_dh_parameters(dh) + config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem)) + config.set(x509_base + ['dh-params'], value=pki_name) + else: + print(f'Failed to migrate DH parameters on openvpn interface {interface}') + + config.delete(x509_base + ['dh-file']) + +# Wireguard +base = ['interfaces', 'wireguard'] + +if config.exists(base): + for interface in config.list_nodes(base): + private_key_path = base + [interface, 'private-key'] + + key_file = 'default' + if config.exists(private_key_path): + key_file = config.return_value(private_key_path) + + full_key_path = f'/config/auth/wireguard/{key_file}/private.key' + + if not os.path.exists(full_key_path): + print(f'Could not find wireguard private key for migration on interface "{interface}"') + continue + + with open(full_key_path, 'r') as f: + key_data = f.read().strip() + config.set(private_key_path, value=key_data) + + for peer in config.list_nodes(base + [interface, 'peer']): + config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key') + +# Ethernet EAPoL +base = ['interfaces', 'ethernet'] + +if config.exists(base): + for interface in config.list_nodes(base): + if not config.exists(base + [interface, 'eapol']): + continue + + x509_base = base + [interface, 'eapol'] + pki_name = f'eapol_{interface}' + + if config.exists(x509_base + ['ca-cert-file']): + if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on eapol config for interface {interface}') + + config.delete(x509_base + ['ca-cert-file']) + + if config.exists(x509_base + ['cert-file']): + if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + + cert_file = config.return_value(x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on eapol config for interface {interface}') + + config.delete(x509_base + ['cert-file']) + + if config.exists(x509_base + ['key-file']): + key_file = config.return_value(x509_base + ['key-file']) + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=None, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=None) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + else: + print(f'Failed to migrate private key on eapol config for interface {interface}') + + config.delete(x509_base + ['key-file']) + +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)) + sys.exit(1) diff --git a/src/migration-scripts/ospf/0-to-1 b/src/migration-scripts/ospf/0-to-1 new file mode 100755 index 000000000..678569d9e --- /dev/null +++ b/src/migration-scripts/ospf/0-to-1 @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# T3753: upgrade to FRR8 and move CLI options to better fit with the new FRR CLI + +from sys import argv +from vyos.configtree import ConfigTree + +def ospf_passive_migration(config, ospf_base): + if config.exists(ospf_base): + if config.exists(ospf_base + ['passive-interface']): + default = False + for interface in config.return_values(ospf_base + ['passive-interface']): + if interface == 'default': + default = True + continue + config.set(ospf_base + ['interface', interface, 'passive']) + + config.delete(ospf_base + ['passive-interface']) + config.set(ospf_base + ['passive-interface'], value='default') + + if config.exists(ospf_base + ['passive-interface-exclude']): + for interface in config.return_values(ospf_base + ['passive-interface-exclude']): + config.set(ospf_base + ['interface', interface, 'passive', 'disable']) + config.delete(ospf_base + ['passive-interface-exclude']) + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +ospfv3_base = ['protocols', 'ospfv3'] +if config.exists(ospfv3_base): + area_base = ospfv3_base + ['area'] + if config.exists(area_base): + for area in config.list_nodes(area_base): + if not config.exists(area_base + [area, 'interface']): + continue + + for interface in config.return_values(area_base + [area, 'interface']): + config.set(ospfv3_base + ['interface', interface, 'area'], value=area) + config.set_tag(ospfv3_base + ['interface']) + + config.delete(area_base + [area, 'interface']) + +# Migrate OSPF syntax in default VRF +ospf_base = ['protocols', 'ospf'] +ospf_passive_migration(config, ospf_base) + +vrf_base = ['vrf', 'name'] +if config.exists(vrf_base): + for vrf in config.list_nodes(vrf_base): + vrf_ospf_base = vrf_base + [vrf, 'protocols', 'ospf'] + if config.exists(vrf_ospf_base): + ospf_passive_migration(config, vrf_ospf_base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index a773aa28e..ffc574362 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2021 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 @@ -17,21 +17,19 @@ import os import argparse -from sys import exit from psutil import process_iter -from time import strftime, localtime, time from vyos.util import call +from vyos.util import DEVNULL +from vyos.util import is_wwan_connected -def check_interface(interface): +def check_ppp_interface(interface): if not os.path.isfile(f'/etc/ppp/peers/{interface}'): - print(f'Interface {interface}: invalid!') + print(f'Interface {interface} does not exist!') exit(1) def check_ppp_running(interface): - """ - Check if ppp process is running in the interface in question - """ + """ Check if PPP process is running in the interface in question """ for p in process_iter(): if "pppd" in p.name(): if interface in p.cmdline(): @@ -40,32 +38,46 @@ def check_ppp_running(interface): return False def connect(interface): - """ - Connect PPP interface - """ - check_interface(interface) + """ Connect dialer interface """ - # Check if interface is already dialed - if os.path.isdir(f'/sys/class/net/{interface}'): - print(f'Interface {interface}: already connected!') - elif check_ppp_running(interface): - print(f'Interface {interface}: connection is beeing established!') + if interface.startswith('ppp'): + check_ppp_interface(interface) + # Check if interface is already dialed + if os.path.isdir(f'/sys/class/net/{interface}'): + print(f'Interface {interface}: already connected!') + elif check_ppp_running(interface): + print(f'Interface {interface}: connection is beeing established!') + else: + print(f'Interface {interface}: connecting...') + call(f'systemctl restart ppp@{interface}.service') + elif interface.startswith('wwan'): + if is_wwan_connected(interface): + print(f'Interface {interface}: already connected!') + else: + call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py') else: - print(f'Interface {interface}: connecting...') - call(f'systemctl restart ppp@{interface}.service') + print(f'Unknown interface {interface}, can not connect. Aborting!') def disconnect(interface): - """ - Disconnect PPP interface - """ - check_interface(interface) + """ Disconnect dialer interface """ - # Check if interface is already down - if not check_ppp_running(interface): - print(f'Interface {interface}: connection is already down') + if interface.startswith('ppp'): + check_ppp_interface(interface) + + # Check if interface is already down + if not check_ppp_running(interface): + print(f'Interface {interface}: connection is already down') + else: + print(f'Interface {interface}: disconnecting...') + call(f'systemctl stop ppp@{interface}.service') + elif interface.startswith('wwan'): + if not is_wwan_connected(interface): + print(f'Interface {interface}: connection is already down') + else: + modem = interface.lstrip('wwan') + call(f'mmcli --modem {modem} --simple-disconnect', stdout=DEVNULL) else: - print(f'Interface {interface}: disconnecting...') - call(f'systemctl stop ppp@{interface}.service') + print(f'Unknown interface {interface}, can not disconnect. Aborting!') def main(): parser = argparse.ArgumentParser() diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py index 66ecf8439..89f6df4b9 100755 --- a/src/op_mode/conntrack_sync.py +++ b/src/op_mode/conntrack_sync.py @@ -20,12 +20,15 @@ import xmltodict from argparse import ArgumentParser from vyos.configquery import CliShellApiConfigQuery +from vyos.configquery import ConfigTreeQuery +from vyos.util import call from vyos.util import cmd from vyos.util import run from vyos.template import render_to_string conntrackd_bin = '/usr/sbin/conntrackd' conntrackd_config = '/run/conntrackd/conntrackd.conf' +failover_state_file = '/var/run/vyatta-conntrackd-failover-state' parser = ArgumentParser(description='Conntrack Sync') group = parser.add_mutually_exclusive_group() @@ -36,6 +39,8 @@ group.add_argument('--show-internal', help='Show internal (main) tracking cache' group.add_argument('--show-external', help='Show external (main) tracking cache', action='store_true') group.add_argument('--show-internal-expect', help='Show internal (expect) tracking cache', action='store_true') group.add_argument('--show-external-expect', help='Show external (expect) tracking cache', action='store_true') +group.add_argument('--show-statistics', help='Show connection syncing statistics', action='store_true') +group.add_argument('--show-status', help='Show conntrack-sync status', action='store_true') def is_configured(): """ Check if conntrack-sync service is configured """ @@ -131,6 +136,46 @@ if __name__ == '__main__': out = cmd(f'sudo {conntrackd_bin} -C {conntrackd_config} {opt} -x') xml_to_stdout(out) + elif args.show_statistics: + is_configured() + config = ConfigTreeQuery() + print('\nMain Table Statistics:\n') + call(f'sudo {conntrackd_bin} -C {conntrackd_config} -s') + print() + if config.exists(['service', 'conntrack-sync', 'expect-sync']): + print('\nExpect Table Statistics:\n') + call(f'sudo {conntrackd_bin} -C {conntrackd_config} -s exp') + print() + + elif args.show_status: + is_configured() + config = ConfigTreeQuery() + ct_sync_intf = config.list_nodes(['service', 'conntrack-sync', 'interface']) + ct_sync_intf = ', '.join(ct_sync_intf) + failover_state = "no transition yet!" + expect_sync_protocols = "disabled" + + if config.exists(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp']): + failover_mechanism = "vrrp" + vrrp_sync_grp = config.value(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group']) + + if os.path.isfile(failover_state_file): + with open(failover_state_file, "r") as f: + failover_state = f.readline() + + if config.exists(['service', 'conntrack-sync', 'expect-sync']): + expect_sync_protocols = config.values(['service', 'conntrack-sync', 'expect-sync']) + if 'all' in expect_sync_protocols: + expect_sync_protocols = ["ftp", "sip", "h323", "nfs", "sqlnet"] + expect_sync_protocols = ', '.join(expect_sync_protocols) + + show_status = (f'\nsync-interface : {ct_sync_intf}\n' + f'failover-mechanism : {failover_mechanism} [sync-group {vrrp_sync_grp}]\n' + f'last state transition : {failover_state}' + f'ExpectationSync : {expect_sync_protocols}') + + print(show_status) + else: parser.print_help() exit(1) diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py new file mode 100755 index 000000000..cf70890a6 --- /dev/null +++ b/src/op_mode/firewall.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 argparse +import json +import re +import tabulate + +from vyos.config import Config +from vyos.util import cmd +from vyos.util import dict_search_args + +def get_firewall_interfaces(conf, firewall, name=None, ipv6=False): + interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + directions = ['in', 'out', 'local'] + + def parse_if(ifname, if_conf): + if 'firewall' in if_conf: + for direction in directions: + if direction in if_conf['firewall']: + fw_conf = if_conf['firewall'][direction] + name_str = f'({ifname},{direction})' + + if 'name' in fw_conf: + fw_name = fw_conf['name'] + + if not name: + firewall['name'][fw_name]['interface'].append(name_str) + elif not ipv6 and name == fw_name: + firewall['interface'].append(name_str) + + if 'ipv6_name' in fw_conf: + fw_name = fw_conf['ipv6_name'] + + if not name: + firewall['ipv6_name'][fw_name]['interface'].append(name_str) + elif ipv6 and name == fw_name: + firewall['interface'].append(name_str) + + for iftype in ['vif', 'vif_s', 'vif_c']: + if iftype in if_conf: + for vifname, vif_conf in if_conf[iftype].items(): + parse_if(f'{ifname}.{vifname}', vif_conf) + + for iftype, iftype_conf in interfaces.items(): + for ifname, if_conf in iftype_conf.items(): + parse_if(ifname, if_conf) + + return firewall + +def get_config_firewall(conf, name=None, ipv6=False, interfaces=True): + config_path = ['firewall'] + if name: + config_path += ['ipv6-name' if ipv6 else 'name', name] + + firewall = conf.get_config_dict(config_path, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + if firewall and interfaces: + if name: + firewall['interface'] = [] + else: + if 'name' in firewall: + for fw_name, name_conf in firewall['name'].items(): + name_conf['interface'] = [] + + if 'ipv6_name' in firewall: + for fw_name, name_conf in firewall['ipv6_name'].items(): + name_conf['interface'] = [] + + get_firewall_interfaces(conf, firewall, name, ipv6) + return firewall + +def get_nftables_details(name, ipv6=False): + suffix = '6' if ipv6 else '' + command = f'sudo nft list chain ip{suffix} filter {name}' + try: + results = cmd(command) + except: + return {} + + out = {} + for line in results.split('\n'): + comment_search = re.search(rf'{name}[\- ](\d+|default-action)', line) + if not comment_search: + continue + + rule = {} + rule_id = comment_search[1] + counter_search = re.search(r'counter packets (\d+) bytes (\d+)', line) + if counter_search: + rule['packets'] = counter_search[1] + rule['bytes'] = counter_search[2] + + rule['conditions'] = re.sub(r'(\b(counter packets \d+ bytes \d+|drop|reject|return|log)\b|comment "[\w\-]+")', '', line).strip() + out[rule_id] = rule + return out + +def output_firewall_name(name, name_conf, ipv6=False, single_rule_id=None): + ip_str = 'IPv6' if ipv6 else 'IPv4' + print(f'\n---------------------------------\n{ip_str} Firewall "{name}"\n') + + if name_conf['interface']: + print('Active on: {0}\n'.format(" ".join(name_conf['interface']))) + + details = get_nftables_details(name, ipv6) + rows = [] + + if 'rule' in name_conf: + for rule_id, rule_conf in name_conf['rule'].items(): + if single_rule_id and rule_id != single_rule_id: + continue + + if 'disable' in rule_conf: + continue + + row = [rule_id, rule_conf['action'], rule_conf['protocol'] if 'protocol' in rule_conf else 'all'] + if rule_id in details: + rule_details = details[rule_id] + row.append(rule_details.get('packets', 0)) + row.append(rule_details.get('bytes', 0)) + row.append(rule_details['conditions']) + rows.append(row) + + if 'default_action' in name_conf and not single_rule_id: + row = ['default', name_conf['default_action'], 'all'] + if 'default-action' in details: + rule_details = details['default-action'] + row.append(rule_details.get('packets', 0)) + row.append(rule_details.get('bytes', 0)) + rows.append(row) + + if rows: + header = ['Rule', 'Action', 'Protocol', 'Packets', 'Bytes', 'Conditions'] + print(tabulate.tabulate(rows, header) + '\n') + +def output_firewall_name_statistics(name, name_conf, ipv6=False, single_rule_id=None): + ip_str = 'IPv6' if ipv6 else 'IPv4' + print(f'\n---------------------------------\n{ip_str} Firewall "{name}"\n') + + if name_conf['interface']: + print('Active on: {0}\n'.format(" ".join(name_conf['interface']))) + + details = get_nftables_details(name, ipv6) + rows = [] + + if 'rule' in name_conf: + for rule_id, rule_conf in name_conf['rule'].items(): + if single_rule_id and rule_id != single_rule_id: + continue + + if 'disable' in rule_conf: + continue + + source_addr = dict_search_args(rule_conf, 'source', 'address') or '0.0.0.0/0' + dest_addr = dict_search_args(rule_conf, 'destination', 'address') or '0.0.0.0/0' + + row = [rule_id] + if rule_id in details: + rule_details = details[rule_id] + row.append(rule_details.get('packets', 0)) + row.append(rule_details.get('bytes', 0)) + else: + row.append('0') + row.append('0') + row.append(rule_conf['action']) + row.append(source_addr) + row.append(dest_addr) + rows.append(row) + + if 'default_action' in name_conf and not single_rule_id: + row = ['default'] + if 'default-action' in details: + rule_details = details['default-action'] + row.append(rule_details.get('packets', 0)) + row.append(rule_details.get('bytes', 0)) + else: + row.append('0') + row.append('0') + row.append(name_conf['default_action']) + row.append('0.0.0.0/0') # Source + row.append('0.0.0.0/0') # Dest + rows.append(row) + + if rows: + header = ['Rule', 'Packets', 'Bytes', 'Action', 'Source', 'Destination'] + print(tabulate.tabulate(rows, header) + '\n') + +def show_firewall(): + print('Rulesets Information') + + conf = Config() + firewall = get_config_firewall(conf) + + if not firewall: + return + + if 'name' in firewall: + for name, name_conf in firewall['name'].items(): + output_firewall_name(name, name_conf, ipv6=False) + + if 'ipv6_name' in firewall: + for name, name_conf in firewall['ipv6_name'].items(): + output_firewall_name(name, name_conf, ipv6=True) + +def show_firewall_name(name, ipv6=False): + print('Ruleset Information') + + conf = Config() + firewall = get_config_firewall(conf, name, ipv6) + if firewall: + output_firewall_name(name, firewall, ipv6) + +def show_firewall_rule(name, rule_id, ipv6=False): + print('Rule Information') + + conf = Config() + firewall = get_config_firewall(conf, name, ipv6) + if firewall: + output_firewall_name(name, firewall, ipv6, rule_id) + +def show_firewall_group(name=None): + conf = Config() + firewall = get_config_firewall(conf, interfaces=False) + + if 'group' not in firewall: + return + + def find_references(group_type, group_name): + out = [] + for name_type in ['name', 'ipv6_name']: + if name_type not in firewall: + continue + for name, name_conf in firewall[name_type].items(): + if 'rule' not in name_conf: + continue + for rule_id, rule_conf in name_conf['rule'].items(): + source_group = dict_search_args(rule_conf, 'source', 'group', group_type) + dest_group = dict_search_args(rule_conf, 'destination', 'group', group_type) + if source_group and group_name == source_group: + out.append(f'{name}-{rule_id}') + elif dest_group and group_name == dest_group: + out.append(f'{name}-{rule_id}') + return out + + header = ['Name', 'Type', 'References', 'Members'] + rows = [] + + for group_type, group_type_conf in firewall['group'].items(): + for group_name, group_conf in group_type_conf.items(): + if name and name != group_name: + continue + + references = find_references(group_type, group_name) + row = [group_name, group_type, ', '.join(references)] + if 'address' in group_conf: + row.append(", ".join(group_conf['address'])) + elif 'network' in group_conf: + row.append(", ".join(group_conf['network'])) + elif 'port' in group_conf: + row.append(", ".join(group_conf['port'])) + rows.append(row) + + if rows: + print('Firewall Groups\n') + print(tabulate.tabulate(rows, header)) + +def show_summary(): + print('Ruleset Summary') + + conf = Config() + firewall = get_config_firewall(conf) + + if not firewall: + return + + header = ['Ruleset Name', 'Description', 'References'] + v4_out = [] + v6_out = [] + + if 'name' in firewall: + for name, name_conf in firewall['name'].items(): + description = name_conf.get('description', '') + interfaces = ", ".join(name_conf['interface']) + v4_out.append([name, description, interfaces]) + + if 'ipv6_name' in firewall: + for name, name_conf in firewall['ipv6_name'].items(): + description = name_conf.get('description', '') + interfaces = ", ".join(name_conf['interface']) + v6_out.append([name, description, interfaces]) + + if v6_out: + print('\nIPv6 name:\n') + print(tabulate.tabulate(v6_out, header) + '\n') + + if v4_out: + print('\nIPv4 name:\n') + print(tabulate.tabulate(v4_out, header) + '\n') + + show_firewall_group() + +def show_statistics(): + print('Rulesets Statistics') + + conf = Config() + firewall = get_config_firewall(conf) + + if not firewall: + return + + if 'name' in firewall: + for name, name_conf in firewall['name'].items(): + output_firewall_name_statistics(name, name_conf, ipv6=False) + + if 'ipv6_name' in firewall: + for name, name_conf in firewall['ipv6_name'].items(): + output_firewall_name_statistics(name, name_conf, ipv6=True) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--action', help='Action', required=False) + parser.add_argument('--name', help='Firewall name', required=False, action='store', nargs='?', default='') + parser.add_argument('--rule', help='Firewall Rule ID', required=False) + parser.add_argument('--ipv6', help='IPv6 toggle', action='store_true') + + args = parser.parse_args() + + if args.action == 'show': + if not args.rule: + show_firewall_name(args.name, args.ipv6) + else: + show_firewall_rule(args.name, args.rule, args.ipv6) + elif args.action == 'show_all': + show_firewall() + elif args.action == 'show_group': + show_firewall_group(args.name) + elif args.action == 'show_statistics': + show_statistics() + elif args.action == 'show_summary': + show_summary() diff --git a/src/op_mode/force_root-partition-auto-resize.sh b/src/op_mode/force_root-partition-auto-resize.sh new file mode 100755 index 000000000..b39e87560 --- /dev/null +++ b/src/op_mode/force_root-partition-auto-resize.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2021 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 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/>. + +# ROOT_PART_DEV – root partition device path +# ROOT_PART_NAME – root partition device name +# ROOT_DEV_NAME – disk device name +# ROOT_DEV – disk device path +# ROOT_PART_NUM – number of root partition on disk +# ROOT_DEV_SIZE – disk total size in 512 bytes sectors +# ROOT_PART_SIZE – root partition total size in 512 bytes sectors +# ROOT_PART_START – number of 512 bytes sector where root partition starts +# AVAILABLE_EXTENSION_SIZE – calculation available disk space after root partition in 512 bytes sectors +ROOT_PART_DEV=$(findmnt /usr/lib/live/mount/persistence -o source -n) +ROOT_PART_NAME=$(echo "$ROOT_PART_DEV" | cut -d "/" -f 3) +ROOT_DEV_NAME=$(echo /sys/block/*/"${ROOT_PART_NAME}" | cut -d "/" -f 4) +ROOT_DEV="/dev/${ROOT_DEV_NAME}" +ROOT_PART_NUM=$(cat "/sys/block/${ROOT_DEV_NAME}/${ROOT_PART_NAME}/partition") +ROOT_DEV_SIZE=$(cat "/sys/block/${ROOT_DEV_NAME}/size") +ROOT_PART_SIZE=$(cat "/sys/block/${ROOT_DEV_NAME}/${ROOT_PART_NAME}/size") +ROOT_PART_START=$(cat "/sys/block/${ROOT_DEV_NAME}/${ROOT_PART_NAME}/start") +AVAILABLE_EXTENSION_SIZE=$((ROOT_DEV_SIZE - ROOT_PART_START - ROOT_PART_SIZE - 8)) + +# +# Check if device have space for root partition growing up. +# +if [ $AVAILABLE_EXTENSION_SIZE -lt 1 ]; then + echo "There is no available space for root partition extension" + exit 0; +fi + +# +# Resize the partition and grow the filesystem. +# +# "print" and "Fix" directives were added to fix GPT table if it corrupted after virtual drive extension. +# If GPT table is corrupted we'll get Fix/Ignore dialogue after "print" command. +# "Fix" will be the answer for this dialogue. +# If GPT table is fine and no auto-fix dialogue appeared the directive "Fix" simply will print parted utility help info. +parted -m ${ROOT_DEV} ---pretend-input-tty > /dev/null 2>&1 <<EOF +print +Fix +resizepart +${ROOT_PART_NUM} +Yes +100% +EOF +partprobe > /dev/null 2>&1 +resize2fs ${ROOT_PART_DEV} > /dev/null 2>&1 diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py index df4486bce..b3ba44e87 100755 --- a/src/op_mode/format_disk.py +++ b/src/op_mode/format_disk.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2021 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 @@ -17,11 +17,10 @@ import argparse import os import re -import sys + from datetime import datetime -from time import sleep -from vyos.util import is_admin, ask_yes_no +from vyos.util import ask_yes_no from vyos.util import call from vyos.util import cmd from vyos.util import DEVNULL @@ -38,16 +37,17 @@ def list_disks(): def is_busy(disk: str): """Check if given disk device is busy by re-reading it's partition table""" - return call(f'sudo blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0 + return call(f'blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0 def backup_partitions(disk: str): """Save sfdisk partitions output to a backup file""" - device_path = '/dev/' + disk - backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M') - backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts) - cmd(f'sudo /sbin/sfdisk -d {device_path} > {backup_file}') + device_path = f'/dev/{disk}' + backup_ts = datetime.now().strftime('%Y%m%d-%H%M') + backup_file = f'/var/tmp/backup_{disk}.{backup_ts}' + call(f'sfdisk -d {device_path} > {backup_file}') + print(f'Partition table backup saved to {backup_file}') def list_partitions(disk: str): @@ -65,11 +65,11 @@ def list_partitions(disk: str): def delete_partition(disk: str, partition_idx: int): - cmd(f'sudo /sbin/parted /dev/{disk} rm {partition_idx}') + cmd(f'parted /dev/{disk} rm {partition_idx}') def format_disk_like(target: str, proto: str): - cmd(f'sudo /sbin/sfdisk -d /dev/{proto} | sudo /sbin/sfdisk --force /dev/{target}') + cmd(f'sfdisk -d /dev/{proto} | sfdisk --force /dev/{target}') if __name__ == '__main__': @@ -79,10 +79,6 @@ if __name__ == '__main__': group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference') args = parser.parse_args() - if not is_admin(): - print('Must be admin or root to format disk') - sys.exit(1) - target_disk = args.target eligible_target_disks = list_disks() @@ -90,54 +86,48 @@ if __name__ == '__main__': eligible_proto_disks = eligible_target_disks.copy() eligible_proto_disks.remove(target_disk) - fmt = { - 'target_disk': target_disk, - 'proto_disk': proto_disk, - } - if proto_disk == target_disk: print('The two disk drives must be different.') - sys.exit(1) + exit(1) - if not os.path.exists('/dev/' + proto_disk): - print('Device /dev/{proto_disk} does not exist'.format_map(fmt)) - sys.exit(1) + if not os.path.exists(f'/dev/{proto_disk}'): + print(f'Device /dev/{proto_disk} does not exist') + exit(1) if not os.path.exists('/dev/' + target_disk): - print('Device /dev/{target_disk} does not exist'.format_map(fmt)) - sys.exit(1) + print(f'Device /dev/{target_disk} does not exist') + exit(1) if target_disk not in eligible_target_disks: - print('Device {target_disk} can not be formatted'.format_map(fmt)) - sys.exit(1) + print(f'Device {target_disk} can not be formatted') + exit(1) if proto_disk not in eligible_proto_disks: - print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt)) - sys.exit(1) + print(f'Device {proto_disk} can not be used as a prototype for {target_disk}') + exit(1) if is_busy(target_disk): - print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt)) - sys.exit(1) + print(f'Disk device {target_disk} is busy, unable to format') + exit(1) - print('This will re-format disk {target_disk} so that it has the same disk\n' - 'partion sizes and offsets as {proto_disk}. This will not copy\n' - 'data from {proto_disk} to {target_disk}. But this will erase all\n' - 'data on {target_disk}.\n'.format_map(fmt)) + print(f'\nThis will re-format disk {target_disk} so that it has the same disk' + f'\npartion sizes and offsets as {proto_disk}. This will not copy' + f'\ndata from {proto_disk} to {target_disk}. But this will erase all' + f'\ndata on {target_disk}.\n') - if not ask_yes_no("Do you wish to proceed?"): - print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt)) - sys.exit(0) + if not ask_yes_no('Do you wish to proceed?'): + print(f'Disk drive {target_disk} will not be re-formated') + exit(0) - print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt)) + print(f'Re-formating disk drive {target_disk}...') print('Making backup copy of partitions...') backup_partitions(target_disk) - sleep(1) print('Deleting old partitions...') for p in list_partitions(target_disk): delete_partition(disk=target_disk, partition_idx=p) - print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt)) + print(f'Creating new partitions on {target_disk} based on {proto_disk}...') format_disk_like(target=target_disk, proto=proto_disk) - print('Done.') + print('Done!') diff --git a/src/op_mode/generate_ipsec_debug_archive.sh b/src/op_mode/generate_ipsec_debug_archive.sh new file mode 100755 index 000000000..53d0a6eaa --- /dev/null +++ b/src/op_mode/generate_ipsec_debug_archive.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Collecting IPSec Debug Information + +DATE=`date +%d-%m-%Y` + +a_CMD=( + "sudo ipsec status" + "sudo swanctl -L" + "sudo swanctl -l" + "sudo swanctl -P" + "sudo ip x sa show" + "sudo ip x policy show" + "sudo ip tunnel show" + "sudo ip address" + "sudo ip rule show" + "sudo ip route" + "sudo ip route show table 220" + ) + + +echo "DEBUG: ${DATE} on host \"$(hostname)\"" > /tmp/ipsec-status-${DATE}.txt +date >> /tmp/ipsec-status-${DATE}.txt + +# Execute all DEBUG commands and save it to file +for cmd in "${a_CMD[@]}"; do + echo -e "\n### ${cmd} ###" >> /tmp/ipsec-status-${DATE}.txt + ${cmd} >> /tmp/ipsec-status-${DATE}.txt 2>/dev/null +done + +# Collect charon logs, build .tgz archive +sudo journalctl /usr/lib/ipsec/charon > /tmp/journalctl-charon-${DATE}.txt && \ +sudo tar -zcvf /tmp/ipsec-debug-${DATE}.tgz /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt >& /dev/null +sudo rm -f /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt + +echo "Debug file is generated and located in /tmp/ipsec-debug-${DATE}.tgz" diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py index 731e71891..b9ebc991a 100755 --- a/src/op_mode/lldp_op.py +++ b/src/op_mode/lldp_op.py @@ -55,6 +55,9 @@ def parse_data(data, interface): if interface is not None and local_if != interface: continue for chassis, c_value in values.get('chassis', {}).items(): + # bail out early if no capabilities found + if 'capability' not in c_value: + continue capabilities = c_value['capability'] if isinstance(capabilities, dict): capabilities = [capabilities] diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index e1428c581..bc7813052 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -35,6 +35,7 @@ from vyos.pki import verify_certificate from vyos.xml import defaults from vyos.util import ask_input, ask_yes_no from vyos.util import cmd +from vyos.util import install_into_config CERT_REQ_END = '-----END CERTIFICATE REQUEST-----' auth_dir = '/config/auth' @@ -142,48 +143,50 @@ def get_revoked_by_serial_numbers(serial_numbers=[]): return certs_out def install_certificate(name, cert='', private_key=None, key_type=None, key_passphrase=None, is_ca=False): - # Show conf commands for installing certificate + # Show/install conf commands for certificate prefix = 'ca' if is_ca else 'certificate' - print('Configure mode commands to install:') - base = f"set pki {prefix} {name}" + base = f"pki {prefix} {name}" + config_paths = [] if cert: cert_pem = "".join(encode_certificate(cert).strip().split("\n")[1:-1]) - print(f"{base} certificate '{cert_pem}'") + config_paths.append(f"{base} certificate '{cert_pem}'") if private_key: key_pem = "".join(encode_private_key(private_key, passphrase=key_passphrase).strip().split("\n")[1:-1]) - print(f"{base} private key '{key_pem}'") + config_paths.append(f"{base} private key '{key_pem}'") if key_passphrase: - print(f"{base} private password-protected") + config_paths.append(f"{base} private password-protected") + + install_into_config(conf, config_paths) def install_crl(ca_name, crl): - # Show conf commands for installing crl - print("Configure mode commands to install CRL:") + # Show/install conf commands for crl crl_pem = "".join(encode_certificate(crl).strip().split("\n")[1:-1]) - print(f"set pki ca {ca_name} crl '{crl_pem}'") + install_into_config(conf, [f"pki ca {ca_name} crl '{crl_pem}'"]) def install_dh_parameters(name, params): - # Show conf commands for installing dh params - print("Configure mode commands to install DH parameters:") + # Show/install conf commands for dh params dh_pem = "".join(encode_dh_parameters(params).strip().split("\n")[1:-1]) - print(f"set pki dh {name} parameters '{dh_pem}'") + install_into_config(conf, [f"pki dh {name} parameters '{dh_pem}'"]) def install_ssh_key(name, public_key, private_key, passphrase=None): - # Show conf commands for installing ssh key + # Show/install conf commands for ssh key key_openssh = encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH') username = os.getlogin() type_key_split = key_openssh.split(" ") - base = f"set system login user {username} authentication public-keys {name}" - print("Configure mode commands to install SSH key:") - print(f"{base} key '{type_key_split[1]}'") - print(f"{base} type '{type_key_split[0]}'", end="\n\n") + base = f"system login user {username} authentication public-keys {name}" + install_into_config(conf, [ + f"{base} key '{type_key_split[1]}'", + f"{base} type '{type_key_split[0]}'" + ]) print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase)) def install_keypair(name, key_type, private_key=None, public_key=None, passphrase=None): - # Show conf commands for installing key-pair - print("Configure mode commands to install key pair:") + # Show/install conf commands for key-pair + + config_paths = [] if public_key: install_public_key = ask_yes_no('Do you want to install the public key?', default=True) @@ -191,7 +194,7 @@ def install_keypair(name, key_type, private_key=None, public_key=None, passphras if install_public_key: install_public_pem = "".join(public_key_pem.strip().split("\n")[1:-1]) - print(f"set pki key-pair {name} public key '{install_public_pem}'") + config_paths.append(f"pki key-pair {name} public key '{install_public_pem}'") else: print("Public key:") print(public_key_pem) @@ -202,13 +205,15 @@ def install_keypair(name, key_type, private_key=None, public_key=None, passphras if install_private_key: install_private_pem = "".join(private_key_pem.strip().split("\n")[1:-1]) - print(f"set pki key-pair {name} private key '{install_private_pem}'") + config_paths.append(f"pki key-pair {name} private key '{install_private_pem}'") if passphrase: - print(f"set pki key-pair {name} private password-protected") + config_paths.append(f"pki key-pair {name} private password-protected") else: print("Private key:") print(private_key_pem) + install_into_config(conf, config_paths) + def install_wireguard_key(interface, private_key, public_key): # Show conf commands for installing wireguard key pairs from vyos.ifconfig import Section @@ -217,20 +222,10 @@ def install_wireguard_key(interface, private_key, public_key): exit(1) # Check if we are running in a config session - if yes, we can directly write to the CLI - cli_string = f"interfaces wireguard {interface} private-key '{private_key}'" - if Config().in_session(): - cmd(f"/opt/vyatta/sbin/my_set {cli_string}") - - print('"generate" CLI command executed from config session.\nGenerated private-key was imported to CLI!',end='\n\n') - print(f'Use the following command to verify: show interfaces wireguard {interface}') - else: - print('"generate" CLI command executed from operational level.\n' - 'Generated private-key is not stored to CLI, use configure mode commands to install key:', end='\n\n') - print(f"set {cli_string}", end="\n\n") + install_into_config(conf, [f"interfaces wireguard {interface} private-key '{private_key}'"]) print(f"Corresponding public-key to use on peer system is: '{public_key}'") - def install_wireguard_psk(interface, peer, psk): from vyos.ifconfig import Section if Section.section(interface) != 'wireguard': @@ -238,17 +233,7 @@ def install_wireguard_psk(interface, peer, psk): exit(1) # Check if we are running in a config session - if yes, we can directly write to the CLI - cli_string = f"interfaces wireguard {interface} peer {peer} preshared-key '{psk}'" - if Config().in_session(): - cmd(f"/opt/vyatta/sbin/my_set {cli_string}") - - print('"generate" CLI command executed from config session.\nGenerated preshared-key was imported to CLI!',end='\n\n') - print(f'Use the following command to verify: show interfaces wireguard {interface}') - else: - print('"generate" CLI command executed from operational level.\n' - 'Generated preshared-key is not stored to CLI, use configure mode commands to install key:', end='\n\n') - print(f"set {cli_string}", end="\n\n") - + install_into_config(conf, [f"interfaces wireguard {interface} peer {peer} preshared-key '{psk}'"]) def ask_passphrase(): passphrase = None @@ -858,8 +843,18 @@ if __name__ == '__main__': elif args.action == 'show': if args.ca: - show_certificate_authority(None if args.ca == 'all' else args.ca) + ca_name = None if args.ca == 'all' else args.ca + if ca_name: + if not conf.exists(['pki', 'ca', ca_name]): + print(f'CA "{ca_name}" does not exist!') + exit(1) + show_certificate_authority(ca_name) elif args.certificate: + cert_name = None if args.certificate == 'all' else args.certificate + if cert_name: + if not conf.exists(['pki', 'certificate', cert_name]): + print(f'Certificate "{cert_name}" does not exist!') + exit(1) show_certificate(None if args.certificate == 'all' else args.certificate) elif args.crl: show_crl(None if args.crl == 'all' else args.crl) diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py new file mode 100755 index 000000000..e0b4ac514 --- /dev/null +++ b/src/op_mode/policy_route.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 argparse +import re +import tabulate + +from vyos.config import Config +from vyos.util import cmd +from vyos.util import dict_search_args + +def get_policy_interfaces(conf, policy, name=None, ipv6=False): + interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + routes = ['route', 'ipv6_route'] + + def parse_if(ifname, if_conf): + if 'policy' in if_conf: + for route in routes: + if route in if_conf['policy']: + route_name = if_conf['policy'][route] + name_str = f'({ifname},{route})' + + if not name: + policy[route][route_name]['interface'].append(name_str) + elif not ipv6 and name == route_name: + policy['interface'].append(name_str) + + for iftype in ['vif', 'vif_s', 'vif_c']: + if iftype in if_conf: + for vifname, vif_conf in if_conf[iftype].items(): + parse_if(f'{ifname}.{vifname}', vif_conf) + + for iftype, iftype_conf in interfaces.items(): + for ifname, if_conf in iftype_conf.items(): + parse_if(ifname, if_conf) + +def get_config_policy(conf, name=None, ipv6=False, interfaces=True): + config_path = ['policy'] + if name: + config_path += ['ipv6-route' if ipv6 else 'route', name] + + policy = conf.get_config_dict(config_path, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + if policy and interfaces: + if name: + policy['interface'] = [] + else: + if 'route' in policy: + for route_name, route_conf in policy['route'].items(): + route_conf['interface'] = [] + + if 'ipv6_route' in policy: + for route_name, route_conf in policy['ipv6_route'].items(): + route_conf['interface'] = [] + + get_policy_interfaces(conf, policy, name, ipv6) + + return policy + +def get_nftables_details(name, ipv6=False): + suffix = '6' if ipv6 else '' + command = f'sudo nft list chain ip{suffix} mangle VYOS_PBR{suffix}_{name}' + try: + results = cmd(command) + except: + return {} + + out = {} + for line in results.split('\n'): + comment_search = re.search(rf'{name}[\- ](\d+|default-action)', line) + if not comment_search: + continue + + rule = {} + rule_id = comment_search[1] + counter_search = re.search(r'counter packets (\d+) bytes (\d+)', line) + if counter_search: + rule['packets'] = counter_search[1] + rule['bytes'] = counter_search[2] + + rule['conditions'] = re.sub(r'(\b(counter packets \d+ bytes \d+|drop|reject|return|log)\b|comment "[\w\-]+")', '', line).strip() + out[rule_id] = rule + return out + +def output_policy_route(name, route_conf, ipv6=False, single_rule_id=None): + ip_str = 'IPv6' if ipv6 else 'IPv4' + print(f'\n---------------------------------\n{ip_str} Policy Route "{name}"\n') + + if route_conf['interface']: + print('Active on: {0}\n'.format(" ".join(route_conf['interface']))) + + details = get_nftables_details(name, ipv6) + rows = [] + + if 'rule' in route_conf: + for rule_id, rule_conf in route_conf['rule'].items(): + if single_rule_id and rule_id != single_rule_id: + continue + + if 'disable' in rule_conf: + continue + + action = rule_conf['action'] if 'action' in rule_conf else 'set' + protocol = rule_conf['protocol'] if 'protocol' in rule_conf else 'all' + + row = [rule_id, action, protocol] + if rule_id in details: + rule_details = details[rule_id] + row.append(rule_details.get('packets', 0)) + row.append(rule_details.get('bytes', 0)) + row.append(rule_details['conditions']) + rows.append(row) + + if 'default_action' in route_conf and not single_rule_id: + row = ['default', route_conf['default_action'], 'all'] + if 'default-action' in details: + rule_details = details['default-action'] + row.append(rule_details.get('packets', 0)) + row.append(rule_details.get('bytes', 0)) + rows.append(row) + + if rows: + header = ['Rule', 'Action', 'Protocol', 'Packets', 'Bytes', 'Conditions'] + print(tabulate.tabulate(rows, header) + '\n') + +def show_policy(ipv6=False): + print('Ruleset Information') + + conf = Config() + policy = get_config_policy(conf) + + if not policy: + return + + if not ipv6 and 'route' in policy: + for route, route_conf in policy['route'].items(): + output_policy_route(route, route_conf, ipv6=False) + + if ipv6 and 'ipv6_route' in policy: + for route, route_conf in policy['ipv6_route'].items(): + output_policy_route(route, route_conf, ipv6=True) + +def show_policy_name(name, ipv6=False): + print('Ruleset Information') + + conf = Config() + policy = get_config_policy(conf, name, ipv6) + if policy: + output_policy_route(name, policy, ipv6) + +def show_policy_rule(name, rule_id, ipv6=False): + print('Rule Information') + + conf = Config() + policy = get_config_policy(conf, name, ipv6) + if policy: + output_policy_route(name, policy, ipv6, rule_id) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--action', help='Action', required=False) + parser.add_argument('--name', help='Policy name', required=False, action='store', nargs='?', default='') + parser.add_argument('--rule', help='Policy Rule ID', required=False) + parser.add_argument('--ipv6', help='IPv6 toggle', action='store_true') + + args = parser.parse_args() + + if args.action == 'show': + if not args.rule: + show_policy_name(args.name, args.ipv6) + else: + show_policy_rule(args.name, args.rule, args.ipv6) + elif args.action == 'show_all': + show_policy(args.ipv6) diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index f8b5a3dda..679b03c0b 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -92,37 +92,40 @@ def cancel_shutdown(): try: run('/sbin/shutdown -c --no-wall') except OSError as e: - exit("Could not cancel a reboot or poweroff: %s" % e) + exit(f'Could not cancel a reboot or poweroff: {e}') - message = 'Scheduled {} has been cancelled {}'.format(output['MODE'], timenow) + mode = output['MODE'] + message = f'Scheduled {mode} has been cancelled {timenow}' run(f'wall {message} > /dev/null 2>&1') else: print("Reboot or poweroff is not scheduled") def execute_shutdown(time, reboot=True, ask=True): + action = "reboot" if reboot else "poweroff" if not ask: - action = "reboot" if reboot else "poweroff" - if not ask_yes_no("Are you sure you want to %s this system?" % action): + if not ask_yes_no(f"Are you sure you want to {action} this system?"): exit(0) - - action = "-r" if reboot else "-P" + action_cmd = "-r" if reboot else "-P" if len(time) == 0: # T870 legacy reboot job support chk_vyatta_based_reboots() ### - out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT) + out = cmd(f'/sbin/shutdown {action_cmd} now', stderr=STDOUT) print(out.split(",", 1)[0]) return elif len(time) == 1: # Assume the argument is just time ts = parse_time(time[0]) if ts: - cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT) + cmd(f'/sbin/shutdown {action_cmd} {time[0]}', stderr=STDOUT) + # Inform all other logged in users about the reboot/shutdown + wall_msg = f'System {action} is scheduled {time[0]}' + cmd(f'/usr/bin/wall "{wall_msg}"') else: - exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) + exit(f'Invalid time "{time[0]}". The valid format is HH:MM') elif len(time) == 2: # Assume it's date and time ts = parse_time(time[0]) @@ -131,14 +134,18 @@ def execute_shutdown(time, reboot=True, ask=True): t = datetime.combine(ds, ts) td = t - datetime.now() t2 = 1 + int(td.total_seconds())//60 # Get total minutes - cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT) + + cmd(f'/sbin/shutdown {action_cmd} {t2}', stderr=STDOUT) + # Inform all other logged in users about the reboot/shutdown + wall_msg = f'System {action} is scheduled {time[1]} {time[0]}' + cmd(f'/usr/bin/wall "{wall_msg}"') else: if not ts: - exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) + exit(f'Invalid time "{time[0]}". The valid format is HH:MM') else: - exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1])) + exit(f'Invalid date "{time[1]}". A valid format is YYYY-MM-DD [HH:MM]') else: - exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM") + exit('Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM') check_shutdown() diff --git a/src/op_mode/ppp-server-ctrl.py b/src/op_mode/ppp-server-ctrl.py index 670cdf879..e93963fdd 100755 --- a/src/op_mode/ppp-server-ctrl.py +++ b/src/op_mode/ppp-server-ctrl.py @@ -60,7 +60,7 @@ def main(): output, err = popen(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][args.proto]) + args.action + ses_pattern, stderr=DEVNULL, decode='utf-8') if not err: try: - print(output) + print(f' {output}') except: sys.exit(0) else: diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index 109c8dd7b..e5014452f 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -138,7 +138,7 @@ def _reload_config(daemon): # define program arguments cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons') cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons') -cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons') +cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ldpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons') # parse arguments cmd_args = cmd_args_parser.parse_args() diff --git a/src/op_mode/show_configuration_json.py b/src/op_mode/show_configuration_json.py new file mode 100755 index 000000000..fdece533b --- /dev/null +++ b/src/op_mode/show_configuration_json.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 argparse +import json + +from vyos.configquery import ConfigTreeQuery + + +config = ConfigTreeQuery() +c = config.get_config_dict() + +parser = argparse.ArgumentParser() +parser.add_argument("-p", "--pretty", action="store_true", help="Show pretty configuration in JSON format") + + +if __name__ == '__main__': + args = parser.parse_args() + + if args.pretty: + print(json.dumps(c, indent=4)) + else: + print(json.dumps(c)) diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index 3d50eb938..eac068274 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -94,10 +94,8 @@ def split_text(text, used=0): used: number of characted already used in the screen """ no_tty = call('tty -s') - if no_tty: - return text.split() - returned = cmd('stty size') + returned = cmd('stty size') if not no_tty else '' if len(returned) == 2: rows, columns = [int(_) for _ in returned] else: diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py index c964caaeb..e72f0f965 100755 --- a/src/op_mode/show_ipsec_sa.py +++ b/src/op_mode/show_ipsec_sa.py @@ -46,7 +46,6 @@ def format_output(conns, sas): if parent_sa["state"] == b"ESTABLISHED" and installed_sas: state = "up" - uptime = vyos.util.seconds_to_human(parent_sa["established"].decode()) remote_host = parent_sa["remote-host"].decode() remote_id = parent_sa["remote-id"].decode() @@ -75,6 +74,8 @@ def format_output(conns, sas): # Remove B from <1K values pkts_str = re.sub(r'B', r'', pkts_str) + uptime = vyos.util.seconds_to_human(isa['install-time'].decode()) + enc = isa["encr-alg"].decode() if "encr-keysize" in isa: key_size = isa["encr-keysize"].decode() diff --git a/src/op_mode/show_nat_rules.py b/src/op_mode/show_nat_rules.py index d68def26a..98adb31dd 100755 --- a/src/op_mode/show_nat_rules.py +++ b/src/op_mode/show_nat_rules.py @@ -32,7 +32,7 @@ args = parser.parse_args() if args.source or args.destination: tmp = cmd('sudo nft -j list table ip nat') tmp = json.loads(tmp) - + format_nat_rule = '{0: <10} {1: <50} {2: <50} {3: <10}' print(format_nat_rule.format("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface")) print(format_nat_rule.format("----", "------" if args.source else "-----------", "-----------", "------------------" if args.source else "-----------------")) @@ -40,7 +40,7 @@ if args.source or args.destination: data_json = jmespath.search('nftables[?rule].rule[?chain]', tmp) for idx in range(0, len(data_json)): data = data_json[idx] - + # The following key values must exist # When the rule JSON does not have some keys, this is not a rule we can work with continue_rule = False @@ -50,9 +50,9 @@ if args.source or args.destination: continue if continue_rule: continue - + comment = data['comment'] - + # Check the annotation to see if the annotation format is created by VYOS continue_rule = True for comment_prefix in ['SRC-NAT-', 'DST-NAT-']: @@ -60,7 +60,7 @@ if args.source or args.destination: continue_rule = False if continue_rule: continue - + rule = int(''.join(list(filter(str.isdigit, comment)))) chain = data['chain'] if not ((args.source and chain == 'POSTROUTING') or (not args.source and chain == 'PREROUTING')): @@ -88,7 +88,7 @@ if args.source or args.destination: else: port_range = srcdest_json['set'][0]['range'] srcdest += 'port ' + str(port_range[0]) + '-' + str(port_range[1]) + ' ' - + tran_addr_json = dict_search('snat' if args.source else 'dnat', data['expr'][i]) if tran_addr_json: if isinstance(tran_addr_json['addr'],str): @@ -98,10 +98,10 @@ if args.source or args.destination: len_tmp = dict_search('snat.addr.prefix.len' if args.source else 'dnat.addr.prefix.len', data['expr'][3]) if addr_tmp and len_tmp: tran_addr += addr_tmp + '/' + str(len_tmp) + ' ' - + if isinstance(tran_addr_json['port'],int): - tran_addr += 'port ' + tran_addr_json['port'] - + tran_addr += 'port ' + str(tran_addr_json['port']) + else: if 'masquerade' in data['expr'][i]: tran_addr = 'masquerade' @@ -112,10 +112,10 @@ if args.source or args.destination: srcdests.append(srcdest) srcdest = '' print(format_nat_rule.format(rule, srcdests[0], tran_addr, interface)) - + for i in range(1, len(srcdests)): print(format_nat_rule.format(' ', srcdests[i], ' ', ' ')) - + exit(0) else: parser.print_help() diff --git a/src/op_mode/show_openvpn_mfa.py b/src/op_mode/show_openvpn_mfa.py new file mode 100755 index 000000000..1ab54600c --- /dev/null +++ b/src/op_mode/show_openvpn_mfa.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +# Copyright 2017, 2021 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 re +import socket +import urllib.parse +import argparse + +from vyos.util import popen + +otp_file = '/config/auth/openvpn/{interface}-otp-secrets' + +def get_mfa_secret(interface, client): + try: + with open(otp_file.format(interface=interface), "r") as f: + users = f.readlines() + for user in users: + if re.search('^' + client + ' ', user): + return user.split(':')[3] + except: + pass + +def get_mfa_uri(client, secret): + hostname = socket.gethostname() + fqdn = socket.getfqdn() + uri = 'otpauth://totp/{hostname}:{client}@{fqdn}?secret={secret}' + + return urllib.parse.quote(uri.format(hostname=hostname, client=client, fqdn=fqdn, secret=secret), safe='/:@?=') + +if __name__ == '__main__': + parser = argparse.ArgumentParser(add_help=False, description='Show two-factor authentication information') + parser.add_argument('--intf', action="store", type=str, default='', help='only show the specified interface') + parser.add_argument('--user', action="store", type=str, default='', help='only show the specified users') + parser.add_argument('--action', action="store", type=str, default='show', help='action to perform') + + args = parser.parse_args() + secret = get_mfa_secret(args.intf, args.user) + + if args.action == "secret" and secret: + print(secret) + + if args.action == "uri" and secret: + uri = get_mfa_uri(args.user, secret) + print(uri) + + if args.action == "qrcode" and secret: + uri = get_mfa_uri(args.user, secret) + qrcode,err = popen('qrencode -t ansiutf8', input=uri) + print(qrcode) + diff --git a/src/op_mode/show_ram.py b/src/op_mode/show_ram.py new file mode 100755 index 000000000..5818ec132 --- /dev/null +++ b/src/op_mode/show_ram.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. +# + +def get_system_memory(): + from re import search as re_search + + def find_value(keyword, mem_data): + regex = keyword + ':\s+(\d+)' + res = re_search(regex, mem_data).group(1) + return int(res) + + with open("/proc/meminfo", "r") as f: + mem_data = f.read() + + total = find_value('MemTotal', mem_data) + available = find_value('MemAvailable', mem_data) + buffers = find_value('Buffers', mem_data) + cached = find_value('Cached', mem_data) + + used = total - available + + res = { + "total": total, + "free": available, + "used": used, + "buffers": buffers, + "cached": cached + } + + return res + +def get_system_memory_human(): + from vyos.util import bytes_to_human + + mem = get_system_memory() + + for key in mem: + # The Linux kernel exposes memory values in kilobytes, + # so we need to normalize them + mem[key] = bytes_to_human(mem[key], initial_exponent=10) + + return mem + +if __name__ == '__main__': + mem = get_system_memory_human() + + print("Total: {}".format(mem["total"])) + print("Free: {}".format(mem["free"])) + print("Used: {}".format(mem["used"])) + diff --git a/src/op_mode/show_ram.sh b/src/op_mode/show_ram.sh deleted file mode 100755 index b013e16f8..000000000 --- a/src/op_mode/show_ram.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# -# Module: vyos-show-ram.sh -# Displays memory usage information in minimalistic format -# -# Copyright (C) 2019 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 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/>. - -MB_DIVISOR=1024 - -TOTAL=$(cat /proc/meminfo | grep -E "^MemTotal:" | awk -F ' ' '{print $2}') -FREE=$(cat /proc/meminfo | grep -E "^MemFree:" | awk -F ' ' '{print $2}') -BUFFERS=$(cat /proc/meminfo | grep -E "^Buffers:" | awk -F ' ' '{print $2}') -CACHED=$(cat /proc/meminfo | grep -E "^Cached:" | awk -F ' ' '{print $2}') - -DISPLAY_FREE=$(( ($FREE + $BUFFERS + $CACHED) / $MB_DIVISOR )) -DISPLAY_TOTAL=$(( $TOTAL / $MB_DIVISOR )) -DISPLAY_USED=$(( $DISPLAY_TOTAL - $DISPLAY_FREE )) - -echo "Total: $DISPLAY_TOTAL" -echo "Free: $DISPLAY_FREE" -echo "Used: $DISPLAY_USED" diff --git a/src/op_mode/show_uptime.py b/src/op_mode/show_uptime.py new file mode 100755 index 000000000..c3dea52e6 --- /dev/null +++ b/src/op_mode/show_uptime.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 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/>. + +def get_uptime_seconds(): + from re import search + from vyos.util import read_file + + data = read_file("/proc/uptime") + seconds = search("([0-9\.]+)\s", data).group(1) + + return int(float(seconds)) + +def get_load_averages(): + from re import search + from vyos.util import cmd + + data = cmd("uptime") + matches = search(r"load average:\s*(?P<one>[0-9\.]+)\s*,\s*(?P<five>[0-9\.]+)\s*,\s*(?P<fifteen>[0-9\.]+)\s*", data) + + res = {} + res[1] = float(matches["one"]) + res[5] = float(matches["five"]) + res[15] = float(matches["fifteen"]) + + return res + +if __name__ == '__main__': + from vyos.util import seconds_to_human + + print("Uptime: {}\n".format(seconds_to_human(get_uptime_seconds()))) + + avgs = get_load_averages() + + print("Load averages:") + print("1 minute: {:.02f}%".format(avgs[1]*100)) + print("5 minutes: {:.02f}%".format(avgs[5]*100)) + print("15 minutes: {:.02f}%".format(avgs[15]*100)) diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py index 06e227ccf..40854fa8f 100755 --- a/src/op_mode/vpn_ipsec.py +++ b/src/op_mode/vpn_ipsec.py @@ -48,7 +48,7 @@ def reset_peer(peer, tunnel): result = True for conn in conns: try: - call(f'sudo /usr/sbin/ipsec down {conn}', timeout = 10) + call(f'sudo /usr/sbin/ipsec down {conn}{{*}}', timeout = 10) call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10) except TimeoutExpired as e: print(f'Timed out while resetting {conn}') diff --git a/src/op_mode/zone_policy.py b/src/op_mode/zone_policy.py new file mode 100755 index 000000000..7b43018c2 --- /dev/null +++ b/src/op_mode/zone_policy.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 argparse +import tabulate + +from vyos.config import Config +from vyos.util import dict_search_args + +def get_config_zone(conf, name=None): + config_path = ['zone-policy'] + if name: + config_path += ['zone', name] + + zone_policy = conf.get_config_dict(config_path, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + return zone_policy + +def output_zone_name(zone, zone_conf): + print(f'\n---------------------------------\nZone: "{zone}"\n') + + interfaces = ', '.join(zone_conf['interface']) if 'interface' in zone_conf else '' + if 'local_zone' in zone_conf: + interfaces = 'LOCAL' + + print(f'Interfaces: {interfaces}\n') + + header = ['From Zone', 'Firewall'] + rows = [] + + if 'from' in zone_conf: + for from_name, from_conf in zone_conf['from'].items(): + row = [from_name] + v4_name = dict_search_args(from_conf, 'firewall', 'name') + v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') + + if v4_name: + rows.append(row + [v4_name]) + + if v6_name: + rows.append(row + [f'{v6_name} [IPv6]']) + + if rows: + print('From Zones:\n') + print(tabulate.tabulate(rows, header)) + +def show_zone_policy(zone): + conf = Config() + zone_policy = get_config_zone(conf, zone) + + if not zone_policy: + return + + if 'zone' in zone_policy: + for zone, zone_conf in zone_policy['zone'].items(): + output_zone_name(zone, zone_conf) + elif zone: + output_zone_name(zone, zone_policy) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--action', help='Action', required=False) + parser.add_argument('--name', help='Zone name', required=False, action='store', nargs='?', default='') + + args = parser.parse_args() + + if args.action == 'show': + show_zone_policy(args.name) diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql index a04138010..1133d79ed 100644 --- a/src/services/api/graphql/README.graphql +++ b/src/services/api/graphql/README.graphql @@ -1,7 +1,12 @@ +The following examples are in the form as entered in the GraphQL +'playground', which is found at: + +https://{{ host_address }}/graphql + Example using GraphQL mutations to configure a DHCP server: -This assumes that the http-api is running: +All examples assume that the http-api is running: 'set service https api' @@ -10,7 +15,7 @@ to run with that address as default router by requesting these 'mutations' in the GraphQL playground: mutation { - createInterfaceEthernet (data: {interface: "eth1", + CreateInterfaceEthernet (data: {interface: "eth1", address: "192.168.0.1/24", description: "BOB"}) { success @@ -22,10 +27,10 @@ mutation { } mutation { - createDhcpServer(data: {sharedNetworkName: "BOB", + CreateDhcpServer(data: {sharedNetworkName: "BOB", subnet: "192.168.0.0/24", defaultRouter: "192.168.0.1", - dnsServer: "192.168.0.1", + nameServer: "192.168.0.1", domainName: "vyos.net", lease: 86400, range: 0, @@ -42,37 +47,133 @@ mutation { } } -The GraphQL playground will be found at: +To save the configuration, use the following mutation: -https://{{ host_address }}/graphql +mutation { + SaveConfigFile(data: {fileName: "/config/config.boot"}) { + success + errors + data { + fileName + } + } +} + +N.B. fileName can be empty (fileName: "") or data can be empty (data: {}) to +save to /config/config.boot; to save to an alternative path, specify +fileName. + +Similarly, using an analogous 'endpoint' (meaning the form of the request +and resolver; the actual enpoint for all GraphQL requests is +https://hostname/graphql), one can load an arbitrary config file from a +path. + +mutation { + LoadConfigFile(data: {fileName: "/home/vyos/config.boot"}) { + success + errors + data { + fileName + } + } +} + +Op-mode 'show' commands may be requested by path, e.g.: + +query { + Show (data: {path: ["interfaces", "ethernet", "detail"]}) { + success + errors + data { + result + } + } +} + +N.B. to see the output the 'data' field 'result' must be present in the +request. + +Mutations to manipulate firewall address groups: + +mutation { + CreateFirewallAddressGroup (data: {name: "ADDR-GRP", address: "10.0.0.1"}) { + success + errors + } +} + +mutation { + UpdateFirewallAddressGroupMembers (data: {name: "ADDR-GRP", + address: ["10.0.0.1-10.0.0.8", "192.168.0.1"]}) { + success + errors + } +} + +mutation { + RemoveFirewallAddressGroupMembers (data: {name: "ADDR-GRP", + address: "192.168.0.1"}) { + success + errors + } +} -An equivalent curl command to the first example above would be: +N.B. The schema for the above specify that 'address' be of the form 'list of +strings' (SDL type [String!]! for UpdateFirewallAddressGroupMembers, where +the ! indicates that the input is required; SDL type [String] in +CreateFirewallAddressGroup, since a group may be created without any +addresses). However, notice that a single string may be passed without being +a member of a list, in which case the specification allows for 'input +coercion': + +http://spec.graphql.org/October2021/#sec-Scalars.Input-Coercion + +Similarly, IPv6 versions of the above: + +CreateFirewallAddressIpv6Group +UpdateFirewallAddressIpv6GroupMembers +RemoveFirewallAddressIpv6GroupMembers + + +Instead of using the GraphQL playground, an equivalent curl command to the +first example above would be: curl -k 'https://192.168.100.168/graphql' -H 'Content-Type: application/json' --data-binary '{"query": "mutation {createInterfaceEthernet (data: {interface: \"eth1\", address: \"192.168.0.1/24\", description: \"BOB\"}) {success errors data {address}}}"}' Note that the 'mutation' term is prefaced by 'query' in the curl command. +Curl equivalents may be read from within the GraphQL playground at the 'copy +curl' button. + What's here: services ├── api │  └── graphql +│  ├── bindings.py │  ├── graphql │  │  ├── directives.py │  │  ├── __init__.py │  │  ├── mutations.py │  │  └── schema +│  │  ├── config_file.graphql │  │  ├── dhcp_server.graphql +│  │  ├── firewall_group.graphql │  │  ├── interface_ethernet.graphql -│  │  └── schema.graphql +│  │  ├── schema.graphql +│  │  ├── show_config.graphql +│  │  └── show.graphql +│  ├── README.graphql │  ├── recipes -│  │  ├── dhcp_server.py │  │  ├── __init__.py -│  │  ├── interface_ethernet.py -│  │  ├── recipe.py +│  │  ├── remove_firewall_address_group_members.py +│  │  ├── session.py │  │  └── templates -│  │  ├── dhcp_server.tmpl -│  │  └── interface_ethernet.tmpl +│  │  ├── create_dhcp_server.tmpl +│  │  ├── create_firewall_address_group.tmpl +│  │  ├── create_interface_ethernet.tmpl +│  │  ├── remove_firewall_address_group_members.tmpl +│  │  └── update_firewall_address_group_members.tmpl │  └── state.py ├── vyos-configd ├── vyos-hostsd @@ -90,13 +191,14 @@ the Ur-data; the GraphQL schema is produced from those files, located in Resolvers for the schema Mutation fields are dynamically generated using a 'directive' added to the respective schema field. The directive, -'@generate', is handled by the class 'DataDirective' in -'api/graphql/graphql/directives.py', which calls the 'make_resolver' function in -'api/graphql/graphql/mutations.py'; the produced resolver calls the appropriate -wrapper in 'api/graphql/recipes', with base class doing the (overridable) -configuration steps of calling all defined 'set'/'delete' commands. - -Integrating the above with vyos-http-api-server is ~10 lines of code. +'@configure', is handled by the class 'ConfigureDirective' in +'api/graphql/graphql/directives.py', which calls the +'make_configure_resolver' function in 'api/graphql/graphql/mutations.py'; +the produced resolver calls the appropriate wrapper in +'api/graphql/recipes', with base class doing the (overridable) configuration +steps of calling all defined 'set'/'delete' commands. + +Integrating the above with vyos-http-api-server is 4 lines of code. What needs to be done: diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py new file mode 100644 index 000000000..84d719fda --- /dev/null +++ b/src/services/api/graphql/bindings.py @@ -0,0 +1,29 @@ +# Copyright 2021 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 vyos.defaults +from . graphql.queries import query +from . graphql.mutations import mutation +from . graphql.directives import directives_dict +from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers + +def generate_schema(): + api_schema_dir = vyos.defaults.directories['api_schema'] + + type_defs = load_schema_from_path(api_schema_dir) + + schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers, directives=directives_dict) + + return schema diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index 651421c35..0a9298f55 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -1,12 +1,27 @@ +# Copyright 2021 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/>. + from ariadne import SchemaDirectiveVisitor, ObjectType -from . mutations import make_resolver +from . queries import * +from . mutations import * -class DataDirective(SchemaDirectiveVisitor): - """ - Class providing implementation of 'generate' directive in schema. +def non(arg): + pass - """ - def visit_field_definition(self, field, object_type): +class VyosDirective(SchemaDirectiveVisitor): + def visit_field_definition(self, field, object_type, make_resolver=non): name = f'{field.type}' # field.type contains the return value of the mutation; trim value # to produce canonical name @@ -15,3 +30,50 @@ class DataDirective(SchemaDirectiveVisitor): func = make_resolver(name) field.resolve = func return field + + +class ConfigureDirective(VyosDirective): + """ + Class providing implementation of 'configure' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_configure_resolver) + +class ShowConfigDirective(VyosDirective): + """ + Class providing implementation of 'show' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_show_config_resolver) + +class ConfigFileDirective(VyosDirective): + """ + Class providing implementation of 'configfile' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_config_file_resolver) + +class ShowDirective(VyosDirective): + """ + Class providing implementation of 'show' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_show_resolver) + +class ImageDirective(VyosDirective): + """ + Class providing implementation of 'image' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_image_resolver) + +directives_dict = {"configure": ConfigureDirective, + "showconfig": ShowConfigDirective, + "configfile": ConfigFileDirective, + "show": ShowDirective, + "image": ImageDirective} diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 98c665c9a..0c3eb702a 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -1,3 +1,17 @@ +# Copyright 2021 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/>. from importlib import import_module from typing import Any, Dict @@ -6,10 +20,11 @@ from graphql import GraphQLResolveInfo from makefun import with_signature from .. import state +from api.graphql.recipes.session import Session mutation = ObjectType("Mutation") -def make_resolver(mutation_name): +def make_mutation_resolver(mutation_name, class_name, session_func): """Dynamically generate a resolver for the mutation named in the schema by 'mutation_name'. @@ -19,11 +34,11 @@ def make_resolver(mutation_name): functools.wraps. :raise Exception: - encapsulating ConfigErrors, or internal errors + raising ConfigErrors, or internal errors """ - class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1) + func_base_name = convert_camel_case_to_snake(class_name) - resolver_name = f'resolve_create_{func_base_name}' + resolver_name = f'resolve_{func_base_name}' func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)' @mutation.field(mutation_name) @@ -40,10 +55,18 @@ def make_resolver(mutation_name): data = kwargs['data'] session = state.settings['app'].state.vyos_session - mod = import_module(f'api.graphql.recipes.{func_base_name}') - klass = getattr(mod, class_name) + # one may override the session functions with a local subclass + try: + mod = import_module(f'api.graphql.recipes.{func_base_name}') + klass = getattr(mod, class_name) + except ImportError: + # otherwise, dynamically generate subclass to invoke subclass + # name based templates + klass = type(class_name, (Session,), {}) k = klass(session, data) - k.configure() + method = getattr(k, session_func) + result = method() + data['result'] = result return { "success": True, @@ -57,4 +80,20 @@ def make_resolver(mutation_name): return func_impl +def make_prefix_resolver(mutation_name, prefix=[]): + for pre in prefix: + Pre = pre.capitalize() + if Pre in mutation_name: + class_name = mutation_name.replace(Pre, '', 1) + return make_mutation_resolver(mutation_name, class_name, pre) + raise Exception + +def make_configure_resolver(mutation_name): + class_name = mutation_name + return make_mutation_resolver(mutation_name, class_name, 'configure') + +def make_config_file_resolver(mutation_name): + return make_prefix_resolver(mutation_name, prefix=['save', 'load']) +def make_image_resolver(mutation_name): + return make_prefix_resolver(mutation_name, prefix=['add', 'delete']) diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py new file mode 100644 index 000000000..e1868091e --- /dev/null +++ b/src/services/api/graphql/graphql/queries.py @@ -0,0 +1,89 @@ +# Copyright 2021 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/>. + +from importlib import import_module +from typing import Any, Dict +from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake +from graphql import GraphQLResolveInfo +from makefun import with_signature + +from .. import state +from api.graphql.recipes.session import Session + +query = ObjectType("Query") + +def make_query_resolver(query_name, class_name, session_func): + """Dynamically generate a resolver for the query named in the + schema by 'query_name'. + + Dynamic generation is provided using the package 'makefun' (via the + decorator 'with_signature'), which provides signature-preserving + function wrappers; it provides several improvements over, say, + functools.wraps. + + :raise Exception: + raising ConfigErrors, or internal errors + """ + + func_base_name = convert_camel_case_to_snake(class_name) + resolver_name = f'resolve_{func_base_name}' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)' + + @query.field(query_name) + @convert_kwargs_to_snake_case + @with_signature(func_sig, func_name=resolver_name) + async def func_impl(*args, **kwargs): + try: + if 'data' not in kwargs: + return { + "success": False, + "errors": ['missing data'] + } + + data = kwargs['data'] + session = state.settings['app'].state.vyos_session + + # one may override the session functions with a local subclass + try: + mod = import_module(f'api.graphql.recipes.{func_base_name}') + klass = getattr(mod, class_name) + except ImportError: + # otherwise, dynamically generate subclass to invoke subclass + # name based templates + klass = type(class_name, (Session,), {}) + k = klass(session, data) + method = getattr(k, session_func) + result = method() + data['result'] = result + + return { + "success": True, + "data": data + } + except Exception as error: + return { + "success": False, + "errors": [str(error)] + } + + return func_impl + +def make_show_config_resolver(query_name): + class_name = query_name + return make_query_resolver(query_name, class_name, 'show_config') + +def make_show_resolver(query_name): + class_name = query_name + return make_query_resolver(query_name, class_name, 'show') diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql new file mode 100644 index 000000000..31ab26b9e --- /dev/null +++ b/src/services/api/graphql/graphql/schema/config_file.graphql @@ -0,0 +1,27 @@ +input SaveConfigFileInput { + fileName: String +} + +type SaveConfigFile { + fileName: String +} + +type SaveConfigFileResult { + data: SaveConfigFile + success: Boolean! + errors: [String] +} + +input LoadConfigFileInput { + fileName: String! +} + +type LoadConfigFile { + fileName: String! +} + +type LoadConfigFileResult { + data: LoadConfigFile + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql index a7ee75d40..25f091bfa 100644 --- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql +++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql @@ -1,8 +1,8 @@ -input dhcpServerConfigInput { +input DhcpServerConfigInput { sharedNetworkName: String subnet: String defaultRouter: String - dnsServer: String + nameServer: String domainName: String lease: Int range: Int @@ -13,11 +13,11 @@ input dhcpServerConfigInput { dnsForwardingListenAddress: String } -type dhcpServerConfig { +type DhcpServerConfig { sharedNetworkName: String subnet: String defaultRouter: String - dnsServer: String + nameServer: String domainName: String lease: Int range: Int @@ -28,8 +28,8 @@ type dhcpServerConfig { dnsForwardingListenAddress: String } -type createDhcpServerResult { - data: dhcpServerConfig +type CreateDhcpServerResult { + data: DhcpServerConfig success: Boolean! errors: [String] } diff --git a/src/services/api/graphql/graphql/schema/firewall_group.graphql b/src/services/api/graphql/graphql/schema/firewall_group.graphql new file mode 100644 index 000000000..d89904b9e --- /dev/null +++ b/src/services/api/graphql/graphql/schema/firewall_group.graphql @@ -0,0 +1,95 @@ +input CreateFirewallAddressGroupInput { + name: String! + address: [String] +} + +type CreateFirewallAddressGroup { + name: String! + address: [String] +} + +type CreateFirewallAddressGroupResult { + data: CreateFirewallAddressGroup + success: Boolean! + errors: [String] +} + +input UpdateFirewallAddressGroupMembersInput { + name: String! + address: [String!]! +} + +type UpdateFirewallAddressGroupMembers { + name: String! + address: [String!]! +} + +type UpdateFirewallAddressGroupMembersResult { + data: UpdateFirewallAddressGroupMembers + success: Boolean! + errors: [String] +} + +input RemoveFirewallAddressGroupMembersInput { + name: String! + address: [String!]! +} + +type RemoveFirewallAddressGroupMembers { + name: String! + address: [String!]! +} + +type RemoveFirewallAddressGroupMembersResult { + data: RemoveFirewallAddressGroupMembers + success: Boolean! + errors: [String] +} + +input CreateFirewallAddressIpv6GroupInput { + name: String! + address: [String] +} + +type CreateFirewallAddressIpv6Group { + name: String! + address: [String] +} + +type CreateFirewallAddressIpv6GroupResult { + data: CreateFirewallAddressIpv6Group + success: Boolean! + errors: [String] +} + +input UpdateFirewallAddressIpv6GroupMembersInput { + name: String! + address: [String!]! +} + +type UpdateFirewallAddressIpv6GroupMembers { + name: String! + address: [String!]! +} + +type UpdateFirewallAddressIpv6GroupMembersResult { + data: UpdateFirewallAddressIpv6GroupMembers + success: Boolean! + errors: [String] +} + +input RemoveFirewallAddressIpv6GroupMembersInput { + name: String! + address: [String!]! +} + +type RemoveFirewallAddressIpv6GroupMembers { + name: String! + address: [String!]! +} + +type RemoveFirewallAddressIpv6GroupMembersResult { + data: RemoveFirewallAddressIpv6GroupMembers + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/image.graphql b/src/services/api/graphql/graphql/schema/image.graphql new file mode 100644 index 000000000..7d1b4f9d0 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/image.graphql @@ -0,0 +1,29 @@ +input AddSystemImageInput { + location: String! +} + +type AddSystemImage { + location: String + result: String +} + +type AddSystemImageResult { + data: AddSystemImage + success: Boolean! + errors: [String] +} + +input DeleteSystemImageInput { + name: String! +} + +type DeleteSystemImage { + name: String + result: String +} + +type DeleteSystemImageResult { + data: DeleteSystemImage + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql index fdcf97bad..32438b315 100644 --- a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql +++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql @@ -1,18 +1,18 @@ -input interfaceEthernetConfigInput { +input InterfaceEthernetConfigInput { interface: String address: String replace: Boolean = true description: String } -type interfaceEthernetConfig { +type InterfaceEthernetConfig { interface: String address: String description: String } -type createInterfaceEthernetResult { - data: interfaceEthernetConfig +type CreateInterfaceEthernetResult { + data: InterfaceEthernetConfig success: Boolean! errors: [String] } diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 8a5e17962..952e46f34 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -3,13 +3,28 @@ schema { mutation: Mutation } +directive @configure on FIELD_DEFINITION +directive @configfile on FIELD_DEFINITION +directive @show on FIELD_DEFINITION +directive @showconfig on FIELD_DEFINITION +directive @image on FIELD_DEFINITION + type Query { - _dummy: String + Show(data: ShowInput) : ShowResult @show + ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig } -directive @generate on FIELD_DEFINITION - type Mutation { - createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate - createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate + CreateDhcpServer(data: DhcpServerConfigInput) : CreateDhcpServerResult @configure + CreateInterfaceEthernet(data: InterfaceEthernetConfigInput) : CreateInterfaceEthernetResult @configure + CreateFirewallAddressGroup(data: CreateFirewallAddressGroupInput) : CreateFirewallAddressGroupResult @configure + UpdateFirewallAddressGroupMembers(data: UpdateFirewallAddressGroupMembersInput) : UpdateFirewallAddressGroupMembersResult @configure + RemoveFirewallAddressGroupMembers(data: RemoveFirewallAddressGroupMembersInput) : RemoveFirewallAddressGroupMembersResult @configure + CreateFirewallAddressIpv6Group(data: CreateFirewallAddressIpv6GroupInput) : CreateFirewallAddressIpv6GroupResult @configure + UpdateFirewallAddressIpv6GroupMembers(data: UpdateFirewallAddressIpv6GroupMembersInput) : UpdateFirewallAddressIpv6GroupMembersResult @configure + RemoveFirewallAddressIpv6GroupMembers(data: RemoveFirewallAddressIpv6GroupMembersInput) : RemoveFirewallAddressIpv6GroupMembersResult @configure + SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configfile + LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configfile + AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @image + DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @image } diff --git a/src/services/api/graphql/graphql/schema/show.graphql b/src/services/api/graphql/graphql/schema/show.graphql new file mode 100644 index 000000000..c7709e48b --- /dev/null +++ b/src/services/api/graphql/graphql/schema/show.graphql @@ -0,0 +1,14 @@ +input ShowInput { + path: [String!]! +} + +type Show { + path: [String] + result: String +} + +type ShowResult { + data: Show + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/show_config.graphql b/src/services/api/graphql/graphql/schema/show_config.graphql new file mode 100644 index 000000000..34afd2aa9 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/show_config.graphql @@ -0,0 +1,21 @@ +""" +Use 'scalar Generic' for show config output, to avoid attempts to +JSON-serialize in case of JSON output. +""" +scalar Generic + +input ShowConfigInput { + path: [String!]! + configFormat: String +} + +type ShowConfig { + path: [String] + result: Generic +} + +type ShowConfigResult { + data: ShowConfig + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py deleted file mode 100644 index 3edb3028e..000000000 --- a/src/services/api/graphql/recipes/dhcp_server.py +++ /dev/null @@ -1,13 +0,0 @@ - -from . recipe import Recipe - -class DhcpServer(Recipe): - def __init__(self, session, command_file): - super().__init__(session, command_file) - - # Define any custom processing of parameters here by overriding - # configure: - # - # def configure(self): - # self.data = transform_data(self.data) - # super().configure() diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py deleted file mode 100644 index f88f5924f..000000000 --- a/src/services/api/graphql/recipes/interface_ethernet.py +++ /dev/null @@ -1,13 +0,0 @@ - -from . recipe import Recipe - -class InterfaceEthernet(Recipe): - def __init__(self, session, command_file): - super().__init__(session, command_file) - - # Define any custom processing of parameters here by overriding - # configure: - # - # def configure(self): - # self.data = transform_data(self.data) - # super().configure() diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py deleted file mode 100644 index 8fbb9e0bf..000000000 --- a/src/services/api/graphql/recipes/recipe.py +++ /dev/null @@ -1,49 +0,0 @@ -from ariadne import convert_camel_case_to_snake -import vyos.defaults -from vyos.template import render - -class Recipe(object): - def __init__(self, session, data): - self._session = session - self.data = data - self._name = convert_camel_case_to_snake(type(self).__name__) - - @property - def data(self): - return self.__data - - @data.setter - def data(self, data): - if isinstance(data, dict): - self.__data = data - else: - raise ValueError("data must be of type dict") - - def configure(self): - session = self._session - data = self.data - func_base_name = self._name - - tmpl_file = f'{func_base_name}.tmpl' - cmd_file = f'/tmp/{func_base_name}.cmds' - tmpl_dir = vyos.defaults.directories['api_templates'] - - try: - render(cmd_file, tmpl_file, data, location=tmpl_dir) - commands = [] - with open(cmd_file) as f: - lines = f.readlines() - for line in lines: - commands.append(line.split()) - for cmd in commands: - if cmd[0] == 'set': - session.set(cmd[1:]) - elif cmd[0] == 'delete': - session.delete(cmd[1:]) - else: - raise ValueError('Operation must be "set" or "delete"') - session.commit() - except Exception as error: - raise error - - diff --git a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py new file mode 100644 index 000000000..b91932e14 --- /dev/null +++ b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py @@ -0,0 +1,35 @@ +# Copyright 2021 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/>. + +from . session import Session + +class RemoveFirewallAddressGroupMembers(Session): + def __init__(self, session, data): + super().__init__(session, data) + + # Define any custom processing of parameters here by overriding + # configure: + # + # def configure(self): + # self._data = transform_data(self._data) + # super().configure() + # self.clean_up() + + def configure(self): + super().configure() + + group_name = self._data['name'] + path = ['firewall', 'group', 'address-group', group_name] + self.delete_path_if_childless(path) diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py new file mode 100644 index 000000000..1f844ff70 --- /dev/null +++ b/src/services/api/graphql/recipes/session.py @@ -0,0 +1,138 @@ +# Copyright 2021 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 json + +from ariadne import convert_camel_case_to_snake + +import vyos.defaults +from vyos.config import Config +from vyos.configtree import ConfigTree +from vyos.template import render + +class Session: + """ + Wrapper for calling configsession functions based on GraphQL requests. + Non-nullable fields in the respective schema allow avoiding a key check + in 'data'. + """ + def __init__(self, session, data): + self._session = session + self._data = data + self._name = convert_camel_case_to_snake(type(self).__name__) + + def configure(self): + session = self._session + data = self._data + func_base_name = self._name + + tmpl_file = f'{func_base_name}.tmpl' + cmd_file = f'/tmp/{func_base_name}.cmds' + tmpl_dir = vyos.defaults.directories['api_templates'] + + try: + render(cmd_file, tmpl_file, data, location=tmpl_dir) + commands = [] + with open(cmd_file) as f: + lines = f.readlines() + for line in lines: + commands.append(line.split()) + for cmd in commands: + if cmd[0] == 'set': + session.set(cmd[1:]) + elif cmd[0] == 'delete': + session.delete(cmd[1:]) + else: + raise ValueError('Operation must be "set" or "delete"') + session.commit() + except Exception as error: + raise error + + def delete_path_if_childless(self, path): + session = self._session + config = Config(session.get_session_env()) + if not config.list_nodes(path): + session.delete(path) + session.commit() + + def show_config(self): + session = self._session + data = self._data + out = '' + + try: + out = session.show_config(data['path']) + if data.get('config_format', '') == 'json': + config_tree = vyos.configtree.ConfigTree(out) + out = json.loads(config_tree.to_json()) + except Exception as error: + raise error + + return out + + def save(self): + session = self._session + data = self._data + if 'file_name' not in data or not data['file_name']: + data['file_name'] = '/config/config.boot' + + try: + session.save_config(data['file_name']) + except Exception as error: + raise error + + def load(self): + session = self._session + data = self._data + + try: + session.load_config(data['file_name']) + session.commit() + except Exception as error: + raise error + + def show(self): + session = self._session + data = self._data + out = '' + + try: + out = session.show(data['path']) + except Exception as error: + raise error + + return out + + def add(self): + session = self._session + data = self._data + + try: + res = session.install_image(data['location']) + except Exception as error: + raise error + + return res + + def delete(self): + session = self._session + data = self._data + + try: + res = session.remove_image(data['name']) + except Exception as error: + raise error + + return res diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl index 629ce83c1..70de43183 100644 --- a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl +++ b/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl @@ -1,5 +1,5 @@ set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }} -set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} dns-server {{ dns_server }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} name-server {{ name_server }} set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }} set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }} set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }} diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl new file mode 100644 index 000000000..a890d0086 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl @@ -0,0 +1,4 @@ +set firewall group address-group {{ name }} +{% for add in address %} +set firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl b/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl new file mode 100644 index 000000000..e9b660722 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl @@ -0,0 +1,4 @@ +set firewall group ipv6-address-group {{ name }} +{% for add in address %} +set firewall group ipv6-address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl index d9d7ed691..d9d7ed691 100644 --- a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl +++ b/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl new file mode 100644 index 000000000..458f3e5fc --- /dev/null +++ b/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +delete firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl new file mode 100644 index 000000000..0efa0b226 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +delete firewall group ipv6-address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl new file mode 100644 index 000000000..f56c61231 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +set firewall group address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl new file mode 100644 index 000000000..f98a5517c --- /dev/null +++ b/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl @@ -0,0 +1,3 @@ +{% for add in address %} +set firewall group ipv6-address-group {{ name }} address {{ add }} +{% endfor %} diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 670b6e66a..48c9135e2 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -28,6 +28,7 @@ import zmq from contextlib import contextmanager from vyos.defaults import directories +from vyos.util import boot_configuration_complete from vyos.configsource import ConfigSourceString, ConfigSourceError from vyos.config import Config from vyos import ConfigError @@ -186,7 +187,7 @@ def initialization(socket): session_out = None # if not a 'live' session, for example on boot, write to file - if not session_out or not os.path.isfile('/tmp/vyos-config-status'): + if not session_out or not boot_configuration_complete(): session_out = script_stdout_log session_mode = 'a' diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 4c4bb036e..df9f18d2d 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -139,6 +139,27 @@ # } # # +### authoritative_zones +## Additional zones hosted authoritatively by pdns-recursor. +## We add NTAs for these zones but do not do much else here. +# +# { 'type': 'authoritative_zones', +# 'op': 'add', +# 'data': ['<str zone>', ...] +# } +# +# { 'type': 'authoritative_zones', +# 'op': 'delete', +# 'data': ['<str zone>', ...] +# } +# +# { 'type': 'authoritative_zones', +# 'op': 'get', +# } +# response: +# { 'data': ['<str zone>', ...] } +# +# ### search_domains # # { 'type': 'search_domains', @@ -255,6 +276,7 @@ STATE = { "name_server_tags_recursor": [], "name_server_tags_system": [], "forward_zones": {}, + "authoritative_zones": [], "hosts": {}, "host_name": "vyos", "domain_name": "", @@ -267,7 +289,8 @@ base_schema = Schema({ Required('op'): Any('add', 'delete', 'set', 'get', 'apply'), 'type': Any('name_servers', 'name_server_tags_recursor', 'name_server_tags_system', - 'forward_zones', 'search_domains', 'hosts', 'host_name'), + 'forward_zones', 'authoritative_zones', 'search_domains', + 'hosts', 'host_name'), 'data': Any(list, dict), 'tag': str, 'tag_regex': str @@ -317,7 +340,7 @@ hosts_add_schema = op_type_schema.extend({ 'data': { str: { str: { - 'address': str, + 'address': [str], 'aliases': [str] } } @@ -347,6 +370,11 @@ msg_schema_map = { 'delete': data_list_schema, 'get': op_type_schema }, + 'authoritative_zones': { + 'add': data_list_schema, + 'delete': data_list_schema, + 'get': op_type_schema + }, 'search_domains': { 'add': data_dict_list_schema, 'delete': data_list_schema, @@ -522,7 +550,7 @@ def handle_message(msg): data = get_option(msg, 'data') if _type in ['name_servers', 'forward_zones', 'search_domains', 'hosts']: delete_items_from_dict(STATE[_type], data) - elif _type in ['name_server_tags_recursor', 'name_server_tags_system']: + elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'authoritative_zones']: delete_items_from_list(STATE[_type], data) else: raise ValueError(f'Operation "{op}" unknown data type "{_type}"') @@ -534,7 +562,7 @@ def handle_message(msg): elif _type in ['forward_zones', 'hosts']: add_items_to_dict(STATE[_type], data) # maybe we need to rec_control clear-nta each domain that was removed here? - elif _type in ['name_server_tags_recursor', 'name_server_tags_system']: + elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'authoritative_zones']: add_items_to_list(STATE[_type], data) else: raise ValueError(f'Operation "{op}" unknown data type "{_type}"') @@ -550,7 +578,7 @@ def handle_message(msg): if _type in ['name_servers', 'search_domains', 'hosts']: tag_regex = get_option(msg, 'tag_regex') result = get_items_from_dict_regex(STATE[_type], tag_regex) - elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'forward_zones']: + elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'forward_zones', 'authoritative_zones']: result = STATE[_type] else: raise ValueError(f'Operation "{op}" unknown data type "{_type}"') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index cb4ce4072..06871f1d6 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -32,16 +32,14 @@ from fastapi.responses import HTMLResponse from fastapi.exceptions import RequestValidationError from fastapi.routing import APIRoute from pydantic import BaseModel, StrictStr, validator -from starlette.datastructures import FormData, MutableHeaders +from starlette.middleware.cors import CORSMiddleware +from starlette.datastructures import FormData from starlette.formparsers import FormParser, MultiPartParser from multipart.multipart import parse_options_header -from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers from ariadne.asgi import GraphQL import vyos.config -import vyos.defaults - from vyos.configsession import ConfigSession, ConfigSessionError import api.graphql.state @@ -69,11 +67,11 @@ def load_server_config(): return config def check_auth(key_list, key): - id = None + key_id = None for k in key_list: if k['key'] == key: - id = k['id'] - return id + key_id = k['id'] + return key_id def error(code, msg): resp = {"success": False, "error": msg, "data": None} @@ -223,10 +221,10 @@ responses = { def auth_required(data: ApiModel): key = data.key api_keys = app.state.vyos_keys - id = check_auth(api_keys, key) - if not id: + key_id = check_auth(api_keys, key) + if not key_id: raise HTTPException(status_code=401, detail="Valid API key is required") - app.state.vyos_id = id + app.state.vyos_id = key_id # override Request and APIRoute classes in order to convert form request to json; # do all explicit validation here, for backwards compatability of error messages; @@ -613,18 +611,19 @@ def show_op(data: ShowModel): # GraphQL integration ### -api.graphql.state.init() - -from api.graphql.graphql.mutations import mutation -from api.graphql.graphql.directives import DataDirective +def graphql_init(fast_api_app): + from api.graphql.bindings import generate_schema -api_schema_dir = vyos.defaults.directories['api_schema'] - -type_defs = load_schema_from_path(api_schema_dir) + api.graphql.state.init() + api.graphql.state.settings['app'] = app -schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective}) + schema = generate_schema() -app.add_route('/graphql', GraphQL(schema, debug=True)) + if app.state.vyos_origins: + origins = app.state.vyos_origins + app.add_route('/graphql', CORSMiddleware(GraphQL(schema, debug=True), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS"))) + else: + app.add_route('/graphql', GraphQL(schema, debug=True)) ### @@ -640,23 +639,28 @@ if __name__ == '__main__': try: server_config = load_server_config() - except Exception as e: - logger.critical("Failed to load the HTTP API server config: {0}".format(e)) + except Exception as err: + logger.critical(f"Failed to load the HTTP API server config: {err}") - session = ConfigSession(os.getpid()) + config_session = ConfigSession(os.getpid()) - app.state.vyos_session = session + app.state.vyos_session = config_session app.state.vyos_keys = server_config['api_keys'] - app.state.vyos_debug = True if server_config['debug'] == 'true' else False - app.state.vyos_strict = True if server_config['strict'] == 'true' else False + app.state.vyos_debug = server_config['debug'] + app.state.vyos_strict = server_config['strict'] + app.state.vyos_origins = server_config.get('cors', {}).get('origins', []) - api.graphql.state.settings['app'] = app + graphql_init(app) try: - uvicorn.run(app, host=server_config["listen_address"], - port=int(server_config["port"]), - proxy_headers=True) - except OSError as e: - logger.critical(f"OSError {e}") + if not server_config['socket']: + uvicorn.run(app, host=server_config["listen_address"], + port=int(server_config["port"]), + proxy_headers=True) + else: + uvicorn.run(app, uds="/run/api.sock", + proxy_headers=True) + except OSError as err: + logger.critical(f"OSError {err}") sys.exit(1) diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index 1fba0d75b..b1fe7e43f 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -29,6 +29,7 @@ from logging.handlers import SysLogHandler from vyos.ifconfig.vrrp import VRRP from vyos.configquery import ConfigTreeQuery from vyos.util import cmd +from vyos.util import dict_search # configure logging logger = logging.getLogger(__name__) @@ -69,22 +70,10 @@ class KeepalivedFifo: raise ValueError() # Read VRRP configuration directly from CLI - vrrp_config_dict = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) - self.vrrp_config = {'vrrp_groups': {}, 'sync_groups': {}} - for key in ['group', 'sync_group']: - if key not in vrrp_config_dict: - continue - for group, group_config in vrrp_config_dict[key].items(): - if 'transition_script' not in group_config: - continue - self.vrrp_config['vrrp_groups'][group] = { - 'STOP': group_config['transition_script'].get('stop'), - 'FAULT': group_config['transition_script'].get('fault'), - 'BACKUP': group_config['transition_script'].get('backup'), - 'MASTER': group_config['transition_script'].get('master'), - } - logger.info(f'Loaded configuration: {self.vrrp_config}') + self.vrrp_config_dict = conf.get_config_dict(base, + key_mangling=('-', '_'), get_first_key=True) + + logger.debug(f'Loaded configuration: {self.vrrp_config_dict}') except Exception as err: logger.error(f'Unable to load configuration: {err}') @@ -129,20 +118,17 @@ class KeepalivedFifo: if os.path.exists(mdns_running_file): cmd(mdns_update_command) - if n_name in self.vrrp_config['vrrp_groups'] and n_state in self.vrrp_config['vrrp_groups'][n_name]: - n_script = self.vrrp_config['vrrp_groups'][n_name].get(n_state) - if n_script: - self._run_command(n_script) + tmp = dict_search(f'group.{n_name}.transition_script.{n_state.lower()}', self.vrrp_config_dict) + if tmp != None: + self._run_command(tmp) # check and run commands for VRRP sync groups - # currently, this is not available in VyOS CLI - if n_type == 'GROUP': + elif n_type == 'GROUP': if os.path.exists(mdns_running_file): cmd(mdns_update_command) - if n_name in self.vrrp_config['sync_groups'] and n_state in self.vrrp_config['sync_groups'][n_name]: - n_script = self.vrrp_config['sync_groups'][n_name].get(n_state) - if n_script: - self._run_command(n_script) + tmp = dict_search(f'sync_group.{n_name}.transition_script.{n_state.lower()}', self.vrrp_config_dict) + if tmp != None: + self._run_command(tmp) # mark task in queue as done self.message_queue.task_done() except Exception as err: diff --git a/src/systemd/dhcp6c@.service b/src/systemd/dhcp6c@.service index fdd6d7d88..9a97ee261 100644 --- a/src/systemd/dhcp6c@.service +++ b/src/systemd/dhcp6c@.service @@ -9,7 +9,7 @@ StartLimitIntervalSec=0 WorkingDirectory=/run/dhcp6c Type=forking PIDFile=/run/dhcp6c/dhcp6c.%i.pid -ExecStart=/usr/sbin/dhcp6c -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i +ExecStart=/usr/sbin/dhcp6c -D -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i Restart=on-failure RestartSec=20 diff --git a/src/systemd/root-partition-auto-resize.service b/src/systemd/root-partition-auto-resize.service new file mode 100644 index 000000000..a57fbc3d8 --- /dev/null +++ b/src/systemd/root-partition-auto-resize.service @@ -0,0 +1,12 @@ +[Unit] +Description=VyOS root partition auto resizing +After=multi-user.target + +[Service] +Type=oneshot +User=root +Group=root +ExecStart=/usr/libexec/vyos/op_mode/force_root-partition-auto-resize.sh + +[Install] +WantedBy=vyos.target
\ No newline at end of file diff --git a/src/systemd/tftpd@.service b/src/systemd/tftpd@.service index 266bc0962..a674bf598 100644 --- a/src/systemd/tftpd@.service +++ b/src/systemd/tftpd@.service @@ -7,7 +7,7 @@ RequiresMountsFor=/run Type=forking #NotifyAccess=main EnvironmentFile=-/etc/default/tftpd%I -ExecStart=/usr/sbin/in.tftpd "$DAEMON_ARGS" +ExecStart=/bin/sh -c "${VRF_ARGS} /usr/sbin/in.tftpd ${DAEMON_ARGS}" Restart=on-failure [Install] diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service index b77335778..4da55f518 100644 --- a/src/systemd/vyos-hostsd.service +++ b/src/systemd/vyos-hostsd.service @@ -7,7 +7,7 @@ DefaultDependencies=no # Seemingly sensible way to say "as early as the system is ready" # All vyos-hostsd needs is read/write mounted root -After=systemd-remount-fs.service +After=systemd-remount-fs.service cloud-init.service [Service] WorkingDirectory=/run/vyos-hostsd diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service deleted file mode 100644 index ba5df5984..000000000 --- a/src/systemd/vyos-http-api.service +++ /dev/null @@ -1,23 +0,0 @@ -[Unit] -Description=VyOS HTTP API service -After=auditd.service systemd-user-sessions.service time-sync.target vyos-router.service -Requires=vyos-router.service - -[Service] -ExecStartPre=/usr/libexec/vyos/init/vyos-config -ExecStart=/usr/libexec/vyos/services/vyos-http-api-server -Type=idle - -SyslogIdentifier=vyos-http-api -SyslogFacility=daemon - -Restart=on-failure - -# Does't work but leave it here -User=root -Group=vyattacfg - -[Install] -# Installing in a earlier target leaves ExecStartPre waiting -WantedBy=getty.target - diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client index d4d38315a..a0515951a 100755 --- a/src/utils/vyos-hostsd-client +++ b/src/utils/vyos-hostsd-client @@ -129,7 +129,8 @@ try: params = h.split(",") if len(params) < 2: raise ValueError("Malformed host entry") - entry['address'] = params[1] + # Address needs to be a list because of changes made in T2683 + entry['address'] = [params[1]] entry['aliases'] = params[2:] data[params[0]] = entry client.add_hosts({args.tag: data}) diff --git a/src/validators/bgp-large-community-list b/src/validators/bgp-large-community-list index c07268e81..80112dfdc 100755 --- a/src/validators/bgp-large-community-list +++ b/src/validators/bgp-large-community-list @@ -30,7 +30,7 @@ if __name__ == '__main__': sys.exit(1) if not (re.match(pattern, sys.argv[1]) and - (is_ipv4(value[0]) or value[0].isdigit()) and value[1].isdigit()): + (is_ipv4(value[0]) or value[0].isdigit()) and (value[1].isdigit() or value[1] == '*')): sys.exit(1) sys.exit(0) diff --git a/src/validators/bgp-route-target b/src/validators/bgp-rd-rt index e7e4d403f..b2b69c9be 100755 --- a/src/validators/bgp-route-target +++ b/src/validators/bgp-rd-rt @@ -19,29 +19,37 @@ from vyos.template import is_ipv4 parser = ArgumentParser() group = parser.add_mutually_exclusive_group() -group.add_argument('--single', action='store', help='Validate and allow only one route-target') -group.add_argument('--multi', action='store', help='Validate multiple, whitespace separated route-targets') +group.add_argument('--route-distinguisher', action='store', help='Validate BGP route distinguisher') +group.add_argument('--route-target', action='store', help='Validate one BGP route-target') +group.add_argument('--route-target-multi', action='store', help='Validate multiple, whitespace separated BGP route-targets') args = parser.parse_args() -def is_valid_rt(rt): - # every route target needs to have a colon and must consists of two parts +def is_valid(rt): + """ Verify BGP RD/RT - both can be verified using the same logic """ + # every RD/RT (route distinguisher/route target) needs to have a colon and + # must consists of two parts value = rt.split(':') if len(value) != 2: return False - # A route target must either be only numbers, or the first part must be an - # IPv4 address + + # An RD/RT must either be only numbers, or the first part must be an IPv4 + # address if (is_ipv4(value[0]) or value[0].isdigit()) and value[1].isdigit(): return True return False if __name__ == '__main__': - if args.single: - if not is_valid_rt(args.single): + if args.route_distinguisher: + if not is_valid(args.route_distinguisher): + exit(1) + + elif args.route_target: + if not is_valid(args.route_target): exit(1) - elif args.multi: - for rt in args.multi.split(' '): - if not is_valid_rt(rt): + elif args.route_target_multi: + for rt in args.route_target_multi.split(' '): + if not is_valid(rt): exit(1) else: diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol index 078f8e319..7898fa6d0 100755 --- a/src/validators/ip-protocol +++ b/src/validators/ip-protocol @@ -31,7 +31,7 @@ if __name__ == '__main__': pattern = "!?\\b(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|" \ "tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|" \ - "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|" \ + "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|icmpv6|" \ "ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|" \ "encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|" \ "udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc)\\b" diff --git a/src/validators/ipv4-multicast b/src/validators/ipv4-multicast index e5cbc9532..5465c728d 100755 --- a/src/validators/ipv4-multicast +++ b/src/validators/ipv4-multicast @@ -1,3 +1,3 @@ #!/bin/sh -ipaddrcheck --is-ipv4-multicast $1 +ipaddrcheck --is-ipv4-multicast $1 && ipaddrcheck --is-ipv4-single $1 diff --git a/src/validators/ipv6-link-local b/src/validators/ipv6-link-local new file mode 100755 index 000000000..05e693b77 --- /dev/null +++ b/src/validators/ipv6-link-local @@ -0,0 +1,12 @@ +#!/usr/bin/python3 + +import sys +from vyos.validate import is_ipv6_link_local + +if __name__ == '__main__': + if len(sys.argv)>1: + addr = sys.argv[1] + if not is_ipv6_link_local(addr): + sys.exit(1) + + sys.exit(0) diff --git a/src/validators/ipv6-multicast b/src/validators/ipv6-multicast index 66cd90c9c..5afc437e5 100755 --- a/src/validators/ipv6-multicast +++ b/src/validators/ipv6-multicast @@ -1,3 +1,3 @@ #!/bin/sh -ipaddrcheck --is-ipv6-multicast $1 +ipaddrcheck --is-ipv6-multicast $1 && ipaddrcheck --is-ipv6-single $1 diff --git a/src/validators/range b/src/validators/range new file mode 100755 index 000000000..d4c25f3c4 --- /dev/null +++ b/src/validators/range @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 re +import sys +import argparse + +class MalformedRange(Exception): + pass + +def validate_range(value, min=None, max=None): + try: + lower, upper = re.match(r'^(\d+)-(\d+)$', value).groups() + + lower, upper = int(lower), int(upper) + + if int(lower) > int(upper): + raise MalformedRange("the lower bound exceeds the upper bound".format(value)) + + if min is not None: + if lower < min: + raise MalformedRange("the lower bound must not be less than {}".format(min)) + + if max is not None: + if upper > max: + raise MalformedRange("the upper bound must not be greater than {}".format(max)) + + except (AttributeError, ValueError): + raise MalformedRange("range syntax error") + +parser = argparse.ArgumentParser(description='Range validator.') +parser.add_argument('--min', type=int, action='store') +parser.add_argument('--max', type=int, action='store') +parser.add_argument('value', action='store') + +if __name__ == '__main__': + args = parser.parse_args() + + try: + validate_range(args.value, min=args.min, max=args.max) + except MalformedRange as e: + print("Incorrect range '{}': {}".format(args.value, e)) + sys.exit(1) diff --git a/src/validators/script b/src/validators/script index 1d8a27e5c..4ffdeb2a0 100755 --- a/src/validators/script +++ b/src/validators/script @@ -36,7 +36,7 @@ if __name__ == '__main__': # File outside the config dir is just a warning if not vyos.util.file_is_persistent(script): - sys.exit( - f'Warning: file {path} is outside the / config directory\n' + sys.exit(0)( + f'Warning: file {script} is outside the "/config" directory\n' 'It will not be automatically migrated to a new image on system update' ) |