Page MenuHomeVyOS Platform

No OneTemporary

Size
69 KB
Referenced Files
None
Subscribers
None
diff --git a/data/templates/firewall/nftables.tmpl b/data/templates/firewall/nftables.tmpl
index bbb111b1f..68e83de64 100644
--- a/data/templates/firewall/nftables.tmpl
+++ b/data/templates/firewall/nftables.tmpl
@@ -1,292 +1,286 @@
#!/usr/sbin/nft -f
{% if cleanup_commands is defined %}
{% for command in cleanup_commands %}
{{ command }}
{% endfor %}
{% endif %}
{% if group is defined %}
{% if group.address_group is defined %}
{% for group_name, group_conf in group.address_group.items() %}
define A_{{ group_name }} = {
{{ group_conf.address | join(",") }}
}
{% endfor %}
{% endif %}
{% if group.ipv6_address_group is defined %}
{% for group_name, group_conf in group.ipv6_address_group.items() %}
define A6_{{ group_name }} = {
{{ group_conf.address | join(",") }}
}
{% endfor %}
{% endif %}
{% if group.network_group is defined %}
{% for group_name, group_conf in group.network_group.items() %}
define N_{{ group_name }} = {
{{ group_conf.network | join(",") }}
}
{% endfor %}
{% endif %}
{% if group.ipv6_network_group is defined %}
{% for group_name, group_conf in group.ipv6_network_group.items() %}
define N6_{{ group_name }} = {
{{ group_conf.network | join(",") }}
}
{% endfor %}
{% endif %}
{% if group.port_group is defined %}
{% for group_name, group_conf in group.port_group.items() %}
define P_{{ group_name }} = {
{{ group_conf.port | join(",") }}
}
{% endfor %}
{% endif %}
{% endif %}
table ip filter {
{% if first_install is defined %}
- chain VYOS_FW_IN {
+ chain VYOS_FW_FORWARD {
type filter hook forward priority 0; policy accept;
- }
- chain VYOS_FW_OUT {
- type filter hook forward priority 1; policy accept;
jump VYOS_POST_FW
}
chain VYOS_FW_LOCAL {
type filter hook input priority 0; policy accept;
jump VYOS_POST_FW
}
chain VYOS_FW_OUTPUT {
type filter hook output priority 0; policy accept;
jump VYOS_POST_FW
}
chain VYOS_POST_FW {
return
}
chain VYOS_FRAG_MARK {
type filter hook prerouting priority -450; policy accept;
ip frag-off & 0x3fff != 0 meta mark set 0xffff1 return
}
{% endif %}
{% if name is defined %}
{% for name_text, conf in name.items() %}
{% set default_log = 'log' if 'enable_default_log' in conf else '' %}
chain {{ name_text }} {
{% if conf.rule is defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %}
{{ rule_conf | nft_rule(name_text, rule_id) }}
{% endfor %}
{% endif %}
{% if conf.default_action is defined %}
counter {{ default_log }} {{ conf.default_action | nft_action }} comment "{{ name_text }} default-action {{ conf.default_action }}"
{% else %}
return
{% endif %}
}
{% endfor %}
{% endif %}
{% if state_policy is defined %}
chain VYOS_STATE_POLICY {
{% if state_policy.established is defined %}
{{ state_policy.established | nft_state_policy('established') }}
{% endif %}
{% if state_policy.invalid is defined %}
{{ state_policy.invalid | nft_state_policy('invalid') }}
{% endif %}
{% if state_policy.related is defined %}
{{ state_policy.related | nft_state_policy('related') }}
{% endif %}
return
}
{% endif %}
}
table ip6 filter {
{% if first_install is defined %}
- chain VYOS_FW6_IN {
+ chain VYOS_FW6_FORWARD {
type filter hook forward priority 0; policy accept;
- }
- chain VYOS_FW6_OUT {
- type filter hook forward priority 1; policy accept;
jump VYOS_POST_FW6
}
chain VYOS_FW6_LOCAL {
type filter hook input priority 0; policy accept;
jump VYOS_POST_FW6
}
chain VYOS_FW6_OUTPUT {
type filter hook output priority 0; policy accept;
jump VYOS_POST_FW6
}
chain VYOS_POST_FW6 {
return
}
chain VYOS_FRAG6_MARK {
type filter hook prerouting priority -450; policy accept;
exthdr frag exists meta mark set 0xffff1 return
}
{% endif %}
{% if ipv6_name is defined %}
{% for name_text, conf in ipv6_name.items() %}
{% set default_log = 'log' if 'enable_default_log' in conf else '' %}
chain {{ name_text }} {
{% if conf.rule is defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %}
{{ rule_conf | nft_rule(name_text, rule_id, 'ip6') }}
{% endfor %}
{% endif %}
{% if conf.default_action is defined %}
counter {{ default_log }} {{ conf.default_action | nft_action }} comment "{{ name_text }} default-action {{ conf.default_action }}"
{% else %}
return
{% endif %}
}
{% endfor %}
{% endif %}
{% if state_policy is defined %}
chain VYOS_STATE_POLICY6 {
{% if state_policy.established is defined %}
{{ state_policy.established | nft_state_policy('established', 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
notrack
}
chain OUTPUT {
type filter hook output priority -200; policy accept;
counter jump VYOS_CT_IGNORE
counter jump VYOS_CT_TIMEOUT
counter jump VYOS_CT_OUTPUT_HOOK
notrack
}
ct helper rpc_tcp {
type "rpc" protocol tcp;
}
ct helper rpc_udp {
type "rpc" protocol udp;
}
ct helper tns_tcp {
type "tns" protocol tcp;
}
chain VYOS_CT_HELPER {
ct helper set "rpc_tcp" tcp dport {111} return
ct helper set "rpc_udp" udp dport {111} return
ct helper set "tns_tcp" tcp dport {1521,1525,1536} return
return
}
chain VYOS_CT_IGNORE {
return
}
chain VYOS_CT_TIMEOUT {
return
}
chain VYOS_CT_PREROUTING_HOOK {
return
}
chain VYOS_CT_OUTPUT_HOOK {
return
}
}
table ip6 raw {
chain VYOS_TCP_MSS {
type filter hook forward priority -300; policy accept;
}
chain PREROUTING {
type filter hook prerouting priority -300; policy accept;
counter jump VYOS_CT_PREROUTING_HOOK
notrack
}
chain OUTPUT {
type filter hook output priority -300; policy accept;
counter jump VYOS_CT_OUTPUT_HOOK
notrack
}
chain VYOS_CT_PREROUTING_HOOK {
return
}
chain VYOS_CT_OUTPUT_HOOK {
return
}
}
{% endif %}
diff --git a/data/templates/zone_policy/nftables.tmpl b/data/templates/zone_policy/nftables.tmpl
index 21230c688..fae6e8c4f 100644
--- a/data/templates/zone_policy/nftables.tmpl
+++ b/data/templates/zone_policy/nftables.tmpl
@@ -1,97 +1,107 @@
#!/usr/sbin/nft -f
{% if cleanup_commands is defined %}
{% for command in cleanup_commands %}
{{ command }}
{% endfor %}
{% endif %}
{% if zone is defined %}
table ip filter {
{% for zone_name, zone_conf in zone.items() if zone_conf.ipv4 %}
{% if zone_conf.local_zone is defined %}
chain VZONE_{{ zone_name }}_IN {
iifname lo counter return
{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %}
iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }}
iifname { {{ zone[from_zone].interface | join(",") }} } counter return
{% endfor %}
counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
}
chain VZONE_{{ zone_name }}_OUT {
oifname lo counter return
{% for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.name is defined %}
oifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }}
oifname { {{ zone[from_zone].interface | join(",") }} } counter return
{% endfor %}
counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
}
{% else %}
chain VZONE_{{ zone_name }} {
iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6=False) }}
{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %}
{% if zone[from_zone].local_zone is not defined %}
iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }}
iifname { {{ zone[from_zone].interface | join(",") }} } counter return
{% endif %}
{% endfor %}
counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
}
{% endif %}
{% endfor %}
}
table ip6 filter {
{% for zone_name, zone_conf in zone.items() if zone_conf.ipv6 %}
{% if zone_conf.local_zone is defined %}
chain VZONE6_{{ zone_name }}_IN {
iifname lo counter return
{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %}
iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }}
iifname { {{ zone[from_zone].interface | join(",") }} } counter return
{% endfor %}
counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
}
chain VZONE6_{{ zone_name }}_OUT {
oifname lo counter return
{% for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.ipv6_name is defined %}
oifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }}
oifname { {{ zone[from_zone].interface | join(",") }} } counter return
{% endfor %}
counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
}
{% else %}
chain VZONE6_{{ zone_name }} {
iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6=True) }}
{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %}
{% if zone[from_zone].local_zone is not defined %}
iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }}
iifname { {{ zone[from_zone].interface | join(",") }} } counter return
{% endif %}
{% endfor %}
counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
}
{% endif %}
{% endfor %}
}
{% for zone_name, zone_conf in zone.items() %}
{% if zone_conf.ipv4 %}
{% if 'local_zone' in zone_conf %}
insert rule ip filter VYOS_FW_LOCAL counter jump VZONE_{{ zone_name }}_IN
insert rule ip filter VYOS_FW_OUTPUT counter jump VZONE_{{ zone_name }}_OUT
{% else %}
-insert rule ip filter VYOS_FW_OUT oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE_{{ zone_name }}
+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_OUT oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE6_{{ zone_name }}
+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/python/vyos/template.py b/python/vyos/template.py
index 7671bf377..6f65c6c98 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -1,550 +1,546 @@
# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import functools
import os
from jinja2 import Environment
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()
_FILTERS = {}
# reuse Environments with identical settings to improve performance
@functools.lru_cache(maxsize=2)
def _get_environment(location=None):
if location is None:
loc_loader=FileSystemLoader(directories["templates"])
else:
loc_loader=FileSystemLoader(location)
env = Environment(
# Don't check if template files were modified upon re-rendering
auto_reload=False,
# Cache up to this number of templates for quick re-rendering
cache_size=100,
loader=loc_loader,
trim_blocks=True,
)
env.filters.update(_FILTERS)
return env
def register_filter(name, func=None):
"""Register a function to be available as filter in templates under given name.
It can also be used as a decorator, see below in this module for examples.
:raise RuntimeError:
when trying to register a filter after a template has been rendered already
:raise ValueError: when trying to register a name which was taken already
"""
if func is None:
return functools.partial(register_filter, name)
if _get_environment.cache_info().currsize:
raise RuntimeError(
"Filters can only be registered before rendering the first template"
)
if name in _FILTERS:
raise ValueError(f"A filter with name {name!r} was registered already")
_FILTERS[name] = func
return func
def render_to_string(template, content, formater=None, location=None):
"""Render a template from the template directory, raise on any errors.
:param template: the path to the template relative to the template folder
:param content: the dictionary of variables to put into rendering context
:param formater:
if given, it has to be a callable the rendered string is passed through
The parsed template files are cached, so rendering the same file multiple times
does not cause as too much overhead.
If used everywhere, it could be changed to load the template from Python
environment variables from an importable Python module generated when the Debian
package is build (recovering the load time and overhead caused by having the
file out of the code).
"""
template = _get_environment(location).get_template(template)
rendered = template.render(content)
if formater is not None:
rendered = formater(rendered)
return rendered
def render(
destination,
template,
content,
formater=None,
permission=None,
user=None,
group=None,
location=None,
):
"""Render a template from the template directory to a file, raise on any errors.
:param destination: path to the file to save the rendered template in
:param permission: permission bitmask to set for the output file
:param user: user to own the output file
:param group: group to own the output file
All other parameters are as for :func:`render_to_string`.
"""
# Create the directory if it does not exist
folder = os.path.dirname(destination)
makedir(folder, user, group)
# As we are opening the file with 'w', we are performing the rendering before
# calling open() to not accidentally erase the file if rendering fails
rendered = render_to_string(template, content, formater, location)
# Write to file
with open(destination, "w") as file:
chmod(file.fileno(), permission)
chown(file.fileno(), user, group)
file.write(rendered)
##################################
# Custom template filters follow #
##################################
@register_filter('ip_from_cidr')
def ip_from_cidr(prefix):
""" Take an IPv4/IPv6 CIDR host and strip cidr mask.
Example:
192.0.2.1/24 -> 192.0.2.1, 2001:db8::1/64 -> 2001:db8::1
"""
from ipaddress import ip_interface
return str(ip_interface(prefix).ip)
@register_filter('address_from_cidr')
def address_from_cidr(prefix):
""" Take an IPv4/IPv6 CIDR prefix and convert the network to an "address".
Example:
192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8::
"""
from ipaddress import ip_network
return str(ip_network(prefix).network_address)
@register_filter('bracketize_ipv6')
def bracketize_ipv6(address):
""" Place a passed IPv6 address into [] brackets, do nothing for IPv4 """
if is_ipv6(address):
return f'[{address}]'
return address
@register_filter('dot_colon_to_dash')
def dot_colon_to_dash(text):
""" Replace dot and colon to dash for string
Example:
192.0.2.1 => 192-0-2-1, 2001:db8::1 => 2001-db8--1
"""
text = text.replace(":", "-")
text = text.replace(".", "-")
return text
@register_filter('netmask_from_cidr')
def netmask_from_cidr(prefix):
""" Take CIDR prefix and convert the prefix length to a "subnet mask".
Example:
- 192.0.2.0/24 -> 255.255.255.0
- 2001:db8::/48 -> ffff:ffff:ffff::
"""
from ipaddress import ip_network
return str(ip_network(prefix).netmask)
@register_filter('netmask_from_ipv4')
def netmask_from_ipv4(address):
""" Take IP address and search all attached interface IP addresses for the
given one. After address has been found, return the associated netmask.
Example:
- 172.18.201.10 -> 255.255.255.128
"""
from netifaces import interfaces
from netifaces import ifaddresses
from netifaces import AF_INET
for interface in interfaces():
tmp = ifaddresses(interface)
if AF_INET in tmp:
for af_addr in tmp[AF_INET]:
if 'addr' in af_addr:
if af_addr['addr'] == address:
return af_addr['netmask']
raise ValueError
@register_filter('is_ip_network')
def is_ip_network(addr):
""" Take IP(v4/v6) address and validate if the passed argument is a network
or a host address.
Example:
- 192.0.2.0 -> False
- 192.0.2.10/24 -> False
- 192.0.2.0/24 -> True
- 2001:db8:: -> False
- 2001:db8::100 -> False
- 2001:db8::/48 -> True
- 2001:db8:1000::/64 -> True
"""
try:
from ipaddress import ip_network
# input variables must contain a / to indicate its CIDR notation
if len(addr.split('/')) != 2:
raise ValueError()
ip_network(addr)
return True
except:
return False
@register_filter('network_from_ipv4')
def network_from_ipv4(address):
""" Take IP address and search all attached interface IP addresses for the
given one. After address has been found, return the associated network
address.
Example:
- 172.18.201.10 has mask 255.255.255.128 -> network is 172.18.201.0
"""
netmask = netmask_from_ipv4(address)
from ipaddress import ip_interface
cidr_prefix = ip_interface(f'{address}/{netmask}').network
return address_from_cidr(cidr_prefix)
@register_filter('is_interface')
def is_interface(interface):
""" Check if parameter is a valid local interface name """
return os.path.exists(f'/sys/class/net/{interface}')
@register_filter('is_ip')
def is_ip(addr):
""" Check addr if it is an IPv4 or IPv6 address """
return is_ipv4(addr) or is_ipv6(addr)
@register_filter('is_ipv4')
def is_ipv4(text):
""" Filter IP address, return True on IPv4 address, False otherwise """
from ipaddress import ip_interface
try: return ip_interface(text).version == 4
except: return False
@register_filter('is_ipv6')
def is_ipv6(text):
""" Filter IP address, return True on IPv6 address, False otherwise """
from ipaddress import ip_interface
try: return ip_interface(text).version == 6
except: return False
@register_filter('first_host_address')
def first_host_address(text):
""" Return first usable (host) IP address from given prefix.
Example:
- 10.0.0.0/24 -> 10.0.0.1
- 2001:db8::/64 -> 2001:db8::
"""
from ipaddress import ip_interface
from ipaddress import IPv4Network
from ipaddress import IPv6Network
addr = ip_interface(text)
if addr.version == 4:
return str(addr.ip +1)
return str(addr.ip)
@register_filter('last_host_address')
def last_host_address(text):
""" Return first usable IP address from given prefix.
Example:
- 10.0.0.0/24 -> 10.0.0.254
- 2001:db8::/64 -> 2001:db8::ffff:ffff:ffff:ffff
"""
from ipaddress import ip_interface
from ipaddress import IPv4Network
from ipaddress import IPv6Network
addr = ip_interface(text)
if addr.version == 4:
return str(IPv4Network(addr).broadcast_address - 1)
return str(IPv6Network(addr).broadcast_address)
@register_filter('inc_ip')
def inc_ip(address, increment):
""" Increment given IP address by 'increment'
Example (inc by 2):
- 10.0.0.0/24 -> 10.0.0.2
- 2001:db8::/64 -> 2001:db8::2
"""
from ipaddress import ip_interface
return str(ip_interface(address).ip + int(increment))
@register_filter('dec_ip')
def dec_ip(address, decrement):
""" Decrement given IP address by 'decrement'
Example (inc by 2):
- 10.0.0.0/24 -> 10.0.0.2
- 2001:db8::/64 -> 2001:db8::2
"""
from ipaddress import ip_interface
return str(ip_interface(address).ip - int(decrement))
@register_filter('compare_netmask')
def compare_netmask(netmask1, netmask2):
"""
Compare two IP netmask if they have the exact same size.
compare_netmask('10.0.0.0/8', '20.0.0.0/8') -> True
compare_netmask('10.0.0.0/8', '20.0.0.0/16') -> False
"""
from ipaddress import ip_network
try:
return ip_network(netmask1).netmask == ip_network(netmask2).netmask
except:
return False
@register_filter('isc_static_route')
def isc_static_route(subnet, router):
# https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
# Option format is:
# <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
# where bytes with the value 0 are omitted.
from ipaddress import ip_network
net = ip_network(subnet)
# add netmask
string = str(net.prefixlen) + ','
# add network bytes
if net.prefixlen:
width = net.prefixlen // 8
if net.prefixlen % 8:
width += 1
string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ','
# add router bytes
string += ','.join(router.split('.'))
return string
@register_filter('is_file')
def is_file(filename):
if os.path.exists(filename):
return os.path.isfile(filename)
return False
@register_filter('get_dhcp_router')
def get_dhcp_router(interface):
""" Static routes can point to a router received by a DHCP reply. This
helper is used to get the current default router from the DHCP reply.
Returns False of no router is found, returns the IP address as string if
a router is found.
"""
lease_file = f'/var/lib/dhcp/dhclient_{interface}.leases'
if not os.path.exists(lease_file):
return None
from vyos.util import read_file
for line in read_file(lease_file).splitlines():
if 'option routers' in line:
(_, _, address) = line.split()
return address.rstrip(';')
@register_filter('natural_sort')
def natural_sort(iterable):
import re
from jinja2.runtime import Undefined
if isinstance(iterable, Undefined) or iterable is None:
return list()
def convert(text):
return int(text) if text.isdigit() else text.lower()
def alphanum_key(key):
return [convert(c) for c in re.split('([0-9]+)', str(key))]
return sorted(iterable, key=alphanum_key)
@register_filter('get_ipv4')
def get_ipv4(interface):
""" Get interface IPv4 addresses"""
from vyos.ifconfig import Interface
return Interface(interface).get_addr_v4()
@register_filter('get_ipv6')
def get_ipv6(interface):
""" Get interface IPv6 addresses"""
from vyos.ifconfig import Interface
return Interface(interface).get_addr_v6()
@register_filter('get_ip')
def get_ip(interface):
""" Get interface IP addresses"""
from vyos.ifconfig import Interface
return Interface(interface).get_addr()
def get_first_ike_dh_group(ike_group):
if ike_group and 'proposal' in ike_group:
for priority, proposal in ike_group['proposal'].items():
if 'dh_group' in proposal:
return 'dh-group' + proposal['dh_group']
return 'dh-group2' # Fallback on dh-group2
@register_filter('get_esp_ike_cipher')
def get_esp_ike_cipher(group_config, ike_group=None):
pfs_lut = {
'dh-group1' : 'modp768',
'dh-group2' : 'modp1024',
'dh-group5' : 'modp1536',
'dh-group14' : 'modp2048',
'dh-group15' : 'modp3072',
'dh-group16' : 'modp4096',
'dh-group17' : 'modp6144',
'dh-group18' : 'modp8192',
'dh-group19' : 'ecp256',
'dh-group20' : 'ecp384',
'dh-group21' : 'ecp521',
'dh-group22' : 'modp1024s160',
'dh-group23' : 'modp2048s224',
'dh-group24' : 'modp2048s256',
'dh-group25' : 'ecp192',
'dh-group26' : 'ecp224',
'dh-group27' : 'ecp224bp',
'dh-group28' : 'ecp256bp',
'dh-group29' : 'ecp384bp',
'dh-group30' : 'ecp512bp',
'dh-group31' : 'curve25519',
'dh-group32' : 'curve448'
}
ciphers = []
if 'proposal' in group_config:
for priority, proposal in group_config['proposal'].items():
# both encryption and hash need to be specified for a proposal
if not {'encryption', 'hash'} <= set(proposal):
continue
tmp = '{encryption}-{hash}'.format(**proposal)
if 'dh_group' in proposal:
tmp += '-' + pfs_lut[ 'dh-group' + proposal['dh_group'] ]
elif 'pfs' in group_config and group_config['pfs'] != 'disable':
group = group_config['pfs']
if group_config['pfs'] == 'enable':
group = get_first_ike_dh_group(ike_group)
tmp += '-' + pfs_lut[group]
ciphers.append(tmp)
return ciphers
@register_filter('get_uuid')
def get_uuid(interface):
""" Get interface IP addresses"""
from uuid import uuid1
return uuid1()
openvpn_translate = {
'des': 'des-cbc',
'3des': 'des-ede3-cbc',
'bf128': 'bf-cbc',
'bf256': 'bf-cbc',
'aes128gcm': 'aes-128-gcm',
'aes128': 'aes-128-cbc',
'aes192gcm': 'aes-192-gcm',
'aes192': 'aes-192-cbc',
'aes256gcm': 'aes-256-gcm',
'aes256': 'aes-256-cbc'
}
@register_filter('openvpn_cipher')
def get_openvpn_cipher(cipher):
if cipher in openvpn_translate:
return openvpn_translate[cipher].upper()
return cipher.upper()
@register_filter('openvpn_ncp_ciphers')
def get_openvpn_ncp_ciphers(ciphers):
out = []
for cipher in ciphers:
if cipher in openvpn_translate:
out.append(openvpn_translate[cipher])
else:
out.append(cipher)
return ':'.join(out).upper()
@register_filter('snmp_auth_oid')
def snmp_auth_oid(type):
if type not in ['md5', 'sha', 'aes', 'des', 'none']:
raise ValueError()
OIDs = {
'md5' : '.1.3.6.1.6.3.10.1.1.2',
'sha' : '.1.3.6.1.6.3.10.1.1.3',
'aes' : '.1.3.6.1.6.3.10.1.2.4',
'des' : '.1.3.6.1.6.3.10.1.2.2',
'none': '.1.3.6.1.6.3.10.1.2.1'
}
return OIDs[type]
@register_filter('nft_action')
def nft_action(vyos_action):
if vyos_action == 'accept':
return 'return'
return vyos_action
@register_filter('nft_rule')
def nft_rule(rule_conf, fw_name, rule_id, ip_name='ip'):
from vyos.firewall import parse_rule
return parse_rule(rule_conf, fw_name, rule_id, ip_name)
@register_filter('nft_state_policy')
def nft_state_policy(conf, state, 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:
- if conf['action'] == 'accept':
- jump_target = 'VYOS_POST_FW6' if ipv6 else 'VYOS_POST_FW'
- out.append(f'jump {jump_target}')
- else:
- out.append(conf['action'])
+ out.append(conf['action'])
return " ".join(out)
@register_filter('nft_intra_zone_action')
def nft_intra_zone_action(zone_conf, ipv6=False):
if 'intra_zone_filtering' in zone_conf:
intra_zone = zone_conf['intra_zone_filtering']
fw_name = 'ipv6_name' if ipv6 else 'name'
if 'action' in intra_zone:
if intra_zone['action'] == 'accept':
return 'return'
return intra_zone['action']
elif dict_search_args(intra_zone, 'firewall', fw_name):
name = dict_search_args(intra_zone, 'firewall', fw_name)
return f'jump {name}'
return 'return'
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index 5f728f0cd..2b3b354ba 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -1,172 +1,172 @@
#!/usr/bin/env python3
#
# Copyright (C) 2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from glob import glob
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.util import cmd
sysfs_config = {
'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'default': '0', 'test_value': 'disable'},
'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'default': '1', 'test_value': 'enable'},
'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route', 'default': '0', 'test_value': 'enable'},
'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'},
'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'default': '-1', 'test_value': 'enable'},
'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians', 'default': '1', 'test_value': 'disable'},
'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'},
'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects', 'default': '1', 'test_value': 'disable'},
'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies', 'default': '1', 'test_value': 'disable'},
'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337', 'default': '0', 'test_value': 'enable'}
}
class TestFirewall(VyOSUnitTestSHIM.TestCase):
def setUp(self):
self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '172.16.10.1/24'])
def tearDown(self):
self.cli_delete(['interfaces', 'ethernet', 'eth0'])
self.cli_commit()
self.cli_delete(['firewall'])
self.cli_commit()
def test_groups(self):
self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24'])
self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53'])
self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '123'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'protocol', 'tcp'])
self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest'])
self.cli_commit()
nftables_search = [
['iifname "eth0"', 'jump smoketest'],
['ip saddr { 172.16.99.0/24 }', 'ip daddr 172.16.10.10', 'tcp dport { 53, 123 }', 'return'],
]
nftables_output = cmd('sudo nft list table ip filter')
for search in nftables_search:
matched = False
for line in nftables_output.split("\n"):
if all(item in line for item in search):
matched = True
break
self.assertTrue(matched)
def test_basic_rules(self):
self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'action', 'reject'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'protocol', 'tcp_udp'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'destination', 'port', '8888'])
self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest'])
self.cli_commit()
nftables_search = [
['iifname "eth0"', 'jump smoketest'],
['saddr 172.16.20.10', 'daddr 172.16.10.10', 'return'],
['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'reject'],
['smoketest default-action', 'drop']
]
nftables_output = cmd('sudo nft list table ip filter')
for search in nftables_search:
matched = False
for line in nftables_output.split("\n"):
if all(item in line for item in search):
matched = True
break
self.assertTrue(matched)
def test_basic_rules_ipv6(self):
self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'default-action', 'drop'])
self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'action', 'accept'])
self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'source', 'address', '2002::1'])
self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'destination', 'address', '2002::1:1'])
self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'action', 'reject'])
self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'protocol', 'tcp_udp'])
self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'destination', 'port', '8888'])
self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'ipv6-name', 'v6-smoketest'])
self.cli_commit()
nftables_search = [
['iifname "eth0"', 'jump v6-smoketest'],
['saddr 2002::1', 'daddr 2002::1:1', 'return'],
['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'reject'],
['smoketest default-action', 'drop']
]
nftables_output = cmd('sudo nft list table ip6 filter')
for search in nftables_search:
matched = False
for line in nftables_output.split("\n"):
if all(item in line for item in search):
matched = True
break
self.assertTrue(matched)
def test_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_IN', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL'],
- 'ip6 filter': ['VYOS_FW6_IN', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']
+ '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/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py
index 516fa6c48..b0df9dff4 100755
--- a/src/conf_mode/firewall-interface.py
+++ b/src/conf_mode/firewall-interface.py
@@ -1,169 +1,172 @@
#!/usr/bin/env python3
#
# Copyright (C) 2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import re
from sys import argv
from sys import exit
from vyos.config import Config
from vyos.configdict import leaf_node_changed
from vyos.ifconfig import Section
from vyos.template import render
from vyos.util import cmd
from vyos.util import dict_search_args
from vyos.util import run
from vyos import ConfigError
from vyos import airbag
airbag.enable()
NFT_CHAINS = {
- 'in': 'VYOS_FW_IN',
- 'out': 'VYOS_FW_OUT',
+ 'in': 'VYOS_FW_FORWARD',
+ 'out': 'VYOS_FW_FORWARD',
'local': 'VYOS_FW_LOCAL'
}
NFT6_CHAINS = {
- 'in': 'VYOS_FW6_IN',
- 'out': 'VYOS_FW6_OUT',
+ '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, ifname, new_name=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'ifname "{ifname}"' in line:
+ 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, ifname, name)
- rule_action = 'insert'
- rule_prefix = ''
+ rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, 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}')
else:
- cleanup_rule('ip filter', chain, ifname)
+ 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, ifname, ipv6_name)
- rule_action = 'insert'
- rule_prefix = ''
+ rule_exists = cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname, ipv6_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 ip6 filter {ipv6_chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {ipv6_name}')
else:
- cleanup_rule('ip6 filter', ipv6_chain, ifname)
+ 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 8e037c679..6016d94fa 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -1,340 +1,338 @@
#!/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 glob import glob
from json import loads
from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdiff import get_config_diff, Diff
from vyos.template import render
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
airbag.enable()
nftables_conf = '/run/nftables.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'}
}
preserve_chains = [
'INPUT',
'FORWARD',
'OUTPUT',
- 'VYOS_FW_IN',
- 'VYOS_FW_OUT',
+ 'VYOS_FW_FORWARD',
'VYOS_FW_LOCAL',
'VYOS_FW_OUTPUT',
'VYOS_POST_FW',
'VYOS_FRAG_MARK',
- 'VYOS_FW6_IN',
- 'VYOS_FW6_OUT',
+ 'VYOS_FW6_FORWARD',
'VYOS_FW6_LOCAL',
'VYOS_FW6_OUTPUT',
'VYOS_POST_FW6',
'VYOS_FRAG6_MARK'
]
valid_groups = [
'address_group',
'network_group',
'port_group'
]
snmp_change_type = {
'unknown': 0,
'add': 1,
'delete': 2,
'change': 3
}
snmp_event_source = 1
snmp_trap_mib = 'VYATTA-TRAP-MIB'
snmp_trap_name = 'mgmtEventTrap'
def get_firewall_interfaces(conf):
out = {}
interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
def find_interfaces(iftype_conf, output={}, prefix=''):
for ifname, if_conf in iftype_conf.items():
if 'firewall' in if_conf:
output[prefix + ifname] = if_conf['firewall']
for vif in ['vif', 'vif_s', 'vif_c']:
if vif in if_conf:
output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.'))
return output
for iftype, iftype_conf in interfaces.items():
out.update(find_interfaces(iftype_conf))
return out
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
base = ['firewall']
if not conf.exists(base):
return {}
firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
default_values = defaults(base)
firewall = dict_merge(default_values, firewall)
firewall['interfaces'] = get_firewall_interfaces(conf)
if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
diff = get_config_diff(conf)
firewall['trap_diff'] = diff.get_child_nodes_diff_str(base)
firewall['trap_targets'] = conf.get_config_dict(['service', 'snmp', 'trap-target'],
key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
return firewall
def verify_rule(firewall, rule_conf, ipv6):
if 'action' not in rule_conf:
raise ConfigError('Rule action must be defined')
if 'fragment' in rule_conf:
if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']):
raise ConfigError('Cannot specify both "match-frag" and "match-non-frag"')
if 'ipsec' in rule_conf:
if {'match_ipsec', 'match_non_ipsec'} <= set(rule_conf['ipsec']):
raise ConfigError('Cannot specify both "match-ipsec" and "match-non-ipsec"')
if 'recent' in rule_conf:
if not {'count', 'time'} <= set(rule_conf['recent']):
raise ConfigError('Recent "count" and "time" values must be defined')
for side in ['destination', 'source']:
if side in rule_conf:
side_conf = rule_conf[side]
if 'group' in side_conf:
if {'address_group', 'network_group'} <= set(side_conf['group']):
raise ConfigError('Only one address-group or network-group can be specified')
for group in valid_groups:
if group in side_conf['group']:
group_name = side_conf['group'][group]
fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
if not dict_search_args(firewall, 'group', fw_group):
error_group = fw_group.replace("_", "-")
raise ConfigError(f'Group defined in rule but {error_group} is not configured')
if group_name not in firewall['group'][fw_group]:
error_group = group.replace("_", "-")
raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule')
if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'):
if 'protocol' not in rule_conf:
raise ConfigError('Protocol must be defined if specifying a port or port-group')
if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group')
def verify(firewall):
# bail out early - looks like removal from running config
if not firewall:
return None
if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
if not firewall['trap_targets']:
raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined')
for name in ['name', 'ipv6_name']:
if name in firewall:
for name_id, name_conf in firewall[name].items():
if name_id in preserve_chains:
raise ConfigError(f'Firewall name "{name_id}" is reserved for VyOS')
if 'rule' in name_conf:
for rule_id, rule_conf in name_conf['rule'].items():
verify_rule(firewall, rule_conf, name == 'ipv6_name')
for ifname, if_firewall in firewall['interfaces'].items():
for direction in ['in', 'out', 'local']:
name = dict_search_args(if_firewall, direction, 'name')
ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
if name and not dict_search_args(firewall, 'name', name):
raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}')
if ipv6_name and not dict_search_args(firewall, 'ipv6_name', ipv6_name):
raise ConfigError(f'Firewall ipv6-name "{ipv6_name}" is still referenced on interface {ifname}')
return None
def cleanup_commands(firewall):
commands = []
for table in ['ip filter', 'ip6 filter']:
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:
if item['chain']['name'] in ['VYOS_STATE_POLICY', 'VYOS_STATE_POLICY6']:
chain = item['chain']['name']
if 'state_policy' not in firewall:
commands.append(f'delete chain {table} {chain}')
else:
commands.append(f'flush chain {table} {chain}')
elif item['chain']['name'] not in preserve_chains:
chain = item['chain']['name']
if table == 'ip filter' and dict_search_args(firewall, 'name', chain):
commands.append(f'flush chain {table} {chain}')
elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain):
commands.append(f'flush chain {table} {chain}')
else:
commands.append(f'delete chain {table} {chain}')
elif 'rule' in item:
rule = item['rule']
- if rule['chain'] in ['VYOS_FW_IN', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL', 'VYOS_FW6_IN', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']:
+ 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}')
return commands
def generate(firewall):
if not os.path.exists(nftables_conf):
firewall['first_install'] = True
else:
firewall['cleanup_commands'] = cleanup_commands(firewall)
render(nftables_conf, 'firewall/nftables.tmpl', firewall)
return None
def apply_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_IN')
+ search_str = cmd(f'nft list chain ip filter VYOS_FW_FORWARD')
return 'VYOS_STATE_POLICY' in search_str
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_IN', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']:
+ 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_IN', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']:
+ 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)
post_apply_trap(firewall)
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/zone_policy.py b/src/conf_mode/zone_policy.py
index 2535ea33b..d605e9639 100755
--- a/src/conf_mode/zone_policy.py
+++ b/src/conf_mode/zone_policy.py
@@ -1,196 +1,198 @@
#!/usr/bin/env python3
#
# Copyright (C) 2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from json import loads
from sys import exit
from vyos.config import Config
from vyos.template import render
from vyos.util import cmd
from vyos.util import dict_search_args
from vyos.util import run
from vyos import ConfigError
from vyos import airbag
airbag.enable()
nftables_conf = '/run/nftables_zone.conf'
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
base = ['zone-policy']
zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
if zone_policy:
zone_policy['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
return zone_policy
def verify(zone_policy):
# bail out early - looks like removal from running config
if not zone_policy:
return None
local_zone = False
interfaces = []
if 'zone' in zone_policy:
for zone, zone_conf in zone_policy['zone'].items():
if 'local_zone' not in zone_conf and 'interface' not in zone_conf:
raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone')
if 'local_zone' in zone_conf:
if local_zone:
raise ConfigError('There cannot be multiple local zones')
if 'interface' in zone_conf:
raise ConfigError('Local zone cannot have interfaces assigned')
if 'intra_zone_filtering' in zone_conf:
raise ConfigError('Local zone cannot use intra-zone-filtering')
local_zone = True
if 'interface' in zone_conf:
found_duplicates = [intf for intf in zone_conf['interface'] if intf in interfaces]
if found_duplicates:
raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
interfaces += zone_conf['interface']
if 'intra_zone_filtering' in zone_conf:
intra_zone = zone_conf['intra_zone_filtering']
if len(intra_zone) > 1:
raise ConfigError('Only one intra-zone-filtering action must be specified')
if 'firewall' in intra_zone:
v4_name = dict_search_args(intra_zone, 'firewall', 'name')
if v4_name and not dict_search_args(zone_policy, 'firewall', 'name', v4_name):
raise ConfigError(f'Firewall name "{v4_name}" does not exist')
v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6-name')
if v6_name and not dict_search_args(zone_policy, 'firewall', 'ipv6-name', v6_name):
raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
if not v4_name and not v6_name:
raise ConfigError('No firewall names specified for intra-zone-filtering')
if 'from' in zone_conf:
for from_zone, from_conf in zone_conf['from'].items():
v4_name = dict_search_args(from_conf, 'firewall', 'name')
if v4_name:
if 'name' not in zone_policy['firewall']:
raise ConfigError(f'Firewall name "{v4_name}" does not exist')
if not dict_search_args(zone_policy, 'firewall', 'name', v4_name):
raise ConfigError(f'Firewall name "{v4_name}" does not exist')
v6_name = dict_search_args(from_conf, 'firewall', 'v6_name')
if v6_name:
if 'ipv6_name' not in zone_policy['firewall']:
raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
if not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name):
raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
return None
def has_ipv4_fw(zone_conf):
if 'from' not in zone_conf:
return False
zone_from = zone_conf['from']
return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'name')])
def has_ipv6_fw(zone_conf):
if 'from' not in zone_conf:
return False
zone_from = zone_conf['from']
return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'ipv6_name')])
def get_local_from(zone_policy, local_zone_name):
# Get all zone firewall names from the local zone
out = {}
for zone, zone_conf in zone_policy['zone'].items():
if zone == local_zone_name:
continue
if 'from' not in zone_conf:
continue
if local_zone_name in zone_conf['from']:
out[zone] = zone_conf['from'][local_zone_name]
return out
def cleanup_commands():
commands = []
for table in ['ip filter', 'ip6 filter']:
json_str = cmd(f'nft -j list table {table}')
obj = loads(json_str)
if 'nftables' not in obj:
continue
for item in obj['nftables']:
if 'rule' in item:
chain = item['rule']['chain']
handle = item['rule']['handle']
if 'expr' not in item['rule']:
continue
for expr in item['rule']['expr']:
target = dict_search_args(expr, 'jump', 'target')
- if target and target.startswith("VZONE"):
+ 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 == 1:
+ 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)

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 17, 12:55 AM (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4416753
Default Alt Text
(69 KB)

Event Timeline