diff options
171 files changed, 9276 insertions, 1034 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' diff --git a/data/configd-include.json b/data/configd-include.json index ee4cb0d42..c85ab0725 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -7,6 +7,7 @@ "dns_forwarding.py", "dynamic_dns.py", "flow_accounting_conf.py", +"high-availability.py", "host_name.py", "https.py", "igmp_proxy.py", @@ -54,6 +55,7 @@ "service_mdns-repeater.py", "service_pppoe-server.py", "service_router-advert.py", +"service_upnp.py", "ssh.py", "system-ip.py", "system-ipv6.py", @@ -69,6 +71,5 @@ "vpn_pptp.py", "vpn_sstp.py", "vrf.py", -"vrf_vni.py", -"vrrp.py" +"vrf_vni.py" ] diff --git a/data/templates/conntrack/nftables-ct.tmpl b/data/templates/conntrack/nftables-ct.tmpl new file mode 100644 index 000000000..c0fe5297d --- /dev/null +++ b/data/templates/conntrack/nftables-ct.tmpl @@ -0,0 +1,52 @@ +#!/usr/sbin/nft -f + +{% set nft_ct_ignore_name = 'VYOS_CT_IGNORE' %} +{% set nft_ct_timeout_name = 'VYOS_CT_TIMEOUT' %} + +# we first flush all chains and render the content from scratch - this makes +# any delta check obsolete +flush chain raw {{ nft_ct_ignore_name }} +flush chain raw {{ nft_ct_timeout_name }} + +table raw { + chain {{ nft_ct_ignore_name }} { +{% if ignore is defined and ignore.rule is defined and ignore.rule is not none %} +{% for rule, rule_config in ignore.rule.items() %} + # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is defined and rule_config.description is not none }} +{% set nft_command = '' %} +{% if rule_config.inbound_interface is defined and rule_config.inbound_interface is not none %} +{% set nft_command = nft_command ~ ' iifname ' ~ rule_config.inbound_interface %} +{% endif %} +{% if rule_config.protocol is defined and rule_config.protocol is not none %} +{% set nft_command = nft_command ~ ' ip protocol ' ~ rule_config.protocol %} +{% endif %} +{% if rule_config.destination is defined and rule_config.destination is not none %} +{% if rule_config.destination.address is defined and rule_config.destination.address is not none %} +{% set nft_command = nft_command ~ ' ip daddr ' ~ rule_config.destination.address %} +{% endif %} +{% if rule_config.destination.port is defined and rule_config.destination.port is not none %} +{% set nft_command = nft_command ~ ' ' ~ rule_config.protocol ~ ' dport { ' ~ rule_config.destination.port ~ ' }' %} +{% endif %} +{% endif %} +{% if rule_config.source is defined and rule_config.source is not none %} +{% if rule_config.source.address is defined and rule_config.source.address is not none %} +{% set nft_command = nft_command ~ ' ip saddr ' ~ rule_config.source.address %} +{% endif %} +{% if rule_config.source.port is defined and rule_config.source.port is not none %} +{% set nft_command = nft_command ~ ' ' ~ rule_config.protocol ~ ' sport { ' ~ rule_config.source.port ~ ' }' %} +{% endif %} +{% endif %} + {{ nft_command }} counter notrack comment ignore-{{ rule }} +{% endfor %} +{% endif %} + return + } + chain {{ nft_ct_timeout_name }} { +{% if timeout is defined and timeout.custom is defined and timeout.custom.rule is defined and timeout.custom.rule is not none %} +{% for rule, rule_config in timeout.custom.rule.items() %} + # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is defined and rule_config.description is not none }} +{% endfor %} +{% endif %} + return + } +} diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl index 233e2cc53..da2f28ced 100644 --- a/data/templates/dhcp-server/dhcpd.conf.tmpl +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -165,7 +165,7 @@ shared-network {{ network | replace('_','-') }} { option wpad-url "{{ subnet_config.wpad_url }}"; {% endif %} {% if subnet_config.client_prefix_length is defined and subnet_config.client_prefix_length is not none %} - option subnet-mask {{ subnet_config.client_prefix_length }}; + option subnet-mask {{ ('0.0.0.0/' ~ subnet_config.client_prefix_length) | netmask_from_cidr }}; {% endif %} {% if subnet_config.lease is defined and subnet_config.lease is not none %} default-lease-time {{ subnet_config.lease }}; diff --git a/data/templates/firewall/nftables-defines.tmpl b/data/templates/firewall/nftables-defines.tmpl new file mode 100644 index 000000000..d9eb7c199 --- /dev/null +++ b/data/templates/firewall/nftables-defines.tmpl @@ -0,0 +1,32 @@ +{% 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.mac_group is defined %} +{% for group_name, group_conf in group.mac_group.items() %} +define M_{{ group_name }} = { {{ group_conf.mac_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 %}
\ No newline at end of file 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..905ffcd09 --- /dev/null +++ b/data/templates/firewall/nftables-policy.tmpl @@ -0,0 +1,55 @@ +#!/usr/sbin/nft -f + +{% if cleanup_commands is defined %} +{% for command in cleanup_commands %} +{{ command }} +{% endfor %} +{% endif %} + +include "/run/nftables_defines.conf" + +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 and route is not none -%} +{% 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 %} + {{ conf | nft_default_rule(route_text) }} + } +{% 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 route6 is defined and route6 is not none %} +{% for route_text, conf in route6.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 %} + {{ conf | nft_default_rule(route_text) }} + } +{% endfor %} +{% endif %} +} diff --git a/data/templates/firewall/nftables.tmpl b/data/templates/firewall/nftables.tmpl new file mode 100644 index 000000000..0cc977cf9 --- /dev/null +++ b/data/templates/firewall/nftables.tmpl @@ -0,0 +1,274 @@ +#!/usr/sbin/nft -f + +{% if cleanup_commands is defined %} +{% for command in cleanup_commands %} +{{ command }} +{% endfor %} +{% endif %} + +include "/run/nftables_defines.conf" + +table ip filter { +{% if first_install is defined %} + chain VYOS_FW_FORWARD { + type filter hook forward priority 0; 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 %} +{% set ns = namespace(sets=[]) %} +{% for name_text, conf in name.items() %} + chain NAME_{{ 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) }} +{% if rule_conf.recent is defined %} +{% set ns.sets = ns.sets + [name_text + '_' + rule_id] %} +{% endif %} +{% endfor %} +{% endif %} + {{ conf | nft_default_rule(name_text) }} + } +{% endfor %} +{% for set_name in ns.sets %} + set RECENT_{{ set_name }} { + type ipv4_addr + size 65535 + flags dynamic + } +{% 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_FORWARD { + type filter hook forward priority 0; 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 %} +{% set ns = namespace(sets=[]) %} +{% for name_text, conf in ipv6_name.items() %} + chain NAME6_{{ 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') }} +{% if rule_conf.recent is defined %} +{% set ns.sets = ns.sets + [name_text + '_' + rule_id] %} +{% endif %} +{% endfor %} +{% endif %} + {{ conf | nft_default_rule(name_text) }} + } +{% endfor %} +{% for set_name in ns.sets %} + set RECENT6_{{ set_name }} { + type ipv6_addr + size 65535 + flags dynamic + } +{% endfor %} +{% endif %} +{% if state_policy is defined %} + chain VYOS_STATE_POLICY6 { +{% if state_policy.established is defined %} + {{ state_policy.established | nft_state_policy('established', ipv6=True) }} +{% endif %} +{% if state_policy.invalid is defined %} + {{ state_policy.invalid | nft_state_policy('invalid', ipv6=True) }} +{% endif %} +{% if state_policy.related is defined %} + {{ state_policy.related | nft_state_policy('related', ipv6=True) }} +{% 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 + counter jump FW_CONNTRACK + 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 + counter jump FW_CONNTRACK + notrack + } + + ct helper rpc_tcp { + type "rpc" protocol tcp; + } + + ct helper rpc_udp { + type "rpc" protocol udp; + } + + ct helper tns_tcp { + type "tns" protocol tcp; + } + + chain VYOS_CT_HELPER { + ct helper set "rpc_tcp" tcp dport {111} return + ct helper set "rpc_udp" udp dport {111} return + ct helper set "tns_tcp" tcp dport {1521,1525,1536} return + return + } + + chain VYOS_CT_IGNORE { + return + } + + chain VYOS_CT_TIMEOUT { + return + } + + chain VYOS_CT_PREROUTING_HOOK { + return + } + + chain VYOS_CT_OUTPUT_HOOK { + return + } + + chain FW_CONNTRACK { + accept + } +} + +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 + counter jump FW_CONNTRACK + notrack + } + + chain OUTPUT { + type filter hook output priority -300; policy accept; + counter jump VYOS_CT_OUTPUT_HOOK + counter jump FW_CONNTRACK + notrack + } + + chain VYOS_CT_PREROUTING_HOOK { + return + } + + chain VYOS_CT_OUTPUT_HOOK { + return + } + + chain FW_CONNTRACK { + accept + } +} +{% endif %} diff --git a/data/templates/firewall/upnpd.conf.tmpl b/data/templates/firewall/upnpd.conf.tmpl new file mode 100644 index 000000000..39cb21373 --- /dev/null +++ b/data/templates/firewall/upnpd.conf.tmpl @@ -0,0 +1,172 @@ +# This is the UPNP configuration file + +# WAN network interface +ext_ifname={{ wan_interface }} +{% if wan_ip is defined %} +# If the WAN interface has several IP addresses, you +# can specify the one to use below +{% for addr in wan_ip %} +ext_ip={{ addr }} +{% endfor %} +{% endif %} + +# LAN network interfaces IPs / networks +{% if listen is defined %} +# There can be multiple listening IPs for SSDP traffic, in that case +# use multiple 'listening_ip=...' lines, one for each network interface. +# It can be IP address or network interface name (ie. "eth0") +# It is mandatory to use the network interface name in order to enable IPv6 +# HTTP is available on all interfaces. +# When MULTIPLE_EXTERNAL_IP is enabled, the external IP +# address associated with the subnet follows. For example: +# listening_ip=192.168.0.1/24 88.22.44.13 +{% for addr in listen %} +{% if addr | is_ipv4 %} +listening_ip={{ addr }} +{% elif addr | is_ipv6 %} +ipv6_listening_ip={{ addr }} +{% else %} +listening_ip={{ addr }} +{% endif %} +{% endfor %} +{% endif %} + +# CAUTION: mixing up WAN and LAN interfaces may introduce security risks! +# Be sure to assign the correct interfaces to LAN and WAN and consider +# implementing UPnP permission rules at the bottom of this configuration file + +# Port for HTTP (descriptions and SOAP) traffic. Set to 0 for autoselect. +#http_port=0 +# Port for HTTPS. Set to 0 for autoselect (default) +#https_port=0 + +# Path to the UNIX socket used to communicate with MiniSSDPd +# If running, MiniSSDPd will manage M-SEARCH answering. +# default is /var/run/minissdpd.sock +#minissdpdsocket=/var/run/minissdpd.sock + +{% if nat_pmp is defined %} +# Enable NAT-PMP support (default is no) +enable_natpmp=yes +{% endif %} + +# Enable UPNP support (default is yes) +enable_upnp=yes + +{% if pcp_lifetime is defined %} +# PCP +# Configure the minimum and maximum lifetime of a port mapping in seconds +# 120s and 86400s (24h) are suggested values from PCP-base +{% if pcp_lifetime.max is defined %} +max_lifetime={{ pcp_lifetime.max }} +{% endif %} +{% if pcp_lifetime.min is defined %} +min_lifetime={{ pcp_lifetime.min }} +{% endif %} +{% endif %} + + +# To enable the next few runtime options, see compile time +# ENABLE_MANUFACTURER_INFO_CONFIGURATION (config.h) + +{% if friendly_name is defined %} +# Name of this service, default is "`uname -s` router" +friendly_name= {{ friendly_name }} +{% endif %} + +# Manufacturer name, default is "`uname -s`" +manufacturer_name=VyOS + +# Manufacturer URL, default is URL of OS vendor +manufacturer_url=https://vyos.io/ + +# Model name, default is "`uname -s` router" +model_name=VyOS Router Model + +# Model description, default is "`uname -s` router" +model_description=Vyos open source enterprise router/firewall operating system + +# Model URL, default is URL of OS vendor +model_url=https://vyos.io/ + +{% if secure_mode is defined %} +# Secure Mode, UPnP clients can only add mappings to their own IP +secure_mode=yes +{% else %} +# Secure Mode, UPnP clients can only add mappings to their own IP +secure_mode=no +{% endif %} + +{% if presentation_url is defined %} +# Default presentation URL is HTTP address on port 80 +# If set to an empty string, no presentationURL element will appear +# in the XML description of the device, which prevents MS Windows +# from displaying an icon in the "Network Connections" panel. +#presentation_url= {{ presentation_url }} +{% endif %} + +# Report system uptime instead of daemon uptime +system_uptime=yes + +# Unused rules cleaning. +# never remove any rule before this threshold for the number +# of redirections is exceeded. default to 20 +clean_ruleset_threshold=10 +# Clean process work interval in seconds. default to 0 (disabled). +# a 600 seconds (10 minutes) interval makes sense +clean_ruleset_interval=600 + +# Anchor name in pf (default is miniupnpd) +anchor=VyOS + +uuid={{ uuid }} + +# Lease file location +lease_file=/config/upnp.leases + +# Daemon's serial and model number when reporting to clients +# (in XML description) +#serial=12345678 +#model_number=1 + +{% if rules is defined %} +# UPnP permission rules +# (allow|deny) (external port range) IP/mask (internal port range) +# A port range is <min port>-<max port> or <port> if there is only +# one port in the range. +# IP/mask format must be nnn.nnn.nnn.nnn/nn +# It is advised to only allow redirection of port >= 1024 +# and end the rule set with "deny 0-65535 0.0.0.0/0 0-65535" +# The following default ruleset allows specific LAN side IP addresses +# to request only ephemeral ports. It is recommended that users +# modify the IP ranges to match their own internal networks, and +# also consider implementing network-specific restrictions +# CAUTION: failure to enforce any rules may permit insecure requests to be made! +{% for rule, config in rules.items() %} +{% if config.disable is defined %} +{{ config.action}} {{ config.external_port_range }} {{ config.ip }} {{ config.internal_port_range }} +{% endif %} +{% endfor %} +{% endif %} + +{% if stun is defined %} +# WAN interface must have public IP address. Otherwise it is behind NAT +# and port forwarding is impossible. In some cases WAN interface can be +# behind unrestricted NAT 1:1 when all incoming traffic is NAT-ed and +# routed to WAN interfaces without any filtering. In this cases miniupnpd +# needs to know public IP address and it can be learnt by asking external +# server via STUN protocol. Following option enable retrieving external +# public IP address from STUN server and detection of NAT type. You need +# to specify also external STUN server in stun_host option below. +# This option is disabled by default. +ext_perform_stun=yes +# Specify STUN server, either hostname or IP address +# Some public STUN servers: +# stun.stunprotocol.org +# stun.sipgate.net +# stun.xten.com +# stun.l.google.com (on non standard port 19302) +ext_stun_host={{ stun.host }} +# Specify STUN UDP port, by default it is standard port 3478. +ext_stun_port={{ stun.port }} +{% endif %} diff --git a/data/templates/frr/ospfd.frr.tmpl b/data/templates/frr/ospfd.frr.tmpl index af66baf53..a6618b6af 100644 --- a/data/templates/frr/ospfd.frr.tmpl +++ b/data/templates/frr/ospfd.frr.tmpl @@ -126,6 +126,9 @@ router ospf {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} {% if default_metric is defined and default_metric is not none %} default-metric {{ default_metric }} {% endif %} +{% if maximum_paths is defined and maximum_paths is not none %} + maximum-paths {{ maximum_paths }} +{% endif %} {% if distance is defined and distance is not none %} {% if distance.global is defined and distance.global is not none %} distance {{ distance.global }} diff --git a/data/templates/frr/static_routes_macro.j2 b/data/templates/frr/static_routes_macro.j2 index 3b432b49b..86c7470ca 100644 --- a/data/templates/frr/static_routes_macro.j2 +++ b/data/templates/frr/static_routes_macro.j2 @@ -5,7 +5,7 @@ {% if prefix_config.dhcp_interface is defined and prefix_config.dhcp_interface is not none %} {% set next_hop = prefix_config.dhcp_interface | get_dhcp_router %} {% if next_hop is defined and next_hop is not none %} -{{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} {{ prefix_config.dhcp_interface }} +{{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} {{ prefix_config.dhcp_interface }} {{ 'table ' + table if table is defined and table is not none }} {% endif %} {% endif %} {% if prefix_config.interface is defined and prefix_config.interface is not none %} diff --git a/data/templates/vrrp/keepalived.conf.tmpl b/data/templates/high-availability/keepalived.conf.tmpl index 6585fc60b..68c707f17 100644 --- a/data/templates/vrrp/keepalived.conf.tmpl +++ b/data/templates/high-availability/keepalived.conf.tmpl @@ -9,8 +9,8 @@ global_defs { notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py } -{% if group is defined and group is not none %} -{% for name, group_config in group.items() if group_config.disable is not defined %} +{% if vrrp is defined and vrrp.group is defined and vrrp.group is not none %} +{% for name, group_config in vrrp.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 %} vrrp_script healthcheck_{{ name }} { script "{{ group_config.health_check.script }}" @@ -28,6 +28,9 @@ vrrp_instance {{ name }} { virtual_router_id {{ group_config.vrid }} priority {{ group_config.priority }} advert_int {{ group_config.advertise_interval }} +{% if group_config.track is defined and group_config.track.exclude_vrrp_interface is defined %} + dont_track_primary +{% endif %} {% if group_config.no_preempt is not defined and group_config.preempt_delay is defined and group_config.preempt_delay is not none %} preempt_delay {{ group_config.preempt_delay }} {% elif group_config.no_preempt is defined %} @@ -61,8 +64,8 @@ vrrp_instance {{ name }} { {% endif %} {% if group_config.address is defined and group_config.address is not none %} virtual_ipaddress { -{% for addr in group_config.address %} - {{ addr }} +{% for addr, addr_config in group_config.address.items() %} + {{ addr }}{{ ' dev ' + addr_config.interface if addr_config.interface is defined }} {% endfor %} } {% endif %} @@ -73,6 +76,13 @@ vrrp_instance {{ name }} { {% endfor %} } {% endif %} +{% if group_config.track is defined and group_config.track.interface is defined and group_config.track.interface is not none %} + track_interface { +{% for interface in group_config.track.interface %} + {{ interface }} +{% endfor %} + } +{% endif %} {% if group_config.health_check is defined and group_config.health_check.script is defined and group_config.health_check.script is not none %} track_script { healthcheck_{{ name }} @@ -82,8 +92,8 @@ vrrp_instance {{ name }} { {% endfor %} {% endif %} -{% if sync_group is defined and sync_group is not none %} -{% for name, sync_group_config in sync_group.items() if sync_group_config.disable is not defined %} +{% if vrrp is defined and vrrp.sync_group is defined and vrrp.sync_group is not none %} +{% for name, sync_group_config in vrrp.sync_group.items() if sync_group_config.disable is not defined %} vrrp_sync_group {{ name }} { group { {% if sync_group_config.member is defined and sync_group_config.member is not none %} @@ -94,13 +104,15 @@ vrrp_sync_group {{ name }} { } {# 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 %} +{% if vrrp is defined and vrrp.group is defined and vrrp.group is not none %} +{% for name, group_config in vrrp.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 %} {% 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 }}" @@ -110,3 +122,48 @@ vrrp_sync_group {{ name }} { } {% endfor %} {% endif %} + +{% if virtual_server is defined and virtual_server is not none %} +# Virtual-server configuration +{% for vserver, vserver_config in virtual_server.items() %} +virtual_server {{ vserver }} {{ vserver_config.port }} { + delay_loop {{ vserver_config.delay_loop }} +{% if vserver_config.algorithm == 'round-robin' %} + lb_algo rr +{% elif vserver_config.algorithm == 'weighted-round-robin' %} + lb_algo wrr +{% elif vserver_config.algorithm == 'least-connection' %} + lb_algo lc +{% elif vserver_config.algorithm == 'weighted-least-connection' %} + lb_algo wlc +{% elif vserver_config.algorithm == 'source-hashing' %} + lb_algo sh +{% elif vserver_config.algorithm == 'destination-hashing' %} + lb_algo dh +{% elif vserver_config.algorithm == 'locality-based-least-connection' %} + lb_algo lblc +{% endif %} +{% if vserver_config.forward_method == "nat" %} + lb_kind NAT +{% elif vserver_config.forward_method == "direct" %} + lb_kind DR +{% elif vserver_config.forward_method == "tunnel" %} + lb_kind TUN +{% endif %} + persistence_timeout {{ vserver_config.persistence_timeout }} + protocol {{ vserver_config.protocol | upper }} +{% if vserver_config.real_server is defined and vserver_config.real_server is not none %} +{% for rserver, rserver_config in vserver_config.real_server.items() %} + real_server {{ rserver }} {{ rserver_config.port }} { + weight 1 + {{ vserver_config.protocol | upper }}_CHECK { +{% if rserver_config.connection_timeout is defined and rserver_config.connection_timeout is not none %} + connect_timeout {{ rserver_config.connection_timeout }} +{% endif %} + } + } +{% endfor %} +{% endif %} +} +{% endfor %} +{% endif %} diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl index ac9203e83..e8511bd62 100644 --- a/data/templates/https/nginx.default.tmpl +++ b/data/templates/https/nginx.default.tmpl @@ -1,12 +1,6 @@ ### Autogenerated by https.py ### # Default server configuration # -server { - listen 80 default_server; - listen [::]:80 default_server; - server_name _; - return 301 https://$host$request_uri; -} {% for server in server_block_list %} server { diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl index 1b221814e..c6b71f2a1 100644 --- a/data/templates/ipsec/swanctl/peer.tmpl +++ b/data/templates/ipsec/swanctl/peer.tmpl @@ -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/monitoring/override.conf.tmpl b/data/templates/monitoring/override.conf.tmpl new file mode 100644 index 000000000..f8f150791 --- /dev/null +++ b/data/templates/monitoring/override.conf.tmpl @@ -0,0 +1,7 @@ +[Unit] +After=vyos-router.service +ConditionPathExists=/run/telegraf/vyos-telegraf.conf +[Service] +Environment=INFLUX_TOKEN={{ authentication.token }} +CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_ADMIN +AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN diff --git a/data/templates/monitoring/syslog_telegraf.tmpl b/data/templates/monitoring/syslog_telegraf.tmpl new file mode 100644 index 000000000..cdcbd92a4 --- /dev/null +++ b/data/templates/monitoring/syslog_telegraf.tmpl @@ -0,0 +1,5 @@ +# Generated by /usr/libexec/vyos/conf_mode/service_monitoring_telegraf.py + +$ModLoad omuxsock +$OMUxSockSocket /run/telegraf/telegraf_syslog.sock +*.notice :omuxsock: diff --git a/data/templates/monitoring/systemd_vyos_telegraf_service.tmpl b/data/templates/monitoring/systemd_vyos_telegraf_service.tmpl new file mode 100644 index 000000000..234ef5586 --- /dev/null +++ b/data/templates/monitoring/systemd_vyos_telegraf_service.tmpl @@ -0,0 +1,16 @@ +[Unit] +Description=The plugin-driven server agent for reporting metrics into InfluxDB +Documentation=https://github.com/influxdata/telegraf +After=network.target + +[Service] +EnvironmentFile=-/etc/default/telegraf +User=telegraf +ExecStart=/usr/bin/telegraf -config /run/telegraf/vyos-telegraf.conf -config-directory /etc/telegraf/telegraf.d $TELEGRAF_OPTS +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure +RestartForceExitStatus=SIGPIPE +KillMode=control-group + +[Install] +WantedBy=multi-user.target diff --git a/data/templates/monitoring/telegraf.tmpl b/data/templates/monitoring/telegraf.tmpl new file mode 100644 index 000000000..d3145a500 --- /dev/null +++ b/data/templates/monitoring/telegraf.tmpl @@ -0,0 +1,60 @@ +# Generated by /usr/libexec/vyos/conf_mode/service_monitoring_telegraf.py + +[agent] + interval = "10s" + round_interval = true + metric_batch_size = 1000 + metric_buffer_limit = 10000 + collection_jitter = "0s" + flush_interval = "10s" + flush_jitter = "0s" + precision = "" + debug = false + quiet = false + logfile = "" + hostname = "" + omit_hostname = false +[[outputs.influxdb_v2]] + urls = ["{{ url }}:{{ port }}"] + insecure_skip_verify = true + token = "$INFLUX_TOKEN" + organization = "{{ authentication.organization }}" + bucket = "{{ bucket }}" +[[inputs.cpu]] + percpu = true + totalcpu = true + collect_cpu_time = false + report_active = false +[[inputs.disk]] + ignore_fs = ["devtmpfs", "devfs"] +[[inputs.diskio]] +[[inputs.mem]] +[[inputs.net]] +[[inputs.system]] +[[inputs.netstat]] +[[inputs.processes]] +[[inputs.kernel]] +[[inputs.interrupts]] +[[inputs.linux_sysctl_fs]] +[[inputs.systemd_units]] +[[inputs.conntrack]] + files = ["ip_conntrack_count","ip_conntrack_max","nf_conntrack_count","nf_conntrack_max"] + dirs = ["/proc/sys/net/ipv4/netfilter","/proc/sys/net/netfilter"] +[[inputs.ethtool]] + interface_include = {{ interfaces_ethernet }} +[[inputs.ntpq]] + dns_lookup = true +[[inputs.internal]] +[[inputs.nstat]] +[[inputs.syslog]] + server = "unixgram:///run/telegraf/telegraf_syslog.sock" + best_effort = true + syslog_standard = "RFC3164" +[[inputs.exec]] + commands = [ + "{{ custom_scripts_dir }}/show_firewall_input_filter.py", + "{{ custom_scripts_dir }}/show_interfaces_input_filter.py", + "{{ custom_scripts_dir }}/vyos_services_input_filter.py" + ] + timeout = "10s" + data_format = "influx" diff --git a/data/templates/nhrp/opennhrp.conf.tmpl b/data/templates/nhrp/opennhrp.conf.tmpl index 948327198..e9e9f692a 100644 --- a/data/templates/nhrp/opennhrp.conf.tmpl +++ b/data/templates/nhrp/opennhrp.conf.tmpl @@ -33,7 +33,7 @@ interface {{ name }} #{{ type }} {{ profile_name }} {% endfor %} {% if tunnel_conf.shortcut_target is defined and tunnel_conf.shortcut_target is not none %} {% for target, shortcut_conf in tunnel_conf.shortcut_target.items() %} - shortcut-target {{ target }} {{ shortcut_conf.holding_time if shortcut_conf.holding_time is defined else '' }} + shortcut-target {{ target }}{{ ' holding-time ' + shortcut_conf.holding_time if shortcut_conf.holding_time is defined }} {% endfor %} {% endif %} diff --git a/data/templates/ntp/ntpd.conf.tmpl b/data/templates/ntp/ntpd.conf.tmpl index 38e68f24f..e7afcc16b 100644 --- a/data/templates/ntp/ntpd.conf.tmpl +++ b/data/templates/ntp/ntpd.conf.tmpl @@ -27,6 +27,7 @@ restrict -6 ::1 {% if allow_clients is defined and allow_clients.address is defined %} # Allowed clients configuration +restrict default ignore {% for address in allow_clients.address %} restrict {{ address|address_from_cidr }} mask {{ address|netmask_from_cidr }} nomodify notrap nopeer {% endfor %} diff --git a/data/templates/ssh/sshd_config.tmpl b/data/templates/ssh/sshd_config.tmpl index 2f2b78a66..670cf85a1 100644 --- a/data/templates/ssh/sshd_config.tmpl +++ b/data/templates/ssh/sshd_config.tmpl @@ -29,6 +29,7 @@ UsePAM yes PermitRootLogin no PidFile /run/sshd/sshd.pid AddressFamily any +DebianBanner no # # User configurable section diff --git a/data/templates/zone_policy/nftables.tmpl b/data/templates/zone_policy/nftables.tmpl new file mode 100644 index 000000000..093da6bd8 --- /dev/null +++ b/data/templates/zone_policy/nftables.tmpl @@ -0,0 +1,113 @@ +#!/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 NAME_{{ 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 NAME_{{ 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) }} +{% if zone_conf.intra_zone_filtering is defined %} + iifname { {{ zone_conf.interface | join(",") }} } counter return +{% endif %} +{% 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 NAME_{{ 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 NAME6_{{ 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 NAME6_{{ 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) }} +{% if zone_conf.intra_zone_filtering is defined %} + iifname { {{ zone_conf.interface | join(",") }} } counter return +{% endif %} +{% 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 NAME6_{{ 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_FORWARD 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_FORWARD oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE6_{{ zone_name }} +{% endif %} +{% endif %} +{% endfor %} + +{# Ensure that state-policy rule is first in the chain #} +{% if firewall.state_policy is defined %} +{% for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL'] %} +insert rule ip filter {{ chain }} jump VYOS_STATE_POLICY +{% endfor %} +{% for chain in ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL'] %} +insert rule ip6 filter {{ chain }} jump VYOS_STATE_POLICY6 +{% endfor %} +{% endif %} + +{% endif %} diff --git a/debian/control b/debian/control index ef124679b..c53e4d3b8 100644 --- a/debian/control +++ b/debian/control @@ -94,6 +94,7 @@ Depends: ndisc6, ndppd, netplug, + nfct, nftables (>= 0.9.3), nginx-light, ntp, @@ -151,6 +152,7 @@ Depends: strongswan-swanctl (>= 5.9), sudo, systemd, + telegraf (>= 1.20), tcpdump, tcptraceroute, telnet, @@ -168,7 +170,9 @@ Depends: wide-dhcpv6-client, wireguard-tools, wireless-regdb, - wpasupplicant (>= 0.6.7) + wpasupplicant (>= 0.6.7), + ndppd, + miniupnpd-nftables Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index 29d74390f..63dff43a5 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -12,6 +12,7 @@ etc/security etc/sudoers.d etc/systemd etc/sysctl.d +etc/telegraf etc/udev etc/update-motd.d etc/vyos diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index 1f3c14208..f2aca4b3a 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> @@ -71,6 +74,9 @@ <tagNode name="address-group"> <properties> <help>Firewall address-group</help> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> </properties> <children> <leafNode name="address"> @@ -97,6 +103,9 @@ <tagNode name="ipv6-address-group"> <properties> <help>Firewall ipv6-address-group</help> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> </properties> <children> <leafNode name="address"> @@ -106,8 +115,13 @@ <format>ipv6</format> <description>IPv6 address to match</description> </valueHelp> + <valueHelp> + <format>ipv6range</format> + <description>IPv6 range to match (e.g. 2002::1-2002::ff)</description> + </valueHelp> <constraint> <validator name="ipv6-address"/> + <validator name="ipv6-range"/> </constraint> <multi/> </properties> @@ -117,7 +131,10 @@ </tagNode> <tagNode name="ipv6-network-group"> <properties> - <help>Network-group member</help> + <help>Firewall ipv6-network-group</help> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> </properties> <children> #include <include/generic-description.xml.i> @@ -136,9 +153,36 @@ </leafNode> </children> </tagNode> + <tagNode name="mac-group"> + <properties> + <help>Firewall mac-group</help> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> + </properties> + <children> + #include <include/generic-description.xml.i> + <leafNode name="mac-address"> + <properties> + <help>Mac-group member</help> + <valueHelp> + <format><MAC address></format> + <description>MAC address to match</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> <tagNode name="network-group"> <properties> <help>Firewall network-group</help> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> </properties> <children> #include <include/generic-description.xml.i> @@ -160,6 +204,9 @@ <tagNode name="port-group"> <properties> <help>Firewall port-group</help> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> </properties> <children> #include <include/generic-description.xml.i> @@ -179,6 +226,9 @@ <description>Numbered port range (e.g. 1001-1050)</description> </valueHelp> <multi/> + <constraint> + <validator name="port-range"/> + </constraint> </properties> </leafNode> </children> @@ -203,10 +253,14 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <tagNode name="ipv6-name"> <properties> <help>IPv6 firewall rule-set name</help> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> </properties> <children> #include <include/firewall/name-default-action.xml.i> @@ -233,7 +287,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> @@ -243,7 +297,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> @@ -296,227 +350,31 @@ <help>ICMPv6 type and code information</help> </properties> <children> - <leafNode name="type"> + <leafNode name="code"> <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> - </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> + <help>ICMPv6 code (0-255)</help> <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> + <format>u32:0-255</format> + <description>ICMPv6 code (0-255)</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> <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"> + <leafNode name="type"> <properties> - <help>KaZaA application packets</help> - <valueless/> + <help>ICMPv6 type (0-255)</help> + <valueHelp> + <format>u32:0-255</format> + <description>ICMPv6 type (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> </properties> </leafNode> + #include <include/firewall/icmpv6-type-name.xml.i> </children> </node> </children> @@ -541,6 +399,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <leafNode name="ipv6-src-route"> <properties> @@ -560,6 +419,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <leafNode name="log-martians"> <properties> @@ -579,10 +439,14 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>enable</defaultValue> </leafNode> <tagNode name="name"> <properties> <help>IPv4 firewall rule-set name</help> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> </properties> <children> #include <include/firewall/name-default-action.xml.i> @@ -678,6 +542,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <leafNode name="send-redirects"> <properties> @@ -697,6 +562,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>enable</defaultValue> </leafNode> <leafNode name="source-validation"> <properties> @@ -720,6 +586,7 @@ <regex>^(strict|loose|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <node name="state-policy"> <properties> @@ -773,6 +640,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>enable</defaultValue> </leafNode> <leafNode name="twa-hazards-protection"> <properties> @@ -792,6 +660,7 @@ <regex>^(enable|disable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> </children> </node> diff --git a/interface-definitions/vrrp.xml.in b/interface-definitions/high-availability.xml.in index 53d79caac..ee1d70484 100644 --- a/interface-definitions/vrrp.xml.in +++ b/interface-definitions/high-availability.xml.in @@ -1,13 +1,13 @@ <?xml version="1.0"?> <interfaceDefinition> - <node name="high-availability"> + <node name="high-availability" owner="${vyos_conf_scripts_dir}/high-availability.py"> <properties> + <priority>800</priority> <!-- after all interfaces and conntrack-sync --> <help>High availability settings</help> </properties> <children> - <node name="vrrp" owner="${vyos_conf_scripts_dir}/vrrp.py"> + <node name="vrrp"> <properties> - <priority>800</priority> <!-- after all interfaces and conntrack-sync --> <help>Virtual Router Redundancy Protocol settings</help> </properties> <children> @@ -177,8 +177,37 @@ <valueless/> </properties> </leafNode> + <node name="track"> + <properties> + <help>Track settings</help> + </properties> + <children> + <leafNode name="exclude-vrrp-interface"> + <properties> + <valueless/> + <help>Disable track state of main interface</help> + </properties> + </leafNode> + <leafNode name="interface"> + <properties> + <help>Interface name state check</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> + </children> + </node> #include <include/vrrp-transition-script.xml.i> - <leafNode name="address"> + <tagNode name="address"> <properties> <help>Virtual IP address</help> <valueHelp> @@ -193,9 +222,11 @@ <validator name="ipv4-host"/> <validator name="ipv6-host"/> </constraint> - <multi/> </properties> - </leafNode> + <children> + #include <include/generic-interface-broadcast.xml.i> + </children> + </tagNode> <leafNode name="excluded-address"> <properties> <help>Virtual address (If you need additional IPv4 and IPv6 in same group)</help> @@ -252,6 +283,144 @@ </tagNode> </children> </node> + <tagNode name="virtual-server"> + <properties> + <help>Load-balancing virtual server address</help> + </properties> + <children> + <leafNode name="algorithm"> + <properties> + <help>Schedule algorithm (default - least-connection)</help> + <completionHelp> + <list>round-robin weighted-round-robin least-connection weighted-least-connection source-hashing destination-hashing locality-based-least-connection</list> + </completionHelp> + <valueHelp> + <format>round-robin</format> + <description>Round robin</description> + </valueHelp> + <valueHelp> + <format>weighted-round-robin</format> + <description>Weighted round robin</description> + </valueHelp> + <valueHelp> + <format>least-connection</format> + <description>Least connection</description> + </valueHelp> + <valueHelp> + <format>weighted-least-connection</format> + <description>Weighted least connection</description> + </valueHelp> + <valueHelp> + <format>source-hashing</format> + <description>Source hashing</description> + </valueHelp> + <valueHelp> + <format>destination-hashing</format> + <description>Destination hashing</description> + </valueHelp> + <valueHelp> + <format>locality-based-least-connection</format> + <description>Locality-Based least connection</description> + </valueHelp> + <constraint> + <regex>^(round-robin|weighted-round-robin|least-connection|weighted-least-connection|source-hashing|destination-hashing|locality-based-least-connection)$</regex> + </constraint> + </properties> + <defaultValue>least-connection</defaultValue> + </leafNode> + <leafNode name="delay-loop"> + <properties> + <help>Interval between health-checks (in seconds)</help> + <valueHelp> + <format>u32:1-600</format> + <description>Interval in seconds (default: 10)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> + <defaultValue>10</defaultValue> + </leafNode> + <leafNode name="forward-method"> + <properties> + <help>Forwarding method (default: NAT)</help> + <completionHelp> + <list>direct nat tunnel</list> + </completionHelp> + <valueHelp> + <format>direct</format> + <description>Direct routing</description> + </valueHelp> + <valueHelp> + <format>nat</format> + <description>NAT</description> + </valueHelp> + <valueHelp> + <format>tunnel</format> + <description>Tunneling</description> + </valueHelp> + <constraint> + <regex>^(direct|nat|tunnel)$</regex> + </constraint> + </properties> + <defaultValue>nat</defaultValue> + </leafNode> + #include <include/port-number.xml.i> + <leafNode name="persistence-timeout"> + <properties> + <help>Timeout for persistent connections</help> + <valueHelp> + <format>u32:1-86400</format> + <description>Timeout for persistent connections (default: 300)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-86400"/> + </constraint> + </properties> + <defaultValue>300</defaultValue> + </leafNode> + <leafNode name="protocol"> + <properties> + <help>Protocol for port checks (default: TCP)</help> + <completionHelp> + <list>tcp udp</list> + </completionHelp> + <valueHelp> + <format>tcp</format> + <description>TCP</description> + </valueHelp> + <valueHelp> + <format>udp</format> + <description>UDP</description> + </valueHelp> + <constraint> + <regex>^(tcp|udp)$</regex> + </constraint> + </properties> + <defaultValue>tcp</defaultValue> + </leafNode> + <tagNode name="real-server"> + <properties> + <help>Real server address</help> + </properties> + <children> + #include <include/port-number.xml.i> + <leafNode name="connection-timeout"> + <properties> + <help>Server connection timeout</help> + <valueHelp> + <format>u32:1-86400</format> + <description>Connection timeout to remote server</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-86400"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> </children> </node> </interfaceDefinition> diff --git a/interface-definitions/include/conntrack/log-common.xml.i b/interface-definitions/include/conntrack/log-common.xml.i new file mode 100644 index 000000000..38799f8f4 --- /dev/null +++ b/interface-definitions/include/conntrack/log-common.xml.i @@ -0,0 +1,20 @@ +<!-- include start from conntrack/log-common.xml.i --> +<leafNode name="destroy"> + <properties> + <help>Log connection deletion</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="new"> + <properties> + <help>Log connection creation</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="update"> + <properties> + <help>Log connection updates</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/conntrack/timeout-common-protocols.xml.i b/interface-definitions/include/conntrack/timeout-common-protocols.xml.i new file mode 100644 index 000000000..2676d846e --- /dev/null +++ b/interface-definitions/include/conntrack/timeout-common-protocols.xml.i @@ -0,0 +1,172 @@ +<!-- include start from conntrack/timeout-common-protocols.xml.i --> +<leafNode name="icmp"> + <properties> + <help>ICMP timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>ICMP timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>30</defaultValue> +</leafNode> +<leafNode name="other"> + <properties> + <help>Generic connection timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>Generic connection timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>600</defaultValue> +</leafNode> +<node name="tcp"> + <properties> + <help>TCP connection timeout options</help> + </properties> + <children> + <leafNode name="close-wait"> + <properties> + <help>TCP CLOSE-WAIT timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP CLOSE-WAIT timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>60</defaultValue> + </leafNode> + <leafNode name="close"> + <properties> + <help>TCP CLOSE timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP CLOSE timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>10</defaultValue> + </leafNode> + <leafNode name="established"> + <properties> + <help>TCP ESTABLISHED timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP ESTABLISHED timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>432000</defaultValue> + </leafNode> + <leafNode name="fin-wait"> + <properties> + <help>TCP FIN-WAIT timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP FIN-WAIT timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>120</defaultValue> + </leafNode> + <leafNode name="last-ack"> + <properties> + <help>TCP LAST-ACK timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP LAST-ACK timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>30</defaultValue> + </leafNode> + <leafNode name="syn-recv"> + <properties> + <help>TCP SYN-RECEIVED timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP SYN-RECEIVED timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>60</defaultValue> + </leafNode> + <leafNode name="syn-sent"> + <properties> + <help>TCP SYN-SENT timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP SYN-SENT timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>120</defaultValue> + </leafNode> + <leafNode name="time-wait"> + <properties> + <help>TCP TIME-WAIT timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP TIME-WAIT timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>120</defaultValue> + </leafNode> + </children> +</node> +<node name="udp"> + <properties> + <help>UDP timeout options</help> + </properties> + <children> + <leafNode name="other"> + <properties> + <help>UDP generic timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>UDP generic timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>30</defaultValue> + </leafNode> + <leafNode name="stream"> + <properties> + <help>UDP stream timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>UDP stream timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + <defaultValue>180</defaultValue> + </leafNode> + </children> +</node> +<!-- 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..353804990 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"/> @@ -99,7 +99,8 @@ <properties> <help>Protocol to match (protocol name, number, or "all")</help> <completionHelp> - <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script> + <script>${vyos_completion_dir}/list_protocols.sh</script> + <list>all tcp_udp</list> </completionHelp> <valueHelp> <format>all</format> @@ -114,14 +115,17 @@ <description>IP protocol number</description> </valueHelp> <valueHelp> + <format><protocol></format> + <description>IP protocol name</description> + </valueHelp> + <valueHelp> <format>!<protocol></format> - <description>IP protocol number</description> + <description>IP protocol name</description> </valueHelp> <constraint> <validator name="ip-protocol"/> </constraint> </properties> - <defaultValue>all</defaultValue> </leafNode> <node name="recent"> <properties> @@ -142,13 +146,24 @@ </leafNode> <leafNode name="time"> <properties> - <help>Source addresses seen in the last N seconds</help> + <help>Source addresses seen in the last second/minute/hour</help> + <completionHelp> + <list>second minute hour</list> + </completionHelp> <valueHelp> - <format>u32:0-4294967295</format> - <description>Source addresses seen in the last N seconds</description> + <format>second</format> + <description>Source addresses seen COUNT times in the last second</description> + </valueHelp> + <valueHelp> + <format>minute</format> + <description>Source addresses seen COUNT times in the last minute</description> + </valueHelp> + <valueHelp> + <format>hour</format> + <description>Source addresses seen COUNT times in the last hour</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> + <regex>^(second|minute|hour)$</regex> </constraint> </properties> </leafNode> @@ -172,6 +187,9 @@ <format>!<MAC address></format> <description>Match everything except the specified MAC address</description> </valueHelp> + <constraint> + <validator name="mac-address-firewall"/> + </constraint> </properties> </leafNode> #include <include/firewall/port.xml.i> @@ -260,65 +278,71 @@ </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> +#include <include/firewall/tcp-flags.xml.i> <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> + <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/icmp-type-name.xml.i b/interface-definitions/include/firewall/icmp-type-name.xml.i index b45fb619b..f57def3e1 100644 --- a/interface-definitions/include/firewall/icmp-type-name.xml.i +++ b/interface-definitions/include/firewall/icmp-type-name.xml.i @@ -3,170 +3,70 @@ <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>echo-reply destination-unreachable source-quench redirect echo-request router-advertisement router-solicitation time-exceeded parameter-problem timestamp-request timestamp-reply info-request info-reply address-mask-request address-mask-reply</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> + <description>ICMP type 0: echo-reply</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> + <description>ICMP type 3: destination-unreachable</description> </valueHelp> <valueHelp> <format>source-quench</format> - <description>ICMP type/code name</description> + <description>ICMP type 4: source-quench</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> + <description>ICMP type 5: redirect</description> </valueHelp> <valueHelp> <format>echo-request</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>ping</format> - <description>ICMP type/code name</description> + <description>ICMP type 8: echo-request</description> </valueHelp> <valueHelp> <format>router-advertisement</format> - <description>ICMP type/code name</description> + <description>ICMP type 9: router-advertisement</description> </valueHelp> <valueHelp> <format>router-solicitation</format> - <description>ICMP type/code name</description> + <description>ICMP type 10: router-solicitation</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> + <description>ICMP type 11: time-exceeded</description> </valueHelp> <valueHelp> <format>parameter-problem</format> - <description>ICMP type/code name</description> + <description>ICMP type 12: parameter-problem</description> </valueHelp> <valueHelp> - <format>ip-header-bad</format> - <description>ICMP type/code name</description> + <format>timestamp-request</format> + <description>ICMP type 13: timestamp-request</description> </valueHelp> <valueHelp> - <format>required-option-missing</format> - <description>ICMP type/code name</description> + <format>timestamp-reply</format> + <description>ICMP type 14: timestamp-reply</description> </valueHelp> <valueHelp> - <format>timestamp-request</format> - <description>ICMP type/code name</description> + <format>info-request</format> + <description>ICMP type 15: info-request</description> </valueHelp> <valueHelp> - <format>timestamp-reply</format> - <description>ICMP type/code name</description> + <format>info-reply</format> + <description>ICMP type 16: info-reply</description> </valueHelp> <valueHelp> <format>address-mask-request</format> - <description>ICMP type/code name</description> + <description>ICMP type 17: address-mask-request</description> </valueHelp> <valueHelp> <format>address-mask-reply</format> - <description>ICMP type/code name</description> + <description>ICMP type 18: address-mask-reply</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>^(echo-reply|destination-unreachable|source-quench|redirect|echo-request|router-advertisement|router-solicitation|time-exceeded|parameter-problem|timestamp-request|timestamp-reply|info-request|info-reply|address-mask-request|address-mask-reply)$</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/firewall/icmpv6-type-name.xml.i b/interface-definitions/include/firewall/icmpv6-type-name.xml.i new file mode 100644 index 000000000..b13cf02c4 --- /dev/null +++ b/interface-definitions/include/firewall/icmpv6-type-name.xml.i @@ -0,0 +1,73 @@ +<!-- include start from firewall/icmpv6-type-name.xml.i --> +<leafNode name="type-name"> + <properties> + <help>ICMPv6 type-name</help> + <completionHelp> + <list>destination-unreachable packet-too-big time-exceeded echo-request echo-reply mld-listener-query mld-listener-report mld-listener-reduction nd-router-solicit nd-router-advert nd-neighbor-solicit nd-neighbor-advert nd-redirect parameter-problem router-renumbering</list> + </completionHelp> + <valueHelp> + <format>destination-unreachable</format> + <description>ICMPv6 type 1: destination-unreachable</description> + </valueHelp> + <valueHelp> + <format>packet-too-big</format> + <description>ICMPv6 type 2: packet-too-big</description> + </valueHelp> + <valueHelp> + <format>time-exceeded</format> + <description>ICMPv6 type 3: time-exceeded</description> + </valueHelp> + <valueHelp> + <format>echo-request</format> + <description>ICMPv6 type 128: echo-request</description> + </valueHelp> + <valueHelp> + <format>echo-reply</format> + <description>ICMPv6 type 129: echo-reply</description> + </valueHelp> + <valueHelp> + <format>mld-listener-query</format> + <description>ICMPv6 type 130: mld-listener-query</description> + </valueHelp> + <valueHelp> + <format>mld-listener-report</format> + <description>ICMPv6 type 131: mld-listener-report</description> + </valueHelp> + <valueHelp> + <format>mld-listener-reduction</format> + <description>ICMPv6 type 132: mld-listener-reduction</description> + </valueHelp> + <valueHelp> + <format>nd-router-solicit</format> + <description>ICMPv6 type 133: nd-router-solicit</description> + </valueHelp> + <valueHelp> + <format>nd-router-advert</format> + <description>ICMPv6 type 134: nd-router-advert</description> + </valueHelp> + <valueHelp> + <format>nd-neighbor-solicit</format> + <description>ICMPv6 type 135: nd-neighbor-solicit</description> + </valueHelp> + <valueHelp> + <format>nd-neighbor-advert</format> + <description>ICMPv6 type 136: nd-neighbor-advert</description> + </valueHelp> + <valueHelp> + <format>nd-redirect</format> + <description>ICMPv6 type 137: nd-redirect</description> + </valueHelp> + <valueHelp> + <format>parameter-problem</format> + <description>ICMPv6 type 4: parameter-problem</description> + </valueHelp> + <valueHelp> + <format>router-renumbering</format> + <description>ICMPv6 type 138: router-renumbering</description> + </valueHelp> + <constraint> + <regex>^(destination-unreachable|packet-too-big|time-exceeded|echo-request|echo-reply|mld-listener-query|mld-listener-report|mld-listener-reduction|nd-router-solicit|nd-router-advert|nd-neighbor-solicit|nd-neighbor-advert|nd-redirect|parameter-problem|router-renumbering)$</regex> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/firewall/mac-group.xml.i b/interface-definitions/include/firewall/mac-group.xml.i new file mode 100644 index 000000000..dbce3fc88 --- /dev/null +++ b/interface-definitions/include/firewall/mac-group.xml.i @@ -0,0 +1,10 @@ +<!-- include start from firewall/mac-group.xml.i --> +<leafNode name="mac-group"> + <properties> + <help>Group of MAC addresses</help> + <completionHelp> + <path>firewall group mac-group</path> + </completionHelp> + </properties> +</leafNode> +<!-- include start from firewall/mac-group.xml.i -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/port.xml.i b/interface-definitions/include/firewall/port.xml.i index 59d92978b..3bacafff8 100644 --- a/interface-definitions/include/firewall/port.xml.i +++ b/interface-definitions/include/firewall/port.xml.i @@ -16,8 +16,11 @@ </valueHelp> <valueHelp> <format> </format> - <description>\n\n Multiple destination ports can be specified as a comma-separated list.\n The whole list can also be negated using '!'.\n For example: '!22,telnet,http,123,1001-1005'</description> + <description>\n\n Multiple destination ports can be specified as a comma-separated list.\n For example: 'telnet,http,123,1001-1005'</description> </valueHelp> + <constraint> + <validator name="port-multi"/> + </constraint> </properties> </leafNode> <!-- include end --> 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..c2cc7edb3 --- /dev/null +++ b/interface-definitions/include/firewall/source-destination-group-ipv6.xml.i @@ -0,0 +1,34 @@ +<!-- 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> + #include <include/firewall/mac-group.xml.i> + <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..ab11e89e9 100644 --- a/interface-definitions/include/firewall/source-destination-group.xml.i +++ b/interface-definitions/include/firewall/source-destination-group.xml.i @@ -7,16 +7,26 @@ <leafNode name="address-group"> <properties> <help>Group of addresses</help> + <completionHelp> + <path>firewall group address-group</path> + </completionHelp> </properties> </leafNode> + #include <include/firewall/mac-group.xml.i> <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/firewall/tcp-flags.xml.i b/interface-definitions/include/firewall/tcp-flags.xml.i new file mode 100644 index 000000000..b99896687 --- /dev/null +++ b/interface-definitions/include/firewall/tcp-flags.xml.i @@ -0,0 +1,119 @@ +<!-- include start from firewall/tcp-flags.xml.i --> +<node name="tcp"> + <properties> + <help>TCP flags to match</help> + </properties> + <children> + <node name="flags"> + <properties> + <help>TCP flags to match</help> + </properties> + <children> + <leafNode name="syn"> + <properties> + <help>Synchronise flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ack"> + <properties> + <help>Acknowledge flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="fin"> + <properties> + <help>Finish flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="rst"> + <properties> + <help>Reset flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="urg"> + <properties> + <help>Urgent flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="psh"> + <properties> + <help>Push flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ecn"> + <properties> + <help>Explicit Congestion Notification flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="cwr"> + <properties> + <help>Congestion Window Reduced flag</help> + <valueless/> + </properties> + </leafNode> + <node name="not"> + <properties> + <help>Match flags not set</help> + </properties> + <children> + <leafNode name="syn"> + <properties> + <help>Synchronise flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ack"> + <properties> + <help>Acknowledge flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="fin"> + <properties> + <help>Finish flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="rst"> + <properties> + <help>Reset flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="urg"> + <properties> + <help>Urgent flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="psh"> + <properties> + <help>Push flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ecn"> + <properties> + <help>Explicit Congestion Notification flag</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="cwr"> + <properties> + <help>Congestion Window Reduced flag</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/interface/inbound-interface.xml.i b/interface-definitions/include/interface/inbound-interface.xml.i new file mode 100644 index 000000000..5a8d47280 --- /dev/null +++ b/interface-definitions/include/interface/inbound-interface.xml.i @@ -0,0 +1,10 @@ +<!-- include start from interface/inbound-interface.xml.i --> +<leafNode name="inbound-interface"> + <properties> + <help>Inbound Interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </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..866fcd5c0 --- /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="route6"> + <properties> + <help>IPv6 policy route ruleset for interface</help> + <completionHelp> + <path>policy route6</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..83510fe59 --- /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="route6"> + <properties> + <help>IPv6 policy route ruleset for interface</help> + <completionHelp> + <path>policy route6</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..42a8fd009 --- /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="route6"> + <properties> + <help>IPv6 policy route ruleset for interface</help> + <completionHelp> + <path>policy route6</path> + </completionHelp> + </properties> + </leafNode> + </children> +</node> +<!-- 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/listen-address.xml.i b/interface-definitions/include/listen-address.xml.i index 19ddf0bb0..48003dbf2 100644 --- a/interface-definitions/include/listen-address.xml.i +++ b/interface-definitions/include/listen-address.xml.i @@ -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-port.xml.i b/interface-definitions/include/nat-port.xml.i index ebba43712..5f762cfb3 100644 --- a/interface-definitions/include/nat-port.xml.i +++ b/interface-definitions/include/nat-port.xml.i @@ -3,6 +3,10 @@ <properties> <help>Port number</help> <valueHelp> + <format>txt</format> + <description>Named port (any name in /etc/services, e.g., http)</description> + </valueHelp> + <valueHelp> <format>u32:1-65535</format> <description>Numeric IP port</description> </valueHelp> @@ -11,9 +15,12 @@ <description>Numbered port range (e.g. 1001-1005)</description> </valueHelp> <valueHelp> - <format> </format> + <format/> <description>\n\nMultiple destination ports can be specified as a comma-separated list.\nThe whole list can also be negated using '!'.\nFor example: '!22,telnet,http,123,1001-1005'</description> </valueHelp> + <constraint> + <validator name="port-multi"/> + </constraint> </properties> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i index 084f1f722..bdb86ed9b 100644 --- a/interface-definitions/include/nat-rule.xml.i +++ b/interface-definitions/include/nat-rule.xml.i @@ -4,7 +4,7 @@ <help>Rule number for NAT</help> <valueHelp> <format>u32:1-999999</format> - <description>Number for this NAT rule</description> + <description>Number of NAT rule</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-999999"/> @@ -12,11 +12,7 @@ <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> - <leafNode name="description"> - <properties> - <help>Rule description</help> - </properties> - </leafNode> + #include <include/generic-description.xml.i> <node name="destination"> <properties> <help>NAT destination parameters</help> diff --git a/interface-definitions/include/nat-translation-port.xml.i b/interface-definitions/include/nat-translation-port.xml.i index 6e507353c..6f17df3d9 100644 --- a/interface-definitions/include/nat-translation-port.xml.i +++ b/interface-definitions/include/nat-translation-port.xml.i @@ -10,6 +10,9 @@ <format>range</format> <description>Numbered port range (e.g., 1001-1005)</description> </valueHelp> + <constraint> + <validator name="port-range"/> + </constraint> </properties> </leafNode> <!-- 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 688e78034..e783f4bec 100644 --- a/interface-definitions/include/ospf/protocol-common-config.xml.i +++ b/interface-definitions/include/ospf/protocol-common-config.xml.i @@ -289,6 +289,18 @@ </constraint> </properties> </leafNode> +<leafNode name="maximum-paths"> + <properties> + <help>Maximum multiple paths (ECMP)</help> + <valueHelp> + <format>u32:1-64</format> + <description>Maximum multiple paths (ECMP)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-64"/> + </constraint> + </properties> +</leafNode> <node name="distance"> <properties> <help>Administrative distance</help> 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..406125e55 --- /dev/null +++ b/interface-definitions/include/policy/route-common-rule-ipv6.xml.i @@ -0,0 +1,553 @@ +<!-- 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> + <constraint> + <validator name="mac-address-firewall"/> + </constraint> + </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> +#include <include/firewall/tcp-flags.xml.i> +<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..33c4ba77c --- /dev/null +++ b/interface-definitions/include/policy/route-common-rule.xml.i @@ -0,0 +1,402 @@ +<!-- 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> + <constraint> + <validator name="mac-address-firewall"/> + </constraint> + </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> +#include <include/firewall/tcp-flags.xml.i> +<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/ssh-user.xml.i b/interface-definitions/include/ssh-user.xml.i index 677602dd8..17ba05a90 100644 --- a/interface-definitions/include/ssh-user.xml.i +++ b/interface-definitions/include/ssh-user.xml.i @@ -3,9 +3,9 @@ <properties> <help>Allow specific users to login</help> <constraint> - <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex> + <regex>^[-_a-zA-Z0-9.]{1,100}</regex> </constraint> - <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage> + <constraintErrorMessage>Illegal characters or more than 100 characters</constraintErrorMessage> <multi/> </properties> </leafNode> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index 17879cf1e..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> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index 144f43f32..89a6d2303 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> @@ -57,7 +59,7 @@ </leafNode> <leafNode name="hello-time"> <properties> - <help>Hello packet advertisment interval</help> + <help>Hello packet advertisement interval</help> <valueHelp> <format>u32:1-10</format> <description>Spanning Tree Protocol hello advertisement interval in seconds (default 2)</description> diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in index 4d4c44160..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> 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 6b4440688..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> 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 203685790..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> 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 6c53f8c44..4c3c3ac71 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -53,6 +53,8 @@ #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..1b4b4a816 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> @@ -99,6 +101,7 @@ </valueHelp> <constraint> <validator name="ip-address"/> + <validator name="ipv6-link-local"/> </constraint> </properties> </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/policy-local-route.xml.in b/interface-definitions/policy-local-route.xml.in index 86445b65d..573a7963f 100644 --- a/interface-definitions/policy-local-route.xml.in +++ b/interface-definitions/policy-local-route.xml.in @@ -14,7 +14,7 @@ <valueHelp> <!-- table main with prio 32766 --> <format>u32:1-32765</format> - <description>Local-route rule number (1-219)</description> + <description>Local-route rule number (1-32765)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-32765"/> @@ -70,6 +70,115 @@ <multi/> </properties> </leafNode> + <leafNode name="destination"> + <properties> + <help>Destination address or prefix</help> + <valueHelp> + <format>ipv4</format> + <description>Address to match against</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>Prefix to match against</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + #include <include/interface/inbound-interface.xml.i> + </children> + </tagNode> + </children> + </node> + <node name="local-route6" owner="${vyos_conf_scripts_dir}/policy-local-route.py"> + <properties> + <help>IPv6 policy route of local traffic</help> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>IPv6 policy local-route rule set number</help> + <valueHelp> + <!-- table main with prio 32766 --> + <format>u32:1-32765</format> + <description>Local-route rule number (1-32765)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-32765"/> + </constraint> + </properties> + <children> + <node name="set"> + <properties> + <help>Packet modifications</help> + </properties> + <children> + <leafNode name="table"> + <properties> + <help>Routing table to forward packet with</help> + <valueHelp> + <format>u32:1-200</format> + <description>Table number</description> + </valueHelp> + <completionHelp> + <list>main</list> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="fwmark"> + <properties> + <help>Match fwmark value</help> + <valueHelp> + <format>u32:1-2147483647</format> + <description>Address to match against</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2147483647"/> + </constraint> + </properties> + </leafNode> + <leafNode name="source"> + <properties> + <help>Source address or prefix</help> + <valueHelp> + <format>ipv4</format> + <description>Address to match against</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>Prefix to match against</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="destination"> + <properties> + <help>Destination address or prefix</help> + <valueHelp> + <format>ipv6</format> + <description>Address to match against</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>Prefix to match against</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + #include <include/interface/inbound-interface.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/policy-route.xml.in b/interface-definitions/policy-route.xml.in new file mode 100644 index 000000000..a1c3b50de --- /dev/null +++ b/interface-definitions/policy-route.xml.in @@ -0,0 +1,105 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="policy"> + <children> + <tagNode name="route6" owner="${vyos_conf_scripts_dir}/policy-route.py"> + <properties> + <help>Policy route rule set name for IPv6</help> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> + <priority>201</priority> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/firewall/name-default-log.xml.i> + <tagNode name="rule"> + <properties> + <help>Policy rule number</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number of policy rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>Policy rule number must be between 1 and 999999</constraintErrorMessage> + </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 for IPv4</help> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> + <priority>201</priority> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/firewall/name-default-log.xml.i> + <tagNode name="rule"> + <properties> + <help>Policy rule number</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number of policy rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>Policy rule number must be between 1 and 999999</constraintErrorMessage> + </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 225f9a6f9..61c5ab90a 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -793,7 +793,7 @@ </node> <leafNode name="local-preference"> <properties> - <help>local-preference_help</help> + <help>Local Preference</help> <valueHelp> <format>u32:0-4294967295</format> <description>Local Preference</description> diff --git a/interface-definitions/service_monitoring_telegraf.xml.in b/interface-definitions/service_monitoring_telegraf.xml.in new file mode 100644 index 000000000..0db9052ff --- /dev/null +++ b/interface-definitions/service_monitoring_telegraf.xml.in @@ -0,0 +1,113 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="monitoring"> + <properties> + <help>Monitoring services</help> + <priority>1280</priority> + </properties> + <children> + <node name="telegraf" owner="${vyos_conf_scripts_dir}/service_monitoring_telegraf.py"> + <properties> + <help>Telegraf monitoring</help> + </properties> + <children> + <node name="authentication"> + <properties> + <help>Authentication parameters</help> + </properties> + <children> + <leafNode name="organization"> + <properties> + <help>Authentication organization for InfluxDB v2 [REQUIRED]</help> + <constraint> + <regex>^[a-zA-Z][1-9a-zA-Z@_\-.]{2,50}$</regex> + </constraint> + <constraintErrorMessage>Organization name must be alphanumeric and can contain hyphens, underscores and at symbol.</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="token"> + <properties> + <help>Authentication token for InfluxDB v2 [REQUIRED]</help> + <valueHelp> + <format>txt</format> + <description>Authentication token</description> + </valueHelp> + <constraint> + <regex>^[a-zA-Z0-9-_]{86}==$</regex> + </constraint> + <constraintErrorMessage>Token must be 88 characters long and must contain only [a-zA-Z0-9-_] and '==' characters.</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="bucket"> + <properties> + <help>Remote bucket, by default (main)</help> + </properties> + <defaultValue>main</defaultValue> + </leafNode> + <leafNode name="source"> + <properties> + <help>Source parameters for monitoring (default: all)</help> + <completionHelp> + <list>all hardware-utilization logs network system telegraf</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All parameters (default)</description> + </valueHelp> + <valueHelp> + <format>hardware-utilization</format> + <description>Hardware-utilization parameters (CPU, disk, memory)</description> + </valueHelp> + <valueHelp> + <format>logs</format> + <description>Logs parameters</description> + </valueHelp> + <valueHelp> + <format>network</format> + <description>Network parameters (net, netstat, nftables)</description> + </valueHelp> + <valueHelp> + <format>system</format> + <description>System parameters (system, processes, interrupts)</description> + </valueHelp> + <valueHelp> + <format>telegraf</format> + <description>Telegraf internal statistics</description> + </valueHelp> + <constraint> + <regex>^(all|hardware-utilization|logs|network|system|telegraf)$</regex> + </constraint> + <multi/> + </properties> + <defaultValue>all</defaultValue> + </leafNode> + <leafNode name="url"> + <properties> + <help>Remote URL [REQUIRED]</help> + <valueHelp> + <format>url</format> + <description>Remote URL to InfluxDB v2</description> + </valueHelp> + <constraint> + <regex>^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}?(\/.*)?$</regex> + </constraint> + <constraintErrorMessage>Incorrect URL format.</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Remote port (default: 8086)</help> + </properties> + <defaultValue>8086</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service_upnp.xml.in b/interface-definitions/service_upnp.xml.in new file mode 100644 index 000000000..7cfe1f02e --- /dev/null +++ b/interface-definitions/service_upnp.xml.in @@ -0,0 +1,224 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="upnp" owner="${vyos_conf_scripts_dir}/service_upnp.py"> + <properties> + <help>Universal Plug and Play (UPnP) service</help> + <priority>900</priority> + </properties> + <children> + <leafNode name="friendly-name"> + <properties> + <help>Name of this service</help> + <valueHelp> + <format>txt</format> + <description>Friendly name</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="wan-interface"> + <properties> + <help>WAN network interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <constraint> + <validator name="interface-name" /> + </constraint> + </properties> + </leafNode> + <leafNode name="wan-ip"> + <properties> + <help>WAN network IP</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address" /> + <validator name="ipv6-address" /> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="nat-pmp"> + <properties> + <help>Enable NAT-PMP support</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="secure-mode"> + <properties> + <help>Enable Secure Mode</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="presentation-url"> + <properties> + <help>Presentation Url</help> + <valueHelp> + <format>txt</format> + <description>Presentation Url</description> + </valueHelp> + </properties> + </leafNode> + <node name="pcp-lifetime"> + <properties> + <help>PCP-base lifetime Option</help> + </properties> + <children> + <leafNode name="max"> + <properties> + <help>Max lifetime time</help> + <constraint> + <validator name="numeric" /> + </constraint> + </properties> + </leafNode> + <leafNode name="min"> + <properties> + <help>Min lifetime time</help> + <constraint> + <validator name="numeric" /> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="listen"> + <properties> + <help>Local IP addresses for service to listen on</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <valueHelp> + <format><interface></format> + <description>Monitor interface address</description> + </valueHelp> + <valueHelp> + <format>ipv4</format> + <description>IP address to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv4-prefix</format> + <description>IP prefix to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IP address to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv6-prefix</format> + <description>IP prefix to listen for incoming connections</description> + </valueHelp> + <multi/> + <constraint> + <validator name="interface-name" /> + <validator name="ipv4-address"/> + <validator name="ipv4-prefix"/> + <validator name="ipv6-address"/> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + <node name="stun"> + <properties> + <help>Enable STUN probe support (can be used with NAT 1:1 support for WAN interfaces)</help> + </properties> + <children> + <leafNode name="host"> + <properties> + <help>The STUN server address</help> + <valueHelp> + <format>txt</format> + <description>The STUN server host address</description> + </valueHelp> + <constraint> + <validator name="fqdn"/> + </constraint> + </properties> + </leafNode> + #include <include/port-number.xml.i> + </children> + </node> + <tagNode name="rule"> + <properties> + <help>UPnP Rule</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Rule number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <children> + #include <include/generic-disable-node.xml.i> + <leafNode name="external-port-range"> + <properties> + <help>Port range (REQUIRE)</help> + <valueHelp> + <format><port></format> + <description>single port</description> + </valueHelp> + <valueHelp> + <format><portN>-<portM></format> + <description>Port range (use '-' as delimiter)</description> + </valueHelp> + <constraint> + <validator name="port-range"/> + </constraint> + </properties> + </leafNode> + <leafNode name="internal-port-range"> + <properties> + <help>Port range (REQUIRE)</help> + <valueHelp> + <format><port></format> + <description>single port</description> + </valueHelp> + <valueHelp> + <format><portN>-<portM></format> + <description>Port range (use '-' as delimiter)</description> + </valueHelp> + <constraint> + <validator name="port-range"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ip"> + <properties> + <help>The IP to which this rule applies (REQUIRE)</help> + <valueHelp> + <format>ipv4</format> + <description>The IPv4 to which this rule applies</description> + </valueHelp> + <constraint> + <validator name="ipv4-address" /> + </constraint> + </properties> + </leafNode> + <leafNode name="action"> + <properties> + <help>Actions against the rule (REQUIRE)</help> + <completionHelp> + <list>allow deny</list> + </completionHelp> + <constraint> + <regex>^(allow|deny)$</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-conntrack.xml.in b/interface-definitions/system-conntrack.xml.in index daa4177c9..65edab839 100644 --- a/interface-definitions/system-conntrack.xml.in +++ b/interface-definitions/system-conntrack.xml.in @@ -35,6 +35,128 @@ </properties> <defaultValue>32768</defaultValue> </leafNode> + <node name="ignore"> + <properties> + <help>Customized rules to ignore selective connection tracking</help> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Rule number</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number of conntrack ignore rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> + </properties> + <children> + #include <include/generic-description.xml.i> + <node name="destination"> + <properties> + <help>Destination parameters</help> + </properties> + <children> + #include <include/nat-address.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> + <leafNode name="inbound-interface"> + <properties> + <help>Interface to ignore connections tracking on</help> + <completionHelp> + <list>any</list> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + </leafNode> + #include <include/ip-protocol.xml.i> + <leafNode name="protocol"> + <properties> + <help>Protocol to match (protocol name, number, or "all")</help> + <completionHelp> + <script>${vyos_completion_dir}/list_protocols.sh</script> + <list>all tcp_udp</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All IP protocols</description> + </valueHelp> + <valueHelp> + <format>tcp_udp</format> + <description>Both TCP and UDP</description> + </valueHelp> + <valueHelp> + <format>u32:0-255</format> + <description>IP protocol number</description> + </valueHelp> + <valueHelp> + <format><protocol></format> + <description>IP protocol name</description> + </valueHelp> + <valueHelp> + <format>!<protocol></format> + <description>IP protocol name</description> + </valueHelp> + <constraint> + <validator name="ip-protocol"/> + </constraint> + </properties> + </leafNode> + <node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/nat-address.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> + </children> + </tagNode> + </children> + </node> + <node name="log"> + <properties> + <help>Log connection tracking events per protocol</help> + </properties> + <children> + <node name="icmp"> + <properties> + <help>Log connection tracking events for ICMP</help> + </properties> + <children> + #include <include/conntrack/log-common.xml.i> + </children> + </node> + <node name="other"> + <properties> + <help>Log connection tracking events for all protocols other than TCP, UDP and ICMP</help> + </properties> + <children> + #include <include/conntrack/log-common.xml.i> + </children> + </node> + <node name="tcp"> + <properties> + <help>Log connection tracking events for TCP</help> + </properties> + <children> + #include <include/conntrack/log-common.xml.i> + </children> + </node> + <node name="udp"> + <properties> + <help>Log connection tracking events for UDP</help> + </properties> + <children> + #include <include/conntrack/log-common.xml.i> + </children> + </node> + </children> + </node> <node name="modules"> <properties> <help>Connection tracking modules</help> @@ -155,176 +277,66 @@ <help>Connection timeout options</help> </properties> <children> - <leafNode name="icmp"> - <properties> - <help>ICMP timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>ICMP timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>30</defaultValue> - </leafNode> - <leafNode name="other"> - <properties> - <help>Generic connection timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>Generic connection timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>600</defaultValue> - </leafNode> - <node name="tcp"> - <properties> - <help>TCP connection timeout options</help> - </properties> - <children> - <leafNode name="close-wait"> - <properties> - <help>TCP CLOSE-WAIT timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>TCP CLOSE-WAIT timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>60</defaultValue> - </leafNode> - <leafNode name="close"> - <properties> - <help>TCP CLOSE timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>TCP CLOSE timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>10</defaultValue> - </leafNode> - <leafNode name="established"> - <properties> - <help>TCP ESTABLISHED timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>TCP ESTABLISHED timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>432000</defaultValue> - </leafNode> - <leafNode name="fin-wait"> - <properties> - <help>TCP FIN-WAIT timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>TCP FIN-WAIT timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>120</defaultValue> - </leafNode> - <leafNode name="last-ack"> - <properties> - <help>TCP LAST-ACK timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>TCP LAST-ACK timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>30</defaultValue> - </leafNode> - <leafNode name="syn-recv"> - <properties> - <help>TCP SYN-RECEIVED timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>TCP SYN-RECEIVED timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>60</defaultValue> - </leafNode> - <leafNode name="syn-sent"> - <properties> - <help>TCP SYN-SENT timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>TCP SYN-SENT timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>120</defaultValue> - </leafNode> - <leafNode name="time-wait"> - <properties> - <help>TCP TIME-WAIT timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>TCP TIME-WAIT timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>120</defaultValue> - </leafNode> - </children> - </node> - <node name="udp"> + <node name="custom"> <properties> - <help>UDP timeout options</help> + <help>Define custom timeouts per connection</help> </properties> <children> - <leafNode name="other"> + <tagNode name="rule"> <properties> - <help>UDP generic timeout in seconds</help> + <help>Rule number</help> <valueHelp> - <format>u32:1-21474836</format> - <description>UDP generic timeout in seconds</description> + <format>u32:1-999999</format> + <description>Number of conntrack rule</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-21474836"/> + <validator name="numeric" argument="--range 1-999999"/> </constraint> + <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> </properties> - <defaultValue>30</defaultValue> - </leafNode> - <leafNode name="stream"> - <properties> - <help>UDP stream timeout in seconds</help> - <valueHelp> - <format>u32:1-21474836</format> - <description>UDP stream timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-21474836"/> - </constraint> - </properties> - <defaultValue>180</defaultValue> - </leafNode> + <children> + #include <include/generic-description.xml.i> + <node name="destination"> + <properties> + <help>Destination parameters</help> + </properties> + <children> + #include <include/nat-address.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> + <leafNode name="inbound-interface"> + <properties> + <help>Interface to ignore connections tracking on</help> + <completionHelp> + <list>any</list> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + </leafNode> + #include <include/ip-protocol.xml.i> + <node name="protocol"> + <properties> + <help>Customize protocol specific timers, one protocol configuration per rule</help> + </properties> + <children> + #include <include/conntrack/timeout-common-protocols.xml.i> + </children> + </node> + <node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/nat-address.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> + </children> + </tagNode> </children> </node> + #include <include/conntrack/timeout-common-protocols.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 17ba83bae..afa3d52a0 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -286,7 +286,7 @@ </node> <leafNode name="ikev2-reauth"> <properties> - <help>ikev2-reauth_help</help> + <help>Re-authentication of the remote peer during an IKE re-key. IKEv2 option only</help> <completionHelp> <list>yes no</list> </completionHelp> @@ -1047,6 +1047,18 @@ #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>Match remote addresses</help> diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in new file mode 100644 index 000000000..69ee031c7 --- /dev/null +++ b/interface-definitions/zone-policy.xml.in @@ -0,0 +1,146 @@ +<?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> + <constraint> + <regex>^[a-zA-Z0-9][\w\-\.]*$</regex> + </constraint> + </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/firewall.xml.in b/op-mode-definitions/firewall.xml.in new file mode 100644 index 000000000..b5dee7c9e --- /dev/null +++ b/op-mode-definitions/firewall.xml.in @@ -0,0 +1,191 @@ +<?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> + <tagNode name="group"> + <properties> + <help>Show firewall group</help> + <completionHelp> + <path>firewall group address-group</path> + <path>firewall group network-group</path> + <path>firewall group port-group</path> + <path>firewall group ipv6-address-group</path> + <path>firewall group ipv6-network-group</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show_group --name $4</command> + </tagNode> + <leafNode name="group"> + <properties> + <help>Show firewall group</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show_group</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/policy-route.xml.in b/op-mode-definitions/policy-route.xml.in new file mode 100644 index 000000000..bd4a61dc9 --- /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="route6"> + <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="route6"> + <properties> + <help>Show IPv6 policy chains</help> + <completionHelp> + <path>policy route6</path> + </completionHelp> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Show summary of IPv6 policy rules</help> + <completionHelp> + <path>policy route6 ${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/show-virtual-server.xml.in b/op-mode-definitions/show-virtual-server.xml.in new file mode 100644 index 000000000..5dbd3c759 --- /dev/null +++ b/op-mode-definitions/show-virtual-server.xml.in @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="virtual-server"> + <properties> + <help>Show virtual server information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_virtual_server.py</command> + </node> + </children> + </node> +</interfaceDefinition> 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/configdict.py b/python/vyos/configdict.py index d974a7565..e7f515ea9 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -196,7 +196,7 @@ def is_member(conf, interface, intftype=None): interface name -> Interface is a member of this interface False -> interface type cannot have members """ - ret_val = None + ret_val = {} intftypes = ['bonding', 'bridge'] if intftype not in intftypes + [None]: @@ -216,8 +216,8 @@ def is_member(conf, interface, intftype=None): member = base + [intf, 'member', 'interface', interface] if conf.exists(member): tmp = conf.get_config_dict(member, key_mangling=('-', '_'), - get_first_key=True) - ret_val = {intf : tmp} + get_first_key=True, no_tag_node_value_mangle=True) + ret_val.update({intf : tmp}) old_level = conf.set_level(old_level) return ret_val 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/configsource.py b/python/vyos/configsource.py index a0f6a46b5..510b5b65a 100644 --- a/python/vyos/configsource.py +++ b/python/vyos/configsource.py @@ -162,7 +162,7 @@ class ConfigSourceSession(ConfigSource): if p.returncode != 0: raise VyOSError() else: - return out.decode('ascii', 'ignore') + return out.decode() def set_level(self, path): """ diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index d8ffaca99..866f24e47 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -15,8 +15,9 @@ import re import json -from ctypes import cdll, c_char_p, c_void_p, c_int +from ctypes import cdll, c_char_p, c_void_p, c_int, POINTER +LIBPATH = '/usr/lib/libvyosconfig.so.0' def escape_backslash(string: str) -> str: """Escape single backslashes in string that are not in escape sequence""" @@ -42,7 +43,9 @@ class ConfigTreeError(Exception): class ConfigTree(object): - def __init__(self, config_string, libpath='/usr/lib/libvyosconfig.so.0'): + def __init__(self, config_string=None, address=None, libpath=LIBPATH): + if config_string is None and address is None: + raise TypeError("ConfigTree() requires one of 'config_string' or 'address'") self.__config = None self.__lib = cdll.LoadLibrary(libpath) @@ -60,7 +63,7 @@ class ConfigTree(object): self.__to_string.restype = c_char_p self.__to_commands = self.__lib.to_commands - self.__to_commands.argtypes = [c_void_p] + self.__to_commands.argtypes = [c_void_p, c_char_p] self.__to_commands.restype = c_char_p self.__to_json = self.__lib.to_json @@ -126,15 +129,19 @@ class ConfigTree(object): self.__destroy = self.__lib.destroy self.__destroy.argtypes = [c_void_p] - config_section, version_section = extract_version(config_string) - config_section = escape_backslash(config_section) - config = self.__from_string(config_section.encode()) - if config is None: - msg = self.__get_error().decode() - raise ValueError("Failed to parse config: {0}".format(msg)) + if address is None: + config_section, version_section = extract_version(config_string) + config_section = escape_backslash(config_section) + config = self.__from_string(config_section.encode()) + if config is None: + msg = self.__get_error().decode() + raise ValueError("Failed to parse config: {0}".format(msg)) + else: + self.__config = config + self.__version = version_section else: - self.__config = config - self.__version = version_section + self.__config = address + self.__version = '' def __del__(self): if self.__config is not None: @@ -143,13 +150,16 @@ class ConfigTree(object): def __str__(self): return self.to_string() + def _get_config(self): + return self.__config + def to_string(self): config_string = self.__to_string(self.__config).decode() config_string = "{0}\n{1}".format(config_string, self.__version) return config_string - def to_commands(self): - return self.__to_commands(self.__config).decode() + def to_commands(self, op="set"): + return self.__to_commands(self.__config, op.encode()).decode() def to_json(self): return self.__to_json(self.__config).decode() @@ -281,3 +291,32 @@ class ConfigTree(object): else: raise ConfigTreeError("Path [{}] doesn't exist".format(path_str)) +class Diff: + def __init__(self, left, right, path=[], libpath=LIBPATH): + if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)): + raise TypeError("Arguments must be instances of ConfigTree") + if path: + if not left.exists(path): + raise ConfigTreeError(f"Path {path} doesn't exist in lhs tree") + if not right.exists(path): + raise ConfigTreeError(f"Path {path} doesn't exist in rhs tree") + self.left = left + self.right = right + + check_path(path) + path_str = " ".join(map(str, path)).encode() + df = cdll.LoadLibrary(libpath).diffs + df.restype = POINTER(c_void_p * 3) + res = list(df(path_str, left._get_config(), right._get_config()).contents) + self._diff = {'add': ConfigTree(address=res[0]), + 'del': ConfigTree(address=res[1]), + 'int': ConfigTree(address=res[2]) } + + self.add = self._diff['add'] + self.delete = self._diff['del'] + self.inter = self._diff['int'] + + def to_commands(self): + add = self.add.to_commands() + delete = self.delete.to_commands(op="delete") + return delete + "\n" + add diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py new file mode 100644 index 000000000..55ce318e7 --- /dev/null +++ b/python/vyos/firewall.py @@ -0,0 +1,250 @@ +#!/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']) + + if states: + output.append(f'ct state {{{states}}}') + + if 'protocol' in rule_conf and rule_conf['protocol'] != 'all': + proto = rule_conf['protocol'] + operator = '' + if proto[0] == '!': + operator = '!=' + proto = proto[1:] + if proto == 'tcp_udp': + proto = '{tcp, udp}' + output.append(f'meta l4proto {operator} {proto}') + + for side in ['destination', 'source']: + if side in rule_conf: + prefix = side[0] + side_conf = rule_conf[side] + + if 'address' in side_conf: + suffix = side_conf['address'] + if suffix[0] == '!': + suffix = f'!= {suffix[1:]}' + output.append(f'{ip_name} {prefix}addr {suffix}') + + 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'].split(',') + + ports = [] + negated_ports = [] + + for p in port: + if p[0] == '!': + negated_ports.append(p[1:]) + else: + ports.append(p) + + if proto == 'tcp_udp': + proto = 'th' + + if ports: + ports_str = ','.join(ports) + output.append(f'{proto} {prefix}port {{{ports_str}}}') + + if negated_ports: + negated_ports_str = ','.join(negated_ports) + output.append(f'{proto} {prefix}port != {{{negated_ports_str}}}') + + if 'group' in side_conf: + group = side_conf['group'] + if 'address_group' in group: + group_name = group['address_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'{ip_name} {prefix}addr {operator} $A{def_suffix}_{group_name}') + elif 'network_group' in group: + group_name = group['network_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'{ip_name} {prefix}addr {operator} $N{def_suffix}_{group_name}') + if 'mac_group' in group: + group_name = group['mac_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'ether {prefix}addr {operator} $M_{group_name}') + if 'port_group' in group: + proto = rule_conf['protocol'] + group_name = group['port_group'] + + if proto == 'tcp_udp': + proto = 'th' + + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + + output.append(f'{proto} {prefix}port {operator} $P_{group_name}') + + if 'log' in rule_conf and rule_conf['log'] == 'enable': + action = rule_conf['action'] if 'action' in rule_conf else 'accept' + output.append(f'log prefix "[{fw_name[:19]}-{rule_id}-{action[:1].upper()}] "') + + 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'add @RECENT{def_suffix}_{fw_name}_{rule_id} {{ {ip_name} saddr limit rate over {count}/{time} burst {count} packets }}') + + 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): + include = [flag for flag in flags if flag != 'not'] + exclude = list(flags['not']) if 'not' in flags else [] + return f'tcp flags & ({"|".join(include + exclude)}) == {"|".join(include) if include else "0x0"}' + +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(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 a8f115d9a..cbba19ab7 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -73,15 +73,15 @@ from vyos.util import cmd import logging from logging.handlers import SysLogHandler import os +import sys + LOG = logging.getLogger(__name__) +DEBUG = False -DEBUG = os.path.exists('/tmp/vyos.frr.debug') -if DEBUG: - LOG.setLevel(logging.DEBUG) - ch = SysLogHandler(address='/dev/log') - ch2 = logging.StreamHandler() - LOG.addHandler(ch) - LOG.addHandler(ch2) +ch = SysLogHandler(address='/dev/log') +ch2 = logging.StreamHandler(stream=sys.stdout) +LOG.addHandler(ch) +LOG.addHandler(ch2) _frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd', @@ -121,6 +121,12 @@ class ConfigSectionNotFound(FrrError): """ pass +def init_debugging(): + global DEBUG + + DEBUG = os.path.exists('/tmp/vyos.frr.debug') + if DEBUG: + LOG.setLevel(logging.DEBUG) def get_configuration(daemon=None, marked=False): """ Get current running FRR configuration @@ -424,6 +430,8 @@ class FRRConfig: Using this overwrites the current loaded config objects and replaces the original loaded config ''' + init_debugging() + self.imported_config = get_configuration(daemon=daemon) if daemon: LOG.debug(f'load_configuration: Configuration loaded from FRR daemon {daemon}') diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index 2d3e406ac..a37615c8f 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -26,6 +26,7 @@ from vyos.ifconfig.ethernet import EthernetIf from vyos.ifconfig.geneve import GeneveIf from vyos.ifconfig.loopback import LoopbackIf from vyos.ifconfig.macvlan import MACVLANIf +from vyos.ifconfig.input import InputIf from vyos.ifconfig.vxlan import VXLANIf from vyos.ifconfig.wireguard import WireGuardIf from vyos.ifconfig.vtun import VTunIf diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 5fdd27828..91c7f0c33 100755 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -577,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 @@ -588,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): """ @@ -616,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): """ diff --git a/python/vyos/remote.py b/python/vyos/remote.py index aa62ac60d..66044fa52 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -83,8 +83,7 @@ def check_storage(path, size): 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.') + print_error('Warning: Cannot determine size of remote file. Bravely continuing regardless.') return if size < 1024 * 1024: @@ -227,7 +226,7 @@ class HttpC: r.raise_for_status() # If the request got redirected, keep the last URL we ended up with. final_urlstring = r.url - if r.history: + if r.history and self.progressbar: print_error('Redirecting to ' + final_urlstring) # Check for the prospective file size. try: @@ -317,11 +316,12 @@ def friendly_download(local_path, urlstring, source_host='', source_port=0): sys.exit(1) except: import traceback + print_error(f'Failed to download {urlstring}.') # There are a myriad different reasons a download could fail. # SSH errors, FTP errors, I/O errors, HTTP errors (403, 404...) # We omit the scary stack trace but print the error nevertheless. - print_error(f'Failed to download {urlstring}.') - traceback.print_exception(*sys.exc_info()[:2], None) + exc_type, exc_value, exc_traceback = sys.exc_info() + traceback.print_exception(exc_type, exc_value, None, 0, None, False) sys.exit(1) else: print_error('Download complete.') diff --git a/python/vyos/template.py b/python/vyos/template.py index f694b53e0..dabf53692 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() @@ -126,6 +127,14 @@ def render( ################################## # Custom template filters follow # ################################## +@register_filter('force_to_list') +def force_to_list(value): + """ Convert scalars to single-item lists and leave lists untouched """ + if isinstance(value, list): + return value + else: + return [value] + @register_filter('ip_from_cidr') def ip_from_cidr(prefix): """ Take an IPv4/IPv6 CIDR host and strip cidr mask. @@ -503,3 +512,57 @@ def snmp_auth_oid(type): '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_default_rule') +def nft_default_rule(fw_conf, fw_name): + output = ['counter'] + default_action = fw_conf.get('default_action', 'accept') + + if 'enable_default_log' in fw_conf: + action_suffix = default_action[:1].upper() + output.append(f'log prefix "[{fw_name[:19]}-default-{action_suffix}] "') + + output.append(nft_action(default_action)) + output.append(f'comment "{fw_name} default-action {default_action}"') + return " ".join(output) + +@register_filter('nft_state_policy') +def nft_state_policy(conf, state, ipv6=False): + 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' + name_prefix = 'NAME6_' 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_prefix}{name}' + return 'return' diff --git a/python/vyos/util.py b/python/vyos/util.py index 954c6670d..571d43754 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -952,14 +952,23 @@ def install_into_config(conf, config_paths, override_prompt=True): return None count = 0 + failed = [] 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 + try: + cmd(f'/opt/vyatta/sbin/my_set {path}') + count += 1 + except: + failed.append(path) + + if failed: + print(f'Failed to install {len(failed)} value(s). Commands to manually install:') + for path in failed: + print(f'set {path}') if count > 0: print(f'{count} value(s) installed. Use "compare" to see the pending changes, and "commit" to apply.') diff --git a/smoketest/configs/dialup-router-medium-vpn b/smoketest/configs/dialup-router-medium-vpn index af7c075e4..63d955738 100644 --- a/smoketest/configs/dialup-router-medium-vpn +++ b/smoketest/configs/dialup-router-medium-vpn @@ -6,6 +6,15 @@ firewall { ipv6-src-route disable ip-src-route disable log-martians enable + name test_tcp_flags { + rule 1 { + action drop + protocol tcp + tcp { + flags SYN,ACK,!RST,!FIN + } + } + } options { interface vtun0 { adjust-mss 1380 @@ -83,6 +92,7 @@ interfaces { } policy { route LAN-POLICY-BASED-ROUTING + ipv6-route LAN6-POLICY-BASED-ROUTING } smp-affinity auto speed auto @@ -383,6 +393,29 @@ nat { } } policy { + ipv6-route LAN6-POLICY-BASED-ROUTING { + rule 10 { + destination { + } + disable + set { + table 10 + } + source { + address 2002::1 + } + } + rule 20 { + destination { + } + set { + table 100 + } + source { + address 2008::f + } + } + } prefix-list user2-routes { rule 1 { action permit diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index bc0a6c128..9de961249 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -607,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 @@ -662,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/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py index 50f80e7d1..1652aa0d6 100644 --- a/smoketest/scripts/cli/base_vyostest_shim.py +++ b/smoketest/scripts/cli/base_vyostest_shim.py @@ -73,7 +73,7 @@ class VyOSUnitTestSHIM: def cli_commit(self): self._session.commit() # during a commit there is a process opening commit_lock, and run() returns 0 - while run(f'sudo lsof | grep -q {commit_lock}') == 0: + while run(f'sudo lsof -nP {commit_lock}') == 0: sleep(0.250) def getFRRconfig(self, string, end='$', endsection='^!', daemon=''): diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py new file mode 100755 index 000000000..ecc0c29a0 --- /dev/null +++ b/smoketest/scripts/cli/test_firewall.py @@ -0,0 +1,178 @@ +#!/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', 'mac-group', 'smoketest_mac', 'mac-address', '00:01:02:03:04:05']) + 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_udp']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'group', 'mac-group', 'smoketest_mac']) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) + + self.cli_commit() + + nftables_search = [ + ['iifname "eth0"', 'jump NAME_smoketest'], + ['ip saddr { 172.16.99.0/24 }', 'ip daddr 172.16.10.10', 'th dport { 53, 123 }', 'return'], + ['ether saddr { 00:01:02:03:04:05 }', '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, msg=search) + + 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']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'destination', 'port', '8888']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'tcp', 'flags', 'syn']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'tcp', 'flags', 'not', 'ack']) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) + + self.cli_commit() + + nftables_search = [ + ['iifname "eth0"', 'jump NAME_smoketest'], + ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'return'], + ['tcp flags & (syn | ack) == syn', 'tcp 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, msg=search) + + 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 NAME6_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, msg=search) + + def test_state_policy(self): + self.cli_set(['firewall', 'state-policy', 'established', 'action', 'accept']) + self.cli_set(['firewall', 'state-policy', 'related', 'action', 'accept']) + self.cli_set(['firewall', 'state-policy', 'invalid', 'action', 'drop']) + + self.cli_commit() + + chains = { + 'ip filter': ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL'], + 'ip6 filter': ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL'] + } + + for table in ['ip filter', 'ip6 filter']: + for chain in chains[table]: + nftables_output = cmd(f'sudo nft list chain {table} {chain}') + self.assertTrue('jump VYOS_STATE_POLICY' in nftables_output) + + 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_virtual_server.py b/smoketest/scripts/cli/test_ha_virtual_server.py new file mode 100755 index 000000000..e3a91283e --- /dev/null +++ b/smoketest/scripts/cli/test_ha_virtual_server.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.ifconfig.vrrp import VRRP +from vyos.util import cmd +from vyos.util import process_named_running +from vyos.util import read_file +from vyos.template import inc_ip + +PROCESS_NAME = 'keepalived' +KEEPALIVED_CONF = VRRP.location['config'] +base_path = ['high-availability'] +vrrp_interface = 'eth1' + +class TestHAVirtualServer(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + self.cli_delete(['interfaces', 'ethernet', vrrp_interface, 'address']) + self.cli_delete(base_path) + self.cli_commit() + + # Process must be terminated after deleting the config + self.assertFalse(process_named_running(PROCESS_NAME)) + + def test_01_ha_virtual_server(self): + algo = 'least-connection' + delay = '10' + method = 'nat' + persistence_timeout = '600' + vip = '203.0.113.111' + vport = '2222' + rservers = ['192.0.2.21', '192.0.2.22', '192.0.2.23'] + rport = '22' + proto = 'tcp' + connection_timeout = '30' + + vserver_base = base_path + ['virtual-server'] + + self.cli_set(vserver_base + [vip, 'algorithm', algo]) + self.cli_set(vserver_base + [vip, 'delay-loop', delay]) + self.cli_set(vserver_base + [vip, 'forward-method', method]) + self.cli_set(vserver_base + [vip, 'persistence-timeout', persistence_timeout]) + self.cli_set(vserver_base + [vip, 'port', vport]) + self.cli_set(vserver_base + [vip, 'protocol', proto]) + for rs in rservers: + self.cli_set(vserver_base + [vip, 'real-server', rs, 'connection-timeout', connection_timeout]) + self.cli_set(vserver_base + [vip, 'real-server', rs, 'port', rport]) + + # commit changes + self.cli_commit() + + config = read_file(KEEPALIVED_CONF) + + self.assertIn(f'delay_loop {delay}', config) + self.assertIn(f'lb_algo lc', config) + self.assertIn(f'lb_kind {method.upper()}', config) + self.assertIn(f'persistence_timeout {persistence_timeout}', config) + self.assertIn(f'protocol {proto.upper()}', config) + for rs in rservers: + self.assertIn(f'real_server {rs} {rport}', config) + self.assertIn(f'{proto.upper()}_CHECK', config) + self.assertIn(f'connect_timeout {connection_timeout}', config) + + def test_02_ha_virtual_server_and_vrrp(self): + algo = 'least-connection' + delay = '15' + method = 'nat' + persistence_timeout = '300' + vip = '203.0.113.222' + vport = '22322' + rservers = ['192.0.2.11', '192.0.2.12'] + rport = '222' + proto = 'tcp' + connection_timeout = '23' + group = 'VyOS' + vrid = '99' + + vrrp_base = base_path + ['vrrp', 'group'] + vserver_base = base_path + ['virtual-server'] + + self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'address', '203.0.113.10/24']) + + # VRRP config + self.cli_set(vrrp_base + [group, 'description', group]) + self.cli_set(vrrp_base + [group, 'interface', vrrp_interface]) + self.cli_set(vrrp_base + [group, 'address', vip + '/24']) + self.cli_set(vrrp_base + [group, 'vrid', vrid]) + + # Virtual-server config + self.cli_set(vserver_base + [vip, 'algorithm', algo]) + self.cli_set(vserver_base + [vip, 'delay-loop', delay]) + self.cli_set(vserver_base + [vip, 'forward-method', method]) + self.cli_set(vserver_base + [vip, 'persistence-timeout', persistence_timeout]) + self.cli_set(vserver_base + [vip, 'port', vport]) + self.cli_set(vserver_base + [vip, 'protocol', proto]) + for rs in rservers: + self.cli_set(vserver_base + [vip, 'real-server', rs, 'connection-timeout', connection_timeout]) + self.cli_set(vserver_base + [vip, 'real-server', rs, 'port', rport]) + + # commit changes + self.cli_commit() + + config = read_file(KEEPALIVED_CONF) + + # Keepalived vrrp + self.assertIn(f'# {group}', config) + self.assertIn(f'interface {vrrp_interface}', config) + self.assertIn(f'virtual_router_id {vrid}', config) + 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 + + # Keepalived virtual-server + self.assertIn(f'delay_loop {delay}', config) + self.assertIn(f'lb_algo lc', config) + self.assertIn(f'lb_kind {method.upper()}', config) + self.assertIn(f'persistence_timeout {persistence_timeout}', config) + self.assertIn(f'protocol {proto.upper()}', config) + for rs in rservers: + self.assertIn(f'real_server {rs} {rport}', config) + self.assertIn(f'{proto.upper()}_CHECK', config) + self.assertIn(f'connect_timeout {connection_timeout}', config) + + +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 23a9f7796..68905e447 100755 --- a/smoketest/scripts/cli/test_ha_vrrp.py +++ b/smoketest/scripts/cli/test_ha_vrrp.py @@ -27,7 +27,7 @@ from vyos.template import inc_ip PROCESS_NAME = 'keepalived' KEEPALIVED_CONF = VRRP.location['config'] -base_path = ['high-availability', 'vrrp'] +base_path = ['high-availability'] vrrp_interface = 'eth1' groups = ['VLAN77', 'VLAN78', 'VLAN201'] @@ -56,7 +56,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): for group in groups: vlan_id = group.lstrip('VLAN') vip = f'100.64.{vlan_id}.1/24' - group_base = base_path + ['group', group] + group_base = base_path + ['vrrp', 'group', group] self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', inc_ip(vip, 1) + '/' + vip.split('/')[-1]]) @@ -91,7 +91,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): for group in groups: vlan_id = group.lstrip('VLAN') vip = f'100.64.{vlan_id}.1/24' - group_base = base_path + ['group', group] + group_base = base_path + ['vrrp', 'group', group] self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', inc_ip(vip, 1) + '/' + vip.split('/')[-1]]) @@ -138,7 +138,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): for group in groups: vlan_id = group.lstrip('VLAN') vip = f'100.64.{vlan_id}.1/24' - group_base = base_path + ['group', group] + group_base = base_path + ['vrrp', 'group', group] self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', inc_ip(vip, 1) + '/' + vip.split('/')[-1]]) @@ -146,7 +146,7 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.cli_set(group_base + ['address', vip]) self.cli_set(group_base + ['vrid', vlan_id]) - self.cli_set(base_path + ['sync-group', sync_group, 'member', group]) + self.cli_set(base_path + ['vrrp', 'sync-group', sync_group, 'member', group]) # commit changes self.cli_commit() @@ -166,5 +166,35 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): for group in groups: self.assertIn(f'{group}', config) + def test_04_exclude_vrrp_interface(self): + group = 'VyOS-WAN' + none_vrrp_interface = 'eth2' + vlan_id = '24' + vip = '100.64.24.1/24' + vip_dev = '192.0.2.2/24' + vrid = '150' + group_base = base_path + ['vrrp', 'group', group] + + self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', '100.64.24.11/24']) + self.cli_set(group_base + ['interface', f'{vrrp_interface}.{vlan_id}']) + self.cli_set(group_base + ['address', vip]) + self.cli_set(group_base + ['address', vip_dev, 'interface', none_vrrp_interface]) + self.cli_set(group_base + ['track', 'exclude-vrrp-interface']) + self.cli_set(group_base + ['track', 'interface', none_vrrp_interface]) + self.cli_set(group_base + ['vrid', vrid]) + + # commit changes + self.cli_commit() + + config = getConfig(f'vrrp_instance {group}') + + self.assertIn(f'interface {vrrp_interface}.{vlan_id}', config) + self.assertIn(f'virtual_router_id {vrid}', config) + self.assertIn(f'dont_track_primary', config) + self.assertIn(f' {vip}', config) + self.assertIn(f' {vip_dev} dev {none_vrrp_interface}', config) + self.assertIn(f'track_interface', config) + self.assertIn(f' {none_vrrp_interface}', config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index 86000553e..1d9a887bd 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -36,7 +36,6 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): cls._test_vlan = True cls._test_qinq = True cls._base_path = ['interfaces', 'bonding'] - cls._interfaces = ['bond0'] cls._mirror_interfaces = ['dum21354'] cls._members = [] @@ -52,6 +51,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): cls._options['bond0'] = [] for member in cls._members: cls._options['bond0'].append(f'member interface {member}') + cls._interfaces = list(cls._options) # call base-classes classmethod super(cls, cls).setUpClass() @@ -150,5 +150,19 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): defined_policy = read_file(f'/sys/class/net/{interface}/bonding/xmit_hash_policy').split() self.assertEqual(defined_policy[0], hash_policy) + def test_bonding_multi_use_member(self): + # Define available bonding hash policies + for interface in ['bond10', 'bond20']: + for member in self._members: + self.cli_set(self._base_path + [interface, 'member', 'interface', member]) + + # check validate() - can not use the same member interfaces multiple times + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(self._base_path + ['bond20']) + + self.cli_commit() + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index 5844e1ec1..491f1766d 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1135,18 +1135,13 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - # Check generated configuration - - # Expected values original = """ 50: from 203.0.113.1 lookup 23 50: from 203.0.113.2 lookup 23 """ tmp = cmd('ip rule show prio 50') - original = original.split() - tmp = tmp.split() - self.assertEqual(tmp, original) + self.assertEqual(sort_ip(tmp), sort_ip(original)) # Test set table for fwmark def test_fwmark_table_id(self): @@ -1161,17 +1156,32 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - # Check generated configuration - - # Expected values original = """ 101: from all fwmark 0x18 lookup 154 """ tmp = cmd('ip rule show prio 101') - original = original.split() - tmp = tmp.split() - self.assertEqual(tmp, original) + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for destination + def test_destination_table_id(self): + path = base_path + ['local-route'] + + dst = '203.0.113.1' + rule = '102' + table = '154' + + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'destination', dst]) + + self.cli_commit() + + original = """ + 102: from all to 203.0.113.1 lookup 154 + """ + tmp = cmd('ip rule show prio 102') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) # Test set table for sources with fwmark def test_fwmark_sources_table_id(self): @@ -1188,18 +1198,301 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - # Check generated configuration - - # Expected values original = """ 100: from 203.0.113.11 fwmark 0x17 lookup 150 100: from 203.0.113.12 fwmark 0x17 lookup 150 """ tmp = cmd('ip rule show prio 100') - original = original.split() - tmp = tmp.split() - self.assertEqual(tmp, original) + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources with iif + def test_iif_sources_table_id(self): + path = base_path + ['local-route'] + + sources = ['203.0.113.11', '203.0.113.12'] + iif = 'lo' + rule = '100' + table = '150' + + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'inbound-interface', iif]) + for src in sources: + self.cli_set(path + ['rule', rule, 'source', src]) + + self.cli_commit() + + # Check generated configuration + # Expected values + original = """ + 100: from 203.0.113.11 iif lo lookup 150 + 100: from 203.0.113.12 iif lo lookup 150 + """ + tmp = cmd('ip rule show prio 100') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources and destinations with fwmark + def test_fwmark_sources_destination_table_id(self): + path = base_path + ['local-route'] + + sources = ['203.0.113.11', '203.0.113.12'] + destinations = ['203.0.113.13', '203.0.113.15'] + fwmk = '23' + rule = '103' + table = '150' + for src in sources: + for dst in destinations: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'destination', dst]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 103: from 203.0.113.11 to 203.0.113.13 fwmark 0x17 lookup 150 + 103: from 203.0.113.11 to 203.0.113.15 fwmark 0x17 lookup 150 + 103: from 203.0.113.12 to 203.0.113.13 fwmark 0x17 lookup 150 + 103: from 203.0.113.12 to 203.0.113.15 fwmark 0x17 lookup 150 + """ + tmp = cmd('ip rule show prio 103') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table ipv6 for some sources ipv6 + def test_ipv6_table_id(self): + path = base_path + ['local-route6'] + + sources = ['2001:db8:123::/48', '2001:db8:126::/48'] + rule = '50' + table = '23' + for src in sources: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + + self.cli_commit() + + original = """ + 50: from 2001:db8:123::/48 lookup 23 + 50: from 2001:db8:126::/48 lookup 23 + """ + tmp = cmd('ip -6 rule show prio 50') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for fwmark ipv6 + def test_fwmark_ipv6_table_id(self): + path = base_path + ['local-route6'] + + fwmk = '24' + rule = '100' + table = '154' + + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 100: from all fwmark 0x18 lookup 154 + """ + tmp = cmd('ip -6 rule show prio 100') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for destination ipv6 + def test_destination_ipv6_table_id(self): + path = base_path + ['local-route6'] + + dst = '2001:db8:1337::/126' + rule = '101' + table = '154' + + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'destination', dst]) + + self.cli_commit() + + original = """ + 101: from all to 2001:db8:1337::/126 lookup 154 + """ + tmp = cmd('ip -6 rule show prio 101') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources with fwmark ipv6 + def test_fwmark_sources_ipv6_table_id(self): + path = base_path + ['local-route6'] + + sources = ['2001:db8:1338::/126', '2001:db8:1339::/126'] + fwmk = '23' + rule = '102' + table = '150' + for src in sources: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 102: from 2001:db8:1338::/126 fwmark 0x17 lookup 150 + 102: from 2001:db8:1339::/126 fwmark 0x17 lookup 150 + """ + tmp = cmd('ip -6 rule show prio 102') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources with iif ipv6 + def test_iif_sources_ipv6_table_id(self): + path = base_path + ['local-route6'] + + sources = ['2001:db8:1338::/126', '2001:db8:1339::/126'] + iif = 'lo' + rule = '102' + table = '150' + for src in sources: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'inbound-interface', iif]) + + self.cli_commit() + + # Check generated configuration + # Expected values + original = """ + 102: from 2001:db8:1338::/126 iif lo lookup 150 + 102: from 2001:db8:1339::/126 iif lo lookup 150 + """ + tmp = cmd('ip -6 rule show prio 102') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources and destinations with fwmark ipv6 + def test_fwmark_sources_destination_ipv6_table_id(self): + path = base_path + ['local-route6'] + + sources = ['2001:db8:1338::/126', '2001:db8:1339::/56'] + destinations = ['2001:db8:13::/48', '2001:db8:16::/48'] + fwmk = '23' + rule = '103' + table = '150' + for src in sources: + for dst in destinations: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'destination', dst]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 103: from 2001:db8:1338::/126 to 2001:db8:13::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1338::/126 to 2001:db8:16::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1339::/56 to 2001:db8:13::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1339::/56 to 2001:db8:16::/48 fwmark 0x17 lookup 150 + """ + tmp = cmd('ip -6 rule show prio 103') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test delete table for sources and destination with fwmark ipv4/ipv6 + def test_delete_ipv4_ipv6_table_id(self): + path = base_path + ['local-route'] + path_v6 = base_path + ['local-route6'] + + sources = ['203.0.113.0/24', '203.0.114.5'] + destinations = ['203.0.112.0/24', '203.0.116.5'] + sources_v6 = ['2001:db8:1338::/126', '2001:db8:1339::/56'] + destinations_v6 = ['2001:db8:13::/48', '2001:db8:16::/48'] + fwmk = '23' + rule = '103' + table = '150' + for src in sources: + for dst in destinations: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'destination', dst]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + for src in sources_v6: + for dst in destinations_v6: + self.cli_set(path_v6 + ['rule', rule, 'set', 'table', table]) + self.cli_set(path_v6 + ['rule', rule, 'source', src]) + self.cli_set(path_v6 + ['rule', rule, 'destination', dst]) + self.cli_set(path_v6 + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 103: from 203.0.113.0/24 to 203.0.116.5 fwmark 0x17 lookup 150 + 103: from 203.0.114.5 to 203.0.112.0/24 fwmark 0x17 lookup 150 + 103: from 203.0.114.5 to 203.0.116.5 fwmark 0x17 lookup 150 + 103: from 203.0.113.0/24 to 203.0.112.0/24 fwmark 0x17 lookup 150 + """ + original_v6 = """ + 103: from 2001:db8:1338::/126 to 2001:db8:16::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1339::/56 to 2001:db8:13::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1339::/56 to 2001:db8:16::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1338::/126 to 2001:db8:13::/48 fwmark 0x17 lookup 150 + """ + tmp = cmd('ip rule show prio 103') + tmp_v6 = cmd('ip -6 rule show prio 103') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + self.assertEqual(sort_ip(tmp_v6), sort_ip(original_v6)) + + self.cli_delete(path) + self.cli_delete(path_v6) + self.cli_commit() + + tmp = cmd('ip rule show prio 103') + tmp_v6 = cmd('ip -6 rule show prio 103') + + self.assertEqual(sort_ip(tmp), []) + self.assertEqual(sort_ip(tmp_v6), []) + + # Test multiple commits ipv4 + def test_multiple_commit_ipv4_table_id(self): + path = base_path + ['local-route'] + + sources = ['192.0.2.1', '192.0.2.2'] + destination = '203.0.113.25' + rule = '105' + table = '151' + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + for src in sources: + self.cli_set(path + ['rule', rule, 'source', src]) + + self.cli_commit() + + original_first = """ + 105: from 192.0.2.1 lookup 151 + 105: from 192.0.2.2 lookup 151 + """ + tmp = cmd('ip rule show prio 105') + + self.assertEqual(sort_ip(tmp), sort_ip(original_first)) + + # Create second commit with added destination + self.cli_set(path + ['rule', rule, 'destination', destination]) + self.cli_commit() + + original_second = """ + 105: from 192.0.2.1 to 203.0.113.25 lookup 151 + 105: from 192.0.2.2 to 203.0.113.25 lookup 151 + """ + tmp = cmd('ip rule show prio 105') + + self.assertEqual(sort_ip(tmp), sort_ip(original_second)) + + +def sort_ip(output): + o = '\n'.join([' '.join(line.strip().split()) for line in output.strip().splitlines()]) + o = o.splitlines() + o.sort() + return o if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py new file mode 100755 index 000000000..9035f0832 --- /dev/null +++ b/smoketest/scripts/cli/test_policy_route.py @@ -0,0 +1,134 @@ +#!/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(['protocols', 'static']) + self.cli_delete(['policy', 'route']) + self.cli_delete(['policy', 'route6']) + 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']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'port', '8888']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'syn']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'not', 'ack']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'table', table_id]) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'protocol', 'tcp_udp']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'destination', 'port', '8888']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'set', 'table', table_id]) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route', 'smoketest']) + self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route6', 'smoketest6']) + + self.cli_commit() + + mark_hex = "{0:#010x}".format(table_mark_offset - int(table_id)) + + # IPv4 + + nftables_search = [ + ['iifname "eth0"', 'jump VYOS_PBR_smoketest'], + ['tcp flags & (syn | ack) == syn', 'tcp 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) + + # IPv6 + + nftables6_search = [ + ['iifname "eth0"', 'jump VYOS_PBR6_smoketest'], + ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'meta mark set ' + mark_hex] + ] + + nftables6_output = cmd('sudo nft list table ip6 mangle') + + for search in nftables6_search: + matched = False + for line in nftables6_output.split("\n"): + if all(item in line for item in search): + matched = True + break + self.assertTrue(matched) + + # IP rule fwmark -> table + + 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_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_service_monitoring_telegraf.py b/smoketest/scripts/cli/test_service_monitoring_telegraf.py new file mode 100755 index 000000000..b857926e2 --- /dev/null +++ b/smoketest/scripts/cli/test_service_monitoring_telegraf.py @@ -0,0 +1,65 @@ +#!/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 ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.util import process_named_running +from vyos.util import read_file + +PROCESS_NAME = 'telegraf' +TELEGRAF_CONF = '/run/telegraf/vyos-telegraf.conf' +base_path = ['service', 'monitoring', 'telegraf'] +org = 'log@in.local' +token = 'GuRJc12tIzfjnYdKRAIYbxdWd2aTpOT9PVYNddzDnFV4HkAcD7u7-kndTFXjGuXzJN6TTxmrvPODB4mnFcseDV==' +port = '8888' +url = 'https://foo.local' +bucket = 'main' +inputs = ['cpu', 'disk', 'mem', 'net', 'system', 'kernel', 'interrupts', 'syslog'] + +class TestMonitoringTelegraf(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + def test_01_basic_config(self): + self.cli_set(base_path + ['authentication', 'organization', org]) + self.cli_set(base_path + ['authentication', 'token', token]) + self.cli_set(base_path + ['port', port]) + self.cli_set(base_path + ['url', url]) + + # commit changes + self.cli_commit() + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + config = read_file(TELEGRAF_CONF) + + # Check telegraf config + self.assertIn(f'organization = "{org}"', config) + self.assertIn(token, config) + self.assertIn(f'urls = ["{url}:{port}"]', config) + self.assertIn(f'bucket = "{bucket}"', config) + + for input in inputs: + self.assertIn(input, config) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_upnp.py b/smoketest/scripts/cli/test_service_upnp.py new file mode 100755 index 000000000..c3e9b600f --- /dev/null +++ b/smoketest/scripts/cli/test_service_upnp.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.template import ip_from_cidr +from vyos.util import read_file +from vyos.util import process_named_running + +UPNP_CONF = '/run/upnp/miniupnp.conf' +DAEMON = 'miniupnpd' +interface = 'eth0' +base_path = ['service', 'upnp'] +address_base = ['interfaces', 'ethernet', interface, 'address'] + +ipv4_addr = '100.64.0.1/24' +ipv6_addr = '2001:db8::1/64' + +class TestServiceUPnP(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) + + cls.cli_set(cls, address_base + [ipv4_addr]) + cls.cli_set(cls, address_base + [ipv6_addr]) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, address_base) + cls._session.commit() + + super(cls, cls).tearDownClass() + + def tearDown(self): + # Check for running process + self.assertTrue(process_named_running(DAEMON)) + + self.cli_delete(base_path) + self.cli_commit() + + # Check for running process + self.assertFalse(process_named_running(DAEMON)) + + def test_ipv4_base(self): + self.cli_set(base_path + ['nat-pmp']) + self.cli_set(base_path + ['listen', interface]) + + # check validate() - WAN interface is mandatory + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['wan-interface', interface]) + + self.cli_commit() + + config = read_file(UPNP_CONF) + self.assertIn(f'ext_ifname={interface}', config) + self.assertIn(f'listening_ip={interface}', config) + self.assertIn(f'enable_natpmp=yes', config) + self.assertIn(f'enable_upnp=yes', config) + + def test_ipv6_base(self): + v6_addr = ip_from_cidr(ipv6_addr) + + self.cli_set(base_path + ['nat-pmp']) + self.cli_set(base_path + ['listen', interface]) + self.cli_set(base_path + ['listen', v6_addr]) + + # check validate() - WAN interface is mandatory + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['wan-interface', interface]) + + self.cli_commit() + + config = read_file(UPNP_CONF) + self.assertIn(f'ext_ifname={interface}', config) + self.assertIn(f'listening_ip={interface}', config) + self.assertIn(f'ipv6_listening_ip={v6_addr}', config) + self.assertIn(f'enable_natpmp=yes', config) + self.assertIn(f'enable_upnp=yes', config) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index b2934cf04..95c2a6c55 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -15,10 +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.firewall import find_nftables_rule from vyos.util import cmd from vyos.util import read_file @@ -156,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'], @@ -166,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'], @@ -187,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: @@ -204,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 index a53999461..857df1be6 100755 --- a/smoketest/scripts/cli/test_system_flow-accounting.py +++ b/smoketest/scripts/cli/test_system_flow-accounting.py @@ -62,9 +62,20 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): self.cli_commit() # verify configuration - tmp = cmd('sudo iptables-save -t raw') + nftables_output = cmd('sudo nft list chain raw VYOS_CT_PREROUTING_HOOK').splitlines() for interface in Section.interfaces('ethernet'): - self.assertIn(f'-A VYATTA_CT_PREROUTING_HOOK -i {interface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size 128 --nflog-threshold 100', tmp) + 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 diff --git a/smoketest/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py index e8cc64463..c8cf04b7d 100755 --- a/smoketest/scripts/cli/test_system_ntp.py +++ b/smoketest/scripts/cli/test_system_ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,7 +14,6 @@ # 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 @@ -29,17 +28,14 @@ PROCESS_NAME = 'ntpd' NTP_CONF = '/run/ntpd/ntpd.conf' base_path = ['system', 'ntp'] -def get_config_value(key): - tmp = read_file(NTP_CONF) - tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) - # remove possible trailing whitespaces - return [item.strip() for item in tmp] - class TestSystemNTP(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 :) - self.cli_delete(base_path) + cls.cli_delete(cls, base_path) def tearDown(self): self.cli_delete(base_path) @@ -47,35 +43,38 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.assertFalse(process_named_running(PROCESS_NAME)) - def test_ntp_options(self): + def test_01_ntp_options(self): # Test basic NTP support with multiple servers and their options servers = ['192.0.2.1', '192.0.2.2'] options = ['noselect', 'preempt', 'prefer'] - ntp_pool = 'pool.vyos.io' + pools = ['pool.vyos.io'] for server in servers: for option in options: self.cli_set(base_path + ['server', server, option]) # Test NTP pool - self.cli_set(base_path + ['server', ntp_pool, 'pool']) + for pool in pools: + self.cli_set(base_path + ['server', pool, 'pool']) # commit changes self.cli_commit() # Check generated configuration - tmp = get_config_value('server') - for server in servers: - test = f'{server} iburst ' + ' '.join(options) - self.assertTrue(test in tmp) + config = read_file(NTP_CONF) + self.assertIn('driftfile /var/lib/ntp/ntp.drift', config) + self.assertIn('restrict default noquery nopeer notrap nomodify', config) + self.assertIn('restrict source nomodify notrap noquery', config) + self.assertIn('restrict 127.0.0.1', config) + self.assertIn('restrict -6 ::1', config) - tmp = get_config_value('pool') - self.assertTrue(f'{ntp_pool} iburst' in tmp) + for server in servers: + self.assertIn(f'server {server} iburst ' + ' '.join(options), config) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + for pool in pools: + self.assertIn(f'pool {pool} iburst', config) - def test_ntp_clients(self): + def test_02_ntp_clients(self): # Test the allowed-networks statement listen_address = ['127.0.0.1', '::1'] for listen in listen_address: @@ -96,23 +95,18 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check generated client address configuration + config = read_file(NTP_CONF) + self.assertIn('restrict default ignore', config) + for network in networks: network_address = address_from_cidr(network) network_netmask = netmask_from_cidr(network) - - tmp = get_config_value(f'restrict {network_address}')[0] - test = f'mask {network_netmask} nomodify notrap nopeer' - self.assertTrue(tmp in test) + self.assertIn(f'restrict {network_address} mask {network_netmask} nomodify notrap nopeer', config) # Check listen address - tmp = get_config_value('interface') - test = ['ignore wildcard'] + self.assertIn('interface ignore wildcard', config) for listen in listen_address: - test.append(f'listen {listen}') - self.assertEqual(tmp, test) - - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.assertIn(f'interface listen {listen}', config) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index c710aec6e..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) diff --git a/smoketest/scripts/cli/test_zone_policy.py b/smoketest/scripts/cli/test_zone_policy.py new file mode 100755 index 000000000..00dfe0182 --- /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 NAME_smoketest'], + ['oifname { "eth0" }', 'jump NAME_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/conf_mode/conntrack.py b/src/conf_mode/conntrack.py index 68877f794..aabf2bdf5 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 @@ -32,6 +35,7 @@ airbag.enable() conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf' sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf' +nftables_ct_file = r'/run/nftables-ct.conf' # Every ALG (Application Layer Gateway) consists of either a Kernel Object # also called a Kernel Module/Driver or some rules present in iptables @@ -43,8 +47,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 +57,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'], @@ -80,19 +82,49 @@ def get_config(config=None): # 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) + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'timeout' in default_values and 'custom' in default_values['timeout']: + del default_values['timeout']['custom'] conntrack = dict_merge(default_values, conntrack) return conntrack def verify(conntrack): + if dict_search('ignore.rule', conntrack) != None: + for rule, rule_config in conntrack['ignore']['rule'].items(): + if dict_search('destination.port', rule_config) or \ + dict_search('source.port', rule_config): + if 'protocol' not in rule_config or rule_config['protocol'] not in ['tcp', 'udp']: + raise ConfigError(f'Port requires tcp or udp as protocol in rule {rule}') + return None def generate(conntrack): render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.tmpl', conntrack) render(sysctl_file, 'conntrack/sysctl.conf.tmpl', conntrack) + render(nftables_ct_file, 'conntrack/nftables-ct.tmpl', conntrack) + + # dry-run newly generated configuration + tmp = run(f'nft -c -f {nftables_ct_file}') + if tmp > 0: + if os.path.exists(nftables_ct_file): + os.unlink(nftables_ct_file) + raise ConfigError('Configuration file errors encountered!') return None +def find_nftables_ct_rule(rule): + 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 +135,20 @@ 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}') + + # Load new nftables ruleset + cmd(f'nft -f {nftables_ct_file}') if process_named_running('conntrackd'): # Reload conntrack-sync daemon to fetch new sysctl values diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py index f82a077e6..8f9837c2b 100755 --- a/src/conf_mode/conntrack_sync.py +++ b/src/conf_mode/conntrack_sync.py @@ -36,7 +36,7 @@ airbag.enable() config_file = '/run/conntrackd/conntrackd.conf' def resync_vrrp(): - tmp = run('/usr/libexec/vyos/conf_mode/vrrp.py') + tmp = run('/usr/libexec/vyos/conf_mode/high-availability.py') if tmp > 0: print('ERROR: error restarting VRRP daemon!') diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py index 2e14e0b25..26c50cab6 100755 --- a/src/conf_mode/containers.py +++ b/src/conf_mode/containers.py @@ -298,7 +298,7 @@ def apply(container): f'--memory {memory}m --memory-swap 0 --restart {restart} ' \ f'--name {name} {port} {volume} {env_opt}' if 'allow_host_networks' in container_config: - _cmd(f'{container_base_cmd} --net host {image}') + run(f'{container_base_cmd} --net host {image}') else: for network in container_config['network']: ipparam = '' @@ -306,19 +306,25 @@ def apply(container): address = container_config['network'][network]['address'] ipparam = f'--ip {address}' - 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) + run(f'{container_base_cmd} --net {network} {ipparam} {image}') return None +def run(container_cmd): + counter = 0 + while True: + if counter >= 10: + break + try: + _cmd(container_cmd) + break + except: + counter = counter +1 + sleep(0.5) + + return None + + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py new file mode 100755 index 000000000..9a5d278e9 --- /dev/null +++ b/src/conf_mode/firewall-interface.py @@ -0,0 +1,175 @@ +#!/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() + +NAME_PREFIX = 'NAME_' +NAME6_PREFIX = 'NAME6_' + +NFT_CHAINS = { + 'in': 'VYOS_FW_FORWARD', + 'out': 'VYOS_FW_FORWARD', + 'local': 'VYOS_FW_LOCAL' +} +NFT6_CHAINS = { + 'in': 'VYOS_FW6_FORWARD', + 'out': 'VYOS_FW6_FORWARD', + '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, prefix, ifname, new_name=None): + results = cmd(f'nft -a list chain {table} {chain}').split("\n") + retval = None + for line in results: + if f'{prefix}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 state_policy_handle(table, chain): + # Find any state-policy rule to ensure interface rules are only inserted afterwards + results = cmd(f'nft -a list chain {table} {chain}').split("\n") + for line in results: + if 'jump VYOS_STATE_POLICY' in line: + handle_search = re.search('handle (\d+)', line) + if handle_search: + return handle_search[1] + return None + +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, if_prefix, ifname, f'{NAME_PREFIX}{name}') + + if not rule_exists: + rule_action = 'insert' + rule_prefix = '' + + handle = state_policy_handle('ip filter', chain) + if handle: + rule_action = 'add' + rule_prefix = f'position {handle}' + + run(f'nft {rule_action} rule ip filter {chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME_PREFIX}{name}') + else: + cleanup_rule('ip filter', chain, if_prefix, ifname) + + ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') + if ipv6_name: + rule_exists = cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname, f'{NAME6_PREFIX}{ipv6_name}') + + if not rule_exists: + rule_action = 'insert' + rule_prefix = '' + + handle = state_policy_handle('ip6 filter', ipv6_chain) + if handle: + rule_action = 'add' + rule_prefix = f'position {handle}' + + run(f'nft {rule_action} rule ip6 filter {ipv6_chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME6_PREFIX}{ipv6_name}') + else: + cleanup_rule('ip6 filter', ipv6_chain, if_prefix, 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..41df1b84a 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -15,51 +15,406 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import re +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() +policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py' -def get_config(config=None): +nftables_conf = '/run/nftables.conf' +nftables_defines_conf = '/run/nftables_defines.conf' + +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'} +} + +NAME_PREFIX = 'NAME_' +NAME6_PREFIX = 'NAME6_' + +preserve_chains = [ + 'INPUT', + 'FORWARD', + 'OUTPUT', + 'VYOS_FW_FORWARD', + 'VYOS_FW_LOCAL', + 'VYOS_FW_OUTPUT', + 'VYOS_POST_FW', + 'VYOS_FRAG_MARK', + 'VYOS_FW6_FORWARD', + 'VYOS_FW6_LOCAL', + 'VYOS_FW6_OUTPUT', + 'VYOS_POST_FW6', + 'VYOS_FRAG6_MARK' +] + +nft_iface_chains = ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL'] +nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL'] +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_firewall_zones(conf): + used_v4 = [] + used_v6 = [] + zone_policy = conf.get_config_dict(['zone-policy'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + if 'zone' in zone_policy: + for zone, zone_conf in zone_policy['zone'].items(): + if 'from' in zone_conf: + for from_zone, from_conf in zone_conf['from'].items(): + name = dict_search_args(from_conf, 'firewall', 'name') + if name: + used_v4.append(name) + + ipv6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') + if ipv6_name: + used_v6.append(ipv6_name) + + if 'intra_zone_filtering' in zone_conf: + name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'name') + if name: + used_v4.append(name) + + ipv6_name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'ipv6_name') + if ipv6_name: + used_v6.append(ipv6_name) + + return {'name': used_v4, 'ipv6_name': used_v6} + +def get_config(config=None): if config: conf = config else: conf = Config() - base = ['nfirewall'] + base = ['firewall'] + 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['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) + firewall['interfaces'] = get_firewall_interfaces(conf) + firewall['zone_policy'] = get_firewall_zones(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') + + tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') + if tcp_flags: + if dict_search_args(rule_conf, 'protocol') != 'tcp': + raise ConfigError('Protocol must be tcp when specifying tcp flags') + + not_flags = dict_search_args(rule_conf, 'tcp', 'flags', 'not') + if not_flags: + duplicates = [flag for flag in tcp_flags if flag in not_flags] + if duplicates: + raise ConfigError(f'Cannot match a tcp flag as set and not set') + + if 'protocol' in rule_conf: + if rule_conf['protocol'] == 'icmp' and ipv6: + raise ConfigError(f'Cannot match IPv4 ICMP protocol on IPv6, use ipv6-icmp') + if rule_conf['protocol'] == 'ipv6-icmp' and not ipv6: + raise ConfigError(f'Cannot match IPv6 ICMP protocol on IPv4, use icmp') + + 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] + + if group_name and group_name[0] == '!': + group_name = group_name[1:] + + fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group + error_group = fw_group.replace("_", "-") + group_obj = dict_search_args(firewall, 'group', fw_group, group_name) + + if group_obj is None: + raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule') + + if not group_obj: + print(f'WARNING: {error_group} "{group_name}" has no members') + + 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 name_id.startswith("VZONE"): + raise ConfigError(f'Firewall name "{name_id}" uses reserved prefix') + + 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 dict_search_args(firewall, 'name', name) == None: + raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}') + + if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None: + raise ConfigError(f'Firewall ipv6-name "{ipv6_name}" is still referenced on interface {ifname}') + + for fw_name, used_names in firewall['zone_policy'].items(): + for name in used_names: + if dict_search_args(firewall, fw_name, name) == None: + raise ConfigError(f'Firewall {fw_name.replace("_", "-")} "{name}" is still referenced in zone-policy') return None +def cleanup_rule(table, jump_chain): + commands = [] + chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains + for chain in chains: + results = cmd(f'nft -a list chain {table} {chain}').split("\n") + for line in results: + if f'jump {jump_chain}' in line: + handle_search = re.search('handle (\d+)', line) + if handle_search: + commands.append(f'delete rule {table} {chain} handle {handle_search[1]}') + return commands + +def cleanup_commands(firewall): + commands = [] + commands_end = [] + for table in ['ip filter', 'ip6 filter']: + state_chain = 'VYOS_STATE_POLICY' if table == 'ip filter' else 'VYOS_STATE_POLICY6' + 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: + chain = item['chain']['name'] + if chain in ['VYOS_STATE_POLICY', 'VYOS_STATE_POLICY6']: + if 'state_policy' not in firewall: + commands.append(f'delete chain {table} {chain}') + else: + commands.append(f'flush chain {table} {chain}') + elif chain not in preserve_chains and not chain.startswith("VZONE"): + if table == 'ip filter' and dict_search_args(firewall, 'name', chain.replace(NAME_PREFIX, "", 1)) != None: + commands.append(f'flush chain {table} {chain}') + elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain.replace(NAME6_PREFIX, "", 1)) != None: + commands.append(f'flush chain {table} {chain}') + else: + commands += cleanup_rule(table, chain) + commands.append(f'delete chain {table} {chain}') + elif 'rule' in item: + rule = item['rule'] + if rule['chain'] in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL', 'VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']: + if 'expr' in rule and any([True for expr in rule['expr'] if dict_search_args(expr, 'jump', 'target') == state_chain]): + if 'state_policy' not in firewall: + chain = rule['chain'] + handle = rule['handle'] + commands.append(f'delete rule {table} {chain} handle {handle}') + elif 'set' in item: + set_name = item['set']['name'] + commands_end.append(f'delete set {table} {set_name}') + return commands + commands_end + 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) + render(nftables_defines_conf, 'firewall/nftables-defines.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 state_policy_rule_exists(): + # Determine if state policy rules already exist in nft + search_str = cmd(f'nft list chain ip filter VYOS_FW_FORWARD') + return 'VYOS_STATE_POLICY' in search_str + +def resync_policy_route(): + # Update policy route as firewall groups were updated + tmp = run(policy_route_conf_script) + if tmp > 0: + print('Warning: Failed to re-apply policy route configuration') + +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 and not state_policy_rule_exists(): + for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']: + cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY') + + for chain in ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']: + cmd(f'nft insert rule ip6 filter {chain} jump VYOS_STATE_POLICY6') + + apply_sysfs(firewall) + + if firewall['policy_resync']: + resync_policy_route() + + 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 e01f3066b..975f19acf 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -35,67 +35,64 @@ from vyos import airbag airbag.enable() uacctd_conf_path = '/run/pmacct/uacctd.conf' -iptables_nflog_table = 'raw' -iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK' -egress_iptables_nflog_table = 'mangle' -egress_iptables_nflog_chain = 'FORWARD' +nftables_nflog_table = 'raw' +nftables_nflog_chain = 'VYOS_CT_PREROUTING_HOOK' +egress_nftables_nflog_table = 'inet mangle' +egress_nftables_nflog_chain = 'FORWARD' -# get iptables rule dict for chain in table -def _iptables_get_nflog(chain, table): +# 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, length=None): - # 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 @@ -106,16 +103,12 @@ def _iptables_config(configured_ifaces, direction, length=None): # 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 {length} --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) @@ -249,8 +242,8 @@ def apply(flow_config): action = 'restart' # Check if flow-accounting was removed and define command if not flow_config: - _iptables_config([], 'ingress') - _iptables_config([], 'egress') + _nftables_config([], 'ingress') + _nftables_config([], 'egress') # Stop flow-accounting daemon and remove configuration file cmd('systemctl stop uacctd.service') @@ -261,15 +254,15 @@ def apply(flow_config): # Start/reload flow-accounting daemon cmd(f'systemctl restart uacctd.service') - # configure iptables rules for defined interfaces + # configure nftables rules for defined interfaces if 'interface' in flow_config: - _iptables_config(flow_config['interface'], 'ingress', flow_config['packet_length']) + _nftables_config(flow_config['interface'], 'ingress', flow_config['packet_length']) # configure egress the same way if configured otherwise remove it if 'enable_egress' in flow_config: - _iptables_config(flow_config['interface'], 'egress', flow_config['packet_length']) + _nftables_config(flow_config['interface'], 'egress', flow_config['packet_length']) else: - _iptables_config([], 'egress') + _nftables_config([], 'egress') if __name__ == '__main__': try: diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/high-availability.py index c72efc61f..7d51bb393 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/high-availability.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -40,33 +40,41 @@ def get_config(config=None): else: conf = Config() - base = ['high-availability', 'vrrp'] + base = ['high-availability'] + base_vrrp = ['high-availability', 'vrrp'] if not conf.exists(base): return None - vrrp = conf.get_config_dict(base, key_mangling=('-', '_'), + ha = 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. - if 'group' in vrrp: - default_values = defaults(base + ['group']) - for group in vrrp['group']: - vrrp['group'][group] = dict_merge(default_values, vrrp['group'][group]) + if 'vrrp' in ha: + if 'group' in ha['vrrp']: + default_values_vrrp = defaults(base_vrrp + ['group']) + for group in ha['vrrp']['group']: + ha['vrrp']['group'][group] = dict_merge(default_values_vrrp, ha['vrrp']['group'][group]) + + # Merge per virtual-server default values + if 'virtual_server' in ha: + default_values = defaults(base + ['virtual-server']) + for vs in ha['virtual_server']: + ha['virtual_server'][vs] = dict_merge(default_values, ha['virtual_server'][vs]) ## Get the sync group used for conntrack-sync conntrack_path = ['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group'] if conf.exists(conntrack_path): - vrrp['conntrack_sync_group'] = conf.return_value(conntrack_path) + ha['conntrack_sync_group'] = conf.return_value(conntrack_path) - return vrrp + return ha -def verify(vrrp): - if not vrrp: +def verify(ha): + if not ha: return None used_vrid_if = [] - if 'group' in vrrp: - for group, group_config in vrrp['group'].items(): + if 'vrrp' in ha and 'group' in ha['vrrp']: + for group, group_config in ha['vrrp']['group'].items(): # Check required fields if 'vrid' not in group_config: raise ConfigError(f'VRID is required but not set in VRRP group "{group}"') @@ -119,24 +127,37 @@ def verify(vrrp): if is_ipv4(group_config['peer_address']): raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!') # Check sync groups - if 'sync_group' in vrrp: - for sync_group, sync_config in vrrp['sync_group'].items(): + if 'vrrp' in ha and 'sync_group' in ha['vrrp']: + for sync_group, sync_config in ha['vrrp']['sync_group'].items(): if 'member' in sync_config: for member in sync_config['member']: - if member not in vrrp['group']: + if member not in ha['vrrp']['group']: raise ConfigError(f'VRRP sync-group "{sync_group}" refers to VRRP group "{member}", '\ 'but it does not exist!') -def generate(vrrp): - if not vrrp: + # Virtual-server + if 'virtual_server' in ha: + for vs, vs_config in ha['virtual_server'].items(): + if 'port' not in vs_config: + raise ConfigError(f'Port is required but not set for virtual-server "{vs}"') + if 'real_server' not in vs_config: + raise ConfigError(f'Real-server ip is required but not set for virtual-server "{vs}"') + # Real-server + for rs, rs_config in vs_config['real_server'].items(): + if 'port' not in rs_config: + raise ConfigError(f'Port is required but not set for virtual-server "{vs}" real-server "{rs}"') + + +def generate(ha): + if not ha: return None - render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl', vrrp) + render(VRRP.location['config'], 'high-availability/keepalived.conf.tmpl', ha) return None -def apply(vrrp): +def apply(ha): service_name = 'keepalived.service' - if not vrrp: + if not ha: call(f'systemctl stop {service_name}') return None diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 59939d0fb..9f319fc8a 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -28,6 +28,7 @@ from vyos.configdict import dict_merge from vyos.template import render from vyos.template import is_ip_network from vyos.util import cmd +from vyos.util import run from vyos.util import check_kmod from vyos.util import dict_search from vyos.validate import is_addr_assigned @@ -42,7 +43,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 +94,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 +106,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 +119,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 +180,21 @@ def verify(nat): return None def generate(nat): - render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, - permission=0o755) + render(nftables_nat_config, 'firewall/nftables-nat.tmpl', nat) + + # dry-run newly generated configuration + tmp = run(f'nft -c -f {nftables_nat_config}') + if tmp > 0: + if os.path.exists(nftables_ct_file): + os.unlink(nftables_ct_file) + raise ConfigError('Configuration file errors encountered!') + return None def apply(nat): - cmd(f'{iptables_nat_config}') - if os.path.isfile(iptables_nat_config): - os.unlink(iptables_nat_config) + cmd(f'nft -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 fb376a434..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' @@ -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/policy-local-route.py b/src/conf_mode/policy-local-route.py index 539189442..3f834f55c 100755 --- a/src/conf_mode/policy-local-route.py +++ b/src/conf_mode/policy-local-route.py @@ -18,6 +18,7 @@ import os from sys import exit +from netifaces import interfaces from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed @@ -35,35 +36,92 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['policy', 'local-route'] + base = ['policy'] + pbr = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # delete policy local-route - dict = {} - tmp = node_changed(conf, ['policy', 'local-route', 'rule'], key_mangling=('-', '_')) - if tmp: - for rule in (tmp or []): - src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source']) - fwmk = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'fwmark']) - if src: - dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict) - pbr.update(dict) - if fwmk: - dict = dict_merge({'rule_remove' : {rule : {'fwmark' : fwmk}}}, dict) + for route in ['local_route', 'local_route6']: + dict_id = 'rule_remove' if route == 'local_route' else 'rule6_remove' + route_key = 'local-route' if route == 'local_route' else 'local-route6' + base_rule = base + [route_key, 'rule'] + + # delete policy local-route + dict = {} + tmp = node_changed(conf, base_rule, key_mangling=('-', '_')) + if tmp: + for rule in (tmp or []): + src = leaf_node_changed(conf, base_rule + [rule, 'source']) + fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark']) + iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface']) + dst = leaf_node_changed(conf, base_rule + [rule, 'destination']) + rule_def = {} + if src: + rule_def = dict_merge({'source' : src}, rule_def) + if fwmk: + rule_def = dict_merge({'fwmark' : fwmk}, rule_def) + if iif: + rule_def = dict_merge({'inbound_interface' : iif}, rule_def) + if dst: + rule_def = dict_merge({'destination' : dst}, rule_def) + dict = dict_merge({dict_id : {rule : rule_def}}, dict) pbr.update(dict) - # delete policy local-route rule x source x.x.x.x - # delete policy local-route rule x fwmark x - if 'rule' in pbr: - for rule in pbr['rule']: - src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source']) - fwmk = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'fwmark']) - if src: - dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict) - pbr.update(dict) - if fwmk: - dict = dict_merge({'rule_remove' : {rule : {'fwmark' : fwmk}}}, dict) - pbr.update(dict) + if not route in pbr: + continue + + # delete policy local-route rule x source x.x.x.x + # delete policy local-route rule x fwmark x + # delete policy local-route rule x destination x.x.x.x + if 'rule' in pbr[route]: + for rule, rule_config in pbr[route]['rule'].items(): + src = leaf_node_changed(conf, base_rule + [rule, 'source']) + fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark']) + iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface']) + dst = leaf_node_changed(conf, base_rule + [rule, 'destination']) + # keep track of changes in configuration + # otherwise we might remove an existing node although nothing else has changed + changed = False + + rule_def = {} + # src is None if there are no changes to src + if src is None: + # if src hasn't changed, include it in the removal selector + # if a new selector is added, we have to remove all previous rules without this selector + # to make sure we remove all previous rules with this source(s), it will be included + if 'source' in rule_config: + rule_def = dict_merge({'source': rule_config['source']}, rule_def) + else: + # if src is not None, it's previous content will be returned + # this can be an empty array if it's just being set, or the previous value + # either way, something has to be changed and we only want to remove previous values + changed = True + # set the old value for removal if it's not empty + if len(src) > 0: + rule_def = dict_merge({'source' : src}, rule_def) + if fwmk is None: + if 'fwmark' in rule_config: + rule_def = dict_merge({'fwmark': rule_config['fwmark']}, rule_def) + else: + changed = True + if len(fwmk) > 0: + rule_def = dict_merge({'fwmark' : fwmk}, rule_def) + if iif is None: + if 'inbound_interface' in rule_config: + rule_def = dict_merge({'inbound_interface': rule_config['inbound_interface']}, rule_def) + else: + changed = True + if len(iif) > 0: + rule_def = dict_merge({'inbound_interface' : iif}, rule_def) + if dst is None: + if 'destination' in rule_config: + rule_def = dict_merge({'destination': rule_config['destination']}, rule_def) + else: + changed = True + if len(dst) > 0: + rule_def = dict_merge({'destination' : dst}, rule_def) + if changed: + dict = dict_merge({dict_id : {rule : rule_def}}, dict) + pbr.update(dict) return pbr @@ -72,13 +130,25 @@ def verify(pbr): if not pbr: return None - if 'rule' in pbr: - for rule in pbr['rule']: - if 'source' not in pbr['rule'][rule] and 'fwmark' not in pbr['rule'][rule]: - raise ConfigError('Source address or fwmark is required!') - else: - if 'set' not in pbr['rule'][rule] or 'table' not in pbr['rule'][rule]['set']: - raise ConfigError('Table set is required!') + for route in ['local_route', 'local_route6']: + if not route in pbr: + continue + + pbr_route = pbr[route] + if 'rule' in pbr_route: + for rule in pbr_route['rule']: + if 'source' not in pbr_route['rule'][rule] \ + and 'destination' not in pbr_route['rule'][rule] \ + and 'fwmark' not in pbr_route['rule'][rule] \ + and 'inbound_interface' not in pbr_route['rule'][rule]: + raise ConfigError('Source or destination address or fwmark or inbound-interface is required!') + else: + if 'set' not in pbr_route['rule'][rule] or 'table' not in pbr_route['rule'][rule]['set']: + raise ConfigError('Table set is required!') + if 'inbound_interface' in pbr_route['rule'][rule]: + interface = pbr_route['rule'][rule]['inbound_interface'] + if interface not in interfaces(): + raise ConfigError(f'Interface "{interface}" does not exist') return None @@ -93,36 +163,51 @@ def apply(pbr): return None # Delete old rule if needed - if 'rule_remove' in pbr: - for rule in pbr['rule_remove']: - if 'source' in pbr['rule_remove'][rule]: - for src in pbr['rule_remove'][rule]['source']: - call(f'ip rule del prio {rule} from {src}') - if 'fwmark' in pbr['rule_remove'][rule]: - for fwmk in pbr['rule_remove'][rule]['fwmark']: - call(f'ip rule del prio {rule} from all fwmark {fwmk}') + for rule_rm in ['rule_remove', 'rule6_remove']: + if rule_rm in pbr: + v6 = " -6" if rule_rm == 'rule6_remove' else "" + for rule, rule_config in pbr[rule_rm].items(): + rule_config['source'] = rule_config['source'] if 'source' in rule_config else [''] + for src in rule_config['source']: + f_src = '' if src == '' else f' from {src} ' + rule_config['destination'] = rule_config['destination'] if 'destination' in rule_config else [''] + for dst in rule_config['destination']: + f_dst = '' if dst == '' else f' to {dst} ' + rule_config['fwmark'] = rule_config['fwmark'] if 'fwmark' in rule_config else [''] + for fwmk in rule_config['fwmark']: + f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} ' + rule_config['inbound_interface'] = rule_config['inbound_interface'] if 'inbound_interface' in rule_config else [''] + for iif in rule_config['inbound_interface']: + f_iif = '' if iif == '' else f' iif {iif} ' + call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif}') # Generate new config - if 'rule' in pbr: - for rule in pbr['rule']: - table = pbr['rule'][rule]['set']['table'] - # Only source in the rule - # set policy local-route rule 100 source '203.0.113.1' - if 'source' in pbr['rule'][rule] and not 'fwmark' in pbr['rule'][rule]: - for src in pbr['rule'][rule]['source']: - call(f'ip rule add prio {rule} from {src} lookup {table}') - # Only fwmark in the rule - # set policy local-route rule 101 fwmark '23' - if 'fwmark' in pbr['rule'][rule] and not 'source' in pbr['rule'][rule]: - fwmk = pbr['rule'][rule]['fwmark'] - call(f'ip rule add prio {rule} from all fwmark {fwmk} lookup {table}') - # Source and fwmark in the rule - # set policy local-route rule 100 source '203.0.113.1' - # set policy local-route rule 100 fwmark '23' - if 'source' in pbr['rule'][rule] and 'fwmark' in pbr['rule'][rule]: - fwmk = pbr['rule'][rule]['fwmark'] - for src in pbr['rule'][rule]['source']: - call(f'ip rule add prio {rule} from {src} fwmark {fwmk} lookup {table}') + for route in ['local_route', 'local_route6']: + if not route in pbr: + continue + + v6 = " -6" if route == 'local_route6' else "" + + pbr_route = pbr[route] + if 'rule' in pbr_route: + for rule, rule_config in pbr_route['rule'].items(): + table = rule_config['set']['table'] + + rule_config['source'] = rule_config['source'] if 'source' in rule_config else ['all'] + for src in rule_config['source'] or ['all']: + f_src = '' if src == '' else f' from {src} ' + rule_config['destination'] = rule_config['destination'] if 'destination' in rule_config else ['all'] + for dst in rule_config['destination']: + f_dst = '' if dst == '' else f' to {dst} ' + f_fwmk = '' + if 'fwmark' in rule_config: + fwmk = rule_config['fwmark'] + f_fwmk = f' fwmark {fwmk} ' + f_iif = '' + if 'inbound_interface' in rule_config: + iif = rule_config['inbound_interface'] + f_iif = f' iif {iif} ' + call(f'ip{v6} rule add prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif} lookup {table}') return None diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py new file mode 100755 index 000000000..1108aebe6 --- /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', 'route6']: + 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'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: + 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 'route6' in if_policy: + name = 'VYOS_PBR6_' + if_policy['route6'] + 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..82f668acf --- /dev/null +++ b/src/conf_mode/policy-route.py @@ -0,0 +1,257 @@ +#!/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 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' + +preserve_chains = [ + 'VYOS_PBR_PREROUTING', + 'VYOS_PBR_POSTROUTING', + 'VYOS_PBR6_PREROUTING', + 'VYOS_PBR6_POSTROUTING' +] + +valid_groups = [ + 'address_group', + 'network_group', + 'port_group' +] + +def get_policy_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 'policy' in if_conf: + output[prefix + ifname] = if_conf['policy'] + 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 = ['policy'] + + policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + policy['interfaces'] = get_policy_interfaces(conf) + + return policy + +def verify_rule(policy, name, rule_conf, ipv6): + icmp = 'icmp' if not ipv6 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: + raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS') + + tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') + if tcp_flags: + if dict_search_args(rule_conf, 'protocol') != 'tcp': + raise ConfigError('Protocol must be tcp when specifying tcp flags') + + not_flags = dict_search_args(rule_conf, 'tcp', 'flags', 'not') + if not_flags: + duplicates = [flag for flag in tcp_flags if flag in not_flags] + if duplicates: + raise ConfigError(f'Cannot match a tcp flag as set and not set') + + 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 + error_group = fw_group.replace("_", "-") + group_obj = dict_search_args(policy['firewall_group'], fw_group, group_name) + + if group_obj is None: + raise ConfigError(f'Invalid {error_group} "{group_name}" on policy route rule') + + if not group_obj: + print(f'WARNING: {error_group} "{group_name}" has no members') + + 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(policy): + for route in ['route', 'route6']: + ipv6 = route == 'route6' + 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(): + verify_rule(policy, name, rule_conf, ipv6) + + for ifname, if_policy in policy['interfaces'].items(): + name = dict_search_args(if_policy, 'route') + ipv6_name = dict_search_args(if_policy, 'route6') + + if name and not dict_search_args(policy, 'route', name): + raise ConfigError(f'Policy route "{name}" is still referenced on interface {ifname}') + + if ipv6_name and not dict_search_args(policy, 'route6', ipv6_name): + raise ConfigError(f'Policy route6 "{ipv6_name}" is still referenced on interface {ifname}') + + return None + +def cleanup_rule(table, jump_chain): + commands = [] + results = cmd(f'nft -a list table {table}').split("\n") + for line in results: + if f'jump {jump_chain}' in line: + handle_search = re.search('handle (\d+)', line) + if handle_search: + commands.append(f'delete rule {table} {chain} handle {handle_search[1]}') + return commands + +def cleanup_commands(policy): + commands = [] + for table in ['ip mangle', 'ip6 mangle']: + 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: + chain = item['chain']['name'] + if not chain.startswith("VYOS_PBR"): + continue + if chain not in preserve_chains: + if table == 'ip mangle' and dict_search_args(policy, 'route', chain.replace("VYOS_PBR_", "", 1)): + commands.append(f'flush chain {table} {chain}') + elif table == 'ip6 mangle' and dict_search_args(policy, 'route6', chain.replace("VYOS_PBR6_", "", 1)): + commands.append(f'flush chain {table} {chain}') + else: + commands += cleanup_rule(table, chain) + commands.append(f'delete chain {table} {chain}') + return commands + +def generate(policy): + if not os.path.exists(nftables_conf): + policy['first_install'] = True + else: + policy['cleanup_commands'] = cleanup_commands(policy) + + render(nftables_conf, 'firewall/nftables-policy.tmpl', policy) + return None + +def apply_table_marks(policy): + for route in ['route', 'route6']: + if route in policy: + cmd_str = 'ip' if route == 'route' else 'ip -6' + tables = [] + 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' + if set_table in tables: + continue + tables.append(set_table) + table_mark = mark_offset - int(set_table) + cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}') + +def cleanup_table_marks(): + for cmd_str in ['ip', 'ip -6']: + json_rules = cmd(f'{cmd_str} -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'{cmd_str} rule del fwmark {fwmark} table {table}') + +def apply(policy): + 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 e251396c7..6b1d3bf1a 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -87,6 +87,7 @@ def verify(policy): # human readable instance name (hypen instead of underscore) policy_hr = policy_type.replace('_', '-') + entries = [] for rule, rule_config in instance_config['rule'].items(): mandatory_error = f'must be specified for "{policy_hr} {instance} rule {rule}"!' if 'action' not in rule_config: @@ -113,6 +114,11 @@ def verify(policy): if 'prefix' not in rule_config: raise ConfigError(f'A prefix {mandatory_error}') + # Check prefix duplicates + if rule_config['prefix'] in entries and ('ge' not in rule_config and 'le' not in rule_config): + raise ConfigError(f'Prefix {rule_config["prefix"]} is duplicated!') + entries.append(rule_config['prefix']) + # route-maps tend to be a bit more complex so they get their own verify() section if 'route_map' in policy: 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/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py new file mode 100755 index 000000000..8a972b9fe --- /dev/null +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import json + +from sys import exit +from shutil import rmtree + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.ifconfig import Section +from vyos.template import render +from vyos.util import call +from vyos.util import chown +from vyos.util import cmd +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + + +base_dir = '/run/telegraf' +cache_dir = f'/etc/telegraf/.cache' +config_telegraf = f'{base_dir}/vyos-telegraf.conf' +custom_scripts_dir = '/etc/telegraf/custom_scripts' +syslog_telegraf = '/etc/rsyslog.d/50-telegraf.conf' +systemd_telegraf_service = '/etc/systemd/system/vyos-telegraf.service' +systemd_telegraf_override_dir = '/etc/systemd/system/vyos-telegraf.service.d' +systemd_override = f'{systemd_telegraf_override_dir}/10-override.conf' + + +def get_interfaces(type='', vlan=True): + """ + Get interfaces + get_interfaces() + ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0'] + + get_interfaces("dummy") + ['dum0'] + """ + interfaces = [] + ifaces = Section.interfaces(type) + for iface in ifaces: + if vlan == False and '.' in iface: + continue + interfaces.append(iface) + + return interfaces + +def get_nft_filter_chains(): + """ + Get nft chains for table filter + """ + nft = cmd('nft --json list table ip filter') + nft = json.loads(nft) + chain_list = [] + + for output in nft['nftables']: + if 'chain' in output: + chain = output['chain']['name'] + chain_list.append(chain) + + return chain_list + + +def get_config(config=None): + + if config: + conf = config + else: + conf = Config() + base = ['service', 'monitoring', 'telegraf'] + if not conf.exists(base): + return None + + monitoring = 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. + default_values = defaults(base) + monitoring = dict_merge(default_values, monitoring) + + monitoring['custom_scripts_dir'] = custom_scripts_dir + monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False) + monitoring['nft_chains'] = get_nft_filter_chains() + + return monitoring + +def verify(monitoring): + # bail out early - looks like removal from running config + if not monitoring: + return None + + if 'authentication' not in monitoring or \ + 'organization' not in monitoring['authentication'] or \ + 'token' not in monitoring['authentication']: + raise ConfigError(f'Authentication "organization and token" are mandatory!') + + if 'url' not in monitoring: + raise ConfigError(f'Monitoring "url" is mandatory!') + + return None + +def generate(monitoring): + if not monitoring: + # Delete config and systemd files + config_files = [config_telegraf, systemd_telegraf_service, systemd_override, syslog_telegraf] + for file in config_files: + if os.path.isfile(file): + os.unlink(file) + + # Delete old directories + if os.path.isdir(cache_dir): + rmtree(cache_dir, ignore_errors=True) + + return None + + # Create telegraf cache dir + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + + chown(cache_dir, 'telegraf', 'telegraf') + + # Create systemd override dir + if not os.path.exists(systemd_telegraf_override_dir): + os.mkdir(systemd_telegraf_override_dir) + + # Create custome scripts dir + if not os.path.exists(custom_scripts_dir): + os.mkdir(custom_scripts_dir) + + # Render telegraf configuration and systemd override + render(config_telegraf, 'monitoring/telegraf.tmpl', monitoring) + render(systemd_telegraf_service, 'monitoring/systemd_vyos_telegraf_service.tmpl', monitoring) + render(systemd_override, 'monitoring/override.conf.tmpl', monitoring, permission=0o640) + render(syslog_telegraf, 'monitoring/syslog_telegraf.tmpl', monitoring) + + chown(base_dir, 'telegraf', 'telegraf') + + return None + +def apply(monitoring): + # Reload systemd manager configuration + call('systemctl daemon-reload') + if monitoring: + call('systemctl restart vyos-telegraf.service') + else: + call('systemctl stop vyos-telegraf.service') + # Telegraf include custom rsyslog config changes + call('systemctl restart rsyslog') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py new file mode 100755 index 000000000..d21b31990 --- /dev/null +++ b/src/conf_mode/service_upnp.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +import uuid +import netifaces +from ipaddress import IPv4Network +from ipaddress import IPv6Network + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_vrf +from vyos.util import call +from vyos.template import render +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +config_file = r'/run/upnp/miniupnp.conf' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['service', 'upnp'] + upnpd = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + if not upnpd: + return None + + if 'rule' in upnpd: + default_member_values = defaults(base + ['rule']) + for rule,rule_config in upnpd['rule'].items(): + upnpd['rule'][rule] = dict_merge(default_member_values, upnpd['rule'][rule]) + + uuidgen = uuid.uuid1() + upnpd.update({'uuid': uuidgen}) + + return upnpd + +def get_all_interface_addr(prefix, filter_dev, filter_family): + list_addr = [] + interfaces = netifaces.interfaces() + + for interface in interfaces: + if filter_dev and interface in filter_dev: + continue + addrs = netifaces.ifaddresses(interface) + if netifaces.AF_INET in addrs.keys(): + if netifaces.AF_INET in filter_family: + for addr in addrs[netifaces.AF_INET]: + if prefix: + # we need to manually assemble a list of IPv4 address/prefix + prefix = '/' + \ + str(IPv4Network('0.0.0.0/' + addr['netmask']).prefixlen) + list_addr.append(addr['addr'] + prefix) + else: + list_addr.append(addr['addr']) + if netifaces.AF_INET6 in addrs.keys(): + if netifaces.AF_INET6 in filter_family: + for addr in addrs[netifaces.AF_INET6]: + if prefix: + # we need to manually assemble a list of IPv4 address/prefix + bits = bin(int(addr['netmask'].replace(':', '').split('/')[0], 16)).count('1') + prefix = '/' + str(bits) + list_addr.append(addr['addr'] + prefix) + else: + list_addr.append(addr['addr']) + + return list_addr + +def verify(upnpd): + if not upnpd: + return None + + if 'wan_interface' not in upnpd: + raise ConfigError('To enable UPNP, you must have the "wan-interface" option!') + + if 'rule' in upnpd: + for rule, rule_config in upnpd['rule'].items(): + for option in ['external_port_range', 'internal_port_range', 'ip', 'action']: + if option not in rule_config: + tmp = option.replace('_', '-') + raise ConfigError(f'Every UPNP rule requires "{tmp}" to be set!') + + if 'stun' in upnpd: + for option in ['host', 'port']: + if option not in upnpd['stun']: + raise ConfigError(f'A UPNP stun support must have an "{option}" option!') + + # Check the validity of the IP address + listen_dev = [] + system_addrs_cidr = get_all_interface_addr(True, [], [netifaces.AF_INET, netifaces.AF_INET6]) + system_addrs = get_all_interface_addr(False, [], [netifaces.AF_INET, netifaces.AF_INET6]) + for listen_if_or_addr in upnpd['listen']: + if listen_if_or_addr not in netifaces.interfaces(): + listen_dev.append(listen_if_or_addr) + if (listen_if_or_addr not in system_addrs) and (listen_if_or_addr not in system_addrs_cidr) and (listen_if_or_addr not in netifaces.interfaces()): + if is_ipv4(listen_if_or_addr) and IPv4Network(listen_if_or_addr).is_multicast: + raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!') + if is_ipv6(listen_if_or_addr) and IPv6Network(listen_if_or_addr).is_multicast: + raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!') + + system_listening_dev_addrs_cidr = get_all_interface_addr(True, listen_dev, [netifaces.AF_INET6]) + system_listening_dev_addrs = get_all_interface_addr(False, listen_dev, [netifaces.AF_INET6]) + for listen_if_or_addr in upnpd['listen']: + if listen_if_or_addr not in netifaces.interfaces() and (listen_if_or_addr not in system_listening_dev_addrs_cidr) and (listen_if_or_addr not in system_listening_dev_addrs) and is_ipv6(listen_if_or_addr) and (not IPv6Network(listen_if_or_addr).is_multicast): + raise ConfigError(f'{listen_if_or_addr} must listen on the interface of the network card') + +def generate(upnpd): + if not upnpd: + return None + + if os.path.isfile(config_file): + os.unlink(config_file) + + render(config_file, 'firewall/upnpd.conf.tmpl', upnpd) + +def apply(upnpd): + systemd_service_name = 'miniupnpd.service' + if not upnpd: + # Stop the UPNP service + call(f'systemctl stop {systemd_service_name}') + else: + # Start the UPNP service + call(f'systemctl restart {systemd_service_name}') + +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/zone_policy.py b/src/conf_mode/zone_policy.py new file mode 100755 index 000000000..683f8f034 --- /dev/null +++ b/src/conf_mode/zone_policy.py @@ -0,0 +1,201 @@ +#!/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(): + if from_zone not in zone_policy['zone']: + raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"') + + 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 not target: + continue + if target.startswith("VZONE") or target.startswith("VYOS_STATE_POLICY"): + 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 != 0: + 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/systemd/system/keepalived.service.d/override.conf b/src/etc/systemd/system/keepalived.service.d/override.conf deleted file mode 100644 index 1c68913f2..000000000 --- a/src/etc/systemd/system/keepalived.service.d/override.conf +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -ConditionPathExists= -ConditionPathExists=/run/keepalived/keepalived.conf -After= -After=vyos-router.service - -[Service] -KillMode=process -EnvironmentFile= -ExecStart= -ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork --snmp -PIDFile= -PIDFile=/run/keepalived/keepalived.pid diff --git a/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py new file mode 100755 index 000000000..bf4bfd05d --- /dev/null +++ b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import json +import re +import time + +from vyos.util import cmd + + +def get_nft_filter_chains(): + """ + Get list of nft chains for table filter + """ + nft = cmd('/usr/sbin/nft --json list table ip filter') + nft = json.loads(nft) + chain_list = [] + + for output in nft['nftables']: + if 'chain' in output: + chain = output['chain']['name'] + chain_list.append(chain) + + return chain_list + + +def get_nftables_details(name): + """ + Get dict, counters packets and bytes for chain + """ + command = f'/usr/sbin/nft list chain ip filter {name}' + try: + results = cmd(command) + except: + return {} + + # Trick to remove 'NAME_' from chain name in the comment + # It was added to any chain T4218 + # counter packets 0 bytes 0 return comment "FOO default-action accept" + comment_name = name.replace("NAME_", "") + out = {} + for line in results.split('\n'): + comment_search = re.search(rf'{comment_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 get_nft_telegraf(name): + """ + Get data for telegraf in influxDB format + """ + for rule, rule_config in get_nftables_details(name).items(): + print(f'nftables,table=filter,chain={name},' + f'ruleid={rule} ' + f'pkts={rule_config["packets"]}i,' + f'bytes={rule_config["bytes"]}i ' + f'{str(int(time.time()))}000000000') + + +chains = get_nft_filter_chains() + +for chain in chains: + get_nft_telegraf(chain) diff --git a/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py b/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py new file mode 100755 index 000000000..0c7474156 --- /dev/null +++ b/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +from vyos.ifconfig import Section +from vyos.ifconfig import Interface + +import time + +def get_interfaces(type='', vlan=True): + """ + Get interfaces: + ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0'] + """ + interfaces = [] + ifaces = Section.interfaces(type) + for iface in ifaces: + if vlan == False and '.' in iface: + continue + interfaces.append(iface) + + return interfaces + +def get_interface_addresses(iface, link_local_v6=False): + """ + Get IP and IPv6 addresses from interface in one string + By default don't get IPv6 link-local addresses + If interface doesn't have address, return "-" + """ + addresses = [] + addrs = Interface(iface).get_addr() + + for addr in addrs: + if link_local_v6 == False: + if addr.startswith('fe80::'): + continue + addresses.append(addr) + + if not addresses: + return "-" + + return (" ".join(addresses)) + +def get_interface_description(iface): + """ + Get interface description + If none return "empty" + """ + description = Interface(iface).get_alias() + + if not description: + return "empty" + + return description + +def get_interface_admin_state(iface): + """ + Interface administrative state + up => 0, down => 2 + """ + state = Interface(iface).get_admin_state() + if state == 'up': + admin_state = 0 + if state == 'down': + admin_state = 2 + + return admin_state + +def get_interface_oper_state(iface): + """ + Interface operational state + up => 0, down => 1 + """ + state = Interface(iface).operational.get_state() + if state == 'down': + oper_state = 1 + else: + oper_state = 0 + + return oper_state + +interfaces = get_interfaces() + +for iface in interfaces: + print(f'show_interfaces,interface={iface} ' + f'ip_addresses="{get_interface_addresses(iface)}",' + f'state={get_interface_admin_state(iface)}i,' + f'link={get_interface_oper_state(iface)}i,' + f'description="{get_interface_description(iface)}" ' + f'{str(int(time.time()))}000000000') diff --git a/src/etc/telegraf/custom_scripts/vyos_services_input_filter.py b/src/etc/telegraf/custom_scripts/vyos_services_input_filter.py new file mode 100755 index 000000000..df4eed131 --- /dev/null +++ b/src/etc/telegraf/custom_scripts/vyos_services_input_filter.py @@ -0,0 +1,61 @@ +#!/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 time +from vyos.configquery import ConfigTreeQuery +from vyos.util import is_systemd_service_running, process_named_running + +# Availible services and prouceses +# 1 - service +# 2 - process +services = { + "protocols bgp" : "bgpd", + "protocols ospf" : "ospfd", + "protocols ospfv3" : "ospf6d", + "protocols rip" : "ripd", + "protocols ripng" : "ripngd", + "protocols isis" : "isisd", + "service pppoe" : "accel-ppp@pppoe.service", + "vpn l2tp remote-access" : "accel-ppp@l2tp.service", + "vpn pptp remote-access" : "accel-ppp@pptp.service", + "vpn sstp" : "accel-ppp@sstp.service", + "vpn ipsec" : "charon" +} + +# Configured services +conf_services = { + 'zebra' : 0, + 'staticd' : 0, +} +# Get configured service and create list to check if process running +config = ConfigTreeQuery() +for service in services: + if config.exists(service): + conf_services[services[service]] = 0 + +for conf_service in conf_services: + status = 0 + if ".service" in conf_service: + # Check systemd service + if is_systemd_service_running(conf_service): + status = 1 + else: + # Check process + if process_named_running(conf_service): + status = 1 + print(f'vyos_services,service="{conf_service}" ' + f'status={str(status)}i {str(int(time.time()))}000000000') diff --git a/src/helpers/strip-private.py b/src/helpers/strip-private.py index e4e1fe11d..eb584edaf 100755 --- a/src/helpers/strip-private.py +++ b/src/helpers/strip-private.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -111,6 +111,10 @@ if __name__ == "__main__": (True, re.compile(r'public-keys \S+'), 'public-keys xxxx@xxx.xxx'), (True, re.compile(r'type \'ssh-(rsa|dss)\''), 'type ssh-xxx'), (True, re.compile(r' key \S+'), ' key xxxxxx'), + # Strip bucket + (True, re.compile(r' bucket \S+'), ' bucket xxxxxx'), + # Strip tokens + (True, re.compile(r' token \S+'), ' token xxxxxx'), # Strip OpenVPN secrets (True, re.compile(r'(shared-secret-key-file|ca-cert-file|cert-file|dh-file|key-file|client) (\S+)'), r'\1 xxxxxx'), # Strip IPSEC secrets @@ -123,8 +127,8 @@ if __name__ == "__main__": # Strip MAC addresses (args.mac, re.compile(r'([0-9a-fA-F]{2}\:){5}([0-9a-fA-F]{2}((\:{0,1})){3})'), r'xx:xx:xx:xx:xx:\2'), - # Strip host-name, domain-name, and domain-search - (args.hostname, re.compile(r'(host-name|domain-name|domain-search) \S+'), r'\1 xxxxxx'), + # Strip host-name, domain-name, domain-search and url + (args.hostname, re.compile(r'(host-name|domain-name|domain-search|url) \S+'), r'\1 xxxxxx'), # Strip user-names (args.username, re.compile(r'(user|username|user-id) \S+'), r'\1 xxxxxx'), diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name index afeef8f2d..1798e92db 100755 --- a/src/helpers/vyos_net_name +++ b/src/helpers/vyos_net_name @@ -20,12 +20,14 @@ import os import re import time import logging +import tempfile import threading from sys import argv from vyos.configtree import ConfigTree from vyos.defaults import directories from vyos.util import cmd, boot_configuration_complete +from vyos.migrator import VirtualMigrator vyos_udev_dir = directories['vyos_udev_dir'] vyos_log_dir = '/run/udev/log' @@ -139,14 +141,20 @@ def get_configfile_interfaces() -> dict: 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() + logging.debug(f"updating component version string syntax") + # this will update the component version string syntax, + # required for updates 1.2 --> 1.3/1.4 + with tempfile.NamedTemporaryFile() as fp: + with open(fp.name, 'w') as fd: + fd.write(config_file) + virtual_migration = VirtualMigrator(fp.name) + virtual_migration.run() + with open(fp.name) as fd: + config_file = fd.read() + config = ConfigTree(config_file) + except Exception as e: logging.critical(f"ConfigTree error: {e}") @@ -246,4 +254,3 @@ if not boot_configuration_complete(): 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 index 4c6d5ceb8..e2d3fcd33 100755 --- a/src/migration-scripts/bgp/1-to-2 +++ b/src/migration-scripts/bgp/1-to-2 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -20,7 +20,6 @@ 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!") @@ -51,23 +50,21 @@ if config.exists(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'])) == 0: config.delete(base + ['parameters']) +else: + # 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 - 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) + 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: diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2 index ba10c26f2..a8c930be7 100755 --- a/src/migration-scripts/dns-forwarding/1-to-2 +++ b/src/migration-scripts/dns-forwarding/1-to-2 @@ -16,7 +16,7 @@ # # This migration script will remove the deprecated 'listen-on' statement -# from the dns forwarding service and will add the corresponding +# from the dns forwarding service and will add the corresponding # listen-address nodes instead. This is required as PowerDNS can only listen # on interface addresses and not on interface names. @@ -37,53 +37,50 @@ with open(file_name, 'r') as f: config = ConfigTree(config_file) base = ['service', 'dns', 'forwarding'] -if not config.exists(base): +if not config.exists(base + ['listen-on']): # Nothing to do exit(0) -if config.exists(base + ['listen-on']): - listen_intf = config.return_values(base + ['listen-on']) - # Delete node with abandoned command - config.delete(base + ['listen-on']) +listen_intf = config.return_values(base + ['listen-on']) +# Delete node with abandoned command +config.delete(base + ['listen-on']) - # retrieve interface addresses for every configured listen-on interface - listen_addr = [] - for intf in listen_intf: - # we need to evaluate the interface section before manipulating the 'intf' variable - section = Interface.section(intf) - if not section: - raise ValueError(f'Invalid interface name {intf}') +# retrieve interface addresses for every configured listen-on interface +listen_addr = [] +for intf in listen_intf: + # we need to evaluate the interface section before manipulating the 'intf' variable + section = Interface.section(intf) + if not section: + raise ValueError(f'Invalid interface name {intf}') - # we need to treat vif and vif-s interfaces differently, - # both "real interfaces" use dots for vlan identifiers - those - # need to be exchanged with vif and vif-s identifiers - if intf.count('.') == 1: - # this is a regular VLAN interface - intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1] - elif intf.count('.') == 2: - # this is a QinQ VLAN interface - intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2] - - # retrieve corresponding interface addresses in CIDR format - # those need to be converted in pure IP addresses without network information - path = ['interfaces', section, intf, 'address'] - 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) + # we need to treat vif and vif-s interfaces differently, + # both "real interfaces" use dots for vlan identifiers - those + # need to be exchanged with vif and vif-s identifiers + if intf.count('.') == 1: + # this is a regular VLAN interface + intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1] + elif intf.count('.') == 2: + # this is a QinQ VLAN interface + intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2] + # retrieve corresponding interface addresses in CIDR format + # those need to be converted in pure IP addresses without network information + path = ['interfaces', section, intf, 'address'] 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) + 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") -exit(0) +for addr in listen_addr: + config.set(base + ['listen-address'], value=addr, replace=False) + +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/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7 new file mode 100755 index 000000000..5f4cff90d --- /dev/null +++ b/src/migration-scripts/firewall/6-to-7 @@ -0,0 +1,226 @@ +#!/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 +# icmp/v6: migrate previously available `type-name` to valid type/code +# T4178: Update tcp flags to use multi value node + +import re + +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) + +icmp_remove = ['any'] +icmp_translations = { + 'ping': 'echo-request', + 'pong': 'echo-reply', + 'ttl-exceeded': 'time-exceeded', + # Network Unreachable + 'network-unreachable': [3, 0], + 'host-unreachable': [3, 1], + 'protocol-unreachable': [3, 2], + 'port-unreachable': [3, 3], + 'fragmentation-needed': [3, 4], + 'source-route-failed': [3, 5], + 'network-unknown': [3, 6], + 'host-unknown': [3, 7], + 'network-prohibited': [3, 9], + 'host-prohibited': [3, 10], + 'TOS-network-unreachable': [3, 11], + 'TOS-host-unreachable': [3, 12], + 'communication-prohibited': [3, 13], + 'host-precedence-violation': [3, 14], + 'precedence-cutoff': [3, 15], + # Redirect + 'network-redirect': [5, 0], + 'host-redirect': [5, 1], + 'TOS-network-redirect': [5, 2], + 'TOS host-redirect': [5, 3], + # Time Exceeded + 'ttl-zero-during-transit': [11, 0], + 'ttl-zero-during-reassembly': [11, 1], + # Parameter Problem + 'ip-header-bad': [12, 0], + 'required-option-missing': [12, 1] +} + +icmpv6_remove = [] +icmpv6_translations = { + 'ping': 'echo-request', + 'pong': 'echo-reply', + # Destination Unreachable + 'no-route': [1, 0], + 'communication-prohibited': [1, 1], + 'address-unreachble': [1, 3], + 'port-unreachable': [1, 4], + # Redirect + 'redirect': 'nd-redirect', + # Time Exceeded + 'ttl-zero-during-transit': [3, 0], + 'ttl-zero-during-reassembly': [3, 1], + # Parameter Problem + 'bad-header': [4, 0], + 'unknown-header-type': [4, 1], + 'unknown-option': [4, 2] +} + +if config.exists(base + ['name']): + for name in config.list_nodes(base + ['name']): + if not config.exists(base + ['name', name, 'rule']): + continue + + for rule in config.list_nodes(base + ['name', name, 'rule']): + rule_recent = base + ['name', name, 'rule', rule, 'recent'] + rule_time = base + ['name', name, 'rule', rule, 'time'] + rule_tcp_flags = base + ['name', name, 'rule', rule, 'tcp', 'flags'] + rule_icmp = base + ['name', name, 'rule', rule, 'icmp'] + + 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(rule_recent + ['time']): + tmp = int(config.return_value(rule_recent + ['time'])) + unit = 'minute' + if tmp > 600: + unit = 'hour' + elif tmp < 10: + unit = 'second' + config.set(rule_recent + ['time'], value=unit) + + if config.exists(rule_tcp_flags): + tmp = config.return_value(rule_tcp_flags) + config.delete(rule_tcp_flags) + for flag in tmp.split(","): + if flag[0] == '!': + config.set(rule_tcp_flags + ['not', flag[1:].lower()]) + else: + config.set(rule_tcp_flags + [flag.lower()]) + + if config.exists(rule_icmp + ['type-name']): + tmp = config.return_value(rule_icmp + ['type-name']) + if tmp in icmp_remove: + config.delete(rule_icmp + ['type-name']) + elif tmp in icmp_translations: + translate = icmp_translations[tmp] + if isinstance(translate, str): + config.set(rule_icmp + ['type-name'], value=translate) + elif isinstance(translate, list): + config.delete(rule_icmp + ['type-name']) + config.set(rule_icmp + ['type'], value=translate[0]) + config.set(rule_icmp + ['code'], value=translate[1]) + + for src_dst in ['destination', 'source']: + pg_base = base + ['name', name, 'rule', rule, src_dst, 'group', 'port-group'] + proto_base = base + ['name', name, 'rule', rule, 'protocol'] + if config.exists(pg_base) and not config.exists(proto_base): + config.set(proto_base, value='tcp_udp') + +if config.exists(base + ['ipv6-name']): + for name in config.list_nodes(base + ['ipv6-name']): + if not config.exists(base + ['ipv6-name', name, 'rule']): + continue + + for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): + rule_recent = base + ['ipv6-name', name, 'rule', rule, 'recent'] + rule_time = base + ['ipv6-name', name, 'rule', rule, 'time'] + rule_tcp_flags = base + ['ipv6-name', name, 'rule', rule, 'tcp', 'flags'] + rule_icmp = base + ['ipv6-name', name, 'rule', rule, 'icmpv6'] + + 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(rule_recent + ['time']): + tmp = int(config.return_value(rule_recent + ['time'])) + unit = 'minute' + if tmp > 600: + unit = 'hour' + elif tmp < 10: + unit = 'second' + config.set(rule_recent + ['time'], value=unit) + + if config.exists(rule_tcp_flags): + tmp = config.return_value(rule_tcp_flags) + config.delete(rule_tcp_flags) + for flag in tmp.split(","): + if flag[0] == '!': + config.set(rule_tcp_flags + ['not', flag[1:].lower()]) + else: + config.set(rule_tcp_flags + [flag.lower()]) + + if config.exists(base + ['ipv6-name', name, 'rule', rule, 'protocol']): + tmp = config.return_value(base + ['ipv6-name', name, 'rule', rule, 'protocol']) + if tmp == 'icmpv6': + config.set(base + ['ipv6-name', name, 'rule', rule, 'protocol'], value='ipv6-icmp') + + if config.exists(rule_icmp + ['type']): + tmp = config.return_value(rule_icmp + ['type']) + type_code_match = re.match(r'^(\d+)/(\d+)$', tmp) + + if type_code_match: + config.set(rule_icmp + ['type'], value=type_code_match[1]) + config.set(rule_icmp + ['code'], value=type_code_match[2]) + elif tmp in icmpv6_remove: + config.delete(rule_icmp + ['type']) + elif tmp in icmpv6_translations: + translate = icmpv6_translations[tmp] + if isinstance(translate, str): + config.delete(rule_icmp + ['type']) + config.set(rule_icmp + ['type-name'], value=translate) + elif isinstance(translate, list): + config.set(rule_icmp + ['type'], value=translate[0]) + config.set(rule_icmp + ['code'], value=translate[1]) + else: + config.rename(rule_icmp + ['type'], 'type-name') + + for src_dst in ['destination', 'source']: + pg_base = base + ['ipv6-name', name, 'rule', rule, src_dst, 'group', 'port-group'] + proto_base = base + ['ipv6-name', name, 'rule', rule, 'protocol'] + if config.exists(pg_base) and not config.exists(proto_base): + config.set(proto_base, value='tcp_udp') + +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/policy/1-to-2 b/src/migration-scripts/policy/1-to-2 new file mode 100755 index 000000000..eebbf9d41 --- /dev/null +++ b/src/migration-scripts/policy/1-to-2 @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T4170: rename "policy ipv6-route" to "policy route6" to match common +# IPv4/IPv6 schema +# T4178: Update tcp flags to use multi value node + +from sys import argv +from sys import exit + +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 = ['policy', 'ipv6-route'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +config.rename(base, 'route6') +config.set_tag(['policy', 'route6']) + +for route in ['route', 'route6']: + route_path = ['policy', route] + if config.exists(route_path): + for name in config.list_nodes(route_path): + if config.exists(route_path + [name, 'rule']): + for rule in config.list_nodes(route_path + [name, 'rule']): + rule_tcp_flags = route_path + [name, 'rule', rule, 'tcp', 'flags'] + + if config.exists(rule_tcp_flags): + tmp = config.return_value(rule_tcp_flags) + config.delete(rule_tcp_flags) + for flag in tmp.split(","): + for flag in tmp.split(","): + if flag[0] == '!': + config.set(rule_tcp_flags + ['not', flag[1:].lower()]) + else: + config.set(rule_tcp_flags + [flag.lower()]) + +if config.exists(['interfaces']): + def if_policy_rename(config, path): + if config.exists(path + ['policy', 'ipv6-route']): + config.rename(path + ['policy', 'ipv6-route'], 'route6') + + for if_type in config.list_nodes(['interfaces']): + for ifname in config.list_nodes(['interfaces', if_type]): + if_path = ['interfaces', if_type, ifname] + if_policy_rename(config, if_path) + + for vif_type in ['vif', 'vif-s']: + if config.exists(if_path + [vif_type]): + for vifname in config.list_nodes(if_path + [vif_type]): + if_policy_rename(config, if_path + [vif_type, vifname]) + + if config.exists(if_path + [vif_type, vifname, 'vif-c']): + for vifcname in config.list_nodes(if_path + [vif_type, vifname, 'vif-c']): + if_policy_rename(config, if_path + [vif_type, vifname, 'vif-c', vifcname]) +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/firewall.py b/src/op_mode/firewall.py new file mode 100755 index 000000000..3146fc357 --- /dev/null +++ b/src/op_mode/firewall.py @@ -0,0 +1,361 @@ +#!/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 ipaddress +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 '' + name_prefix = 'NAME6_' if ipv6 else 'NAME_' + command = f'sudo nft list chain ip{suffix} filter {name_prefix}{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, '\n'.join(references) or 'N/A'] + if 'address' in group_conf: + row.append("\n".join(sorted(group_conf['address'], key=ipaddress.ip_address))) + elif 'network' in group_conf: + row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network))) + elif 'mac_address' in group_conf: + row.append("\n".join(sorted(group_conf['mac_address']))) + elif 'port' in group_conf: + row.append("\n".join(sorted(group_conf['port']))) + else: + row.append('N/A') + 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 or 'N/A']) + + 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/monitor_bandwidth_test.sh b/src/op_mode/monitor_bandwidth_test.sh index 900223bca..a6ad0b42c 100755 --- a/src/op_mode/monitor_bandwidth_test.sh +++ b/src/op_mode/monitor_bandwidth_test.sh @@ -24,6 +24,9 @@ elif [[ $(dig $1 AAAA +short | grep -v '\.$' | wc -l) -gt 0 ]]; then # Set address family to IPv6 when FQDN has at least one AAAA record OPT="-V" +else + # It's not IPv6, no option needed + OPT="" fi /usr/bin/iperf $OPT -c $1 $2 diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py new file mode 100755 index 000000000..5be40082f --- /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', 'route6'] + + 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 += ['route6' 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 'route6' in policy: + for route_name, route_conf in policy['route6'].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 'route6' in policy: + for route, route_conf in policy['route6'].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/show_virtual_server.py b/src/op_mode/show_virtual_server.py new file mode 100755 index 000000000..377180dec --- /dev/null +++ b/src/op_mode/show_virtual_server.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from vyos.configquery import ConfigTreeQuery +from vyos.util import call + +def is_configured(): + """ Check if high-availability virtual-server is configured """ + config = ConfigTreeQuery() + if not config.exists(['high-availability', 'virtual-server']): + return False + return True + +if __name__ == '__main__': + + if is_configured() == False: + print('Virtual server not configured!') + exit(0) + + call('sudo ipvsadm --list --numeric') diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py index 2c1db20bf..dab146d28 100755 --- a/src/op_mode/vrrp.py +++ b/src/op_mode/vrrp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -23,6 +23,7 @@ import tabulate import vyos.util +from vyos.configquery import ConfigTreeQuery from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.vrrp import VRRPError, VRRPNoData @@ -35,7 +36,17 @@ group.add_argument("-d", "--data", action="store_true", help="Print detailed VRR args = parser.parse_args() +def is_configured(): + """ Check if VRRP is configured """ + config = ConfigTreeQuery() + if not config.exists(['high-availability', 'vrrp', 'group']): + return False + return True + # Exit early if VRRP is dead or not configured +if is_configured() == False: + print('VRRP not configured!') + exit(0) if not VRRP.is_running(): print('VRRP is not running') sys.exit(0) 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/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index b1fe7e43f..a8df232ae 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -71,7 +71,8 @@ class KeepalivedFifo: # Read VRRP configuration directly from CLI self.vrrp_config_dict = conf.get_config_dict(base, - key_mangling=('-', '_'), get_first_key=True) + key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) logger.debug(f'Loaded configuration: {self.vrrp_config_dict}') except Exception as err: diff --git a/src/systemd/keepalived.service b/src/systemd/keepalived.service new file mode 100644 index 000000000..a462d8614 --- /dev/null +++ b/src/systemd/keepalived.service @@ -0,0 +1,13 @@ +[Unit] +Description=Keepalive Daemon (LVS and VRRP) +After=vyos-router.service +# Only start if there is a configuration file +ConditionFileNotEmpty=/run/keepalived/keepalived.conf + +[Service] +KillMode=process +Type=simple +# Read configuration variable file if it is present +ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork --snmp +ExecReload=/bin/kill -HUP $MAINPID +PIDFile=/run/keepalived/keepalived.pid diff --git a/src/systemd/miniupnpd.service b/src/systemd/miniupnpd.service new file mode 100644 index 000000000..51cb2eed8 --- /dev/null +++ b/src/systemd/miniupnpd.service @@ -0,0 +1,13 @@ +[Unit] +Description=UPnP service +ConditionPathExists=/run/upnp/miniupnp.conf +After=vyos-router.service +StartLimitIntervalSec=0 + +[Service] +WorkingDirectory=/run/upnp +Type=simple +ExecStart=/usr/sbin/miniupnpd -d -f /run/upnp/miniupnp.conf +PrivateTmp=yes +PIDFile=/run/miniupnpd.pid +Restart=on-failure diff --git a/src/tests/test_validate.py b/src/tests/test_validate.py index b43dbd97e..68a257d25 100644 --- a/src/tests/test_validate.py +++ b/src/tests/test_validate.py @@ -30,8 +30,12 @@ class TestVyOSValidate(TestCase): self.assertFalse(vyos.validate.is_ipv6_link_local('169.254.0.1')) self.assertTrue(vyos.validate.is_ipv6_link_local('fe80::')) self.assertTrue(vyos.validate.is_ipv6_link_local('fe80::affe:1')) + self.assertTrue(vyos.validate.is_ipv6_link_local('fe80::affe:1%eth0')) self.assertFalse(vyos.validate.is_ipv6_link_local('2001:db8::')) + self.assertFalse(vyos.validate.is_ipv6_link_local('2001:db8::%eth0')) self.assertFalse(vyos.validate.is_ipv6_link_local('VyOS')) + self.assertFalse(vyos.validate.is_ipv6_link_local('::1')) + self.assertFalse(vyos.validate.is_ipv6_link_local('::1%lo')) def test_is_ipv6_link_local(self): self.assertTrue(vyos.validate.is_loopback_addr('127.0.0.1')) diff --git a/src/validators/ip-address b/src/validators/ip-address index 51fb72c85..11d6df09e 100755 --- a/src/validators/ip-address +++ b/src/validators/ip-address @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-any-single $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IP address" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ip-cidr b/src/validators/ip-cidr index 987bf84ca..60d2ac295 100755 --- a/src/validators/ip-cidr +++ b/src/validators/ip-cidr @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-any-cidr $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IP CIDR" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ip-host b/src/validators/ip-host index f2906e8cf..77c578fa2 100755 --- a/src/validators/ip-host +++ b/src/validators/ip-host @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-any-host $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IP host" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ip-prefix b/src/validators/ip-prefix index e58aad395..e5a64fea8 100755 --- a/src/validators/ip-prefix +++ b/src/validators/ip-prefix @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-any-net $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IP prefix" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol index 078f8e319..c4c882502 100755 --- a/src/validators/ip-protocol +++ b/src/validators/ip-protocol @@ -31,11 +31,12 @@ 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" if re.match(pattern, input): exit(0) + print(f'Error: {input} is not a valid IP protocol') exit(1) diff --git a/src/validators/ipv4 b/src/validators/ipv4 index 53face090..8676d5800 100755 --- a/src/validators/ipv4 +++ b/src/validators/ipv4 @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-ipv4 $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not IPv4" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv4-address b/src/validators/ipv4-address index 872a7645a..058db088b 100755 --- a/src/validators/ipv4-address +++ b/src/validators/ipv4-address @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-ipv4-single $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IPv4 address" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv4-host b/src/validators/ipv4-host index f42feffa4..74b8c36a7 100755 --- a/src/validators/ipv4-host +++ b/src/validators/ipv4-host @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-ipv4-host $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IPv4 host" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv4-multicast b/src/validators/ipv4-multicast index 5465c728d..3f28c51db 100755 --- a/src/validators/ipv4-multicast +++ b/src/validators/ipv4-multicast @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-ipv4-multicast $1 && ipaddrcheck --is-ipv4-single $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IPv4 multicast address" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv4-prefix b/src/validators/ipv4-prefix index 8ec8a2c45..7e1e0e8dd 100755 --- a/src/validators/ipv4-prefix +++ b/src/validators/ipv4-prefix @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-ipv4-net $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IPv4 prefix" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv4-range b/src/validators/ipv4-range index cc59039f1..6492bfc52 100755 --- a/src/validators/ipv4-range +++ b/src/validators/ipv4-range @@ -7,6 +7,11 @@ ip2dec () { printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))" } +error_exit() { + echo "Error: $1 is not a valid IPv4 address range" + exit 1 +} + # Only run this if there is a hypen present in $1 if [[ "$1" =~ "-" ]]; then # This only works with real bash (<<<) - split IP addresses into array with @@ -15,21 +20,21 @@ if [[ "$1" =~ "-" ]]; then ipaddrcheck --is-ipv4-single ${strarr[0]} if [ $? -gt 0 ]; then - exit 1 + error_exit $1 fi ipaddrcheck --is-ipv4-single ${strarr[1]} if [ $? -gt 0 ]; then - exit 1 + error_exit $1 fi start=$(ip2dec ${strarr[0]}) stop=$(ip2dec ${strarr[1]}) if [ $start -ge $stop ]; then - exit 1 + error_exit $1 fi exit 0 fi -exit 1 +error_exit $1 diff --git a/src/validators/ipv6 b/src/validators/ipv6 index f18d4a63e..4ae130eb5 100755 --- a/src/validators/ipv6 +++ b/src/validators/ipv6 @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-ipv6 $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not IPv6" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv6-address b/src/validators/ipv6-address index e5d68d756..1fca77668 100755 --- a/src/validators/ipv6-address +++ b/src/validators/ipv6-address @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-ipv6-single $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IPv6 address" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv6-host b/src/validators/ipv6-host index f7a745077..7085809a9 100755 --- a/src/validators/ipv6-host +++ b/src/validators/ipv6-host @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-ipv6-host $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IPv6 host" + exit 1 +fi + +exit 0
\ No newline at end of file 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 5afc437e5..5aa7d734a 100755 --- a/src/validators/ipv6-multicast +++ b/src/validators/ipv6-multicast @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-ipv6-multicast $1 && ipaddrcheck --is-ipv6-single $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IPv6 multicast address" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv6-prefix b/src/validators/ipv6-prefix index e43616350..890dda723 100755 --- a/src/validators/ipv6-prefix +++ b/src/validators/ipv6-prefix @@ -1,3 +1,10 @@ #!/bin/sh ipaddrcheck --is-ipv6-net $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid IPv6 prefix" + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv6-range b/src/validators/ipv6-range index 033b6461b..a3c401281 100755 --- a/src/validators/ipv6-range +++ b/src/validators/ipv6-range @@ -11,6 +11,7 @@ if __name__ == '__main__': if re.search('([a-f0-9:]+:+)+[a-f0-9]+-([a-f0-9:]+:+)+[a-f0-9]+', ipv6_range): for tmp in ipv6_range.split('-'): if not is_ipv6(tmp): + print(f'Error: {ipv6_range} is not a valid IPv6 range') sys.exit(1) sys.exit(0) diff --git a/src/validators/mac-address-firewall b/src/validators/mac-address-firewall new file mode 100755 index 000000000..70551f86d --- /dev/null +++ b/src/validators/mac-address-firewall @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys + +pattern = "^!?([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$" + +if __name__ == '__main__': + if len(sys.argv) != 2: + sys.exit(1) + if not re.match(pattern, sys.argv[1]): + sys.exit(1) + sys.exit(0) diff --git a/src/validators/port-multi b/src/validators/port-multi new file mode 100755 index 000000000..cef371563 --- /dev/null +++ b/src/validators/port-multi @@ -0,0 +1,45 @@ +#!/usr/bin/python3 + +import sys +import re + +from vyos.util import read_file + +services_file = '/etc/services' + +def get_services(): + names = [] + service_data = read_file(services_file, "") + for line in service_data.split("\n"): + if not line or line[0] == '#': + continue + names.append(line.split(None, 1)[0]) + return names + +if __name__ == '__main__': + if len(sys.argv)>1: + ports = sys.argv[1].split(",") + services = get_services() + + for port in ports: + if port and port[0] == '!': + port = port[1:] + if re.match('^[0-9]{1,5}-[0-9]{1,5}$', port): + port_1, port_2 = port.split('-') + if int(port_1) not in range(1, 65536) or int(port_2) not in range(1, 65536): + print(f'Error: {port} is not a valid port range') + sys.exit(1) + if int(port_1) > int(port_2): + print(f'Error: {port} is not a valid port range') + sys.exit(1) + elif port.isnumeric(): + if int(port) not in range(1, 65536): + print(f'Error: {port} is not a valid port') + sys.exit(1) + elif port not in services: + print(f'Error: {port} is not a valid service name') + sys.exit(1) + else: + sys.exit(2) + + sys.exit(0) diff --git a/src/validators/port-range b/src/validators/port-range index abf0b09d5..5468000a7 100755 --- a/src/validators/port-range +++ b/src/validators/port-range @@ -3,16 +3,37 @@ import sys import re +from vyos.util import read_file + +services_file = '/etc/services' + +def get_services(): + names = [] + service_data = read_file(services_file, "") + for line in service_data.split("\n"): + if not line or line[0] == '#': + continue + names.append(line.split(None, 1)[0]) + return names + +def error(port_range): + print(f'Error: {port_range} is not a valid port or port range') + sys.exit(1) + if __name__ == '__main__': if len(sys.argv)>1: port_range = sys.argv[1] - if re.search('[0-9]{1,5}-[0-9]{1,5}', port_range): - for tmp in port_range.split('-'): - if int(tmp) not in range(1, 65535): - sys.exit(1) - else: - if int(port_range) not in range(1, 65535): - sys.exit(1) + if re.match('^[0-9]{1,5}-[0-9]{1,5}$', port_range): + port_1, port_2 = port_range.split('-') + if int(port_1) not in range(1, 65536) or int(port_2) not in range(1, 65536): + error(port_range) + if int(port_1) > int(port_2): + error(port_range) + elif port_range.isnumeric() and int(port_range) not in range(1, 65536): + error(port_range) + elif not port_range.isnumeric() and port_range not in get_services(): + print(f'Error: {port_range} is not a valid service name') + sys.exit(1) else: sys.exit(2) diff --git a/src/validators/tcp-flag b/src/validators/tcp-flag new file mode 100755 index 000000000..1496b904a --- /dev/null +++ b/src/validators/tcp-flag @@ -0,0 +1,17 @@ +#!/usr/bin/python3 + +import sys +import re + +if __name__ == '__main__': + if len(sys.argv)>1: + flag = sys.argv[1] + if flag and flag[0] == '!': + flag = flag[1:] + if flag not in ['syn', 'ack', 'rst', 'fin', 'urg', 'psh', 'ecn', 'cwr']: + print(f'Error: {flag} is not a valid TCP flag') + sys.exit(1) + else: + sys.exit(2) + + sys.exit(0) diff --git a/test-requirements.txt b/test-requirements.txt index 9348520b5..a475e0a16 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,3 +3,4 @@ lxml pylint nose coverage +jinja2 |