summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/arp.py114
-rwxr-xr-xsrc/conf_mode/bcast_relay.py4
-rwxr-xr-xsrc/conf_mode/conntrack.py66
-rwxr-xr-xsrc/conf_mode/conntrack_sync.py15
-rwxr-xr-xsrc/conf_mode/container.py (renamed from src/conf_mode/containers.py)102
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py7
-rwxr-xr-xsrc/conf_mode/dhcp_server.py8
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py7
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py37
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py234
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py2
-rwxr-xr-xsrc/conf_mode/firewall-interface.py175
-rwxr-xr-xsrc/conf_mode/firewall.py388
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py454
-rwxr-xr-xsrc/conf_mode/high-availability.py (renamed from src/conf_mode/vrrp.py)71
-rwxr-xr-xsrc/conf_mode/host_name.py5
-rwxr-xr-xsrc/conf_mode/http-api.py63
-rwxr-xr-xsrc/conf_mode/https.py18
-rwxr-xr-xsrc/conf_mode/igmp_proxy.py5
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py16
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py11
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py4
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py33
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py30
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py8
-rwxr-xr-xsrc/conf_mode/interfaces-loopback.py4
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py6
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py45
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py41
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py14
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py95
-rwxr-xr-xsrc/conf_mode/interfaces-vti.py4
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py94
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py12
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py14
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py132
-rwxr-xr-xsrc/conf_mode/lldp.py240
-rwxr-xr-xsrc/conf_mode/nat.py42
-rwxr-xr-xsrc/conf_mode/nat66.py31
-rwxr-xr-xsrc/conf_mode/netns.py118
-rwxr-xr-xsrc/conf_mode/ntp.py4
-rwxr-xr-xsrc/conf_mode/policy-local-route.py205
-rwxr-xr-xsrc/conf_mode/policy-route-interface.py120
-rwxr-xr-xsrc/conf_mode/policy-route.py262
-rwxr-xr-xsrc/conf_mode/policy.py30
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py35
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py97
-rwxr-xr-xsrc/conf_mode/protocols_igmp.py2
-rwxr-xr-xsrc/conf_mode/protocols_isis.py76
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py59
-rwxr-xr-xsrc/conf_mode/protocols_nhrp.py38
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py44
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py118
-rwxr-xr-xsrc/conf_mode/protocols_pim.py2
-rwxr-xr-xsrc/conf_mode/protocols_rip.py35
-rwxr-xr-xsrc/conf_mode/protocols_ripng.py29
-rwxr-xr-xsrc/conf_mode/protocols_rpki.py19
-rwxr-xr-xsrc/conf_mode/protocols_static.py28
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py2
-rwxr-xr-xsrc/conf_mode/qos.py87
-rwxr-xr-xsrc/conf_mode/salt-minion.py75
-rwxr-xr-xsrc/conf_mode/service_console-server.py4
-rwxr-xr-xsrc/conf_mode/service_ids_fastnetmon.py4
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py27
-rwxr-xr-xsrc/conf_mode/service_mdns-repeater.py4
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py229
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py19
-rwxr-xr-xsrc/conf_mode/service_router-advert.py2
-rwxr-xr-xsrc/conf_mode/service_upnp.py157
-rwxr-xr-xsrc/conf_mode/service_webproxy.py6
-rwxr-xr-xsrc/conf_mode/snmp.py642
-rwxr-xr-xsrc/conf_mode/ssh.py23
-rwxr-xr-xsrc/conf_mode/system-ip.py43
-rwxr-xr-xsrc/conf_mode/system-ipv6.py99
-rwxr-xr-xsrc/conf_mode/system-login-banner.py35
-rwxr-xr-xsrc/conf_mode/system-login.py25
-rwxr-xr-xsrc/conf_mode/system-logs.py83
-rwxr-xr-xsrc/conf_mode/system-option.py4
-rwxr-xr-xsrc/conf_mode/system-proxy.py90
-rwxr-xr-xsrc/conf_mode/system-syslog.py16
-rwxr-xr-xsrc/conf_mode/system_console.py2
-rwxr-xr-xsrc/conf_mode/system_lcd.py6
-rwxr-xr-xsrc/conf_mode/system_sysctl.py2
-rwxr-xr-xsrc/conf_mode/tftp_server.py16
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py20
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py4
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py100
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py4
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py4
-rwxr-xr-xsrc/conf_mode/vrf.py126
-rwxr-xr-xsrc/conf_mode/vrf_vni.py10
-rwxr-xr-xsrc/conf_mode/zone_policy.py213
-rw-r--r--src/etc/cron.d/check-wwan1
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper2
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf82
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup31
-rw-r--r--src/etc/logrotate.d/conntrackd9
-rw-r--r--src/etc/logrotate.d/vyos-rsyslog12
-rwxr-xr-xsrc/etc/ppp/ip-up.d/99-vyos-pppoe-callback20
-rw-r--r--src/etc/systemd/system/keepalived.service.d/override.conf13
-rwxr-xr-xsrc/etc/telegraf/custom_scripts/show_firewall_input_filter.py73
-rwxr-xr-xsrc/etc/telegraf/custom_scripts/show_interfaces_input_filter.py88
-rwxr-xr-xsrc/etc/telegraf/custom_scripts/vyos_services_input_filter.py61
-rwxr-xr-xsrc/helpers/strip-private.py10
-rwxr-xr-xsrc/helpers/system-versions-foot.py2
-rwxr-xr-xsrc/helpers/vyos-boot-config-loader.py4
-rwxr-xr-xsrc/helpers/vyos-check-wwan.py6
-rwxr-xr-xsrc/helpers/vyos-vrrp-conntracksync.sh8
-rwxr-xr-xsrc/helpers/vyos_net_name31
-rwxr-xr-xsrc/migration-scripts/bgp/0-to-12
-rwxr-xr-xsrc/migration-scripts/bgp/1-to-233
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/1-to-283
-rwxr-xr-xsrc/migration-scripts/firewall/6-to-7226
-rwxr-xr-xsrc/migration-scripts/flow-accounting/0-to-169
-rwxr-xr-xsrc/migration-scripts/interfaces/25-to-2654
-rwxr-xr-xsrc/migration-scripts/ipsec/8-to-948
-rwxr-xr-xsrc/migration-scripts/openconnect/1-to-254
-rwxr-xr-xsrc/migration-scripts/ospf/0-to-181
-rwxr-xr-xsrc/migration-scripts/policy/1-to-286
-rwxr-xr-xsrc/migration-scripts/quagga/9-to-1062
-rwxr-xr-xsrc/migration-scripts/ssh/1-to-250
-rwxr-xr-xsrc/migration-scripts/system/22-to-2350
-rwxr-xr-xsrc/migration-scripts/system/23-to-2485
-rwxr-xr-xsrc/op_mode/conntrack_sync.py47
-rwxr-xr-xsrc/op_mode/containers_op.py78
-rwxr-xr-xsrc/op_mode/cpu_summary.py36
-rwxr-xr-xsrc/op_mode/firewall.py361
-rwxr-xr-xsrc/op_mode/format_disk.py76
-rwxr-xr-xsrc/op_mode/generate_openconnect_otp_key.py65
-rwxr-xr-xsrc/op_mode/generate_ovpn_client_file.py149
-rwxr-xr-xsrc/op_mode/generate_public_key_command.py11
-rwxr-xr-xsrc/op_mode/ikev2_profile_generator.py4
-rwxr-xr-xsrc/op_mode/lldp_op.py5
-rwxr-xr-xsrc/op_mode/monitor_bandwidth_test.sh3
-rwxr-xr-xsrc/op_mode/policy_route.py189
-rwxr-xr-xsrc/op_mode/powerctrl.py25
-rwxr-xr-xsrc/op_mode/ppp-server-ctrl.py2
-rwxr-xr-xsrc/op_mode/restart_frr.py5
-rwxr-xr-xsrc/op_mode/show_configuration_json.py36
-rwxr-xr-xsrc/op_mode/show_cpu.py63
-rwxr-xr-xsrc/op_mode/show_dhcp.py3
-rwxr-xr-xsrc/op_mode/show_dhcpv6.py3
-rwxr-xr-xsrc/op_mode/show_ipsec_sa.py186
-rwxr-xr-xsrc/op_mode/show_nat_rules.py22
-rwxr-xr-xsrc/op_mode/show_openvpn.py23
-rwxr-xr-xsrc/op_mode/show_ram.py19
-rwxr-xr-xsrc/op_mode/show_uptime.py38
-rwxr-xr-xsrc/op_mode/show_version.py22
-rwxr-xr-xsrc/op_mode/show_virtual_server.py33
-rwxr-xr-xsrc/op_mode/traceroute.py207
-rwxr-xr-xsrc/op_mode/vpn_ipsec.py17
-rwxr-xr-xsrc/op_mode/vrrp.py13
-rwxr-xr-xsrc/op_mode/zone_policy.py81
-rw-r--r--src/services/api/graphql/README.graphql84
-rw-r--r--src/services/api/graphql/bindings.py18
-rw-r--r--src/services/api/graphql/graphql/directives.py50
-rw-r--r--src/services/api/graphql/graphql/mutations.py41
-rw-r--r--src/services/api/graphql/graphql/queries.py89
-rw-r--r--src/services/api/graphql/graphql/schema/firewall_group.graphql48
-rw-r--r--src/services/api/graphql/graphql/schema/image.graphql29
-rw-r--r--src/services/api/graphql/graphql/schema/schema.graphql17
-rw-r--r--src/services/api/graphql/graphql/schema/show.graphql14
-rw-r--r--src/services/api/graphql/graphql/schema/show_config.graphql21
-rw-r--r--src/services/api/graphql/recipes/remove_firewall_address_group_members.py14
-rw-r--r--src/services/api/graphql/recipes/session.py75
-rw-r--r--src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl4
-rw-r--r--src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl3
-rw-r--r--src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl3
-rwxr-xr-xsrc/services/vyos-configd3
-rwxr-xr-xsrc/services/vyos-hostsd44
-rwxr-xr-xsrc/services/vyos-http-api-server36
-rwxr-xr-xsrc/system/keepalived-fifo.py39
-rw-r--r--src/systemd/keepalived.service13
-rw-r--r--src/systemd/miniupnpd.service13
-rw-r--r--src/systemd/tftpd@.service2
-rw-r--r--src/systemd/vyos-hostsd.service2
-rw-r--r--src/systemd/vyos-http-api.service23
-rw-r--r--src/tests/test_util.py5
-rw-r--r--src/tests/test_validate.py4
-rwxr-xr-xsrc/utils/vyos-hostsd-client3
-rwxr-xr-xsrc/validators/as-number-list29
-rwxr-xr-xsrc/validators/bgp-rd-rt (renamed from src/validators/bgp-route-target)30
-rwxr-xr-xsrc/validators/ip-address7
-rwxr-xr-xsrc/validators/ip-cidr7
-rwxr-xr-xsrc/validators/ip-host7
-rwxr-xr-xsrc/validators/ip-prefix7
-rwxr-xr-xsrc/validators/ip-protocol3
-rwxr-xr-xsrc/validators/ipv47
-rwxr-xr-xsrc/validators/ipv4-address7
-rwxr-xr-xsrc/validators/ipv4-host7
-rwxr-xr-xsrc/validators/ipv4-multicast9
-rwxr-xr-xsrc/validators/ipv4-prefix7
-rwxr-xr-xsrc/validators/ipv4-range13
-rwxr-xr-xsrc/validators/ipv67
-rwxr-xr-xsrc/validators/ipv6-address7
-rwxr-xr-xsrc/validators/ipv6-host7
-rwxr-xr-xsrc/validators/ipv6-link-local12
-rwxr-xr-xsrc/validators/ipv6-multicast9
-rwxr-xr-xsrc/validators/ipv6-prefix7
-rwxr-xr-xsrc/validators/ipv6-range30
-rwxr-xr-xsrc/validators/mac-address-firewall27
-rwxr-xr-xsrc/validators/port-multi52
-rwxr-xr-xsrc/validators/port-range35
-rwxr-xr-xsrc/validators/range56
-rwxr-xr-xsrc/validators/script4
-rwxr-xr-xsrc/validators/tcp-flag17
206 files changed, 7908 insertions, 2612 deletions
diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py
index aac07bd80..1cd8f5451 100755
--- a/src/conf_mode/arp.py
+++ b/src/conf_mode/arp.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
@@ -13,92 +13,62 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#
-import sys
-import os
-import re
-import syslog as sl
+from sys import exit
from vyos.config import Config
+from vyos.configdict import node_changed
from vyos.util import call
from vyos import ConfigError
-
from vyos import airbag
airbag.enable()
-arp_cmd = '/usr/sbin/arp'
-
-def get_config():
- c = Config()
- if not c.exists('protocols static arp'):
- return None
-
- c.set_level('protocols static')
- config_data = {}
-
- for ip_addr in c.list_nodes('arp'):
- config_data.update(
- {
- ip_addr : c.return_value('arp ' + ip_addr + ' hwaddr')
- }
- )
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
- return config_data
+ base = ['protocols', 'static', 'arp']
+ arp = conf.get_config_dict(base, get_first_key=True)
-def generate(c):
- c_eff = Config()
- c_eff.set_level('protocols static')
- c_eff_cnf = {}
- for ip_addr in c_eff.list_effective_nodes('arp'):
- c_eff_cnf.update(
- {
- ip_addr : c_eff.return_effective_value('arp ' + ip_addr + ' hwaddr')
- }
- )
+ if 'interface' in arp:
+ for interface in arp['interface']:
+ tmp = node_changed(conf, base + ['interface', interface, 'address'], recursive=True)
+ if tmp: arp['interface'][interface].update({'address_old' : tmp})
- config_data = {
- 'remove' : [],
- 'update' : {}
- }
- ### removal
- if c == None:
- for ip_addr in c_eff_cnf:
- config_data['remove'].append(ip_addr)
- else:
- for ip_addr in c_eff_cnf:
- if not ip_addr in c or c[ip_addr] == None:
- config_data['remove'].append(ip_addr)
+ return arp
- ### add/update
- if c != None:
- for ip_addr in c:
- if not ip_addr in c_eff_cnf:
- config_data['update'][ip_addr] = c[ip_addr]
- if ip_addr in c_eff_cnf:
- if c[ip_addr] != c_eff_cnf[ip_addr] and c[ip_addr] != None:
- config_data['update'][ip_addr] = c[ip_addr]
+def verify(arp):
+ pass
- return config_data
+def generate(arp):
+ pass
-def apply(c):
- for ip_addr in c['remove']:
- sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr)
- call(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1')
+def apply(arp):
+ if not arp:
+ return None
- for ip_addr in c['update']:
- sl.syslog(sl.LOG_NOTICE, "arp -s " + ip_addr + " " + c['update'][ip_addr])
- updated = c['update'][ip_addr]
- call(f'{arp_cmd} -s {ip_addr} {updated}')
+ if 'interface' in arp:
+ for interface, interface_config in arp['interface'].items():
+ # Delete old static ARP assignments first
+ if 'address_old' in interface_config:
+ for address in interface_config['address_old']:
+ call(f'ip neigh del {address} dev {interface}')
+ # Add new static ARP entries to interface
+ if 'address' not in interface_config:
+ continue
+ for address, address_config in interface_config['address'].items():
+ mac = address_config['mac']
+ call(f'ip neigh add {address} lladdr {mac} dev {interface}')
if __name__ == '__main__':
- try:
- c = get_config()
- ## syntax verification is done via cli
- config = generate(c)
- apply(config)
- except ConfigError as e:
- print(e)
- sys.exit(1)
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
index d93a2a8f4..39a2971ce 100755
--- a/src/conf_mode/bcast_relay.py
+++ b/src/conf_mode/bcast_relay.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2017-2020 VyOS maintainers and contributors
+# Copyright (C) 2017-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
@@ -78,7 +78,7 @@ def generate(relay):
continue
config['instance'] = instance
- render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.tmpl',
+ render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.j2',
config)
return None
diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py
index 68877f794..82289526f 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(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack)
+ render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack)
+ render(nftables_ct_file, 'conntrack/nftables-ct.j2', conntrack)
+
+ # dry-run newly generated configuration
+ tmp = run(f'nft -c -f {nftables_ct_file}')
+ if tmp > 0:
+ if os.path.exists(nftables_ct_file):
+ os.unlink(nftables_ct_file)
+ raise ConfigError('Configuration file errors encountered!')
return None
+def find_nftables_ct_rule(rule):
+ 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..c4b2bb488 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!')
@@ -93,9 +93,9 @@ def verify(conntrack):
raise ConfigError('Can not configure expect-sync "all" with other protocols!')
if 'listen_address' in conntrack:
- address = conntrack['listen_address']
- if not is_addr_assigned(address):
- raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')
+ for address in conntrack['listen_address']:
+ if not is_addr_assigned(address):
+ raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')
vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack)
if vrrp_group == None:
@@ -111,11 +111,12 @@ def generate(conntrack):
os.unlink(config_file)
return None
- render(config_file, 'conntrackd/conntrackd.conf.tmpl', conntrack)
+ render(config_file, 'conntrackd/conntrackd.conf.j2', conntrack)
return None
def apply(conntrack):
+ systemd_service = 'conntrackd.service'
if not conntrack:
# Failover mechanism daemon should be indicated that it no longer needs
# to execute conntrackd actions on transition. This is only required
@@ -123,7 +124,7 @@ def apply(conntrack):
if process_named_running('conntrackd'):
resync_vrrp()
- call('systemctl stop conntrackd.service')
+ call(f'systemctl stop {systemd_service}')
return None
# Failover mechanism daemon should be indicated that it needs to execute
@@ -132,7 +133,7 @@ def apply(conntrack):
if not process_named_running('conntrackd'):
resync_vrrp()
- call('systemctl restart conntrackd.service')
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/containers.py b/src/conf_mode/container.py
index 2e14e0b25..2110fd9e0 100755
--- a/src/conf_mode/containers.py
+++ b/src/conf_mode/container.py
@@ -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
@@ -15,20 +15,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import json
from ipaddress import ip_address
from ipaddress import ip_network
from time import sleep
from json import dumps as json_write
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.util import call
from vyos.util import cmd
from vyos.util import run
-from vyos.util import read_file
from vyos.util import write_file
from vyos.template import inc_ip
from vyos.template import is_ipv4
@@ -42,6 +41,20 @@ airbag.enable()
config_containers_registry = '/etc/containers/registries.conf'
config_containers_storage = '/etc/containers/storage.conf'
+def _run_rerun(container_cmd):
+ counter = 0
+ while True:
+ if counter >= 10:
+ break
+ try:
+ _cmd(container_cmd)
+ break
+ except:
+ counter = counter +1
+ sleep(0.5)
+
+ return None
+
def _cmd(command):
if os.path.exists('/tmp/vyos.container.debug'):
print(command)
@@ -77,10 +90,10 @@ def get_config(config=None):
container['name'][name] = dict_merge(default_values, container['name'][name])
# Delete container network, delete containers
- tmp = node_changed(conf, ['container', 'network'])
+ tmp = node_changed(conf, base + ['container', 'network'])
if tmp: container.update({'network_remove' : tmp})
- tmp = node_changed(conf, ['container', 'name'])
+ tmp = node_changed(conf, base + ['container', 'name'])
if tmp: container.update({'container_remove' : tmp})
return container
@@ -93,6 +106,26 @@ def verify(container):
# Add new container
if 'name' in container:
for name, container_config in container['name'].items():
+ # Container image is a mandatory option
+ if 'image' not in container_config:
+ raise ConfigError(f'Container image for "{name}" is mandatory!')
+
+ # Check if requested container image exists locally. If it does not
+ # exist locally - inform the user. This is required as there is a
+ # shared container image storage accross all VyOS images. A user can
+ # delete a container image from the system, boot into another version
+ # of VyOS and then it would fail to boot. This is to prevent any
+ # configuration error when container images are deleted from the
+ # global storage. A per image local storage would be a super waste
+ # of diskspace as there will be a full copy (up tu several GB/image)
+ # on upgrade. This is the "cheapest" and fastest solution in terms
+ # of image upgrade and deletion.
+ image = container_config['image']
+ if run(f'podman image exists {image}') != 0:
+ Warning(f'Image "{image}" used in contianer "{name}" does not exist '\
+ f'locally. Please use "add container image {image}" to add it '\
+ f'to the system! Container "{name}" will not be started!')
+
if 'network' in container_config:
if len(container_config['network']) > 1:
raise ConfigError(f'Only one network can be specified for container "{name}"!')
@@ -122,6 +155,18 @@ def verify(container):
raise ConfigError(f'IP address "{address}" can not be used for a container, '\
'reserved for the container engine!')
+ if 'device' in container_config:
+ for dev, dev_config in container_config['device'].items():
+ if 'source' not in dev_config:
+ raise ConfigError(f'Device "{dev}" has no source path configured!')
+
+ if 'destination' not in dev_config:
+ raise ConfigError(f'Device "{dev}" has no destination path configured!')
+
+ source = dev_config['source']
+ if not os.path.exists(source):
+ raise ConfigError(f'Device "{dev}" source path "{source}" does not exist!')
+
if 'environment' in container_config:
for var, cfg in container_config['environment'].items():
if 'value' not in cfg:
@@ -139,10 +184,6 @@ def verify(container):
if not os.path.exists(source):
raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!')
- # Container image is a mandatory option
- if 'image' not in container_config:
- raise ConfigError(f'Container image for "{name}" is mandatory!')
-
# If 'allow-host-networks' or 'network' not set.
if 'allow_host_networks' not in container_config and 'network' not in container_config:
raise ConfigError(f'Must either set "network" or "allow-host-networks" for container "{name}"!')
@@ -182,6 +223,10 @@ def verify(container):
def generate(container):
# bail out early - looks like removal from running config
if not container:
+ if os.path.exists(config_containers_registry):
+ os.unlink(config_containers_registry)
+ if os.path.exists(config_containers_storage):
+ os.unlink(config_containers_storage)
return None
if 'network' in container:
@@ -215,8 +260,8 @@ def generate(container):
write_file(f'/etc/cni/net.d/{network}.conflist', json_write(tmp, indent=2))
- render(config_containers_registry, 'containers/registry.tmpl', container)
- render(config_containers_storage, 'containers/storage.tmpl', container)
+ render(config_containers_registry, 'container/registries.conf.j2', container)
+ render(config_containers_storage, 'container/storage.conf.j2', container)
return None
@@ -240,6 +285,11 @@ def apply(container):
for name, container_config in container['name'].items():
image = container_config['image']
+ if run(f'podman image exists {image}') != 0:
+ # container image does not exist locally - user already got
+ # informed by a WARNING in verfiy() - bail out early
+ continue
+
if 'disable' in container_config:
# check if there is a container by that name running
tmp = _cmd('podman ps -a --format "{{.Names}}"')
@@ -251,13 +301,6 @@ def apply(container):
memory = container_config['memory']
restart = container_config['restart']
- # Check if requested container image exists locally. If it does not, we
- # pull it. print() is the best way to have a good response from the
- # polling process to the user to display progress. If the image exists
- # locally, a user can update it running `update container image <name>`
- tmp = run(f'podman image exists {image}')
- if tmp != 0: print(os.system(f'podman pull {image}'))
-
# Add capability options. Should be in uppercase
cap_add = ''
if 'cap_add' in container_config:
@@ -266,6 +309,14 @@ def apply(container):
c = c.replace('-', '_')
cap_add += f' --cap-add={c}'
+ # Add a host device to the container /dev/x:/dev/x
+ device = ''
+ if 'device' in container_config:
+ for dev, dev_config in container_config['device'].items():
+ source_dev = dev_config['source']
+ dest_dev = dev_config['destination']
+ device += f' --device={source_dev}:{dest_dev}'
+
# Check/set environment options "-e foo=bar"
env_opt = ''
if 'environment' in container_config:
@@ -296,9 +347,9 @@ def apply(container):
container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \
f'--memory {memory}m --memory-swap 0 --restart {restart} ' \
- f'--name {name} {port} {volume} {env_opt}'
+ f'--name {name} {device} {port} {volume} {env_opt}'
if 'allow_host_networks' in container_config:
- _cmd(f'{container_base_cmd} --net host {image}')
+ _run_rerun(f'{container_base_cmd} --net host {image}')
else:
for network in container_config['network']:
ipparam = ''
@@ -306,16 +357,7 @@ 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_rerun(f'{container_base_cmd} --net {network} {ipparam} {image}')
return None
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
index 6352e0b4a..4de2ca2f3 100755
--- a/src/conf_mode/dhcp_relay.py
+++ b/src/conf_mode/dhcp_relay.py
@@ -66,18 +66,19 @@ def generate(relay):
if not relay:
return None
- render(config_file, 'dhcp-relay/dhcrelay.conf.tmpl', relay)
+ render(config_file, 'dhcp-relay/dhcrelay.conf.j2', relay)
return None
def apply(relay):
# bail out early - looks like removal from running config
+ service_name = 'isc-dhcp-relay.service'
if not relay:
- call('systemctl stop isc-dhcp-relay.service')
+ call(f'systemctl stop {service_name}')
if os.path.exists(config_file):
os.unlink(config_file)
return None
- call('systemctl restart isc-dhcp-relay.service')
+ call(f'systemctl restart {service_name}')
return None
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index a8cef5ebf..52b682d6d 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-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
@@ -109,7 +109,7 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
# T2665: defaults include lease time per TAG node which need to be added to
# individual subnet definitions
default_values = defaults(base + ['shared-network-name', 'subnet'])
@@ -286,7 +286,7 @@ def generate(dhcp):
# Please see: https://phabricator.vyos.net/T1129 for quoting of the raw
# parameters we can pass to ISC DHCPd
tmp_file = '/tmp/dhcpd.conf'
- render(tmp_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp,
+ render(tmp_file, 'dhcp-server/dhcpd.conf.j2', dhcp,
formater=lambda _: _.replace("&quot;", '"'))
# XXX: as we have the ability for a user to pass in "raw" options via VyOS
# CLI (see T3544) we now ask ISC dhcpd to test the newly rendered
@@ -299,7 +299,7 @@ def generate(dhcp):
# Now that we know that the newly rendered configuration is "good" we can
# render the "real" configuration
- render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp,
+ render(config_file, 'dhcp-server/dhcpd.conf.j2', dhcp,
formater=lambda _: _.replace("&quot;", '"'))
return None
diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
index aea2c3b73..c1bd51f62 100755
--- a/src/conf_mode/dhcpv6_relay.py
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -82,19 +82,20 @@ def generate(relay):
if not relay:
return None
- render(config_file, 'dhcp-relay/dhcrelay6.conf.tmpl', relay)
+ render(config_file, 'dhcp-relay/dhcrelay6.conf.j2', relay)
return None
def apply(relay):
# bail out early - looks like removal from running config
+ service_name = 'isc-dhcp-relay6.service'
if not relay:
# DHCPv6 relay support is removed in the commit
- call('systemctl stop isc-dhcp-relay6.service')
+ call(f'systemctl stop {service_name}')
if os.path.exists(config_file):
os.unlink(config_file)
return None
- call('systemctl restart isc-dhcp-relay6.service')
+ call(f'systemctl restart {service_name}')
return None
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index e6a2e4486..078ff327c 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 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
@@ -41,7 +41,9 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
return dhcpv6
def verify(dhcpv6):
@@ -51,7 +53,7 @@ def verify(dhcpv6):
# If DHCP is enabled we need one share-network
if 'shared_network_name' not in dhcpv6:
- raise ConfigError('No DHCPv6 shared networks configured. At least\n' \
+ raise ConfigError('No DHCPv6 shared networks configured. At least '\
'one DHCPv6 shared network must be configured.')
# Inspect shared-network/subnet
@@ -60,8 +62,9 @@ def verify(dhcpv6):
for network, network_config in dhcpv6['shared_network_name'].items():
# A shared-network requires a subnet definition
if 'subnet' not in network_config:
- raise ConfigError(f'No DHCPv6 lease subnets configured for "{network}". At least one\n' \
- 'lease subnet must be configured for each shared network!')
+ raise ConfigError(f'No DHCPv6 lease subnets configured for "{network}". '\
+ 'At least one lease subnet must be configured for '\
+ 'each shared network!')
for subnet, subnet_config in network_config['subnet'].items():
if 'address_range' in subnet_config:
@@ -83,20 +86,20 @@ def verify(dhcpv6):
# Stop address must be greater or equal to start address
if not ip_address(stop) >= ip_address(start):
- raise ConfigError(f'address-range stop address "{stop}" must be greater or equal\n' \
+ raise ConfigError(f'address-range stop address "{stop}" must be greater then or equal ' \
f'to the range start address "{start}"!')
# DHCPv6 range start address must be unique - two ranges can't
# start with the same address - makes no sense
if start in range6_start:
- raise ConfigError(f'Conflicting DHCPv6 lease range:\n' \
+ raise ConfigError(f'Conflicting DHCPv6 lease range: '\
f'Pool start address "{start}" defined multipe times!')
range6_start.append(start)
# DHCPv6 range stop address must be unique - two ranges can't
# end with the same address - makes no sense
if stop in range6_stop:
- raise ConfigError(f'Conflicting DHCPv6 lease range:\n' \
+ raise ConfigError(f'Conflicting DHCPv6 lease range: '\
f'Pool stop address "{stop}" defined multipe times!')
range6_stop.append(stop)
@@ -112,7 +115,7 @@ def verify(dhcpv6):
for prefix, prefix_config in subnet_config['prefix_delegation']['start'].items():
if 'stop' not in prefix_config:
- raise ConfigError(f'Stop address of delegated IPv6 prefix range "{prefix}"\n'
+ raise ConfigError(f'Stop address of delegated IPv6 prefix range "{prefix}" '\
f'must be configured')
if 'prefix_length' not in prefix_config:
@@ -126,6 +129,10 @@ def verify(dhcpv6):
if ip_address(mapping_config['ipv6_address']) not in ip_network(subnet):
raise ConfigError(f'static-mapping address for mapping "{mapping}" is not in subnet "{subnet}"!')
+ if 'vendor_option' in subnet_config:
+ if len(dict_search('vendor_option.cisco.tftp_server', subnet_config)) > 2:
+ raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!')
+
# Subnets must be unique
if subnet in subnets:
raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!')
@@ -149,8 +156,8 @@ def verify(dhcpv6):
raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
if not listen_ok:
- raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \
- 'this machine. At least one subnet6 must be connected such that\n' \
+ raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on '\
+ 'this machine. At least one subnet6 must be connected such that '\
'DHCPv6 listens on an interface!')
@@ -161,20 +168,20 @@ def generate(dhcpv6):
if not dhcpv6 or 'disable' in dhcpv6:
return None
- render(config_file, 'dhcp-server/dhcpdv6.conf.tmpl', dhcpv6)
+ render(config_file, 'dhcp-server/dhcpdv6.conf.j2', dhcpv6)
return None
def apply(dhcpv6):
# bail out early - looks like removal from running config
+ service_name = 'isc-dhcp-server6.service'
if not dhcpv6 or 'disable' in dhcpv6:
# DHCP server is removed in the commit
- call('systemctl stop isc-dhcp-server6.service')
+ call(f'systemctl stop {service_name}')
if os.path.exists(config_file):
os.unlink(config_file)
-
return None
- call('systemctl restart isc-dhcp-server6.service')
+ call(f'systemctl restart {service_name}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 06366362a..f1c2d1f43 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 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
@@ -16,7 +16,9 @@
import os
+from netifaces import interfaces
from sys import exit
+from glob import glob
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -50,10 +52,12 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ dns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
# We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
+ # options which we need to update into the dictionary retrieved.
default_values = defaults(base)
+ # T2665 due to how defaults under tag nodes work, we must clear these out before we merge
+ del default_values['authoritative_domain']
dns = dict_merge(default_values, dns)
# some additions to the default dictionary
@@ -62,9 +66,182 @@ def get_config(config=None):
if conf.exists(base_nameservers):
dns.update({'system_name_server': conf.return_values(base_nameservers)})
- base_nameservers_dhcp = ['system', 'name-servers-dhcp']
- if conf.exists(base_nameservers_dhcp):
- dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)})
+ if 'authoritative_domain' in dns:
+ dns['authoritative_zones'] = []
+ dns['authoritative_zone_errors'] = []
+ for node in dns['authoritative_domain']:
+ zonedata = dns['authoritative_domain'][node]
+ if ('disable' in zonedata) or (not 'records' in zonedata):
+ continue
+ zone = {
+ 'name': node,
+ 'file': "{}/zone.{}.conf".format(pdns_rec_run_dir, node),
+ 'records': [],
+ }
+
+ recorddata = zonedata['records']
+
+ for rtype in [ 'a', 'aaaa', 'cname', 'mx', 'ptr', 'txt', 'spf', 'srv', 'naptr' ]:
+ if rtype not in recorddata:
+ continue
+ for subnode in recorddata[rtype]:
+ if 'disable' in recorddata[rtype][subnode]:
+ continue
+
+ rdata = recorddata[rtype][subnode]
+
+ if rtype in [ 'a', 'aaaa' ]:
+ rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
+ rdata = dict_merge(rdefaults, rdata)
+
+ if not 'address' in rdata:
+ dns['authoritative_zone_errors'].append('{}.{}: at least one address is required'.format(subnode, node))
+ continue
+
+ for address in rdata['address']:
+ zone['records'].append({
+ 'name': subnode,
+ 'type': rtype.upper(),
+ 'ttl': rdata['ttl'],
+ 'value': address
+ })
+ elif rtype in ['cname', 'ptr']:
+ rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
+ rdata = dict_merge(rdefaults, rdata)
+
+ if not 'target' in rdata:
+ dns['authoritative_zone_errors'].append('{}.{}: target is required'.format(subnode, node))
+ continue
+
+ zone['records'].append({
+ 'name': subnode,
+ 'type': rtype.upper(),
+ 'ttl': rdata['ttl'],
+ 'value': '{}.'.format(rdata['target'])
+ })
+ elif rtype == 'mx':
+ rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
+ del rdefaults['server']
+ rdata = dict_merge(rdefaults, rdata)
+
+ if not 'server' in rdata:
+ dns['authoritative_zone_errors'].append('{}.{}: at least one server is required'.format(subnode, node))
+ continue
+
+ for servername in rdata['server']:
+ serverdata = rdata['server'][servername]
+ serverdefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'server']) # T2665
+ serverdata = dict_merge(serverdefaults, serverdata)
+ zone['records'].append({
+ 'name': subnode,
+ 'type': rtype.upper(),
+ 'ttl': rdata['ttl'],
+ 'value': '{} {}.'.format(serverdata['priority'], servername)
+ })
+ elif rtype == 'txt':
+ rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
+ rdata = dict_merge(rdefaults, rdata)
+
+ if not 'value' in rdata:
+ dns['authoritative_zone_errors'].append('{}.{}: at least one value is required'.format(subnode, node))
+ continue
+
+ for value in rdata['value']:
+ zone['records'].append({
+ 'name': subnode,
+ 'type': rtype.upper(),
+ 'ttl': rdata['ttl'],
+ 'value': "\"{}\"".format(value.replace("\"", "\\\""))
+ })
+ elif rtype == 'spf':
+ rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
+ rdata = dict_merge(rdefaults, rdata)
+
+ if not 'value' in rdata:
+ dns['authoritative_zone_errors'].append('{}.{}: value is required'.format(subnode, node))
+ continue
+
+ zone['records'].append({
+ 'name': subnode,
+ 'type': rtype.upper(),
+ 'ttl': rdata['ttl'],
+ 'value': '"{}"'.format(rdata['value'].replace("\"", "\\\""))
+ })
+ elif rtype == 'srv':
+ rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
+ del rdefaults['entry']
+ rdata = dict_merge(rdefaults, rdata)
+
+ if not 'entry' in rdata:
+ dns['authoritative_zone_errors'].append('{}.{}: at least one entry is required'.format(subnode, node))
+ continue
+
+ for entryno in rdata['entry']:
+ entrydata = rdata['entry'][entryno]
+ entrydefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'entry']) # T2665
+ entrydata = dict_merge(entrydefaults, entrydata)
+
+ if not 'hostname' in entrydata:
+ dns['authoritative_zone_errors'].append('{}.{}: hostname is required for entry {}'.format(subnode, node, entryno))
+ continue
+
+ if not 'port' in entrydata:
+ dns['authoritative_zone_errors'].append('{}.{}: port is required for entry {}'.format(subnode, node, entryno))
+ continue
+
+ zone['records'].append({
+ 'name': subnode,
+ 'type': rtype.upper(),
+ 'ttl': rdata['ttl'],
+ 'value': '{} {} {} {}.'.format(entrydata['priority'], entrydata['weight'], entrydata['port'], entrydata['hostname'])
+ })
+ elif rtype == 'naptr':
+ rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
+ del rdefaults['rule']
+ rdata = dict_merge(rdefaults, rdata)
+
+
+ if not 'rule' in rdata:
+ dns['authoritative_zone_errors'].append('{}.{}: at least one rule is required'.format(subnode, node))
+ continue
+
+ for ruleno in rdata['rule']:
+ ruledata = rdata['rule'][ruleno]
+ ruledefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'rule']) # T2665
+ ruledata = dict_merge(ruledefaults, ruledata)
+ flags = ""
+ if 'lookup-srv' in ruledata:
+ flags += "S"
+ if 'lookup-a' in ruledata:
+ flags += "A"
+ if 'resolve-uri' in ruledata:
+ flags += "U"
+ if 'protocol-specific' in ruledata:
+ flags += "P"
+
+ if 'order' in ruledata:
+ order = ruledata['order']
+ else:
+ order = ruleno
+
+ if 'regexp' in ruledata:
+ regexp= ruledata['regexp'].replace("\"", "\\\"")
+ else:
+ regexp = ''
+
+ if ruledata['replacement']:
+ replacement = '{}.'.format(ruledata['replacement'])
+ else:
+ replacement = ''
+
+ zone['records'].append({
+ 'name': subnode,
+ 'type': rtype.upper(),
+ 'ttl': rdata['ttl'],
+ 'value': '{} {} "{}" "{}" "{}" {}'.format(order, ruledata['preference'], flags, ruledata['service'], regexp, replacement)
+ })
+
+ dns['authoritative_zones'].append(zone)
return dns
@@ -86,10 +263,14 @@ def verify(dns):
if 'server' not in dns['domain'][domain]:
raise ConfigError(f'No server configured for domain {domain}!')
+ if ('authoritative_zone_errors' in dns) and dns['authoritative_zone_errors']:
+ for error in dns['authoritative_zone_errors']:
+ print(error)
+ raise ConfigError('Invalid authoritative records have been defined')
+
if 'system' in dns:
- if not ('system_name_server' in dns or 'system_name_server_dhcp' in dns):
- print("Warning: No 'system name-server' or 'system " \
- "name-servers-dhcp' configured")
+ if not 'system_name_server' in dns:
+ print('Warning: No "system name-server" configured')
return None
@@ -98,12 +279,21 @@ def generate(dns):
if not dns:
return None
- render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.tmpl',
+ render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.j2',
dns, user=pdns_rec_user, group=pdns_rec_group)
- render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.tmpl',
+ render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.j2',
dns, user=pdns_rec_user, group=pdns_rec_group)
+ for zone_filename in glob(f'{pdns_rec_run_dir}/zone.*.conf'):
+ os.unlink(zone_filename)
+
+ if 'authoritative_zones' in dns:
+ for zone in dns['authoritative_zones']:
+ render(zone['file'], 'dns-forwarding/recursor.zone.conf.j2',
+ zone, user=pdns_rec_user, group=pdns_rec_group)
+
+
# if vyos-hostsd didn't create its files yet, create them (empty)
for file in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]:
with open(file, 'a'):
@@ -119,6 +309,9 @@ def apply(dns):
if os.path.isfile(pdns_rec_config_file):
os.unlink(pdns_rec_config_file)
+
+ for zone_filename in glob(f'{pdns_rec_run_dir}/zone.*.conf'):
+ os.unlink(zone_filename)
else:
### first apply vyos-hostsd config
hc = hostsd_client()
@@ -142,10 +335,15 @@ def apply(dns):
hc.delete_name_server_tags_recursor(['system'])
# add dhcp nameserver tags for configured interfaces
- if 'system_name_server_dhcp' in dns:
- for interface in dns['system_name_server_dhcp']:
- hc.add_name_server_tags_recursor(['dhcp-' + interface,
- 'dhcpv6-' + interface ])
+ if 'system_name_server' in dns:
+ for interface in dns['system_name_server']:
+ # system_name_server key contains both IP addresses and interface
+ # names (DHCP) to use DNS servers. We need to check if the
+ # value is an interface name - only if this is the case, add the
+ # interface based DNS forwarder.
+ if interface in interfaces():
+ hc.add_name_server_tags_recursor(['dhcp-' + interface,
+ 'dhcpv6-' + interface ])
# hostsd will generate the forward-zones file
# the list and keys() are required as get returns a dict, not list
@@ -153,6 +351,12 @@ def apply(dns):
if 'domain' in dns:
hc.add_forward_zones(dns['domain'])
+ # hostsd generates NTAs for the authoritative zones
+ # the list and keys() are required as get returns a dict, not list
+ hc.delete_authoritative_zones(list(hc.get_authoritative_zones()))
+ if 'authoritative_zones' in dns:
+ hc.add_authoritative_zones(list(map(lambda zone: zone['name'], dns['authoritative_zones'])))
+
# call hostsd to generate forward-zones and its lua-config-file
hc.apply()
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index a31e5ed75..06a2f7e15 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -131,7 +131,7 @@ def generate(dyndns):
if not dyndns:
return None
- render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns)
+ render(config_file, 'dynamic-dns/ddclient.conf.j2', dyndns)
return None
def apply(dyndns):
diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py
new file mode 100755
index 000000000..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..6924bf555 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -15,51 +15,413 @@
# 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.base import Warning
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 'limit' in rule_conf:
+ if 'rate' in rule_conf['limit']:
+ rate_int = re.sub(r'\D', '', rule_conf['limit']['rate'])
+ if int(rate_int) < 1:
+ raise ConfigError('Limit rate integer cannot be less than 1')
+
+ 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:
+ Warning(f'{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.j2', firewall)
+ render(nftables_defines_conf, 'firewall/nftables-defines.j2', 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:
+ 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 0a4559ade..7750c1247 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -16,121 +16,87 @@
import os
import re
+
from sys import exit
import ipaddress
from ipaddress import ip_address
-from jinja2 import FileSystemLoader, Environment
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.ifconfig import Section
from vyos.ifconfig import Interface
-from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import cmd
from vyos.template import render
-
+from vyos.util import call
+from vyos.util import cmd
+from vyos.validate import is_addr_assigned
+from vyos.xml import defaults
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
-# default values
-default_sflow_server_port = 6343
-default_netflow_server_port = 2055
-default_plugin_pipe_size = 10
-default_captured_packet_size = 128
-default_netflow_version = '9'
-default_sflow_agentip = 'auto'
-uacctd_conf_path = '/etc/pmacct/uacctd.conf'
-iptables_nflog_table = 'raw'
-iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK'
-egress_iptables_nflog_table = 'mangle'
-egress_iptables_nflog_chain = 'FORWARD'
-
-# helper functions
-# check if node exists and return True if this is true
-def _node_exists(path):
- vyos_config = Config()
- if vyos_config.exists(path):
- return True
-
-# get sFlow agent-ip if agent-address is "auto" (default behaviour)
-def _sflow_default_agentip(config):
- # check if any of BGP, OSPF, OSPFv3 protocols are configured and use router-id from there
- if config.exists('protocols bgp'):
- bgp_router_id = config.return_value("protocols bgp {} parameters router-id".format(config.list_nodes('protocols bgp')[0]))
- if bgp_router_id:
- return bgp_router_id
- if config.return_value('protocols ospf parameters router-id'):
- return config.return_value('protocols ospf parameters router-id')
- if config.return_value('protocols ospfv3 parameters router-id'):
- return config.return_value('protocols ospfv3 parameters router-id')
-
- # if router-id was not found, use first available ip of any interface
- for iface in Section.interfaces():
- for address in Interface(iface).get_addr():
- # return an IP, if this is not loopback
- regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
- if regex_filter.search(address):
- return regex_filter.search(address).group('ipaddr')
-
- # return nothing by default
- return None
-
-# get iptables rule dict for chain in table
-def _iptables_get_nflog(chain, table):
+uacctd_conf_path = '/run/pmacct/uacctd.conf'
+systemd_service = 'uacctd.service'
+systemd_override = f'/etc/systemd/system/{systemd_service}.d/override.conf'
+nftables_nflog_table = 'raw'
+nftables_nflog_chain = 'VYOS_CT_PREROUTING_HOOK'
+egress_nftables_nflog_table = 'inet mangle'
+egress_nftables_nflog_chain = 'FORWARD'
+
+# get nftables rule dict for chain in table
+def _nftables_get_nflog(chain, table):
# define list with rules
rules = []
# prepare regex for parsing rules
- rule_pattern = "^-A (?P<rule_definition>{0} (\-i|\-o) (?P<interface>[\w\.\*\-]+).*--comment FLOW_ACCOUNTING_RULE.* -j NFLOG.*$)".format(chain)
+ rule_pattern = '[io]ifname "(?P<interface>[\w\.\*\-]+)".*handle (?P<handle>[\d]+)'
rule_re = re.compile(rule_pattern)
- for iptables_variant in ['iptables', 'ip6tables']:
- # run iptables, save output and split it by lines
- iptables_command = f'{iptables_variant} -t {table} -S {chain}'
- tmp = cmd(iptables_command, message='Failed to get flows list')
-
- # parse each line and add information to list
- for current_rule in tmp.splitlines():
- current_rule_parsed = rule_re.search(current_rule)
- if current_rule_parsed:
- rules.append({ 'interface': current_rule_parsed.groupdict()["interface"], 'iptables_variant': iptables_variant, 'table': table, 'rule_definition': current_rule_parsed.groupdict()["rule_definition"] })
+ # run nftables, save output and split it by lines
+ nftables_command = f'nft -a list chain {table} {chain}'
+ tmp = cmd(nftables_command, message='Failed to get flows list')
+ # parse each line and add information to list
+ for current_rule in tmp.splitlines():
+ if 'FLOW_ACCOUNTING_RULE' not in current_rule:
+ continue
+ current_rule_parsed = rule_re.search(current_rule)
+ if current_rule_parsed:
+ groups = current_rule_parsed.groupdict()
+ rules.append({ 'interface': groups["interface"], 'table': table, 'handle': groups["handle"] })
# return list with rules
return rules
-# modify iptables rules
-def _iptables_config(configured_ifaces, direction):
- # define list of iptables commands to modify settings
- iptable_commands = []
- iptables_chain = iptables_nflog_chain
- iptables_table = iptables_nflog_table
+def _nftables_config(configured_ifaces, direction, length=None):
+ # define list of nftables commands to modify settings
+ nftable_commands = []
+ nftables_chain = nftables_nflog_chain
+ nftables_table = nftables_nflog_table
if direction == "egress":
- iptables_chain = egress_iptables_nflog_chain
- iptables_table = egress_iptables_nflog_table
+ nftables_chain = egress_nftables_nflog_chain
+ nftables_table = egress_nftables_nflog_table
# prepare extended list with configured interfaces
configured_ifaces_extended = []
for iface in configured_ifaces:
- configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'iptables' })
- configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'ip6tables' })
+ configured_ifaces_extended.append({ 'iface': iface })
- # get currently configured interfaces with iptables rules
- active_nflog_rules = _iptables_get_nflog(iptables_chain, iptables_table)
+ # get currently configured interfaces with nftables rules
+ active_nflog_rules = _nftables_get_nflog(nftables_chain, nftables_table)
# compare current active list with configured one and delete excessive interfaces, add missed
active_nflog_ifaces = []
for rule in active_nflog_rules:
- iptables = rule['iptables_variant']
interface = rule['interface']
if interface not in configured_ifaces:
table = rule['table']
- rule = rule['rule_definition']
- iptable_commands.append(f'{iptables} -t {table} -D {rule}')
+ handle = rule['handle']
+ nftable_commands.append(f'nft delete rule {table} {nftables_chain} handle {handle}')
else:
active_nflog_ifaces.append({
'iface': interface,
- 'iptables_variant': iptables,
})
# do not create new rules for already configured interfaces
@@ -141,244 +107,178 @@ def _iptables_config(configured_ifaces, direction):
# create missed rules
for iface_extended in configured_ifaces_extended:
iface = iface_extended['iface']
- iptables = iface_extended['iptables_variant']
- iptables_op = "-i"
- if direction == "egress":
- iptables_op = "-o"
-
- rule_definition = f'{iptables_chain} {iptables_op} {iface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {default_captured_packet_size} --nflog-threshold 100'
- iptable_commands.append(f'{iptables} -t {iptables_table} -I {rule_definition}')
-
- # change iptables
- for command in iptable_commands:
+ 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}')
+ # Also add IPv6 ingres logging
+ if nftables_table == nftables_nflog_table:
+ nftable_commands.append(f'nft insert rule ip6 {nftables_table} {nftables_chain} {rule_definition}')
+
+ # change nftables
+ for command in nftable_commands:
cmd(command, raising=ConfigError)
-def get_config():
- vc = Config()
- vc.set_level('')
- # Convert the VyOS config to an abstract internal representation
- flow_config = {
- 'flow-accounting-configured': vc.exists('system flow-accounting'),
- 'buffer-size': vc.return_value('system flow-accounting buffer-size'),
- 'enable-egress': _node_exists('system flow-accounting enable-egress'),
- 'disable-imt': _node_exists('system flow-accounting disable-imt'),
- 'syslog-facility': vc.return_value('system flow-accounting syslog-facility'),
- 'interfaces': None,
- 'sflow': {
- 'configured': vc.exists('system flow-accounting sflow'),
- 'agent-address': vc.return_value('system flow-accounting sflow agent-address'),
- 'sampling-rate': vc.return_value('system flow-accounting sflow sampling-rate'),
- 'servers': None
- },
- 'netflow': {
- 'configured': vc.exists('system flow-accounting netflow'),
- 'engine-id': vc.return_value('system flow-accounting netflow engine-id'),
- 'max-flows': vc.return_value('system flow-accounting netflow max-flows'),
- 'sampling-rate': vc.return_value('system flow-accounting netflow sampling-rate'),
- 'source-ip': vc.return_value('system flow-accounting netflow source-ip'),
- 'version': vc.return_value('system flow-accounting netflow version'),
- 'timeout': {
- 'expint': vc.return_value('system flow-accounting netflow timeout expiry-interval'),
- 'general': vc.return_value('system flow-accounting netflow timeout flow-generic'),
- 'icmp': vc.return_value('system flow-accounting netflow timeout icmp'),
- 'maxlife': vc.return_value('system flow-accounting netflow timeout max-active-life'),
- 'tcp.fin': vc.return_value('system flow-accounting netflow timeout tcp-fin'),
- 'tcp': vc.return_value('system flow-accounting netflow timeout tcp-generic'),
- 'tcp.rst': vc.return_value('system flow-accounting netflow timeout tcp-rst'),
- 'udp': vc.return_value('system flow-accounting netflow timeout udp')
- },
- 'servers': None
- }
- }
-
- # get interfaces list
- if vc.exists('system flow-accounting interface'):
- flow_config['interfaces'] = vc.return_values('system flow-accounting interface')
-
- # get sFlow collectors list
- if vc.exists('system flow-accounting sflow server'):
- flow_config['sflow']['servers'] = []
- sflow_collectors = vc.list_nodes('system flow-accounting sflow server')
- for collector in sflow_collectors:
- port = default_sflow_server_port
- if vc.return_value("system flow-accounting sflow server {} port".format(collector)):
- port = vc.return_value("system flow-accounting sflow server {} port".format(collector))
- flow_config['sflow']['servers'].append({ 'address': collector, 'port': port })
-
- # get NetFlow collectors list
- if vc.exists('system flow-accounting netflow server'):
- flow_config['netflow']['servers'] = []
- netflow_collectors = vc.list_nodes('system flow-accounting netflow server')
- for collector in netflow_collectors:
- port = default_netflow_server_port
- if vc.return_value("system flow-accounting netflow server {} port".format(collector)):
- port = vc.return_value("system flow-accounting netflow server {} port".format(collector))
- flow_config['netflow']['servers'].append({ 'address': collector, 'port': port })
-
- # get sflow agent-id
- if flow_config['sflow']['agent-address'] == None or flow_config['sflow']['agent-address'] == 'auto':
- flow_config['sflow']['agent-address'] = _sflow_default_agentip(vc)
-
- # get NetFlow version
- if not flow_config['netflow']['version']:
- flow_config['netflow']['version'] = default_netflow_version
-
- # convert NetFlow engine-id format, if this is necessary
- if flow_config['netflow']['engine-id'] and flow_config['netflow']['version'] == '5':
- regex_filter = re.compile('^\d+$')
- if regex_filter.search(flow_config['netflow']['engine-id']):
- flow_config['netflow']['engine-id'] = "{}:0".format(flow_config['netflow']['engine-id'])
-
- # return dict with flow-accounting configuration
- return flow_config
-
-def verify(config):
- # Verify that configuration is valid
- # skip all checks if flow-accounting was removed
- if not config['flow-accounting-configured']:
- return True
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['system', 'flow-accounting']
+ if not conf.exists(base):
+ return None
+
+ flow_accounting = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+
+ # delete individual flow type default - should only be added if user uses
+ # this feature
+ for flow_type in ['sflow', 'netflow']:
+ if flow_type in default_values:
+ del default_values[flow_type]
+ flow_accounting = dict_merge(default_values, flow_accounting)
+
+ for flow_type in ['sflow', 'netflow']:
+ if flow_type in flow_accounting:
+ default_values = defaults(base + [flow_type])
+ # we need to merge individual server configurations
+ if 'server' in default_values:
+ del default_values['server']
+ flow_accounting[flow_type] = dict_merge(default_values, flow_accounting[flow_type])
+
+ if 'server' in flow_accounting[flow_type]:
+ default_values = defaults(base + [flow_type, 'server'])
+ for server in flow_accounting[flow_type]['server']:
+ flow_accounting[flow_type]['server'][server] = dict_merge(
+ default_values,flow_accounting[flow_type]['server'][server])
+
+ return flow_accounting
+
+def verify(flow_config):
+ if not flow_config:
+ return None
# check if at least one collector is enabled
- if not (config['sflow']['configured'] or config['netflow']['configured'] or not config['disable-imt']):
- raise ConfigError("You need to configure at least one sFlow or NetFlow protocol, or not set \"disable-imt\" for flow-accounting")
+ if 'sflow' not in flow_config and 'netflow' not in flow_config and 'disable_imt' in flow_config:
+ raise ConfigError('You need to configure at least sFlow or NetFlow, ' \
+ 'or not set "disable-imt" for flow-accounting!')
# Check if at least one interface is configured
- if not config['interfaces']:
- raise ConfigError("You need to configure at least one interface for flow-accounting")
+ if 'interface' not in flow_config:
+ raise ConfigError('Flow accounting requires at least one interface to ' \
+ 'be configured!')
# check that all configured interfaces exists in the system
- for iface in config['interfaces']:
- if not iface in Section.interfaces():
- # chnged from error to warning to allow adding dynamic interfaces and interface templates
- # raise ConfigError("The {} interface is not presented in the system".format(iface))
- print("Warning: the {} interface is not presented in the system".format(iface))
+ for interface in flow_config['interface']:
+ if interface not in Section.interfaces():
+ # Changed from error to warning to allow adding dynamic interfaces
+ # and interface templates
+ Warning(f'Interface "{interface}" is not presented in the system')
# check sFlow configuration
- if config['sflow']['configured']:
- # check if at least one sFlow collector is configured if sFlow configuration is presented
- if not config['sflow']['servers']:
- raise ConfigError("You need to configure at least one sFlow server")
+ if 'sflow' in flow_config:
+ # check if at least one sFlow collector is configured
+ if 'server' not in flow_config['sflow']:
+ raise ConfigError('You need to configure at least one sFlow server!')
# check that all sFlow collectors use the same IP protocol version
sflow_collector_ipver = None
- for sflow_collector in config['sflow']['servers']:
+ for server in flow_config['sflow']['server']:
if sflow_collector_ipver:
- if sflow_collector_ipver != ip_address(sflow_collector['address']).version:
+ if sflow_collector_ipver != ip_address(server).version:
raise ConfigError("All sFlow servers must use the same IP protocol")
else:
- sflow_collector_ipver = ip_address(sflow_collector['address']).version
-
+ sflow_collector_ipver = ip_address(server).version
# check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa
- for sflow_collector in config['sflow']['servers']:
- if ip_address(sflow_collector['address']).version != ip_address(config['sflow']['agent-address']).version:
- raise ConfigError("Different IP address versions cannot be mixed in \"sflow agent-address\" and \"sflow server\". You need to set manually the same IP version for \"agent-address\" as for all sFlow servers")
-
- # check if configured sFlow agent-id exist in the system
- agent_id_presented = None
- for iface in Section.interfaces():
- for address in Interface(iface).get_addr():
- # check an IP, if this is not loopback
- regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
- if regex_filter.search(address):
- if regex_filter.search(address).group('ipaddr') == config['sflow']['agent-address']:
- agent_id_presented = True
- break
- if not agent_id_presented:
- raise ConfigError("Your \"sflow agent-address\" does not exist in the system")
+ for server in flow_config['sflow']['server']:
+ if 'agent_address' in flow_config['sflow']:
+ if ip_address(server).version != ip_address(flow_config['sflow']['agent_address']).version:
+ raise ConfigError('IPv4 and IPv6 addresses can not be mixed in "sflow agent-address" and "sflow '\
+ 'server". You need to set the same IP version for both "agent-address" and '\
+ 'all sFlow servers')
+
+ if 'agent_address' in flow_config['sflow']:
+ tmp = flow_config['sflow']['agent_address']
+ if not is_addr_assigned(tmp):
+ raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!')
+
+ # Check if configured netflow source-address exist in the system
+ if 'source_address' in flow_config['sflow']:
+ if not is_addr_assigned(flow_config['sflow']['source_address']):
+ tmp = flow_config['sflow']['source_address']
+ raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!')
# check NetFlow configuration
- if config['netflow']['configured']:
+ if 'netflow' in flow_config:
# check if at least one NetFlow collector is configured if NetFlow configuration is presented
- if not config['netflow']['servers']:
- raise ConfigError("You need to configure at least one NetFlow server")
-
- # check if configured netflow source-ip exist in the system
- if config['netflow']['source-ip']:
- source_ip_presented = None
- for iface in Section.interfaces():
- for address in Interface(iface).get_addr():
- # check an IP
- regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
- if regex_filter.search(address):
- if regex_filter.search(address).group('ipaddr') == config['netflow']['source-ip']:
- source_ip_presented = True
- break
- if not source_ip_presented:
- print("Warning: your \"netflow source-ip\" does not exist in the system")
-
- # check if engine-id compatible with selected protocol version
- if config['netflow']['engine-id']:
+ if 'server' not in flow_config['netflow']:
+ raise ConfigError('You need to configure at least one NetFlow server!')
+
+ # Check if configured netflow source-address exist in the system
+ if 'source_address' in flow_config['netflow']:
+ if not is_addr_assigned(flow_config['netflow']['source_address']):
+ tmp = flow_config['netflow']['source_address']
+ raise ConfigError(f'Configured "netflow source-address {tmp}" does not exist on the system!')
+
+ # Check if engine-id compatible with selected protocol version
+ if 'engine_id' in flow_config['netflow']:
v5_filter = '^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$'
v9v10_filter = '^(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$'
- if config['netflow']['version'] == '5':
+ engine_id = flow_config['netflow']['engine_id']
+ version = flow_config['netflow']['version']
+
+ if flow_config['netflow']['version'] == '5':
regex_filter = re.compile(v5_filter)
- if not regex_filter.search(config['netflow']['engine-id']):
- raise ConfigError("You cannot use NetFlow engine-id {} together with NetFlow protocol version {}".format(config['netflow']['engine-id'], config['netflow']['version']))
+ if not regex_filter.search(engine_id):
+ raise ConfigError(f'You cannot use NetFlow engine-id "{engine_id}" '\
+ f'together with NetFlow protocol version "{version}"!')
else:
regex_filter = re.compile(v9v10_filter)
- if not regex_filter.search(config['netflow']['engine-id']):
- raise ConfigError("You cannot use NetFlow engine-id {} together with NetFlow protocol version {}".format(config['netflow']['engine-id'], config['netflow']['version']))
+ if not regex_filter.search(flow_config['netflow']['engine_id']):
+ raise ConfigError(f'Can not use NetFlow engine-id "{engine_id}" together '\
+ f'with NetFlow protocol version "{version}"!')
# return True if all checks were passed
return True
-def generate(config):
- # skip all checks if flow-accounting was removed
- if not config['flow-accounting-configured']:
- return True
-
- # Calculate all necessary values
- if config['buffer-size']:
- # circular queue size
- config['plugin_pipe_size'] = int(config['buffer-size']) * 1024**2
- else:
- config['plugin_pipe_size'] = default_plugin_pipe_size * 1024**2
- # transfer buffer size
- # recommended value from pmacct developers 1/1000 of pipe size
- config['plugin_buffer_size'] = int(config['plugin_pipe_size'] / 1000)
-
- # Prepare a timeouts string
- timeout_string = ''
- for timeout_type, timeout_value in config['netflow']['timeout'].items():
- if timeout_value:
- if timeout_string == '':
- timeout_string = "{}{}={}".format(timeout_string, timeout_type, timeout_value)
- else:
- timeout_string = "{}:{}={}".format(timeout_string, timeout_type, timeout_value)
- config['netflow']['timeout_string'] = timeout_string
-
- render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', {
- 'templatecfg': config,
- 'snaplen': default_captured_packet_size,
- })
+def generate(flow_config):
+ if not flow_config:
+ return None
+ render(uacctd_conf_path, 'pmacct/uacctd.conf.j2', flow_config)
+ render(systemd_override, 'pmacct/override.conf.j2', flow_config)
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
-def apply(config):
- # define variables
- command = None
+def apply(flow_config):
+ action = 'restart'
# Check if flow-accounting was removed and define command
- if not config['flow-accounting-configured']:
- command = 'systemctl stop uacctd.service'
- else:
- command = 'systemctl restart uacctd.service'
+ if not flow_config:
+ _nftables_config([], 'ingress')
+ _nftables_config([], 'egress')
+
+ # Stop flow-accounting daemon and remove configuration file
+ call(f'systemctl stop {systemd_service}')
+ if os.path.exists(uacctd_conf_path):
+ os.unlink(uacctd_conf_path)
+ return
- # run command to start or stop flow-accounting
- cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting')
+ # Start/reload flow-accounting daemon
+ call(f'systemctl restart {systemd_service}')
- # configure iptables rules for defined interfaces
- if config['interfaces']:
- _iptables_config(config['interfaces'], 'ingress')
+ # configure nftables rules for defined interfaces
+ if 'interface' in flow_config:
+ _nftables_config(flow_config['interface'], 'ingress', flow_config['packet_length'])
# configure egress the same way if configured otherwise remove it
- if config['enable-egress']:
- _iptables_config(config['interfaces'], 'egress')
+ if 'enable_egress' in flow_config:
+ _nftables_config(flow_config['interface'], 'egress', flow_config['packet_length'])
else:
- _iptables_config([], 'egress')
- else:
- _iptables_config([], 'ingress')
- _iptables_config([], 'egress')
+ _nftables_config([], 'egress')
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/high-availability.py
index c72efc61f..e14050dd3 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
@@ -28,7 +28,6 @@ from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.util import call
-from vyos.util import is_systemd_service_running
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
@@ -40,33 +39,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,33 +126,41 @@ 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.j2', 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
- # XXX: T3944 - reload keepalived configuration if service is already running
- # to not cause any service disruption when applying changes.
- if is_systemd_service_running(service_name):
- call(f'systemctl reload {service_name}')
- else:
- call(f'systemctl restart {service_name}')
+ call(f'systemctl reload-or-restart {service_name}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index 87bad0dc6..93f244f42 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -21,13 +21,14 @@ import copy
import vyos.util
import vyos.hostsd_client
-from vyos import ConfigError
+from vyos.base import Warning
from vyos.config import Config
from vyos.ifconfig import Section
from vyos.template import is_ip
from vyos.util import cmd
from vyos.util import call
from vyos.util import process_named_running
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -113,7 +114,7 @@ def verify(hosts):
for interface, interface_config in hosts['nameservers_dhcp_interfaces'].items():
# Warnin user if interface does not have DHCP or DHCPv6 configured
if not set(interface_config).intersection(['dhcp', 'dhcpv6']):
- print(f'WARNING: "{interface}" is not a DHCP interface but uses DHCP name-server option!')
+ Warning(f'"{interface}" is not a DHCP interface but uses DHCP name-server option!')
return None
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 7e4b117c8..4a7906c17 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -13,25 +13,26 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#
import sys
import os
import json
-import time
+
+from time import sleep
from copy import deepcopy
import vyos.defaults
+
from vyos.config import Config
-from vyos import ConfigError
+from vyos.template import render
from vyos.util import cmd
from vyos.util import call
-
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
-config_file = '/etc/vyos/http-api.conf'
+api_conf_file = '/etc/vyos/http-api.conf'
+systemd_service = '/run/systemd/system/vyos-http-api.service'
vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode']
@@ -49,21 +50,44 @@ def get_config(config=None):
else:
conf = Config()
- if not conf.exists('service https api'):
+ base = ['service', 'https', 'api']
+ if not conf.exists(base):
return None
- else:
- conf.set_level('service https api')
+ # Do we run inside a VRF context?
+ vrf_path = ['service', 'https', 'vrf']
+ if conf.exists(vrf_path):
+ http_api['vrf'] = conf.return_value(vrf_path)
+
+ conf.set_level('service https api')
if conf.exists('strict'):
- http_api['strict'] = 'true'
+ http_api['strict'] = True
if conf.exists('debug'):
- http_api['debug'] = 'true'
+ http_api['debug'] = True
+
+ # this node is not available by CLI by default, and is reserved for
+ # the graphql tools. One can enable it for testing, with the warning
+ # that this will open an unauthenticated server. To do so
+ # mkdir /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql
+ # touch /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql/node.def
+ # and configure; editing the config alone is insufficient.
+ if conf.exists('gql'):
+ http_api['gql'] = True
+
+ if conf.exists('socket'):
+ http_api['socket'] = True
if conf.exists('port'):
port = conf.return_value('port')
http_api['port'] = port
+ if conf.exists('cors'):
+ http_api['cors'] = {}
+ if conf.exists('cors allow-origin'):
+ origins = conf.return_values('cors allow-origin')
+ http_api['cors']['origins'] = origins[:]
+
if conf.exists('keys'):
for name in conf.list_nodes('keys id'):
if conf.exists('keys id {0} key'.format(name)):
@@ -83,24 +107,31 @@ def verify(http_api):
def generate(http_api):
if http_api is None:
+ if os.path.exists(systemd_service):
+ os.unlink(systemd_service)
return None
if not os.path.exists('/etc/vyos'):
os.mkdir('/etc/vyos')
- with open(config_file, 'w') as f:
+ with open(api_conf_file, 'w') as f:
json.dump(http_api, f, indent=2)
+ render(systemd_service, 'https/vyos-http-api.service.j2', http_api)
return None
def apply(http_api):
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+ service_name = 'vyos-http-api.service'
+
if http_api is not None:
- call('systemctl restart vyos-http-api.service')
+ call(f'systemctl restart {service_name}')
else:
- call('systemctl stop vyos-http-api.service')
+ call(f'systemctl stop {service_name}')
# Let uvicorn settle before restarting Nginx
- time.sleep(2)
+ sleep(1)
cmd(f'{vyos_conf_scripts_dir}/https.py', raising=ConfigError)
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index 92dc4a410..3057357fc 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -23,6 +23,7 @@ import vyos.defaults
import vyos.certbot_util
from vyos.config import Config
+from vyos.configverify import verify_vrf
from vyos import ConfigError
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
@@ -34,6 +35,7 @@ from vyos import airbag
airbag.enable()
config_file = '/etc/nginx/sites-available/default'
+systemd_override = r'/etc/systemd/system/nginx.service.d/override.conf'
cert_dir = '/etc/ssl/certs'
key_dir = '/etc/ssl/private'
certbot_dir = vyos.defaults.directories['certbot']
@@ -59,10 +61,11 @@ def get_config(config=None):
else:
conf = Config()
- if not conf.exists('service https'):
+ base = ['service', 'https']
+ if not conf.exists(base):
return None
- https = conf.get_config_dict('service https', get_first_key=True)
+ https = conf.get_config_dict(base, get_first_key=True)
if https:
https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
@@ -103,6 +106,8 @@ def verify(https):
if not domains_found:
raise ConfigError("At least one 'virtual-host <id> server-name' "
"matching the 'certbot domain-name' is required.")
+
+ verify_vrf(https)
return None
def generate(https):
@@ -143,7 +148,6 @@ def generate(https):
server_cert = str(wrap_certificate(pki_cert['certificate']))
if 'ca-certificate' in cert_dict:
ca_cert = cert_dict['ca-certificate']
- print(ca_cert)
server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate']))
write_file(cert_path, server_cert)
@@ -188,6 +192,8 @@ def generate(https):
vhosts = https.get('api-restrict', {}).get('virtual-host', [])
if vhosts:
api_data['vhost'] = vhosts[:]
+ if 'socket' in list(api_settings):
+ api_data['socket'] = True
if api_data:
vhost_list = api_data.get('vhost', [])
@@ -208,11 +214,13 @@ def generate(https):
'certbot': certbot
}
- render(config_file, 'https/nginx.default.tmpl', data)
-
+ render(config_file, 'https/nginx.default.j2', data)
+ render(systemd_override, 'https/override.conf.j2', https)
return None
def apply(https):
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
if https is not None:
call('systemctl restart nginx.service')
else:
diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py
index fb030c9f3..de6a51c64 100755
--- a/src/conf_mode/igmp_proxy.py
+++ b/src/conf_mode/igmp_proxy.py
@@ -19,6 +19,7 @@ import os
from sys import exit
from netifaces import interfaces
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.template import render
@@ -92,10 +93,10 @@ def generate(igmp_proxy):
# bail out early - service is disabled, but inform user
if 'disable' in igmp_proxy:
- print('WARNING: IGMP Proxy will be deactivated because it is disabled')
+ Warning('IGMP Proxy will be deactivated because it is disabled')
return None
- render(config_file, 'igmp-proxy/igmpproxy.conf.tmpl', igmp_proxy)
+ render(config_file, 'igmp-proxy/igmpproxy.conf.j2', igmp_proxy)
return None
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 431d65f1f..4167594e3 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -27,8 +27,9 @@ from vyos.configdict import is_source_interface
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_dhcpv6
-from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_source_interface
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ifconfig import BondIf
@@ -67,7 +68,7 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'bonding']
- bond = get_interface_dict(conf, base)
+ ifname, bond = get_interface_dict(conf, base)
# To make our own life easier transfor the list of member interfaces
# into a dictionary - we will use this to add additional information
@@ -80,14 +81,14 @@ def get_config(config=None):
if 'mode' in bond:
bond['mode'] = get_bond_mode(bond['mode'])
- tmp = leaf_node_changed(conf, ['mode'])
+ tmp = leaf_node_changed(conf, base + [ifname, 'mode'])
if tmp: bond.update({'shutdown_required': {}})
- tmp = leaf_node_changed(conf, ['lacp-rate'])
+ tmp = leaf_node_changed(conf, base + [ifname, 'lacp-rate'])
if tmp: bond.update({'shutdown_required': {}})
# determine which members have been removed
- interfaces_removed = leaf_node_changed(conf, ['member', 'interface'])
+ interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface'])
if interfaces_removed:
bond.update({'shutdown_required': {}})
if 'member' not in bond:
@@ -132,10 +133,10 @@ def verify(bond):
return None
if 'arp_monitor' in bond:
- if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16:
+ if 'target' in bond['arp_monitor'] and len(bond['arp_monitor']['target']) > 16:
raise ConfigError('The maximum number of arp-monitor targets is 16')
- if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0:
+ if 'interval' in bond['arp_monitor'] and int(bond['arp_monitor']['interval']) > 0:
if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
'transmit-load-balance or adaptive-load-balance')
@@ -149,6 +150,7 @@ def verify(bond):
verify_address(bond)
verify_dhcpv6(bond)
verify_vrf(bond)
+ verify_mirror_redirect(bond)
# use common function to verify VLAN configuration
verify_vlan_config(bond)
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 4d3ebc587..38ae727c1 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -22,12 +22,12 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import node_changed
-from vyos.configdict import leaf_node_changed
from vyos.configdict import is_member
from vyos.configdict import is_source_interface
from vyos.configdict import has_vlan_subinterface_configured
from vyos.configdict import dict_merge
from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
from vyos.ifconfig import BridgeIf
from vyos.validate import has_address_configured
@@ -50,15 +50,15 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'bridge']
- bridge = get_interface_dict(conf, base)
+ ifname, bridge = get_interface_dict(conf, base)
# determine which members have been removed
- tmp = node_changed(conf, ['member', 'interface'], key_mangling=('-', '_'))
+ tmp = node_changed(conf, base + [ifname, 'member', 'interface'], key_mangling=('-', '_'))
if tmp:
if 'member' in bridge:
- bridge['member'].update({'interface_remove': tmp })
+ bridge['member'].update({'interface_remove' : tmp })
else:
- bridge.update({'member': {'interface_remove': tmp }})
+ bridge.update({'member' : {'interface_remove' : tmp }})
if dict_search('member.interface', bridge):
# XXX: T2665: we need a copy of the dict keys for iteration, else we will get:
@@ -106,6 +106,7 @@ def verify(bridge):
verify_dhcpv6(bridge)
verify_vrf(bridge)
+ verify_mirror_redirect(bridge)
ifname = bridge['ifname']
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index 55c783f38..e771581e1 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -21,6 +21,7 @@ from vyos.configdict import get_interface_dict
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import DummyIf
from vyos import ConfigError
from vyos import airbag
@@ -36,7 +37,7 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'dummy']
- dummy = get_interface_dict(conf, base)
+ _, dummy = get_interface_dict(conf, base)
return dummy
def verify(dummy):
@@ -46,6 +47,7 @@ def verify(dummy):
verify_vrf(dummy)
verify_address(dummy)
+ verify_mirror_redirect(dummy)
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index e7250fb49..fec4456fb 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -19,20 +19,23 @@ import os
from glob import glob
from sys import exit
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configverify import verify_address
from vyos.configverify import verify_dhcpv6
from vyos.configverify import verify_eapol
from vyos.configverify import verify_interface_exists
-from vyos.configverify import verify_mirror
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_mtu
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ethtool import Ethtool
from vyos.ifconfig import EthernetIf
-from vyos.pki import wrap_certificate
+from vyos.pki import find_chain
+from vyos.pki import encode_certificate
+from vyos.pki import load_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
@@ -62,7 +65,7 @@ def get_config(config=None):
get_first_key=True, no_tag_node_value_mangle=True)
base = ['interfaces', 'ethernet']
- ethernet = get_interface_dict(conf, base)
+ _, ethernet = get_interface_dict(conf, base)
if 'deleted' not in ethernet:
if pki: ethernet['pki'] = pki
@@ -81,7 +84,7 @@ def verify(ethernet):
verify_address(ethernet)
verify_vrf(ethernet)
verify_eapol(ethernet)
- verify_mirror(ethernet)
+ verify_mirror_redirect(ethernet)
ethtool = Ethtool(ifname)
# No need to check speed and duplex keys as both have default values.
@@ -140,8 +143,8 @@ def verify(ethernet):
raise ConfigError('XDP requires additional TX queues, too few available!')
if {'is_bond_member', 'mac'} <= set(ethernet):
- print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" '
- f'is a member of bond "{is_bond_member}"'.format(**ethernet))
+ Warning(f'changing mac address "{mac}" will be ignored as "{ifname}" ' \
+ f'is a member of bond "{is_bond_member}"'.format(**ethernet))
# use common function to verify VLAN configuration
verify_vlan_config(ethernet)
@@ -150,7 +153,7 @@ def verify(ethernet):
def generate(ethernet):
if 'eapol' in ethernet:
render(wpa_suppl_conf.format(**ethernet),
- 'ethernet/wpa_supplicant.conf.tmpl', ethernet)
+ 'ethernet/wpa_supplicant.conf.j2', ethernet)
ifname = ethernet['ifname']
cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem')
@@ -159,16 +162,26 @@ def generate(ethernet):
cert_name = ethernet['eapol']['certificate']
pki_cert = ethernet['pki']['certificate'][cert_name]
- write_file(cert_file_path, wrap_certificate(pki_cert['certificate']))
+ loaded_pki_cert = load_certificate(pki_cert['certificate'])
+ loaded_ca_certs = {load_certificate(c['certificate'])
+ for c in ethernet['pki']['ca'].values()}
+
+ cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
+
+ write_file(cert_file_path,
+ '\n'.join(encode_certificate(c) for c in cert_full_chain))
write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
if 'ca_certificate' in ethernet['eapol']:
ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')
ca_cert_name = ethernet['eapol']['ca_certificate']
- pki_ca_cert = ethernet['pki']['ca'][cert_name]
+ pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
+
+ loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
+ ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
write_file(ca_cert_file_path,
- wrap_certificate(pki_ca_cert['certificate']))
+ '\n'.join(encode_certificate(c) for c in ca_full_chain))
else:
# delete configuration on interface removal
if os.path.isfile(wpa_suppl_conf.format(**ethernet)):
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index 2a63b60aa..b9cf2fa3c 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.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
@@ -21,9 +21,12 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import GeneveIf
from vyos import ConfigError
@@ -40,7 +43,18 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'geneve']
- geneve = get_interface_dict(conf, base)
+ ifname, geneve = get_interface_dict(conf, base)
+
+ # GENEVE interfaces are picky and require recreation if certain parameters
+ # change. But a GENEVE interface should - of course - not be re-created if
+ # it's description or IP address is adjusted. Feels somehow logic doesn't it?
+ for cli_option in ['remote', 'vni']:
+ if leaf_node_changed(conf, base + [ifname, cli_option]):
+ geneve.update({'rebuild_required': {}})
+
+ if is_node_changed(conf, base + [ifname, 'parameters']):
+ geneve.update({'rebuild_required': {}})
+
return geneve
def verify(geneve):
@@ -50,6 +64,7 @@ def verify(geneve):
verify_mtu_ipv6(geneve)
verify_address(geneve)
+ verify_mirror_redirect(geneve)
if 'remote' not in geneve:
raise ConfigError('Remote side must be configured')
@@ -65,11 +80,12 @@ def generate(geneve):
def apply(geneve):
# Check if GENEVE interface already exists
- if geneve['ifname'] in interfaces():
- g = GeneveIf(geneve['ifname'])
- # GENEVE is super picky and the tunnel always needs to be recreated,
- # thus we can simply always delete it first.
- g.remove()
+ if 'rebuild_required' in geneve or 'delete' in geneve:
+ if geneve['ifname'] in interfaces():
+ g = GeneveIf(geneve['ifname'])
+ # GENEVE is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ g.remove()
if 'deleted' not in geneve:
# Finally create the new interface
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 9b6ddd5aa..6a486f969 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -25,6 +25,7 @@ from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import L2TPv3If
from vyos.util import check_kmod
from vyos.validate import is_addr_assigned
@@ -44,15 +45,15 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'l2tpv3']
- l2tpv3 = get_interface_dict(conf, base)
+ ifname, l2tpv3 = get_interface_dict(conf, base)
# To delete an l2tpv3 interface we need the current tunnel and session-id
if 'deleted' in l2tpv3:
- tmp = leaf_node_changed(conf, ['tunnel-id'])
+ tmp = leaf_node_changed(conf, base + [ifname, 'tunnel-id'])
# leaf_node_changed() returns a list
l2tpv3.update({'tunnel_id': tmp[0]})
- tmp = leaf_node_changed(conf, ['session-id'])
+ tmp = leaf_node_changed(conf, base + [ifname, 'session-id'])
l2tpv3.update({'session_id': tmp[0]})
return l2tpv3
@@ -76,6 +77,7 @@ def verify(l2tpv3):
verify_mtu_ipv6(l2tpv3)
verify_address(l2tpv3)
+ verify_mirror_redirect(l2tpv3)
return None
def generate(l2tpv3):
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
index 193334443..08d34477a 100755
--- a/src/conf_mode/interfaces-loopback.py
+++ b/src/conf_mode/interfaces-loopback.py
@@ -20,6 +20,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import LoopbackIf
from vyos import ConfigError
from vyos import airbag
@@ -35,10 +36,11 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'loopback']
- loopback = get_interface_dict(conf, base)
+ _, loopback = get_interface_dict(conf, base)
return loopback
def verify(loopback):
+ verify_mirror_redirect(loopback)
return None
def generate(loopback):
diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
index eab69f36e..279dd119b 100755
--- a/src/conf_mode/interfaces-macsec.py
+++ b/src/conf_mode/interfaces-macsec.py
@@ -29,6 +29,7 @@ from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_source_interface
from vyos import ConfigError
from vyos import airbag
@@ -47,7 +48,7 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'macsec']
- macsec = get_interface_dict(conf, base)
+ ifname, macsec = get_interface_dict(conf, base)
# Check if interface has been removed
if 'deleted' in macsec:
@@ -66,6 +67,7 @@ def verify(macsec):
verify_vrf(macsec)
verify_mtu_ipv6(macsec)
verify_address(macsec)
+ verify_mirror_redirect(macsec)
if not (('security' in macsec) and
('cipher' in macsec['security'])):
@@ -96,7 +98,7 @@ def verify(macsec):
def generate(macsec):
render(wpa_suppl_conf.format(**macsec),
- 'macsec/wpa_supplicant.conf.tmpl', macsec)
+ 'macsec/wpa_supplicant.conf.j2', macsec)
return None
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 3b8fae710..4750ca3e8 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2021 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
@@ -32,8 +32,10 @@ from shutil import rmtree
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import VTunIf
from vyos.pki import load_dh_parameters
from vyos.pki import load_private_key
@@ -47,6 +49,7 @@ from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.util import call
from vyos.util import chown
+from vyos.util import cmd
from vyos.util import dict_search
from vyos.util import dict_search_args
from vyos.util import makedir
@@ -82,10 +85,12 @@ def get_config(config=None):
tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
- openvpn = get_interface_dict(conf, base)
+ ifname, openvpn = get_interface_dict(conf, base)
if 'deleted' not in openvpn:
openvpn['pki'] = tmp_pki
+ if is_node_changed(conf, base + [ifname, 'openvpn-option']):
+ openvpn.update({'restart_required': {}})
# We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'
# as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.
@@ -225,11 +230,12 @@ def verify(openvpn):
if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:
raise ConfigError('Must specify "local-address" or add interface to bridge')
- if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
- raise ConfigError('Only one IPv4 local-address can be specified')
+ if 'local_address' in openvpn:
+ if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
+ raise ConfigError('Only one IPv4 local-address can be specified')
- if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
- raise ConfigError('Only one IPv6 local-address can be specified')
+ if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
+ raise ConfigError('Only one IPv6 local-address can be specified')
if openvpn['device_type'] == 'tun':
if 'remote_address' not in openvpn:
@@ -268,7 +274,7 @@ def verify(openvpn):
if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn):
raise ConfigError('"remote-address" and "remote-host" can not be the same')
- if openvpn['device_type'] == 'tap':
+ if openvpn['device_type'] == 'tap' and 'local_address' in openvpn:
# we can only have one local_address, this is ensured above
v4addr = None
for laddr in openvpn['local_address']:
@@ -423,8 +429,8 @@ def verify(openvpn):
# verify specified IP address is present on any interface on this system
if 'local_host' in openvpn:
if not is_addr_assigned(openvpn['local_host']):
- raise ConfigError('local-host IP address "{local_host}" not assigned' \
- ' to any interface'.format(**openvpn))
+ print('local-host IP address "{local_host}" not assigned' \
+ ' to any interface'.format(**openvpn))
# TCP active
if openvpn['protocol'] == 'tcp-active':
@@ -489,6 +495,7 @@ def verify(openvpn):
raise ConfigError('Username for authentication is missing')
verify_vrf(openvpn)
+ verify_mirror_redirect(openvpn)
return None
@@ -600,7 +607,7 @@ def generate(openvpn):
# Generate User/Password authentication file
if 'authentication' in openvpn:
- render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.tmpl', openvpn,
+ render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.j2', openvpn,
user=user, group=group, permission=0o600)
else:
# delete old auth file if present
@@ -616,16 +623,16 @@ def generate(openvpn):
# Our client need's to know its subnet mask ...
client_config['server_subnet'] = dict_search('server.subnet', openvpn)
- render(client_file, 'openvpn/client.conf.tmpl', client_config,
+ render(client_file, 'openvpn/client.conf.j2', client_config,
user=user, group=group)
# we need to support quoting of raw parameters from OpenVPN CLI
# see https://phabricator.vyos.net/T1632
- render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn,
+ render(cfg_file.format(**openvpn), 'openvpn/server.conf.j2', openvpn,
formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
# Render 20-override.conf for OpenVPN service
- render(service_file.format(**openvpn), 'openvpn/service-override.conf.tmpl', openvpn,
+ render(service_file.format(**openvpn), 'openvpn/service-override.conf.j2', openvpn,
formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
# Reload systemd services config to apply an override
call(f'systemctl daemon-reload')
@@ -647,9 +654,19 @@ def apply(openvpn):
return None
+ # verify specified IP address is present on any interface on this system
+ # Allow to bind service to nonlocal address, if it virtaual-vrrp address
+ # or if address will be assign later
+ if 'local_host' in openvpn:
+ if not is_addr_assigned(openvpn['local_host']):
+ cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1')
+
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- call(f'systemctl reload-or-restart openvpn@{interface}.service')
+ action = 'reload-or-restart'
+ if 'restart_required' in openvpn:
+ action = 'restart'
+ call(f'systemctl {action} openvpn@{interface}.service')
o = VTunIf(**openvpn)
o.update(openvpn)
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index 584adc75e..e2fdc7a42 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -22,12 +22,15 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
from vyos.configdict import leaf_node_changed
+from vyos.configdict import get_pppoe_interfaces
from vyos.configverify import verify_authentication
from vyos.configverify import verify_source_interface
from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_vrf
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import PPPoEIf
from vyos.template import render
from vyos.util import call
@@ -46,33 +49,17 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'pppoe']
- pppoe = get_interface_dict(conf, base)
+ ifname, pppoe = get_interface_dict(conf, base)
# We should only terminate the PPPoE session if critical parameters change.
# All parameters that can be changed on-the-fly (like interface description)
# should not lead to a reconnect!
- tmp = leaf_node_changed(conf, ['access-concentrator'])
- if tmp: pppoe.update({'shutdown_required': {}})
-
- tmp = leaf_node_changed(conf, ['connect-on-demand'])
- if tmp: pppoe.update({'shutdown_required': {}})
-
- tmp = leaf_node_changed(conf, ['service-name'])
- if tmp: pppoe.update({'shutdown_required': {}})
-
- tmp = leaf_node_changed(conf, ['source-interface'])
- if tmp: pppoe.update({'shutdown_required': {}})
-
- tmp = leaf_node_changed(conf, ['vrf'])
- # leaf_node_changed() returns a list, as VRF is a non-multi node, there
- # will be only one list element
- if tmp: pppoe.update({'vrf_old': tmp[0]})
-
- tmp = leaf_node_changed(conf, ['authentication', 'user'])
- if tmp: pppoe.update({'shutdown_required': {}})
-
- tmp = leaf_node_changed(conf, ['authentication', 'password'])
- if tmp: pppoe.update({'shutdown_required': {}})
+ for options in ['access-concentrator', 'connect-on-demand', 'service-name',
+ 'source-interface', 'vrf', 'no-default-route', 'authentication']:
+ if is_node_changed(conf, base + [ifname, options]):
+ pppoe.update({'shutdown_required': {}})
+ # bail out early - no need to further process other nodes
+ break
return pppoe
@@ -85,6 +72,7 @@ def verify(pppoe):
verify_authentication(pppoe)
verify_vrf(pppoe)
verify_mtu_ipv6(pppoe)
+ verify_mirror_redirect(pppoe)
if {'connect_on_demand', 'vrf'} <= set(pppoe):
raise ConfigError('On-demand dialing and VRF can not be used at the same time')
@@ -104,7 +92,7 @@ def generate(pppoe):
return None
# Create PPP configuration files
- render(config_pppoe, 'pppoe/peer.tmpl', pppoe, permission=0o640)
+ render(config_pppoe, 'pppoe/peer.j2', pppoe, permission=0o640)
return None
@@ -118,7 +106,7 @@ def apply(pppoe):
return None
# reconnect should only be necessary when certain config options change,
- # like ACS name, authentication, no-peer-dns, source-interface
+ # like ACS name, authentication ... (see get_config() for details)
if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or
'shutdown_required' in pppoe):
@@ -128,6 +116,9 @@ def apply(pppoe):
p.remove()
call(f'systemctl restart ppp@{ifname}.service')
+ # When interface comes "live" a hook is called:
+ # /etc/ppp/ip-up.d/99-vyos-pppoe-callback
+ # which triggers PPPoEIf.update()
else:
if os.path.isdir(f'/sys/class/net/{ifname}'):
p = PPPoEIf(ifname)
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index 945a2ea9c..1cd3fe276 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -18,13 +18,14 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
-from vyos.configdict import leaf_node_changed
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_source_interface
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_mtu_parent
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import MACVLANIf
from vyos import ConfigError
@@ -41,14 +42,14 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'pseudo-ethernet']
- peth = get_interface_dict(conf, base)
+ ifname, peth = get_interface_dict(conf, base)
- mode = leaf_node_changed(conf, ['mode'])
- if mode: peth.update({'mode_old' : mode})
+ mode = is_node_changed(conf, ['mode'])
+ if mode: peth.update({'shutdown_required' : {}})
if 'source_interface' in peth:
- peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'],
- peth['source_interface'])
+ _, peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'],
+ peth['source_interface'])
return peth
def verify(peth):
@@ -60,6 +61,7 @@ def verify(peth):
verify_vrf(peth)
verify_address(peth)
verify_mtu_parent(peth, peth['parent'])
+ verify_mirror_redirect(peth)
# use common function to verify VLAN configuration
verify_vlan_config(peth)
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 30f57ec0c..eff7f373c 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 yOS 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
@@ -18,24 +18,20 @@ import os
from sys import exit
from netifaces import interfaces
-from ipaddress import IPv4Address
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configdict import get_interface_dict
-from vyos.configdict import node_changed
from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
from vyos.configverify import verify_tunnel
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.ifconfig import TunnelIf
-from vyos.template import is_ipv4
-from vyos.template import is_ipv6
from vyos.util import get_interface_config
from vyos.util import dict_search
from vyos import ConfigError
@@ -52,10 +48,26 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'tunnel']
- tunnel = get_interface_dict(conf, base)
-
- tmp = leaf_node_changed(conf, ['encapsulation'])
- if tmp: tunnel.update({'encapsulation_changed': {}})
+ ifname, tunnel = get_interface_dict(conf, base)
+
+ if 'deleted' not in tunnel:
+ tmp = leaf_node_changed(conf, base + [ifname, 'encapsulation'])
+ if tmp: tunnel.update({'encapsulation_changed': {}})
+
+ # We also need to inspect other configured tunnels as there are Kernel
+ # restrictions where we need to comply. E.g. GRE tunnel key can't be used
+ # twice, or with multiple GRE tunnels to the same location we must specify
+ # a GRE key
+ conf.set_level(base)
+ tunnel['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+ # delete our own instance from this dict
+ ifname = tunnel['ifname']
+ del tunnel['other_tunnels'][ifname]
+ # if only one tunnel is present on the system, no need to keep this key
+ if len(tunnel['other_tunnels']) == 0:
+ del tunnel['other_tunnels']
# We must check if our interface is configured to be a DMVPN member
nhrp_base = ['protocols', 'nhrp', 'tunnel']
@@ -96,35 +108,47 @@ def verify(tunnel):
if 'direction' not in tunnel['parameters']['erspan']:
raise ConfigError('ERSPAN version 2 requires direction to be set!')
- # If tunnel source address any and key not set
+ # If tunnel source is any and gre key is not set
+ interface = tunnel['ifname']
if tunnel['encapsulation'] in ['gre'] and \
dict_search('source_address', tunnel) == '0.0.0.0' and \
dict_search('parameters.ip.key', tunnel) == None:
- raise ConfigError('Tunnel parameters ip key must be set!')
-
- if tunnel['encapsulation'] in ['gre', 'gretap']:
- if dict_search('parameters.ip.key', tunnel) != None:
- # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
- # Prevent the same key for 2 tunnels with same source-address/encap. T2920
- for tunnel_if in Section.interfaces('tunnel'):
- # It makes no sense to run the test for re-used GRE keys on our
- # own interface we are currently working on
- if tunnel['ifname'] == tunnel_if:
- continue
- tunnel_cfg = get_interface_config(tunnel_if)
- # no match on encapsulation - bail out
- if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']:
- continue
- new_source_address = dict_search('source_address', tunnel)
- # Convert tunnel key to ip key, format "ip -j link show"
- # 1 => 0.0.0.1, 999 => 0.0.3.231
- orig_new_key = dict_search('parameters.ip.key', tunnel)
- new_key = IPv4Address(int(orig_new_key))
- new_key = str(new_key)
- if dict_search('address', tunnel_cfg) == new_source_address and \
- dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key:
- raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \
+ raise ConfigError(f'"parameters ip key" must be set for {interface} when '\
+ 'encapsulation is GRE!')
+
+ gre_encapsulations = ['gre', 'gretap']
+ if tunnel['encapsulation'] in gre_encapsulations and 'other_tunnels' in tunnel:
+ # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
+ # Prevent the same key for 2 tunnels with same source-address/encap. T2920
+ for o_tunnel, o_tunnel_conf in tunnel['other_tunnels'].items():
+ # no match on encapsulation - bail out
+ our_encapsulation = tunnel['encapsulation']
+ their_encapsulation = o_tunnel_conf['encapsulation']
+ if our_encapsulation in gre_encapsulations and their_encapsulation \
+ not in gre_encapsulations:
+ continue
+
+ our_address = dict_search('source_address', tunnel)
+ our_key = dict_search('parameters.ip.key', tunnel)
+ their_address = dict_search('source_address', o_tunnel_conf)
+ their_key = dict_search('parameters.ip.key', o_tunnel_conf)
+ if our_key != None:
+ if their_address == our_address and their_key == our_key:
+ raise ConfigError(f'Key "{our_key}" for source-address "{our_address}" ' \
f'is already used for tunnel "{tunnel_if}"!')
+ else:
+ our_source_if = dict_search('source_interface', tunnel)
+ their_source_if = dict_search('source_interface', o_tunnel_conf)
+ our_remote = dict_search('remote', tunnel)
+ their_remote = dict_search('remote', o_tunnel_conf)
+ # If no IP GRE key is defined we can not have more then one GRE tunnel
+ # bound to any one interface/IP address and the same remote. This will
+ # result in a OS PermissionError: add tunnel "gre0" failed: File exists
+ if (their_address == our_address or our_source_if == their_source_if) and \
+ our_remote == their_remote:
+ raise ConfigError(f'Missing required "ip key" parameter when '\
+ 'running more then one GRE based tunnel on the '\
+ 'same source-interface/source-address')
# Keys are not allowed with ipip and sit tunnels
if tunnel['encapsulation'] in ['ipip', 'sit']:
@@ -134,6 +158,7 @@ def verify(tunnel):
verify_mtu_ipv6(tunnel)
verify_address(tunnel)
verify_vrf(tunnel)
+ verify_mirror_redirect(tunnel)
if 'source_interface' in tunnel:
verify_interface_exists(tunnel['source_interface'])
diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py
index 57950ffea..f4b0436af 100755
--- a/src/conf_mode/interfaces-vti.py
+++ b/src/conf_mode/interfaces-vti.py
@@ -19,6 +19,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import VTIIf
from vyos.util import dict_search
from vyos import ConfigError
@@ -35,10 +36,11 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'vti']
- vti = get_interface_dict(conf, base)
+ _, vti = get_interface_dict(conf, base)
return vti
def verify(vti):
+ verify_mirror_redirect(vti)
return None
def generate(vti):
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 804f2d14f..f44d754ba 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-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
@@ -19,11 +19,15 @@ import os
from sys import exit
from netifaces import interfaces
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_source_interface
from vyos.ifconfig import Interface
from vyos.ifconfig import VXLANIf
@@ -34,15 +38,40 @@ airbag.enable()
def get_config(config=None):
"""
- Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
- interface name will be added or a deleted flag
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least
+ the interface name will be added or a deleted flag
"""
if config:
conf = config
else:
conf = Config()
base = ['interfaces', 'vxlan']
- vxlan = get_interface_dict(conf, base)
+ ifname, vxlan = get_interface_dict(conf, base)
+
+ # VXLAN interfaces are picky and require recreation if certain parameters
+ # change. But a VXLAN interface should - of course - not be re-created if
+ # it's description or IP address is adjusted. Feels somehow logic doesn't it?
+ for cli_option in ['external', 'gpe', 'group', 'port', 'remote',
+ 'source-address', 'source-interface', 'vni']:
+ if leaf_node_changed(conf, base + [ifname, cli_option]):
+ vxlan.update({'rebuild_required': {}})
+
+ if is_node_changed(conf, base + [ifname, 'parameters']):
+ vxlan.update({'rebuild_required': {}})
+
+ # We need to verify that no other VXLAN tunnel is configured when external
+ # mode is in use - Linux Kernel limitation
+ conf.set_level(base)
+ vxlan['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ # This if-clause is just to be sure - it will always evaluate to true
+ ifname = vxlan['ifname']
+ if ifname in vxlan['other_tunnels']:
+ del vxlan['other_tunnels'][ifname]
+ if len(vxlan['other_tunnels']) == 0:
+ del vxlan['other_tunnels']
return vxlan
@@ -52,19 +81,31 @@ def verify(vxlan):
return None
if int(vxlan['mtu']) < 1500:
- print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
+ Warning('RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
if 'group' in vxlan:
if 'source_interface' not in vxlan:
- raise ConfigError('Multicast VXLAN requires an underlaying interface ')
-
+ raise ConfigError('Multicast VXLAN requires an underlaying interface')
verify_source_interface(vxlan)
if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan):
raise ConfigError('Group, remote or source-address must be configured')
- if 'vni' not in vxlan:
- raise ConfigError('Must configure VNI for VXLAN')
+ if 'vni' not in vxlan and 'external' not in vxlan:
+ raise ConfigError(
+ 'Must either configure VXLAN "vni" or use "external" CLI option!')
+
+ if {'external', 'vni'} <= set(vxlan):
+ raise ConfigError('Can not specify both "external" and "VNI"!')
+
+ if {'external', 'other_tunnels'} <= set(vxlan):
+ other_tunnels = ', '.join(vxlan['other_tunnels'])
+ raise ConfigError(f'Only one VXLAN tunnel is supported when "external" '\
+ f'CLI option is used. Additional tunnels: {other_tunnels}')
+
+ if 'gpe' in vxlan and 'external' not in vxlan:
+ raise ConfigError(f'VXLAN-GPE is only supported when "external" '\
+ f'CLI option is used.')
if 'source_interface' in vxlan:
# VXLAN adds at least an overhead of 50 byte - we need to check the
@@ -81,22 +122,42 @@ def verify(vxlan):
raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)')
+ # Check for mixed IPv4 and IPv6 addresses
+ protocol = None
+ if 'source_address' in vxlan:
+ if is_ipv6(vxlan['source_address']):
+ protocol = 'ipv6'
+ else:
+ protocol = 'ipv4'
+
+ if 'remote' in vxlan:
+ error_msg = 'Can not mix both IPv4 and IPv6 for VXLAN underlay'
+ for remote in vxlan['remote']:
+ if is_ipv6(remote):
+ if protocol == 'ipv4':
+ raise ConfigError(error_msg)
+ protocol = 'ipv6'
+ else:
+ if protocol == 'ipv6':
+ raise ConfigError(error_msg)
+ protocol = 'ipv4'
+
verify_mtu_ipv6(vxlan)
verify_address(vxlan)
+ verify_mirror_redirect(vxlan)
return None
-
def generate(vxlan):
return None
-
def apply(vxlan):
# Check if the VXLAN interface already exists
- if vxlan['ifname'] in interfaces():
- v = VXLANIf(vxlan['ifname'])
- # VXLAN is super picky and the tunnel always needs to be recreated,
- # thus we can simply always delete it first.
- v.remove()
+ if 'rebuild_required' in vxlan or 'delete' in vxlan:
+ if vxlan['ifname'] in interfaces():
+ v = VXLANIf(vxlan['ifname'])
+ # VXLAN is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ v.remove()
if 'deleted' not in vxlan:
# Finally create the new interface
@@ -105,7 +166,6 @@ def apply(vxlan):
return None
-
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index da64dd076..180ffa507 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 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
@@ -28,6 +28,7 @@ from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import WireGuardIf
from vyos.util import check_kmod
from vyos.util import check_port_availability
@@ -45,17 +46,17 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'wireguard']
- wireguard = get_interface_dict(conf, base)
+ ifname, wireguard = get_interface_dict(conf, base)
# Check if a port was changed
- wireguard['port_changed'] = leaf_node_changed(conf, ['port'])
+ wireguard['port_changed'] = leaf_node_changed(conf, base + [ifname, 'port'])
# Determine which Wireguard peer has been removed.
# Peers can only be removed with their public key!
dict = {}
- tmp = node_changed(conf, ['peer'], key_mangling=('-', '_'))
+ tmp = node_changed(conf, base + [ifname, 'peer'], key_mangling=('-', '_'))
for peer in (tmp or []):
- public_key = leaf_node_changed(conf, ['peer', peer, 'public_key'])
+ public_key = leaf_node_changed(conf, base + [ifname, 'peer', peer, 'public_key'])
if public_key:
dict = dict_merge({'peer_remove' : {peer : {'public_key' : public_key[0]}}}, dict)
wireguard.update(dict)
@@ -70,6 +71,7 @@ def verify(wireguard):
verify_mtu_ipv6(wireguard)
verify_address(wireguard)
verify_vrf(wireguard)
+ verify_mirror_redirect(wireguard)
if 'private_key' not in wireguard:
raise ConfigError('Wireguard private-key not defined')
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index af35b5f03..d34297063 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -27,6 +27,7 @@ from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_dhcpv6
from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ifconfig import WiFiIf
@@ -75,15 +76,19 @@ def get_config(config=None):
conf = Config()
base = ['interfaces', 'wireless']
- wifi = get_interface_dict(conf, base)
+ ifname, wifi = get_interface_dict(conf, base)
# Cleanup "delete" default values when required user selectable values are
# not defined at all
- tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+ tmp = conf.get_config_dict(base + [ifname], key_mangling=('-', '_'),
+ get_first_key=True)
if not (dict_search('security.wpa.passphrase', tmp) or
dict_search('security.wpa.radius', tmp)):
if 'deleted' not in wifi:
del wifi['security']['wpa']
+ # if 'security' key is empty, drop it too
+ if len(wifi['security']) == 0:
+ del wifi['security']
# defaults include RADIUS server specifics per TAG node which need to be
# added to individual RADIUS servers instead - so we can simply delete them
@@ -189,6 +194,7 @@ def verify(wifi):
verify_address(wifi)
verify_vrf(wifi)
+ verify_mirror_redirect(wifi)
# use common function to verify VLAN configuration
verify_vlan_config(wifi)
@@ -242,11 +248,11 @@ def generate(wifi):
# render appropriate new config files depending on access-point or station mode
if wifi['type'] == 'access-point':
- render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl',
+ render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2',
wifi)
elif wifi['type'] == 'station':
- render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl',
+ render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.j2',
wifi)
return None
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index f013e5411..e275ace84 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.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
@@ -17,21 +17,29 @@
import os
from sys import exit
+from time import sleep
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_authentication
from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
from vyos.ifconfig import WWANIf
from vyos.util import cmd
from vyos.util import call
from vyos.util import dict_search
from vyos.util import DEVNULL
+from vyos.util import is_systemd_service_active
+from vyos.util import write_file
from vyos import ConfigError
from vyos import airbag
airbag.enable()
+service_name = 'ModemManager.service'
+cron_script = '/etc/cron.d/vyos-wwan'
+
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -42,7 +50,41 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'wwan']
- wwan = get_interface_dict(conf, base)
+ ifname, wwan = get_interface_dict(conf, base)
+
+ # We should only terminate the WWAN session if critical parameters change.
+ # All parameters that can be changed on-the-fly (like interface description)
+ # should not lead to a reconnect!
+ tmp = is_node_changed(conf, base + [ifname, 'address'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = is_node_changed(conf, base + [ifname, 'apn'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = is_node_changed(conf, base + [ifname, 'disable'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = is_node_changed(conf, base + [ifname, 'vrf'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = is_node_changed(conf, base + [ifname, 'authentication'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = is_node_changed(conf, base + [ifname, 'ipv6', 'address', 'autoconf'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ # We need to know the amount of other WWAN interfaces as ModemManager needs
+ # to be started or stopped.
+ conf.set_level(base)
+ _, wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ # This if-clause is just to be sure - it will always evaluate to true
+ if ifname in wwan['other_interfaces']:
+ del wwan['other_interfaces'][ifname]
+ if len(wwan['other_interfaces']) == 0:
+ del wwan['other_interfaces']
return wwan
@@ -57,42 +99,84 @@ def verify(wwan):
verify_interface_exists(ifname)
verify_authentication(wwan)
verify_vrf(wwan)
+ verify_mirror_redirect(wwan)
return None
def generate(wwan):
+ if 'deleted' in wwan:
+ # We are the last WWAN interface - there are no other ones remaining
+ # thus the cronjob needs to go away, too
+ if 'other_interfaces' not in wwan:
+ if os.path.exists(cron_script):
+ os.unlink(cron_script)
+ return None
+
+ # Install cron triggered helper script to re-dial WWAN interfaces on
+ # disconnect - e.g. happens during RF signal loss. The script watches every
+ # WWAN interface - so there is only one instance.
+ if not os.path.exists(cron_script):
+ write_file(cron_script, '*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py')
+
return None
def apply(wwan):
- # we only need the modem number. wwan0 -> 0, wwan1 -> 1
- modem = wwan['ifname'].lstrip('wwan')
- base_cmd = f'mmcli --modem {modem}'
- # Number of bearers is limited - always disconnect first
- cmd(f'{base_cmd} --simple-disconnect')
+ # ModemManager is required to dial WWAN connections - one instance is
+ # required to serve all modems. Activate ModemManager on first invocation
+ # of any WWAN interface.
+ if not is_systemd_service_active(service_name):
+ cmd(f'systemctl start {service_name}')
+
+ counter = 100
+ # Wait until a modem is detected and then we can continue
+ while counter > 0:
+ counter -= 1
+ tmp = cmd('mmcli -L')
+ if tmp != 'No modems were found':
+ break
+ sleep(0.250)
+
+ if 'shutdown_required' in wwan:
+ # we only need the modem number. wwan0 -> 0, wwan1 -> 1
+ modem = wwan['ifname'].lstrip('wwan')
+ base_cmd = f'mmcli --modem {modem}'
+ # Number of bearers is limited - always disconnect first
+ cmd(f'{base_cmd} --simple-disconnect')
w = WWANIf(wwan['ifname'])
if 'deleted' in wwan or 'disable' in wwan:
w.remove()
+
+ # We are the last WWAN interface - there are no other WWAN interfaces
+ # remaining, thus we can stop ModemManager and free resources.
+ if 'other_interfaces' not in wwan:
+ cmd(f'systemctl stop {service_name}')
+ # Clean CRON helper script which is used for to re-connect when
+ # RF signal is lost
+ if os.path.exists(cron_script):
+ os.unlink(cron_script)
+
return None
- ip_type = 'ipv4'
- slaac = dict_search('ipv6.address.autoconf', wwan) != None
- if 'address' in wwan:
- if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac):
- ip_type = 'ipv4v6'
- elif 'dhcpv6' in wwan['address'] or slaac:
- ip_type = 'ipv6'
- elif 'dhcp' in wwan['address']:
- ip_type = 'ipv4'
-
- options = f'ip-type={ip_type},apn=' + wwan['apn']
- if 'authentication' in wwan:
- options += ',user={user},password={password}'.format(**wwan['authentication'])
-
- command = f'{base_cmd} --simple-connect="{options}"'
- call(command, stdout=DEVNULL)
- w.update(wwan)
+ if 'shutdown_required' in wwan:
+ ip_type = 'ipv4'
+ slaac = dict_search('ipv6.address.autoconf', wwan) != None
+ if 'address' in wwan:
+ if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac):
+ ip_type = 'ipv4v6'
+ elif 'dhcpv6' in wwan['address'] or slaac:
+ ip_type = 'ipv6'
+ elif 'dhcp' in wwan['address']:
+ ip_type = 'ipv4'
+
+ options = f'ip-type={ip_type},apn=' + wwan['apn']
+ if 'authentication' in wwan:
+ options += ',user={user},password={password}'.format(**wwan['authentication'])
+ command = f'{base_cmd} --simple-connect="{options}"'
+ call(command, stdout=DEVNULL)
+
+ w.update(wwan)
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py
index 082c3e128..c703c1fe0 100755
--- a/src/conf_mode/lldp.py
+++ b/src/conf_mode/lldp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2017-2020 VyOS maintainers and contributors
+# Copyright (C) 2017-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
@@ -15,19 +15,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import re
-from copy import deepcopy
from sys import exit
+from vyos.base import Warning
from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.validate import is_addr_assigned
from vyos.validate import is_loopback_addr
from vyos.version import get_version_data
-from vyos import ConfigError
from vyos.util import call
+from vyos.util import dict_search
+from vyos.xml import defaults
from vyos.template import render
-
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -35,178 +36,73 @@ config_file = "/etc/default/lldpd"
vyos_config_file = "/etc/lldpd.d/01-vyos.conf"
base = ['service', 'lldp']
-default_config_data = {
- "options": '',
- "interface_list": '',
- "location": ''
-}
-
-def get_options(config):
- options = {}
- config.set_level(base)
-
- options['listen_vlan'] = config.exists('listen-vlan')
- options['mgmt_addr'] = []
- for addr in config.return_values('management-address'):
- if is_addr_assigned(addr) and not is_loopback_addr(addr):
- options['mgmt_addr'].append(addr)
- else:
- message = 'WARNING: LLDP management address {0} invalid - '.format(addr)
- if is_loopback_addr(addr):
- message += '(loopback address).'
- else:
- message += 'address not found.'
- print(message)
-
- snmp = config.exists('snmp enable')
- options["snmp"] = snmp
- if snmp:
- config.set_level('')
- options["sys_snmp"] = config.exists('service snmp')
- config.set_level(base)
-
- config.set_level(base + ['legacy-protocols'])
- options['cdp'] = config.exists('cdp')
- options['edp'] = config.exists('edp')
- options['fdp'] = config.exists('fdp')
- options['sonmp'] = config.exists('sonmp')
-
- # start with an unknown version information
- version_data = get_version_data()
- options['description'] = version_data['version']
- options['listen_on'] = []
-
- return options
-
-def get_interface_list(config):
- config.set_level(base)
- intfs_names = config.list_nodes(['interface'])
- if len(intfs_names) < 0:
- return 0
-
- interface_list = []
- for name in intfs_names:
- config.set_level(base + ['interface', name])
- disable = config.exists(['disable'])
- intf = {
- 'name': name,
- 'disable': disable
- }
- interface_list.append(intf)
- return interface_list
-
-
-def get_location_intf(config, name):
- path = base + ['interface', name]
- config.set_level(path)
-
- config.set_level(path + ['location'])
- elin = ''
- coordinate_based = {}
-
- if config.exists('elin'):
- elin = config.return_value('elin')
-
- if config.exists('coordinate-based'):
- config.set_level(path + ['location', 'coordinate-based'])
-
- coordinate_based['latitude'] = config.return_value(['latitude'])
- coordinate_based['longitude'] = config.return_value(['longitude'])
-
- coordinate_based['altitude'] = '0'
- if config.exists(['altitude']):
- coordinate_based['altitude'] = config.return_value(['altitude'])
-
- coordinate_based['datum'] = 'WGS84'
- if config.exists(['datum']):
- coordinate_based['datum'] = config.return_value(['datum'])
-
- intf = {
- 'name': name,
- 'elin': elin,
- 'coordinate_based': coordinate_based
-
- }
- return intf
-
-
-def get_location(config):
- config.set_level(base)
- intfs_names = config.list_nodes(['interface'])
- if len(intfs_names) < 0:
- return 0
-
- if config.exists('disable'):
- return 0
-
- intfs_location = []
- for name in intfs_names:
- intf = get_location_intf(config, name)
- intfs_location.append(intf)
-
- return intfs_location
-
-
def get_config(config=None):
- lldp = deepcopy(default_config_data)
if config:
conf = config
else:
conf = Config()
+
if not conf.exists(base):
- return None
- else:
- lldp['options'] = get_options(conf)
- lldp['interface_list'] = get_interface_list(conf)
- lldp['location'] = get_location(conf)
+ return {}
- return lldp
+ lldp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ if conf.exists(['service', 'snmp']):
+ lldp['system_snmp_enabled'] = ''
+
+ version_data = get_version_data()
+ lldp['version'] = version_data['version']
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ # location coordinates have a default value
+ if 'interface' in lldp:
+ for interface, interface_config in lldp['interface'].items():
+ default_values = defaults(base + ['interface'])
+ if dict_search('location.coordinate_based', interface_config) == None:
+ # no location specified - no need to add defaults
+ del default_values['location']['coordinate_based']['datum']
+ del default_values['location']['coordinate_based']['altitude']
+
+ # cleanup default_values dictionary from inner to outer
+ # this might feel overkill here, but it does support easy extension
+ # in the future with additional default values
+ if len(default_values['location']['coordinate_based']) == 0:
+ del default_values['location']['coordinate_based']
+ if len(default_values['location']) == 0:
+ del default_values['location']
+
+ lldp['interface'][interface] = dict_merge(default_values,
+ lldp['interface'][interface])
+
+ return lldp
def verify(lldp):
# bail out early - looks like removal from running config
if lldp is None:
return
- # check location
- for location in lldp['location']:
- # check coordinate-based
- if len(location['coordinate_based']) > 0:
- # check longitude and latitude
- if not location['coordinate_based']['longitude']:
- raise ConfigError('Must define longitude for interface {0}'.format(location['name']))
-
- if not location['coordinate_based']['latitude']:
- raise ConfigError('Must define latitude for interface {0}'.format(location['name']))
-
- if not re.match(r'^(\d+)(\.\d+)?[nNsS]$', location['coordinate_based']['latitude']):
- raise ConfigError('Invalid location for interface {0}:\n' \
- 'latitude should be a number followed by S or N'.format(location['name']))
-
- if not re.match(r'^(\d+)(\.\d+)?[eEwW]$', location['coordinate_based']['longitude']):
- raise ConfigError('Invalid location for interface {0}:\n' \
- 'longitude should be a number followed by E or W'.format(location['name']))
-
- # check altitude and datum if exist
- if location['coordinate_based']['altitude']:
- if not re.match(r'^[-+0-9\.]+$', location['coordinate_based']['altitude']):
- raise ConfigError('Invalid location for interface {0}:\n' \
- 'altitude should be a positive or negative number'.format(location['name']))
-
- if location['coordinate_based']['datum']:
- if not re.match(r'^(WGS84|NAD83|MLLW)$', location['coordinate_based']['datum']):
- raise ConfigError("Invalid location for interface {0}:\n' \
- 'datum should be WGS84, NAD83, or MLLW".format(location['name']))
-
- # check elin
- elif location['elin']:
- if not re.match(r'^[0-9]{10,25}$', location['elin']):
- raise ConfigError('Invalid location for interface {0}:\n' \
- 'ELIN number must be between 10-25 numbers'.format(location['name']))
+ if 'management_address' in lldp:
+ for address in lldp['management_address']:
+ message = f'LLDP management address "{address}" is invalid'
+ if is_loopback_addr(address):
+ Warning(f'{message} - loopback address')
+ elif not is_addr_assigned(address):
+ Warning(f'{message} - not assigned to any interface')
+
+ if 'interface' in lldp:
+ for interface, interface_config in lldp['interface'].items():
+ # bail out early if no location info present in interface config
+ if 'location' not in interface_config:
+ continue
+ if 'coordinate_based' in interface_config['location']:
+ if not {'latitude', 'latitude'} <= set(interface_config['location']['coordinate_based']):
+ raise ConfigError(f'Must define both longitude and latitude for "{interface}" location!')
# check options
- if lldp['options']['snmp']:
- if not lldp['options']['sys_snmp']:
+ if 'snmp' in lldp and 'enable' in lldp['snmp']:
+ if 'system_snmp_enabled' not in lldp:
raise ConfigError('SNMP must be configured to enable LLDP SNMP')
@@ -215,29 +111,17 @@ def generate(lldp):
if lldp is None:
return
- # generate listen on interfaces
- for intf in lldp['interface_list']:
- tmp = ''
- # add exclamation mark if interface is disabled
- if intf['disable']:
- tmp = '!'
-
- tmp += intf['name']
- lldp['options']['listen_on'].append(tmp)
-
- # generate /etc/default/lldpd
- render(config_file, 'lldp/lldpd.tmpl', lldp)
- # generate /etc/lldpd.d/01-vyos.conf
- render(vyos_config_file, 'lldp/vyos.conf.tmpl', lldp)
-
+ render(config_file, 'lldp/lldpd.j2', lldp)
+ render(vyos_config_file, 'lldp/vyos.conf.j2', lldp)
def apply(lldp):
+ systemd_service = 'lldpd.service'
if lldp:
# start/restart lldp service
- call('systemctl restart lldpd.service')
+ call(f'systemctl restart {systemd_service}')
else:
# LLDP service has been terminated
- call('systemctl stop lldpd.service')
+ call(f'systemctl stop {systemd_service}')
if os.path.isfile(config_file):
os.unlink(config_file)
if os.path.isfile(vyos_config_file):
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 59939d0fb..85819a77e 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
@@ -23,11 +23,13 @@ from platform import release as kernel_version
from sys import exit
from netifaces import interfaces
+from vyos.base import Warning
from vyos.config import Config
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 +44,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 +95,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 +107,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 +120,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
@@ -142,14 +143,14 @@ def verify(nat):
raise ConfigError(f'{err_msg} outbound-interface not specified')
if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces():
- print(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
+ Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
addr = dict_search('translation.address', config)
if addr != None:
if addr != 'masquerade' and not is_ip_network(addr):
for ip in addr.split('-'):
if not is_addr_assigned(ip):
- print(f'WARNING: IP address {ip} does not exist on the system!')
+ Warning(f'IP address {ip} does not exist on the system!')
elif 'exclude' not in config:
raise ConfigError(f'{err_msg}\n' \
'translation address not specified')
@@ -167,7 +168,7 @@ def verify(nat):
'inbound-interface not specified')
else:
if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces():
- print(f'WARNING: rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system')
+ Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system')
if dict_search('translation.address', config) == None and 'exclude' not in config:
@@ -180,14 +181,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.j2', nat)
+
+ # dry-run newly generated configuration
+ tmp = run(f'nft -c -f {nftables_nat_config}')
+ if tmp > 0:
+ if os.path.exists(nftables_nat_config):
+ os.unlink(nftables_nat_config)
+ 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..0972151a0 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -21,6 +21,7 @@ import os
from sys import exit
from netifaces import interfaces
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.template import render
@@ -35,7 +36,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 +80,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 +93,10 @@ def get_config(config=None):
nat['helper_functions'] = 'add'
# Retrieve current table handler positions
- nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
- nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
- nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
- nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_IGNORE')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_PREROUTING_HOOK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_IGNORE')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_OUTPUT_HOOK')
else:
nat['helper_functions'] = 'has'
@@ -117,12 +118,12 @@ def verify(nat):
raise ConfigError(f'{err_msg} outbound-interface not specified')
if config['outbound_interface'] not in interfaces():
- raise ConfigError(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
+ raise ConfigError(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
addr = dict_search('translation.address', config)
if addr != None:
if addr != 'masquerade' and not is_ipv6(addr):
- raise ConfigError(f'Warning: IPv6 address {addr} is not a valid address')
+ raise ConfigError(f'IPv6 address {addr} is not a valid address')
else:
raise ConfigError(f'{err_msg} translation address not specified')
@@ -140,27 +141,27 @@ def verify(nat):
'inbound-interface not specified')
else:
if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces():
- print(f'WARNING: rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system')
+ Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system')
return None
def generate(nat):
- render(iptables_nat_config, 'firewall/nftables-nat66.tmpl', nat, permission=0o755)
- render(ndppd_config, 'ndppd/ndppd.conf.tmpl', nat, permission=0o755)
+ render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat, permission=0o755)
+ render(ndppd_config, 'ndppd/ndppd.conf.j2', nat, permission=0o755)
return None
def apply(nat):
if not nat:
return None
- cmd(f'{iptables_nat_config}')
+ cmd(f'{nftables_nat66_config}')
if 'deleted' in nat or not dict_search('source.rule', nat):
cmd('systemctl stop ndppd')
if os.path.isfile(ndppd_config):
os.unlink(ndppd_config)
else:
cmd('systemctl restart ndppd')
- if os.path.isfile(iptables_nat_config):
- os.unlink(iptables_nat_config)
+ if os.path.isfile(nftables_nat66_config):
+ os.unlink(nftables_nat66_config)
return None
diff --git a/src/conf_mode/netns.py b/src/conf_mode/netns.py
new file mode 100755
index 000000000..0924eb616
--- /dev/null
+++ b/src/conf_mode/netns.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+from tempfile import NamedTemporaryFile
+
+from vyos.config import Config
+from vyos.configdict import node_changed
+from vyos.ifconfig import Interface
+from vyos.util import call
+from vyos.util import dict_search
+from vyos.util import get_interface_config
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+
+def netns_interfaces(c, match):
+ """
+ get NETNS bound interfaces
+ """
+ matched = []
+ old_level = c.get_level()
+ c.set_level(['interfaces'])
+ section = c.get_config_dict([], get_first_key=True)
+ for type in section:
+ interfaces = section[type]
+ for name in interfaces:
+ interface = interfaces[name]
+ if 'netns' in interface:
+ v = interface.get('netns', '')
+ if v == match:
+ matched.append(name)
+
+ c.set_level(old_level)
+ return matched
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['netns']
+ netns = conf.get_config_dict(base, get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ # determine which NETNS has been removed
+ for name in node_changed(conf, base + ['name']):
+ if 'netns_remove' not in netns:
+ netns.update({'netns_remove' : {}})
+
+ netns['netns_remove'][name] = {}
+ # get NETNS bound interfaces
+ interfaces = netns_interfaces(conf, name)
+ if interfaces: netns['netns_remove'][name]['interface'] = interfaces
+
+ return netns
+
+def verify(netns):
+ # ensure NETNS is not assigned to any interface
+ if 'netns_remove' in netns:
+ for name, config in netns['netns_remove'].items():
+ if 'interface' in config:
+ raise ConfigError(f'Can not remove NETNS "{name}", it still has '\
+ f'member interfaces!')
+
+ if 'name' in netns:
+ for name, config in netns['name'].items():
+ print(name)
+
+ return None
+
+
+def generate(netns):
+ if not netns:
+ return None
+
+ return None
+
+
+def apply(netns):
+
+ for tmp in (dict_search('netns_remove', netns) or []):
+ if os.path.isfile(f'/run/netns/{tmp}'):
+ call(f'ip netns del {tmp}')
+
+ if 'name' in netns:
+ for name, config in netns['name'].items():
+ if not os.path.isfile(f'/run/netns/{name}'):
+ call(f'ip netns add {name}')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index 52070aabc..0d6ec9ace 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.py
@@ -56,8 +56,8 @@ def generate(ntp):
if not ntp:
return None
- render(config_file, 'ntp/ntpd.conf.tmpl', ntp)
- render(systemd_override, 'ntp/override.conf.tmpl', ntp)
+ render(config_file, 'ntp/ntpd.conf.j2', ntp)
+ render(systemd_override, 'ntp/override.conf.j2', ntp)
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..5de341beb
--- /dev/null
+++ b/src/conf_mode/policy-route.py
@@ -0,0 +1,262 @@
+#!/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.base import Warning
+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]
+
+ if group_name.startswith('!'):
+ 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(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:
+ Warning(f'{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.j2', 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 1a03d520b..ef6008140 100755
--- a/src/conf_mode/policy.py
+++ b/src/conf_mode/policy.py
@@ -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
@@ -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,10 @@ def verify(policy):
if 'prefix' not in rule_config:
raise ConfigError(f'A prefix {mandatory_error}')
+ if rule_config in entries:
+ raise ConfigError(f'Rule "{rule}" contains a duplicate prefix definition!')
+ entries.append(rule_config)
+
# route-maps tend to be a bit more complex so they get their own verify() section
if 'route_map' in policy:
@@ -171,10 +176,8 @@ def verify(policy):
def generate(policy):
if not policy:
- policy['new_frr_config'] = ''
return None
-
- policy['new_frr_config'] = render_to_string('frr/policy.frr.tmpl', policy)
+ policy['new_frr_config'] = render_to_string('frr/policy.frr.j2', policy)
return None
def apply(policy):
@@ -190,8 +193,9 @@ def apply(policy):
frr_cfg.modify_section(r'^bgp community-list .*')
frr_cfg.modify_section(r'^bgp extcommunity-list .*')
frr_cfg.modify_section(r'^bgp large-community-list .*')
- frr_cfg.modify_section(r'^route-map .*')
- frr_cfg.add_before('^line vty', policy['new_frr_config'])
+ frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', remove_stop_mark=True)
+ if 'new_frr_config' in policy:
+ frr_cfg.add_before(frr.default_add_before, policy['new_frr_config'])
frr_cfg.commit_configuration(bgp_daemon)
# The route-map used for the FIB (zebra) is part of the zebra daemon
@@ -200,19 +204,11 @@ def apply(policy):
frr_cfg.modify_section(r'^ipv6 access-list .*')
frr_cfg.modify_section(r'^ip prefix-list .*')
frr_cfg.modify_section(r'^ipv6 prefix-list .*')
- frr_cfg.modify_section(r'^route-map .*')
- frr_cfg.add_before('^line vty', policy['new_frr_config'])
+ frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', remove_stop_mark=True)
+ if 'new_frr_config' in policy:
+ frr_cfg.add_before(frr.default_add_before, policy['new_frr_config'])
frr_cfg.commit_configuration(zebra_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if policy['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(zebra_daemon)
-
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index 539fd7b8e..0436abaf9 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -16,10 +16,9 @@
import os
-from sys import exit
-
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configverify import verify_vrf
from vyos.template import is_ipv6
from vyos.template import render_to_string
from vyos.validate import is_ipv6_link_local
@@ -35,8 +34,9 @@ def get_config(config=None):
else:
conf = Config()
base = ['protocols', 'bfd']
- bfd = conf.get_config_dict(base, get_first_key=True)
-
+ bfd = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
# Bail out early if configuration tree does not exist
if not conf.exists(base):
return bfd
@@ -79,28 +79,37 @@ def verify(bfd):
# multihop and echo-mode cannot be used together
if 'echo_mode' in peer_config:
- raise ConfigError('Multihop and echo-mode cannot be used together')
+ raise ConfigError('BFD multihop and echo-mode cannot be used together')
# multihop doesn't accept interface names
if 'source' in peer_config and 'interface' in peer_config['source']:
- raise ConfigError('Multihop and source interface cannot be used together')
+ raise ConfigError('BFD multihop and source interface cannot be used together')
+
+ if 'profile' in peer_config:
+ profile_name = peer_config['profile']
+ if 'profile' not in bfd or profile_name not in bfd['profile']:
+ raise ConfigError(f'BFD profile "{profile_name}" does not exist!')
+
+ if 'vrf' in peer_config:
+ verify_vrf(peer_config)
return None
def generate(bfd):
if not bfd:
- bfd['new_frr_config'] = ''
return None
-
- bfd['new_frr_config'] = render_to_string('frr/bfdd.frr.tmpl', bfd)
+ bfd['new_frr_config'] = render_to_string('frr/bfdd.frr.j2', bfd)
def apply(bfd):
+ bfd_daemon = 'bfdd'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration()
- frr_cfg.modify_section('^bfd', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bfd['new_frr_config'])
- frr_cfg.commit_configuration()
+ frr_cfg.load_configuration(bfd_daemon)
+ frr_cfg.modify_section('^bfd', stop_pattern='^exit', remove_stop_mark=True)
+ if 'new_frr_config' in bfd:
+ frr_cfg.add_before(frr.default_add_before, bfd['new_frr_config'])
+ frr_cfg.commit_configuration(bfd_daemon)
return None
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 68284e0f9..cd46cbcb4 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.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
@@ -138,13 +138,20 @@ def verify(bgp):
if asn == bgp['local_as']:
raise ConfigError('Cannot have local-as same as BGP AS number')
+ # Neighbor AS specified for local-as and remote-as can not be the same
+ if dict_search('remote_as', peer_config) == asn:
+ raise ConfigError(f'Neighbor "{peer}" has local-as specified which is '\
+ 'the same as remote-as, this is not allowed!')
+
# ttl-security and ebgp-multihop can't be used in the same configration
if 'ebgp_multihop' in peer_config and 'ttl_security' in peer_config:
raise ConfigError('You can not set both ebgp-multihop and ttl-security hops')
- # Check if neighbor has both override capability and strict capability match configured at the same time.
+ # Check if neighbor has both override capability and strict capability match
+ # configured at the same time.
if 'override_capability' in peer_config and 'strict_capability_match' in peer_config:
- raise ConfigError(f'Neighbor "{peer}" cannot have both override-capability and strict-capability-match configured at the same time!')
+ raise ConfigError(f'Neighbor "{peer}" cannot have both override-capability and '\
+ 'strict-capability-match configured at the same time!')
# Check spaces in the password
if 'password' in peer_config and ' ' in peer_config['password']:
@@ -157,15 +164,39 @@ def verify(bgp):
if not verify_remote_as(peer_config, bgp):
raise ConfigError(f'Neighbor "{peer}" remote-as must be set!')
+ # Peer-group member cannot override remote-as of peer-group
+ if 'peer_group' in peer_config:
+ peer_group = peer_config['peer_group']
+ if 'remote_as' in peer_config and 'remote_as' in bgp['peer_group'][peer_group]:
+ raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!')
+ if 'interface' in peer_config:
+ if 'peer_group' in peer_config['interface']:
+ peer_group = peer_config['interface']['peer_group']
+ if 'remote_as' in peer_config['interface'] and 'remote_as' in bgp['peer_group'][peer_group]:
+ raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!')
+ if 'v6only' in peer_config['interface']:
+ if 'peer_group' in peer_config['interface']['v6only']:
+ peer_group = peer_config['interface']['v6only']['peer_group']
+ if 'remote_as' in peer_config['interface']['v6only'] and 'remote_as' in bgp['peer_group'][peer_group]:
+ raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!')
+
# Only checks for ipv4 and ipv6 neighbors
# Check if neighbor address is assigned as system interface address
- if is_ip(peer) and is_addr_assigned(peer):
- raise ConfigError(f'Can not configure a local address as neighbor "{peer}"')
+ vrf = None
+ vrf_error_msg = f' in default VRF!'
+ if 'vrf' in bgp:
+ vrf = bgp['vrf']
+ vrf_error_msg = f' in VRF "{vrf}"!'
+
+ if is_ip(peer) and is_addr_assigned(peer, vrf):
+ raise ConfigError(f'Can not configure local address as neighbor "{peer}"{vrf_error_msg}')
elif is_interface(peer):
if 'peer_group' in peer_config:
raise ConfigError(f'peer-group must be set under the interface node of "{peer}"')
if 'remote_as' in peer_config:
raise ConfigError(f'remote-as must be set under the interface node of "{peer}"')
+ if 'source_interface' in peer_config['interface']:
+ raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"')
for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec',
'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec',
@@ -183,6 +214,33 @@ def verify(bgp):
raise ConfigError(f'Neighbor "{peer}" cannot have both ipv6-unicast and ipv6-labeled-unicast configured at the same time!')
afi_config = peer_config['address_family'][afi]
+
+ if 'conditionally_advertise' in afi_config:
+ if 'advertise_map' not in afi_config['conditionally_advertise']:
+ raise ConfigError('Must speficy advertise-map when conditionally-advertise is in use!')
+ # Verify advertise-map (which is a route-map) exists
+ verify_route_map(afi_config['conditionally_advertise']['advertise_map'], bgp)
+
+ if ('exist_map' not in afi_config['conditionally_advertise'] and
+ 'non_exist_map' not in afi_config['conditionally_advertise']):
+ raise ConfigError('Must either speficy exist-map or non-exist-map when ' \
+ 'conditionally-advertise is in use!')
+
+ if {'exist_map', 'non_exist_map'} <= set(afi_config['conditionally_advertise']):
+ raise ConfigError('Can not specify both exist-map and non-exist-map for ' \
+ 'conditionally-advertise!')
+
+ if 'exist_map' in afi_config['conditionally_advertise']:
+ verify_route_map(afi_config['conditionally_advertise']['exist_map'], bgp)
+
+ if 'non_exist_map' in afi_config['conditionally_advertise']:
+ verify_route_map(afi_config['conditionally_advertise']['non_exist_map'], bgp)
+
+ # T4332: bgp deterministic-med cannot be disabled while addpath-tx-bestpath-per-AS is in use
+ if 'addpath_tx_per_as' in afi_config:
+ if dict_search('parameters.deterministic_med', bgp) == None:
+ raise ConfigError('addpath-tx-per-as requires BGP deterministic-med paramtere to be set!')
+
# Validate if configured Prefix list exists
if 'prefix_list' in afi_config:
for tmp in ['import', 'export']:
@@ -255,26 +313,16 @@ def verify(bgp):
tmp = dict_search(f'route_map.vpn.{export_import}', afi_config)
if tmp: verify_route_map(tmp, bgp)
- if afi in ['l2vpn_evpn'] and 'vrf' not in bgp:
- # Some L2VPN EVPN AFI options are only supported under VRF
- if 'vni' in afi_config:
- for vni, vni_config in afi_config['vni'].items():
- if 'rd' in vni_config:
- raise ConfigError('VNI route-distinguisher is only supported under EVPN VRF')
- if 'route_target' in vni_config:
- raise ConfigError('VNI route-target is only supported under EVPN VRF')
return None
def generate(bgp):
if not bgp or 'deleted' in bgp:
- bgp['frr_bgpd_config'] = ''
- bgp['frr_zebra_config'] = ''
return None
- bgp['protocol'] = 'bgp' # required for frr/vrf.route-map.frr.tmpl
- bgp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.tmpl', bgp)
- bgp['frr_bgpd_config'] = render_to_string('frr/bgpd.frr.tmpl', bgp)
+ bgp['protocol'] = 'bgp' # required for frr/vrf.route-map.frr.j2
+ bgp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', bgp)
+ bgp['frr_bgpd_config'] = render_to_string('frr/bgpd.frr.j2', bgp)
return None
@@ -287,8 +335,9 @@ def apply(bgp):
# The route-map used for the FIB (zebra) is part of the zebra daemon
frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'(\s+)?ip protocol bgp route-map [-a-zA-Z0-9.]+$', '', '(\s|!)')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['frr_zebra_config'])
+ frr_cfg.modify_section(r'(\s+)?ip protocol bgp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in bgp:
+ frr_cfg.add_before(frr.default_add_before, bgp['frr_zebra_config'])
frr_cfg.commit_configuration(zebra_daemon)
# Generate empty helper string which can be ammended to FRR commands, it
@@ -298,13 +347,11 @@ def apply(bgp):
vrf = ' vrf ' + bgp['vrf']
frr_cfg.load_configuration(bgp_daemon)
- frr_cfg.modify_section(f'^router bgp \d+{vrf}$', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['frr_bgpd_config'])
+ frr_cfg.modify_section(f'^router bgp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+ if 'frr_bgpd_config' in bgp:
+ frr_cfg.add_before(frr.default_add_before, bgp['frr_bgpd_config'])
frr_cfg.commit_configuration(bgp_daemon)
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py
index 28d560d03..65cc2beba 100755
--- a/src/conf_mode/protocols_igmp.py
+++ b/src/conf_mode/protocols_igmp.py
@@ -108,7 +108,7 @@ def generate(igmp):
if igmp is None:
return None
- render(config_file, 'frr/igmp.frr.tmpl', igmp)
+ render(config_file, 'frr/igmp.frr.j2', igmp)
return None
def apply(igmp):
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index 4505e2496..5dafd26d0 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.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
@@ -56,10 +56,10 @@ def get_config(config=None):
# instead of the VRF instance.
if vrf: isis['vrf'] = vrf
- # As we no re-use this Python handler for both VRF and non VRF instances for
- # IS-IS we need to find out if any interfaces changed so properly adjust
- # the FRR configuration and not by acctident change interfaces from a
- # different VRF.
+ # FRR has VRF support for different routing daemons. As interfaces belong
+ # to VRFs - or the global VRF, we need to check for changed interfaces so
+ # that they will be properly rendered for the FRR config. Also this eases
+ # removal of interfaces from the running configuration.
interfaces_removed = node_changed(conf, base + ['interface'])
if interfaces_removed:
isis['interface_removed'] = list(interfaces_removed)
@@ -169,40 +169,50 @@ def verify(isis):
# Segment routing checks
if dict_search('segment_routing.global_block', isis):
- high_label_value = dict_search('segment_routing.global_block.high_label_value', isis)
- low_label_value = dict_search('segment_routing.global_block.low_label_value', isis)
+ g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis)
+ g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis)
- # If segment routing global block high value is blank, throw error
- if (low_label_value and not high_label_value) or (high_label_value and not low_label_value):
- raise ConfigError('Segment routing global block requires both low and high value!')
+ # If segment routing global block high or low value is blank, throw error
+ if not (g_low_label_value or g_high_label_value):
+ raise ConfigError('Segment routing global-block requires both low and high value!')
# If segment routing global block low value is higher than the high value, throw error
- if int(low_label_value) > int(high_label_value):
- raise ConfigError('Segment routing global block low value must be lower than high value')
+ if int(g_low_label_value) > int(g_high_label_value):
+ raise ConfigError('Segment routing global-block low value must be lower than high value')
if dict_search('segment_routing.local_block', isis):
- high_label_value = dict_search('segment_routing.local_block.high_label_value', isis)
- low_label_value = dict_search('segment_routing.local_block.low_label_value', isis)
+ if dict_search('segment_routing.global_block', isis) == None:
+ raise ConfigError('Segment routing local-block requires global-block to be configured!')
- # If segment routing local block high value is blank, throw error
- if (low_label_value and not high_label_value) or (high_label_value and not low_label_value):
- raise ConfigError('Segment routing local block requires both high and low value!')
+ l_high_label_value = dict_search('segment_routing.local_block.high_label_value', isis)
+ l_low_label_value = dict_search('segment_routing.local_block.low_label_value', isis)
- # If segment routing local block low value is higher than the high value, throw error
- if int(low_label_value) > int(high_label_value):
- raise ConfigError('Segment routing local block low value must be lower than high value')
+ # If segment routing local-block high or low value is blank, throw error
+ if not (l_low_label_value or l_high_label_value):
+ raise ConfigError('Segment routing local-block requires both high and low value!')
+
+ # If segment routing local-block low value is higher than the high value, throw error
+ if int(l_low_label_value) > int(l_high_label_value):
+ raise ConfigError('Segment routing local-block low value must be lower than high value')
+
+ # local-block most live outside global block
+ global_range = range(int(g_low_label_value), int(g_high_label_value) +1)
+ local_range = range(int(l_low_label_value), int(l_high_label_value) +1)
+
+ # Check for overlapping ranges
+ if list(set(global_range) & set(local_range)):
+ raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\
+ f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!')
return None
def generate(isis):
if not isis or 'deleted' in isis:
- isis['frr_isisd_config'] = ''
- isis['frr_zebra_config'] = ''
return None
- isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.tmpl
- isis['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.tmpl', isis)
- isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.tmpl', isis)
+ isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.j2
+ isis['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', isis)
+ isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.j2', isis)
return None
def apply(isis):
@@ -214,8 +224,9 @@ def apply(isis):
# The route-map used for the FIB (zebra) is part of the zebra daemon
frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+$', '', '(\s|!)')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_zebra_config'])
+ frr_cfg.modify_section('(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in isis:
+ frr_cfg.add_before(frr.default_add_before, isis['frr_zebra_config'])
frr_cfg.commit_configuration(zebra_daemon)
# Generate empty helper string which can be ammended to FRR commands, it
@@ -225,19 +236,18 @@ def apply(isis):
vrf = ' vrf ' + isis['vrf']
frr_cfg.load_configuration(isis_daemon)
- frr_cfg.modify_section(f'^router isis VyOS{vrf}$', '')
+ frr_cfg.modify_section(f'^router isis VyOS{vrf}', stop_pattern='^exit', remove_stop_mark=True)
for key in ['interface', 'interface_removed']:
if key not in isis:
continue
for interface in isis[key]:
- frr_cfg.modify_section(f'^interface {interface}{vrf}$', '')
+ frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True)
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_isisd_config'])
- frr_cfg.commit_configuration(isis_daemon)
+ if 'frr_isisd_config' in isis:
+ frr_cfg.add_before(frr.default_add_before, isis['frr_isisd_config'])
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
+ frr_cfg.commit_configuration(isis_daemon)
return None
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
index 3b27608da..5da8e7b06 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 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
@@ -20,11 +20,10 @@ from sys import exit
from glob import glob
from vyos.config import Config
-from vyos.configdict import node_changed
from vyos.template import render_to_string
-from vyos.util import call
from vyos.util import dict_search
from vyos.util import read_file
+from vyos.util import sysctl_write
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -66,82 +65,70 @@ def verify(mpls):
def generate(mpls):
# If there's no MPLS config generated, create dictionary key with no value.
- if not mpls:
- mpls['new_frr_config'] = ''
+ if not mpls or 'deleted' in mpls:
return None
- mpls['new_frr_config'] = render_to_string('frr/ldpd.frr.tmpl', mpls)
+ mpls['frr_ldpd_config'] = render_to_string('frr/ldpd.frr.j2', mpls)
return None
def apply(mpls):
- # Define dictionary that will load FRR config
- frr_cfg = {}
+ ldpd_damon = 'ldpd'
+
# Save original configuration prior to starting any commit actions
- frr_cfg['original_config'] = frr.get_configuration(daemon='ldpd')
- frr_cfg['modified_config'] = frr.replace_section(frr_cfg['original_config'], mpls['new_frr_config'], from_re='mpls.*')
-
- # If FRR config is blank, rerun the blank commit three times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if mpls['new_frr_config'] == '':
- for x in range(3):
- frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd')
- elif not 'ldp' in mpls:
- for x in range(3):
- frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd')
- else:
- # FRR mark configuration will test for syntax errors and throws an
- # exception if any syntax errors is detected
- frr.mark_configuration(frr_cfg['modified_config'])
+ frr_cfg = frr.FRRConfig()
+
+ frr_cfg.load_configuration(ldpd_damon)
+ frr_cfg.modify_section(f'^mpls ldp', stop_pattern='^exit', remove_stop_mark=True)
- # Commit resulting configuration to FRR, this will throw CommitError
- # on failure
- frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd')
+ if 'frr_ldpd_config' in mpls:
+ frr_cfg.add_before(frr.default_add_before, mpls['frr_ldpd_config'])
+ frr_cfg.commit_configuration(ldpd_damon)
# Set number of entries in the platform label tables
labels = '0'
if 'interface' in mpls:
labels = '1048575'
- call(f'sysctl -wq net.mpls.platform_labels={labels}')
+ sysctl_write('net.mpls.platform_labels', labels)
# Check for changes in global MPLS options
if 'parameters' in mpls:
# Choose whether to copy IP TTL to MPLS header TTL
if 'no_propagate_ttl' in mpls['parameters']:
- call('sysctl -wq net.mpls.ip_ttl_propagate=0')
+ sysctl_write('net.mpls.ip_ttl_propagate', 0)
# Choose whether to limit maximum MPLS header TTL
if 'maximum_ttl' in mpls['parameters']:
ttl = mpls['parameters']['maximum_ttl']
- call(f'sysctl -wq net.mpls.default_ttl={ttl}')
+ sysctl_write('net.mpls.default_ttl', ttl)
else:
# Set default global MPLS options if not defined.
- call('sysctl -wq net.mpls.ip_ttl_propagate=1')
- call('sysctl -wq net.mpls.default_ttl=255')
+ sysctl_write('net.mpls.ip_ttl_propagate', 1)
+ sysctl_write('net.mpls.default_ttl', 255)
# Enable and disable MPLS processing on interfaces per configuration
if 'interface' in mpls:
system_interfaces = []
# Populate system interfaces list with local MPLS capable interfaces
for interface in glob('/proc/sys/net/mpls/conf/*'):
- system_interfaces.append(os.path.basename(interface))
+ system_interfaces.append(os.path.basename(interface))
# This is where the comparison is done on if an interface needs to be enabled/disabled.
for system_interface in system_interfaces:
interface_state = read_file(f'/proc/sys/net/mpls/conf/{system_interface}/input')
if '1' in interface_state:
if system_interface not in mpls['interface']:
system_interface = system_interface.replace('.', '/')
- call(f'sysctl -wq net.mpls.conf.{system_interface}.input=0')
+ sysctl_write(f'net.mpls.conf.{system_interface}.input', 0)
elif '0' in interface_state:
if system_interface in mpls['interface']:
system_interface = system_interface.replace('.', '/')
- call(f'sysctl -wq net.mpls.conf.{system_interface}.input=1')
+ sysctl_write(f'net.mpls.conf.{system_interface}.input', 1)
else:
system_interfaces = []
# If MPLS interfaces are not configured, set MPLS processing disabled
for interface in glob('/proc/sys/net/mpls/conf/*'):
- system_interfaces.append(os.path.basename(interface))
+ system_interfaces.append(os.path.basename(interface))
for system_interface in system_interfaces:
system_interface = system_interface.replace('.', '/')
- call(f'sysctl -wq net.mpls.conf.{system_interface}.input=0')
+ sysctl_write(f'net.mpls.conf.{system_interface}.input', 0)
return None
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
index 12dacdba0..56939955d 100755
--- a/src/conf_mode/protocols_nhrp.py
+++ b/src/conf_mode/protocols_nhrp.py
@@ -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
@@ -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
@@ -79,36 +81,36 @@ def verify(nhrp):
for map_name, map_conf in nhrp_conf['dynamic_map'].items():
if 'nbma_domain_name' not in map_conf:
raise ConfigError(f'nbma-domain-name missing on dynamic-map {map_name} on tunnel {name}')
+
+ if 'cisco_authentication' in nhrp_conf:
+ if len(nhrp_conf['cisco_authentication']) > 8:
+ raise ConfigError('Maximum length of the secret is 8 characters!')
+
return None
def generate(nhrp):
- render(opennhrp_conf, 'nhrp/opennhrp.conf.tmpl', nhrp)
+ render(opennhrp_conf, 'nhrp/opennhrp.conf.j2', nhrp)
return None
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')
+ run(f'systemctl {action} opennhrp.service')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 6ccda2e5a..5b4874ba2 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -25,6 +25,7 @@ from vyos.configdict import node_changed
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_route_map
from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_access_list
from vyos.template import render_to_string
from vyos.util import dict_search
from vyos.util import get_interface_config
@@ -56,10 +57,10 @@ def get_config(config=None):
# instead of the VRF instance.
if vrf: ospf['vrf'] = vrf
- # As we no re-use this Python handler for both VRF and non VRF instances for
- # OSPF we need to find out if any interfaces changed so properly adjust
- # the FRR configuration and not by acctident change interfaces from a
- # different VRF.
+ # FRR has VRF support for different routing daemons. As interfaces belong
+ # to VRFs - or the global VRF, we need to check for changed interfaces so
+ # that they will be properly rendered for the FRR config. Also this eases
+ # removal of interfaces from the running configuration.
interfaces_removed = node_changed(conf, base + ['interface'])
if interfaces_removed:
ospf['interface_removed'] = list(interfaces_removed)
@@ -159,6 +160,16 @@ def verify(ospf):
route_map_name = dict_search('default_information.originate.route_map', ospf)
if route_map_name: verify_route_map(route_map_name, ospf)
+ # Validate if configured Access-list exists
+ if 'area' in ospf:
+ for area, area_config in ospf['area'].items():
+ if 'import_list' in area_config:
+ acl_import = area_config['import_list']
+ if acl_import: verify_access_list(acl_import, ospf)
+ if 'export_list' in area_config:
+ acl_export = area_config['export_list']
+ if acl_export: verify_access_list(acl_export, ospf)
+
if 'interface' in ospf:
for interface, interface_config in ospf['interface'].items():
verify_interface_exists(interface)
@@ -177,11 +188,11 @@ def verify(ospf):
raise ConfigError('Can not use OSPF interface area and area ' \
'network configuration at the same time!')
- if 'vrf' in ospf:
# If interface specific options are set, we must ensure that the
# interface is bound to our requesting VRF. Due to the VyOS
# priorities the interface is bound to the VRF after creation of
# the VRF itself, and before any routing protocol is configured.
+ if 'vrf' in ospf:
vrf = ospf['vrf']
tmp = get_interface_config(interface)
if 'master' not in tmp or tmp['master'] != vrf:
@@ -191,13 +202,11 @@ def verify(ospf):
def generate(ospf):
if not ospf or 'deleted' in ospf:
- ospf['frr_ospfd_config'] = ''
- ospf['frr_zebra_config'] = ''
return None
- ospf['protocol'] = 'ospf' # required for frr/vrf.route-map.frr.tmpl
- ospf['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.tmpl', ospf)
- ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.tmpl', ospf)
+ ospf['protocol'] = 'ospf' # required for frr/vrf.route-map.frr.j2
+ ospf['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', ospf)
+ ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.j2', ospf)
return None
def apply(ospf):
@@ -209,8 +218,9 @@ def apply(ospf):
# The route-map used for the FIB (zebra) is part of the zebra daemon
frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'(\s+)?ip protocol ospf route-map [-a-zA-Z0-9.]+$', '', '(\s|!)')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['frr_zebra_config'])
+ frr_cfg.modify_section('(\s+)?ip protocol ospf route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in ospf:
+ frr_cfg.add_before(frr.default_add_before, ospf['frr_zebra_config'])
frr_cfg.commit_configuration(zebra_daemon)
# Generate empty helper string which can be ammended to FRR commands, it
@@ -220,20 +230,18 @@ def apply(ospf):
vrf = ' vrf ' + ospf['vrf']
frr_cfg.load_configuration(ospf_daemon)
- frr_cfg.modify_section(f'^router ospf{vrf}$', '')
+ frr_cfg.modify_section(f'^router ospf{vrf}', stop_pattern='^exit', remove_stop_mark=True)
for key in ['interface', 'interface_removed']:
if key not in ospf:
continue
for interface in ospf[key]:
- frr_cfg.modify_section(f'^interface {interface}{vrf}$', '')
+ frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True)
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['frr_ospfd_config'])
+ if 'frr_ospfd_config' in ospf:
+ frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config'])
frr_cfg.commit_configuration(ospf_daemon)
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
index 536ffa690..ee4eaf59d 100755
--- a/src/conf_mode/protocols_ospfv3.py
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -17,32 +17,80 @@
import os
from sys import exit
+from sys import argv
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configdict import node_changed
from vyos.configverify import verify_common_route_maps
+from vyos.configverify import verify_route_map
+from vyos.configverify import verify_interface_exists
from vyos.template import render_to_string
from vyos.ifconfig import Interface
+from vyos.util import dict_search
+from vyos.util import get_interface_config
from vyos.xml import defaults
from vyos import ConfigError
from vyos import frr
from vyos import airbag
airbag.enable()
-frr_daemon = 'ospf6d'
-
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['protocols', 'ospfv3']
+
+ vrf = None
+ if len(argv) > 1:
+ vrf = argv[1]
+
+ base_path = ['protocols', 'ospfv3']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospfv3'] or base_path
ospfv3 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ # Assign the name of our VRF context. This MUST be done before the return
+ # statement below, else on deletion we will delete the default instance
+ # instead of the VRF instance.
+ if vrf: ospfv3['vrf'] = vrf
+
+ # FRR has VRF support for different routing daemons. As interfaces belong
+ # to VRFs - or the global VRF, we need to check for changed interfaces so
+ # that they will be properly rendered for the FRR config. Also this eases
+ # removal of interfaces from the running configuration.
+ interfaces_removed = node_changed(conf, base + ['interface'])
+ if interfaces_removed:
+ ospfv3['interface_removed'] = list(interfaces_removed)
+
# Bail out early if configuration tree does not exist
if not conf.exists(base):
+ ospfv3.update({'deleted' : ''})
return ospfv3
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ # XXX: Note that we can not call defaults(base), as defaults does not work
+ # on an instance of a tag node. As we use the exact same CLI definition for
+ # both the non-vrf and vrf version this is absolutely safe!
+ default_values = defaults(base_path)
+
+ # We have to cleanup the default dict, as default values could enable features
+ # which are not explicitly enabled on the CLI. Example: default-information
+ # originate comes with a default metric-type of 2, which will enable the
+ # entire default-information originate tree, even when not set via CLI so we
+ # need to check this first and probably drop that key.
+ if dict_search('default_information.originate', ospfv3) is None:
+ del default_values['default_information']
+
+ # XXX: T2665: we currently have no nice way for defaults under tag nodes,
+ # clean them out and add them manually :(
+ del default_values['interface']
+
+ # merge in remaining default values
+ ospfv3 = dict_merge(default_values, ospfv3)
+
# We also need some additional information from the config, prefix-lists
# and route-maps for instance. They will be used in verify().
#
@@ -60,34 +108,68 @@ def verify(ospfv3):
verify_common_route_maps(ospfv3)
+ # As we can have a default-information route-map, we need to validate it!
+ route_map_name = dict_search('default_information.originate.route_map', ospfv3)
+ if route_map_name: verify_route_map(route_map_name, ospfv3)
+
+ if 'area' in ospfv3:
+ for area, area_config in ospfv3['area'].items():
+ if 'area_type' in area_config:
+ if len(area_config['area_type']) > 1:
+ raise ConfigError(f'Can only configure one area-type for OSPFv3 area "{area}"!')
+
if 'interface' in ospfv3:
- for ifname, if_config in ospfv3['interface'].items():
- if 'ifmtu' in if_config:
- mtu = Interface(ifname).get_mtu()
- if int(if_config['ifmtu']) > int(mtu):
+ for interface, interface_config in ospfv3['interface'].items():
+ verify_interface_exists(interface)
+ if 'ifmtu' in interface_config:
+ mtu = Interface(interface).get_mtu()
+ if int(interface_config['ifmtu']) > int(mtu):
raise ConfigError(f'OSPFv3 ifmtu can not exceed physical MTU of "{mtu}"')
+ # If interface specific options are set, we must ensure that the
+ # interface is bound to our requesting VRF. Due to the VyOS
+ # priorities the interface is bound to the VRF after creation of
+ # the VRF itself, and before any routing protocol is configured.
+ if 'vrf' in ospfv3:
+ vrf = ospfv3['vrf']
+ tmp = get_interface_config(interface)
+ if 'master' not in tmp or tmp['master'] != vrf:
+ raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!')
+
return None
def generate(ospfv3):
- if not ospfv3:
- ospfv3['new_frr_config'] = ''
+ if not ospfv3 or 'deleted' in ospfv3:
return None
- ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.tmpl', ospfv3)
+ ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.j2', ospfv3)
return None
def apply(ospfv3):
+ ospf6_daemon = 'ospf6d'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(frr_daemon)
- frr_cfg.modify_section(r'^interface \S+', '')
- frr_cfg.modify_section('^router ospf6$', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospfv3['new_frr_config'])
- frr_cfg.commit_configuration(frr_daemon)
-
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
+
+ # Generate empty helper string which can be ammended to FRR commands, it
+ # will be either empty (default VRF) or contain the "vrf <name" statement
+ vrf = ''
+ if 'vrf' in ospfv3:
+ vrf = ' vrf ' + ospfv3['vrf']
+
+ frr_cfg.load_configuration(ospf6_daemon)
+ frr_cfg.modify_section(f'^router ospf6{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+
+ for key in ['interface', 'interface_removed']:
+ if key not in ospfv3:
+ continue
+ for interface in ospfv3[key]:
+ frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+
+ if 'new_frr_config' in ospfv3:
+ frr_cfg.add_before(frr.default_add_before, ospfv3['new_frr_config'])
+
+ frr_cfg.commit_configuration(ospf6_daemon)
return None
diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py
index df2e6f941..78df9b6f8 100755
--- a/src/conf_mode/protocols_pim.py
+++ b/src/conf_mode/protocols_pim.py
@@ -135,7 +135,7 @@ def generate(pim):
if pim is None:
return None
- render(config_file, 'frr/pimd.frr.tmpl', pim)
+ render(config_file, 'frr/pimd.frr.j2', pim)
return None
def apply(pim):
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index 6b78f6f2d..a76c1ce76 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -20,6 +20,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configdict import node_changed
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_access_list
from vyos.configverify import verify_prefix_list
@@ -39,8 +40,17 @@ def get_config(config=None):
base = ['protocols', 'rip']
rip = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ # FRR has VRF support for different routing daemons. As interfaces belong
+ # to VRFs - or the global VRF, we need to check for changed interfaces so
+ # that they will be properly rendered for the FRR config. Also this eases
+ # removal of interfaces from the running configuration.
+ interfaces_removed = node_changed(conf, base + ['interface'])
+ if interfaces_removed:
+ rip['interface_removed'] = list(interfaces_removed)
+
# Bail out early if configuration tree does not exist
if not conf.exists(base):
+ rip.update({'deleted' : ''})
return rip
# We have gathered the dict representation of the CLI, but there are default
@@ -89,12 +99,10 @@ def verify(rip):
f'with "split-horizon disable" for "{interface}"!')
def generate(rip):
- if not rip:
- rip['new_frr_config'] = ''
+ if not rip or 'deleted' in rip:
return None
- rip['new_frr_config'] = render_to_string('frr/ripd.frr.tmpl', rip)
-
+ rip['new_frr_config'] = render_to_string('frr/ripd.frr.j2', rip)
return None
def apply(rip):
@@ -106,19 +114,22 @@ def apply(rip):
# The route-map used for the FIB (zebra) is part of the zebra daemon
frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'^ip protocol rip route-map [-a-zA-Z0-9.]+$', '')
+ frr_cfg.modify_section('^ip protocol rip route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
frr_cfg.commit_configuration(zebra_daemon)
frr_cfg.load_configuration(rip_daemon)
- frr_cfg.modify_section(r'key chain \S+', '')
- frr_cfg.modify_section(r'interface \S+', '')
- frr_cfg.modify_section('^router rip$', '')
+ frr_cfg.modify_section('^key chain \S+', stop_pattern='^exit', remove_stop_mark=True)
+ frr_cfg.modify_section('^router rip', stop_pattern='^exit', remove_stop_mark=True)
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rip['new_frr_config'])
- frr_cfg.commit_configuration(rip_daemon)
+ for key in ['interface', 'interface_removed']:
+ if key not in rip:
+ continue
+ for interface in rip[key]:
+ frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
+ if 'new_frr_config' in rip:
+ frr_cfg.add_before(frr.default_add_before, rip['new_frr_config'])
+ frr_cfg.commit_configuration(rip_daemon)
return None
diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py
index bc4954f63..21ff710b3 100755
--- a/src/conf_mode/protocols_ripng.py
+++ b/src/conf_mode/protocols_ripng.py
@@ -31,8 +31,6 @@ from vyos import frr
from vyos import airbag
airbag.enable()
-frr_daemon = 'ripngd'
-
def get_config(config=None):
if config:
conf = config
@@ -95,21 +93,28 @@ def generate(ripng):
ripng['new_frr_config'] = ''
return None
- ripng['new_frr_config'] = render_to_string('frr/ripngd.frr.tmpl', ripng)
+ ripng['new_frr_config'] = render_to_string('frr/ripngd.frr.j2', ripng)
return None
def apply(ripng):
+ ripng_daemon = 'ripngd'
+ zebra_daemon = 'zebra'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(frr_daemon)
- frr_cfg.modify_section(r'key chain \S+', '')
- frr_cfg.modify_section(r'interface \S+', '')
- frr_cfg.modify_section('router ripng', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ripng['new_frr_config'])
- frr_cfg.commit_configuration(frr_daemon)
-
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
+
+ # The route-map used for the FIB (zebra) is part of the zebra daemon
+ frr_cfg.load_configuration(zebra_daemon)
+ frr_cfg.modify_section('^ipv6 protocol ripng route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ frr_cfg.commit_configuration(zebra_daemon)
+
+ frr_cfg.load_configuration(ripng_daemon)
+ frr_cfg.modify_section('key chain \S+', stop_pattern='^exit', remove_stop_mark=True)
+ frr_cfg.modify_section('interface \S+', stop_pattern='^exit', remove_stop_mark=True)
+ frr_cfg.modify_section('^router ripng', stop_pattern='^exit', remove_stop_mark=True)
+ if 'new_frr_config' in ripng:
+ frr_cfg.add_before(frr.default_add_before, ripng['new_frr_config'])
+ frr_cfg.commit_configuration(ripng_daemon)
return None
diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py
index 947c8ab7a..62ea9c878 100755
--- a/src/conf_mode/protocols_rpki.py
+++ b/src/conf_mode/protocols_rpki.py
@@ -28,8 +28,6 @@ from vyos import frr
from vyos import airbag
airbag.enable()
-frr_daemon = 'bgpd'
-
def get_config(config=None):
if config:
conf = config
@@ -38,7 +36,9 @@ def get_config(config=None):
base = ['protocols', 'rpki']
rpki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ # Bail out early if configuration tree does not exist
if not conf.exists(base):
+ rpki.update({'deleted' : ''})
return rpki
# We have gathered the dict representation of the CLI, but there are default
@@ -79,17 +79,22 @@ def verify(rpki):
return None
def generate(rpki):
- rpki['new_frr_config'] = render_to_string('frr/rpki.frr.tmpl', rpki)
+ if not rpki:
+ return
+ rpki['new_frr_config'] = render_to_string('frr/rpki.frr.j2', rpki)
return None
def apply(rpki):
+ bgp_daemon = 'bgpd'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(frr_daemon)
- frr_cfg.modify_section('rpki', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rpki['new_frr_config'])
- frr_cfg.commit_configuration(frr_daemon)
+ frr_cfg.load_configuration(bgp_daemon)
+ frr_cfg.modify_section('^rpki', stop_pattern='^exit', remove_stop_mark=True)
+ if 'new_frr_config' in rpki:
+ frr_cfg.add_before(frr.default_add_before, rpki['new_frr_config'])
+ frr_cfg.commit_configuration(bgp_daemon)
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index f010141e9..58e202928 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -22,6 +22,7 @@ from sys import argv
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import get_dhcp_interfaces
+from vyos.configdict import get_pppoe_interfaces
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_vrf
from vyos.template import render_to_string
@@ -59,7 +60,9 @@ def get_config(config=None):
# T3680 - get a list of all interfaces currently configured to use DHCP
tmp = get_dhcp_interfaces(conf, vrf)
- if tmp: static['dhcp'] = tmp
+ if tmp: static.update({'dhcp' : tmp})
+ tmp = get_pppoe_interfaces(conf, vrf)
+ if tmp: static.update({'pppoe' : tmp})
return static
@@ -82,10 +85,16 @@ def verify(static):
for interface, interface_config in prefix_options[type].items():
verify_vrf(interface_config)
+ if {'blackhole', 'reject'} <= set(prefix_options):
+ raise ConfigError(f'Can not use both blackhole and reject for '\
+ 'prefix "{prefix}"!')
+
return None
def generate(static):
- static['new_frr_config'] = render_to_string('frr/staticd.frr.tmpl', static)
+ if not static:
+ return None
+ static['new_frr_config'] = render_to_string('frr/staticd.frr.j2', static)
return None
def apply(static):
@@ -97,24 +106,21 @@ def apply(static):
# The route-map used for the FIB (zebra) is part of the zebra daemon
frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'^ip protocol static route-map [-a-zA-Z0-9.]+$', '')
+ frr_cfg.modify_section(r'^ip protocol static route-map [-a-zA-Z0-9.]+', '')
frr_cfg.commit_configuration(zebra_daemon)
-
frr_cfg.load_configuration(static_daemon)
if 'vrf' in static:
vrf = static['vrf']
- frr_cfg.modify_section(f'^vrf {vrf}$', '')
+ frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit', remove_stop_mark=True)
else:
- frr_cfg.modify_section(r'^ip route .*', '')
- frr_cfg.modify_section(r'^ipv6 route .*', '')
+ frr_cfg.modify_section(r'^ip route .*')
+ frr_cfg.modify_section(r'^ipv6 route .*')
- frr_cfg.add_before(r'(interface .*|line vty)', static['new_frr_config'])
+ if 'new_frr_config' in static:
+ frr_cfg.add_before(frr.default_add_before, static['new_frr_config'])
frr_cfg.commit_configuration(static_daemon)
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
index 99157835a..6afdf31f3 100755
--- a/src/conf_mode/protocols_static_multicast.py
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -96,7 +96,7 @@ def generate(mroute):
if mroute is None:
return None
- render(config_file, 'frr/static_mcast.frr.tmpl', mroute)
+ render(config_file, 'frr/static_mcast.frr.j2', mroute)
return None
def apply(mroute):
diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py
new file mode 100755
index 000000000..dbe3be225
--- /dev/null
+++ b/src/conf_mode/qos.py
@@ -0,0 +1,87 @@
+#!/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 sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['qos']
+ if not conf.exists(base):
+ return None
+
+ qos = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ if 'policy' in qos:
+ for policy in qos['policy']:
+ # CLI mangles - to _ for better Jinja2 compatibility - do we need
+ # Jinja2 here?
+ policy = policy.replace('-','_')
+
+ default_values = defaults(base + ['policy', policy])
+
+ # class is another tag node which requires individual handling
+ class_default_values = defaults(base + ['policy', policy, 'class'])
+ if 'class' in default_values:
+ del default_values['class']
+
+ for p_name, p_config in qos['policy'][policy].items():
+ qos['policy'][policy][p_name] = dict_merge(
+ default_values, qos['policy'][policy][p_name])
+
+ if 'class' in p_config:
+ for p_class in p_config['class']:
+ qos['policy'][policy][p_name]['class'][p_class] = dict_merge(
+ class_default_values, qos['policy'][policy][p_name]['class'][p_class])
+
+ import pprint
+ pprint.pprint(qos)
+ return qos
+
+def verify(qos):
+ if not qos:
+ return None
+
+ # network policy emulator
+ # reorder rerquires delay to be set
+
+ raise ConfigError('123')
+ return None
+
+def generate(qos):
+ return None
+
+def apply(qos):
+ 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/salt-minion.py b/src/conf_mode/salt-minion.py
index 841bf6a39..00b889a11 100755
--- a/src/conf_mode/salt-minion.py
+++ b/src/conf_mode/salt-minion.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 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
@@ -16,14 +16,18 @@
import os
-from copy import deepcopy
from socket import gethostname
from sys import exit
from urllib3 import PoolManager
+from vyos.base import Warning
from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_interface_exists
from vyos.template import render
-from vyos.util import call, chown
+from vyos.util import call
+from vyos.util import chown
+from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
@@ -32,20 +36,10 @@ airbag.enable()
config_file = r'/etc/salt/minion'
master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'
-default_config_data = {
- 'hash': 'sha256',
- 'log_level': 'warning',
- 'master' : 'salt',
- 'user': 'minion',
- 'group': 'vyattacfg',
- 'salt_id': gethostname(),
- 'mine_interval': '60',
- 'verify_master_pubkey_sign': 'false',
- 'master_key': ''
-}
+user='minion'
+group='vyattacfg'
def get_config(config=None):
- salt = deepcopy(default_config_data)
if config:
conf = config
else:
@@ -54,44 +48,44 @@ def get_config(config=None):
if not conf.exists(base):
return None
- else:
- conf.set_level(base)
- if conf.exists(['hash']):
- salt['hash'] = conf.return_value(['hash'])
+ salt = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ # ID default is dynamic thus we can not use defaults()
+ if 'id' not in salt:
+ salt['id'] = gethostname()
+ # 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)
+ salt = dict_merge(default_values, salt)
- if conf.exists(['master']):
- salt['master'] = conf.return_values(['master'])
-
- if conf.exists(['id']):
- salt['salt_id'] = conf.return_value(['id'])
+ if not conf.exists(base):
+ return None
+ else:
+ conf.set_level(base)
- if conf.exists(['user']):
- salt['user'] = conf.return_value(['user'])
+ return salt
- if conf.exists(['interval']):
- salt['interval'] = conf.return_value(['interval'])
+def verify(salt):
+ if not salt:
+ return None
- if conf.exists(['master-key']):
- salt['master_key'] = conf.return_value(['master-key'])
- salt['verify_master_pubkey_sign'] = 'true'
+ if 'hash' in salt and salt['hash'] == 'sha1':
+ Warning('Do not use sha1 hashing algorithm, upgrade to sha256 or later!')
- return salt
+ if 'source_interface' in salt:
+ verify_interface_exists(salt['source_interface'])
-def verify(salt):
return None
def generate(salt):
if not salt:
return None
- render(config_file, 'salt-minion/minion.tmpl', salt,
- user=salt['user'], group=salt['group'])
+ render(config_file, 'salt-minion/minion.j2', salt, user=user, group=group)
if not os.path.exists(master_keyfile):
- if salt['master_key']:
+ if 'master_key' in salt:
req = PoolManager().request('GET', salt['master_key'], preload_content=False)
-
with open(master_keyfile, 'wb') as f:
while True:
data = req.read(1024)
@@ -100,18 +94,19 @@ def generate(salt):
f.write(data)
req.release_conn()
- chown(master_keyfile, salt['user'], salt['group'])
+ chown(master_keyfile, user, group)
return None
def apply(salt):
+ service_name = 'salt-minion.service'
if not salt:
# Salt removed from running config
- call('systemctl stop salt-minion.service')
+ call(f'systemctl stop {service_name}')
if os.path.exists(config_file):
os.unlink(config_file)
else:
- call('systemctl restart salt-minion.service')
+ call(f'systemctl restart {service_name}')
return None
diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py
index 51050e702..a2e411e49 100755
--- a/src/conf_mode/service_console-server.py
+++ b/src/conf_mode/service_console-server.py
@@ -81,7 +81,7 @@ def generate(proxy):
if not proxy:
return None
- render(config_file, 'conserver/conserver.conf.tmpl', proxy)
+ render(config_file, 'conserver/conserver.conf.j2', proxy)
if 'device' in proxy:
for device, device_config in proxy['device'].items():
if 'ssh' not in device_config:
@@ -92,7 +92,7 @@ def generate(proxy):
'port' : device_config['ssh']['port'],
}
render(dropbear_systemd_file.format(**tmp),
- 'conserver/dropbear@.service.tmpl', tmp)
+ 'conserver/dropbear@.service.j2', tmp)
return None
diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py
index 67edeb630..ae7e582ec 100755
--- a/src/conf_mode/service_ids_fastnetmon.py
+++ b/src/conf_mode/service_ids_fastnetmon.py
@@ -67,8 +67,8 @@ def generate(fastnetmon):
return
- render(config_file, 'ids/fastnetmon.tmpl', fastnetmon)
- render(networks_list, 'ids/fastnetmon_networks_list.tmpl', fastnetmon)
+ render(config_file, 'ids/fastnetmon.j2', fastnetmon)
+ render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon)
return None
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index f676fdbbe..559d1bcd5 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 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
@@ -41,6 +41,7 @@ default_config_data = {
'interfaces': [],
'dnsv4': [],
'dnsv6': [],
+ 'client_named_ip_pool': [],
'client_ipv6_pool': [],
'client_ipv6_delegate_prefix': [],
'radius_server': [],
@@ -219,6 +220,22 @@ def get_config(config=None):
conf.set_level(base_path)
+ # Named client-ip-pool
+ if conf.exists(['client-ip-pool', 'name']):
+ for name in conf.list_nodes(['client-ip-pool', 'name']):
+ tmp = {
+ 'name': name,
+ 'gateway_address': '',
+ 'subnet': ''
+ }
+
+ if conf.exists(['client-ip-pool', 'name', name, 'gateway-address']):
+ tmp['gateway_address'] += conf.return_value(['client-ip-pool', 'name', name, 'gateway-address'])
+ if conf.exists(['client-ip-pool', 'name', name, 'subnet']):
+ tmp['subnet'] += conf.return_value(['client-ip-pool', 'name', name, 'subnet'])
+
+ ipoe['client_named_ip_pool'].append(tmp)
+
if conf.exists(['client-ipv6-pool', 'prefix']):
for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']):
tmp = {
@@ -254,10 +271,6 @@ def verify(ipoe):
if not ipoe['interfaces']:
raise ConfigError('No IPoE interface configured')
- for interface in ipoe['interfaces']:
- if not interface['range']:
- raise ConfigError(f'No IPoE client subnet defined on interface "{ interface }"')
-
if len(ipoe['dnsv4']) > 2:
raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
@@ -283,10 +296,10 @@ def generate(ipoe):
if not ipoe:
return None
- render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe)
+ render(ipoe_conf, 'accel-ppp/ipoe.config.j2', ipoe)
if ipoe['auth_mode'] == 'local':
- render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.tmpl', ipoe)
+ render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', ipoe)
os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
else:
diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py
index d31a0c49e..2383a53fb 100755
--- a/src/conf_mode/service_mdns-repeater.py
+++ b/src/conf_mode/service_mdns-repeater.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2017-2020 VyOS maintainers and contributors
+# Copyright (C) 2017-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
@@ -92,7 +92,7 @@ def generate(mdns):
if len(mdns['interface']) < 2:
return None
- render(config_file, 'mdns-repeater/avahi-daemon.tmpl', mdns)
+ render(config_file, 'mdns-repeater/avahi-daemon.j2', mdns)
return None
def apply(mdns):
diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
new file mode 100755
index 000000000..daf75d740
--- /dev/null
+++ b/src/conf_mode/service_monitoring_telegraf.py
@@ -0,0 +1,229 @@
+#!/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()
+
+ if 'authentication' in monitoring or \
+ 'url' in monitoring:
+ monitoring['influxdb_configured'] = True
+
+ # Redefine azure group-metrics 'single-table' and 'table-per-metric'
+ if 'azure_data_explorer' in monitoring:
+ if 'single-table' in monitoring['azure_data_explorer']['group_metrics']:
+ monitoring['azure_data_explorer']['group_metrics'] = 'SingleTable'
+ else:
+ monitoring['azure_data_explorer']['group_metrics'] = 'TablePerMetric'
+ # Set azure env
+ if 'authentication' in monitoring['azure_data_explorer']:
+ auth_config = monitoring['azure_data_explorer']['authentication']
+ if {'client_id', 'client_secret', 'tenant_id'} <= set(auth_config):
+ os.environ['AZURE_CLIENT_ID'] = auth_config['client_id']
+ os.environ['AZURE_CLIENT_SECRET'] = auth_config['client_secret']
+ os.environ['AZURE_TENANT_ID'] = auth_config['tenant_id']
+
+ # Ignore default XML values if config doesn't exists
+ # Delete key from dict
+ if not conf.exists(base + ['prometheus-client']):
+ del monitoring['prometheus_client']
+
+ if not conf.exists(base + ['azure-data-explorer']):
+ del monitoring['azure_data_explorer']
+
+ return monitoring
+
+def verify(monitoring):
+ # bail out early - looks like removal from running config
+ if not monitoring:
+ return None
+
+ if 'influxdb_configured' in monitoring:
+ 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!')
+
+ # Verify azure-data-explorer
+ if 'azure_data_explorer' in monitoring:
+ if 'authentication' not in monitoring['azure_data_explorer'] or \
+ 'client_id' not in monitoring['azure_data_explorer']['authentication'] or \
+ 'client_secret' not in monitoring['azure_data_explorer']['authentication'] or \
+ 'tenant_id' not in monitoring['azure_data_explorer']['authentication']:
+ raise ConfigError(f'Authentication "client-id, client-secret and tenant-id" are mandatory!')
+
+ if 'database' not in monitoring['azure_data_explorer']:
+ raise ConfigError(f'Monitoring "database" is mandatory!')
+
+ if 'url' not in monitoring['azure_data_explorer']:
+ raise ConfigError(f'Monitoring "url" is mandatory!')
+
+ if monitoring['azure_data_explorer']['group_metrics'] == 'SingleTable' and \
+ 'table' not in monitoring['azure_data_explorer']:
+ raise ConfigError(f'Monitoring "table" name for single-table mode is mandatory!')
+
+ # Verify Splunk
+ if 'splunk' in monitoring:
+ if 'authentication' not in monitoring['splunk'] or \
+ 'token' not in monitoring['splunk']['authentication']:
+ raise ConfigError(f'Authentication "organization and token" are mandatory!')
+
+ if 'url' not in monitoring['splunk']:
+ raise ConfigError(f'Monitoring splunk "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.j2', monitoring)
+ render(systemd_telegraf_service, 'monitoring/systemd_vyos_telegraf_service.j2', monitoring)
+ render(systemd_override, 'monitoring/override.conf.j2', monitoring, permission=0o640)
+ render(syslog_telegraf, 'monitoring/syslog_telegraf.j2', 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_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index 9fbd531da..6086ef859 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -24,8 +24,11 @@ from vyos.configverify import verify_accel_ppp_base_service
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
+from vyos.util import get_interface_config
from vyos import ConfigError
from vyos import airbag
+from vyos.range_regex import range_to_regex
+
airbag.enable()
pppoe_conf = r'/run/accel-pppd/pppoe.conf'
@@ -56,6 +59,11 @@ def verify(pppoe):
if 'interface' not in pppoe:
raise ConfigError('At least one listen interface must be defined!')
+ # Check is interface exists in the system
+ for iface in pppoe['interface']:
+ if not get_interface_config(iface):
+ raise ConfigError(f'Interface {iface} does not exist!')
+
# local ippool and gateway settings config checks
if not (dict_search('client_ip_pool.subnet', pppoe) or
(dict_search('client_ip_pool.start', pppoe) and
@@ -73,10 +81,17 @@ def generate(pppoe):
if not pppoe:
return None
- render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe)
+ # Generate special regex for dynamic interfaces
+ for iface in pppoe['interface']:
+ if 'vlan_range' in pppoe['interface'][iface]:
+ pppoe['interface'][iface]['regex'] = []
+ for vlan_range in pppoe['interface'][iface]['vlan_range']:
+ pppoe['interface'][iface]['regex'].append(range_to_regex(vlan_range))
+
+ render(pppoe_conf, 'accel-ppp/pppoe.config.j2', pppoe)
if dict_search('authentication.mode', pppoe) == 'local':
- render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.config_dict.tmpl',
+ render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.config_dict.j2',
pppoe, permission=0o640)
else:
if os.path.exists(pppoe_chap_secrets):
diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py
index 9afcdd63e..71b758399 100755
--- a/src/conf_mode/service_router-advert.py
+++ b/src/conf_mode/service_router-advert.py
@@ -101,7 +101,7 @@ def generate(rtradv):
if not rtradv:
return None
- render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, permission=0o644)
+ render(config_file, 'router-advert/radvd.conf.j2', rtradv, permission=0o644)
return None
def apply(rtradv):
diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py
new file mode 100755
index 000000000..36f3e18a7
--- /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.j2', 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/service_webproxy.py b/src/conf_mode/service_webproxy.py
index a16cc4aeb..32af31bde 100755
--- a/src/conf_mode/service_webproxy.py
+++ b/src/conf_mode/service_webproxy.py
@@ -61,7 +61,7 @@ def generate_sg_localdb(category, list_type, role, proxy):
user=user_group, group=user_group)
# temporary config file, deleted after generation
- render(sg_tmp_file, 'squid/sg_acl.conf.tmpl', tmp,
+ render(sg_tmp_file, 'squid/sg_acl.conf.j2', tmp,
user=user_group, group=user_group)
call(f'su - {user_group} -c "squidGuard -d -c {sg_tmp_file} -C {db_file}"')
@@ -166,8 +166,8 @@ def generate(proxy):
if not proxy:
return None
- render(squid_config_file, 'squid/squid.conf.tmpl', proxy)
- render(squidguard_config_file, 'squid/squidGuard.conf.tmpl', proxy)
+ render(squid_config_file, 'squid/squid.conf.j2', proxy)
+ render(squidguard_config_file, 'squid/squidGuard.conf.j2', proxy)
cat_dict = {
'local-block' : 'domains',
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 6c6367045..5cd24db32 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -18,17 +18,20 @@ import os
from sys import exit
+from vyos.base import Warning
from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.configverify import verify_vrf
from vyos.snmpv3_hashgen import plaintext_to_md5
from vyos.snmpv3_hashgen import plaintext_to_sha1
from vyos.snmpv3_hashgen import random
from vyos.template import render
-from vyos.template import is_ipv4
from vyos.util import call
from vyos.util import chmod_755
+from vyos.util import dict_search
from vyos.validate import is_addr_assigned
from vyos.version import get_version_data
+from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -37,57 +40,26 @@ config_file_client = r'/etc/snmp/snmp.conf'
config_file_daemon = r'/etc/snmp/snmpd.conf'
config_file_access = r'/usr/share/snmp/snmpd.conf'
config_file_user = r'/var/lib/snmp/snmpd.conf'
-default_script_dir = r'/config/user-data/'
systemd_override = r'/etc/systemd/system/snmpd.service.d/override.conf'
+systemd_service = 'snmpd.service'
-# SNMP OIDs used to mark auth/priv type
-OIDs = {
- 'md5' : '.1.3.6.1.6.3.10.1.1.2',
- 'sha' : '.1.3.6.1.6.3.10.1.1.3',
- 'aes' : '.1.3.6.1.6.3.10.1.2.4',
- 'des' : '.1.3.6.1.6.3.10.1.2.2',
- 'none': '.1.3.6.1.6.3.10.1.2.1'
-}
-
-default_config_data = {
- 'listen_on': [],
- 'listen_address': [],
- 'ipv6_enabled': 'True',
- 'communities': [],
- 'smux_peers': [],
- 'location' : '',
- 'protocol' : 'udp',
- 'description' : '',
- 'contact' : '',
- 'route_table': 'False',
- 'trap_source': '',
- 'trap_targets': [],
- 'vyos_user': '',
- 'vyos_user_pass': '',
- 'version': '',
- 'v3_enabled': 'False',
- 'v3_engineid': '',
- 'v3_groups': [],
- 'v3_traps': [],
- 'v3_users': [],
- 'v3_views': [],
- 'script_ext': []
-}
-
-def rmfile(file):
- if os.path.isfile(file):
- os.unlink(file)
-
-def get_config():
- snmp = default_config_data
- conf = Config()
- if not conf.exists('service snmp'):
- return None
+def get_config(config=None):
+ if config:
+ conf = config
else:
- if conf.exists('system ipv6 disable'):
- snmp['ipv6_enabled'] = False
+ conf = Config()
+ base = ['service', 'snmp']
- conf.set_level('service snmp')
+ snmp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ if not conf.exists(base):
+ snmp.update({'deleted' : ''})
+
+ if conf.exists(['service', 'lldp', 'snmp', 'enable']):
+ snmp.update({'lldp_snmp' : ''})
+
+ if 'deleted' in snmp:
+ return snmp
version_data = get_version_data()
snmp['version'] = version_data['version']
@@ -96,477 +68,217 @@ def get_config():
snmp['vyos_user'] = 'vyos' + random(8)
snmp['vyos_user_pass'] = random(16)
- if conf.exists('community'):
- for name in conf.list_nodes('community'):
- community = {
- 'name': name,
- 'authorization': 'ro',
- 'network_v4': [],
- 'network_v6': [],
- 'has_source' : False
- }
-
- if conf.exists('community {0} authorization'.format(name)):
- community['authorization'] = conf.return_value('community {0} authorization'.format(name))
-
- # Subnet of SNMP client(s) allowed to contact system
- if conf.exists('community {0} network'.format(name)):
- for addr in conf.return_values('community {0} network'.format(name)):
- if is_ipv4(addr):
- community['network_v4'].append(addr)
- else:
- community['network_v6'].append(addr)
-
- # IP address of SNMP client allowed to contact system
- if conf.exists('community {0} client'.format(name)):
- for addr in conf.return_values('community {0} client'.format(name)):
- if is_ipv4(addr):
- community['network_v4'].append(addr)
- else:
- community['network_v6'].append(addr)
-
- if (len(community['network_v4']) > 0) or (len(community['network_v6']) > 0):
- community['has_source'] = True
-
- snmp['communities'].append(community)
-
- if conf.exists('contact'):
- snmp['contact'] = conf.return_value('contact')
-
- if conf.exists('description'):
- snmp['description'] = conf.return_value('description')
-
- if conf.exists('listen-address'):
- for addr in conf.list_nodes('listen-address'):
- port = '161'
- if conf.exists('listen-address {0} port'.format(addr)):
- port = conf.return_value('listen-address {0} port'.format(addr))
-
- snmp['listen_address'].append((addr, port))
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+
+ # We can not merge defaults for tagNodes - those need to be blended in
+ # per tagNode instance
+ if 'listen_address' in default_values:
+ del default_values['listen_address']
+ if 'community' in default_values:
+ del default_values['community']
+ if 'trap_target' in default_values:
+ del default_values['trap_target']
+ if 'v3' in default_values:
+ del default_values['v3']
+ snmp = dict_merge(default_values, snmp)
+
+ if 'listen_address' in snmp:
+ default_values = defaults(base + ['listen-address'])
+ for address in snmp['listen_address']:
+ snmp['listen_address'][address] = dict_merge(
+ default_values, snmp['listen_address'][address])
# Always listen on localhost if an explicit address has been configured
# This is a safety measure to not end up with invalid listen addresses
# that are not configured on this system. See https://phabricator.vyos.net/T850
- if not '127.0.0.1' in conf.list_nodes('listen-address'):
- snmp['listen_address'].append(('127.0.0.1', '161'))
-
- if not '::1' in conf.list_nodes('listen-address'):
- snmp['listen_address'].append(('::1', '161'))
-
- if conf.exists('location'):
- snmp['location'] = conf.return_value('location')
-
- if conf.exists('protocol'):
- snmp['protocol'] = conf.return_value('protocol')
-
- if conf.exists('smux-peer'):
- snmp['smux_peers'] = conf.return_values('smux-peer')
-
- if conf.exists('trap-source'):
- snmp['trap_source'] = conf.return_value('trap-source')
-
- if conf.exists('trap-target'):
- for target in conf.list_nodes('trap-target'):
- trap_tgt = {
- 'target': target,
- 'community': '',
- 'port': ''
- }
-
- if conf.exists('trap-target {0} community'.format(target)):
- trap_tgt['community'] = conf.return_value('trap-target {0} community'.format(target))
-
- if conf.exists('trap-target {0} port'.format(target)):
- trap_tgt['port'] = conf.return_value('trap-target {0} port'.format(target))
-
- snmp['trap_targets'].append(trap_tgt)
-
- if conf.exists('script-extensions'):
- for extname in conf.list_nodes('script-extensions extension-name'):
- conf_script = conf.return_value('script-extensions extension-name {} script'.format(extname))
- # if script has not absolute path, use pre configured path
- if "/" not in conf_script:
- conf_script = default_script_dir + conf_script
-
- extension = {
- 'name': extname,
- 'script' : conf_script
- }
-
- snmp['script_ext'].append(extension)
-
- if conf.exists('oid-enable route-table'):
- snmp['route_table'] = True
-
- if conf.exists('vrf'):
- # Append key to dict but don't place it in the default dictionary.
- # This is required to make the override.conf.tmpl work until we
- # migrate to get_config_dict().
- snmp['vrf'] = conf.return_value('vrf')
-
-
- #########################################################################
- # ____ _ _ __ __ ____ _____ #
- # / ___|| \ | | \/ | _ \ __ _|___ / #
- # \___ \| \| | |\/| | |_) | \ \ / / |_ \ #
- # ___) | |\ | | | | __/ \ V / ___) | #
- # |____/|_| \_|_| |_|_| \_/ |____/ #
- # #
- # now take care about the fancy SNMP v3 stuff, or bail out eraly #
- #########################################################################
- if not conf.exists('v3'):
- return snmp
- else:
- snmp['v3_enabled'] = True
-
- # 'set service snmp v3 engineid'
- if conf.exists('v3 engineid'):
- snmp['v3_engineid'] = conf.return_value('v3 engineid')
-
- # 'set service snmp v3 group'
- if conf.exists('v3 group'):
- for group in conf.list_nodes('v3 group'):
- v3_group = {
- 'name': group,
- 'mode': 'ro',
- 'seclevel': 'auth',
- 'view': ''
- }
-
- if conf.exists('v3 group {0} mode'.format(group)):
- v3_group['mode'] = conf.return_value('v3 group {0} mode'.format(group))
-
- if conf.exists('v3 group {0} seclevel'.format(group)):
- v3_group['seclevel'] = conf.return_value('v3 group {0} seclevel'.format(group))
-
- if conf.exists('v3 group {0} view'.format(group)):
- v3_group['view'] = conf.return_value('v3 group {0} view'.format(group))
-
- snmp['v3_groups'].append(v3_group)
-
- # 'set service snmp v3 trap-target'
- if conf.exists('v3 trap-target'):
- for trap in conf.list_nodes('v3 trap-target'):
- trap_cfg = {
- 'ipAddr': trap,
- 'secName': '',
- 'authProtocol': 'md5',
- 'authPassword': '',
- 'authMasterKey': '',
- 'privProtocol': 'des',
- 'privPassword': '',
- 'privMasterKey': '',
- 'ipProto': 'udp',
- 'ipPort': '162',
- 'type': '',
- 'secLevel': 'noAuthNoPriv'
- }
-
- if conf.exists('v3 trap-target {0} user'.format(trap)):
- # Set the securityName used for authenticated SNMPv3 messages.
- trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap))
-
- if conf.exists('v3 trap-target {0} auth type'.format(trap)):
- # Set the authentication protocol (MD5 or SHA) used for authenticated SNMPv3 messages
- # cmdline option '-a'
- trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap))
-
- if conf.exists('v3 trap-target {0} auth plaintext-password'.format(trap)):
- # Set the authentication pass phrase used for authenticated SNMPv3 messages.
- # cmdline option '-A'
- trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-password'.format(trap))
-
- if conf.exists('v3 trap-target {0} auth encrypted-password'.format(trap)):
- # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys.
- # cmdline option '-3m'
- trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-password'.format(trap))
-
- if conf.exists('v3 trap-target {0} privacy type'.format(trap)):
- # Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages.
- # cmdline option '-x'
- trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap))
-
- if conf.exists('v3 trap-target {0} privacy plaintext-password'.format(trap)):
- # Set the privacy pass phrase used for encrypted SNMPv3 messages.
- # cmdline option '-X'
- trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-password'.format(trap))
-
- if conf.exists('v3 trap-target {0} privacy encrypted-password'.format(trap)):
- # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys.
- # cmdline option '-3M'
- trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-password'.format(trap))
-
- if conf.exists('v3 trap-target {0} protocol'.format(trap)):
- trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap))
-
- if conf.exists('v3 trap-target {0} port'.format(trap)):
- trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap))
-
- if conf.exists('v3 trap-target {0} type'.format(trap)):
- trap_cfg['type'] = conf.return_value('v3 trap-target {0} type'.format(trap))
-
- # Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv).
- # Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv.
- if trap_cfg['authPassword'] or trap_cfg['authMasterKey']:
- if trap_cfg['privProtocol'] or trap_cfg['privPassword']:
- trap_cfg['secLevel'] = 'authPriv'
- else:
- trap_cfg['secLevel'] = 'authNoPriv'
-
- snmp['v3_traps'].append(trap_cfg)
-
- # 'set service snmp v3 user'
- if conf.exists('v3 user'):
- for user in conf.list_nodes('v3 user'):
- user_cfg = {
- 'name': user,
- 'authMasterKey': '',
- 'authPassword': '',
- 'authProtocol': 'md5',
- 'authOID': 'none',
- 'group': '',
- 'mode': 'ro',
- 'privMasterKey': '',
- 'privPassword': '',
- 'privOID': '',
- 'privProtocol': 'des'
- }
-
- # v3 user {0} auth
- if conf.exists('v3 user {0} auth encrypted-password'.format(user)):
- user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-password'.format(user))
-
- if conf.exists('v3 user {0} auth plaintext-password'.format(user)):
- user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-password'.format(user))
-
- # load default value
- type = user_cfg['authProtocol']
- if conf.exists('v3 user {0} auth type'.format(user)):
- type = conf.return_value('v3 user {0} auth type'.format(user))
-
- # (re-)update with either default value or value from CLI
- user_cfg['authProtocol'] = type
- user_cfg['authOID'] = OIDs[type]
-
- # v3 user {0} group
- if conf.exists('v3 user {0} group'.format(user)):
- user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user))
-
- # v3 user {0} mode
- if conf.exists('v3 user {0} mode'.format(user)):
- user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user))
-
- # v3 user {0} privacy
- if conf.exists('v3 user {0} privacy encrypted-password'.format(user)):
- user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-password'.format(user))
-
- if conf.exists('v3 user {0} privacy plaintext-password'.format(user)):
- user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-password'.format(user))
-
- # load default value
- type = user_cfg['privProtocol']
- if conf.exists('v3 user {0} privacy type'.format(user)):
- type = conf.return_value('v3 user {0} privacy type'.format(user))
-
- # (re-)update with either default value or value from CLI
- user_cfg['privProtocol'] = type
- user_cfg['privOID'] = OIDs[type]
-
- snmp['v3_users'].append(user_cfg)
-
- # 'set service snmp v3 view'
- if conf.exists('v3 view'):
- for view in conf.list_nodes('v3 view'):
- view_cfg = {
- 'name': view,
- 'oids': []
- }
-
- if conf.exists('v3 view {0} oid'.format(view)):
- for oid in conf.list_nodes('v3 view {0} oid'.format(view)):
- oid_cfg = {
- 'oid': oid
- }
- view_cfg['oids'].append(oid_cfg)
- snmp['v3_views'].append(view_cfg)
+ if '127.0.0.1' not in snmp['listen_address']:
+ tmp = {'127.0.0.1': {'port': '161'}}
+ snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])
+
+ if '::1' not in snmp['listen_address']:
+ tmp = {'::1': {'port': '161'}}
+ snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])
+
+ if 'community' in snmp:
+ default_values = defaults(base + ['community'])
+ for community in snmp['community']:
+ snmp['community'][community] = dict_merge(
+ default_values, snmp['community'][community])
+
+ if 'trap_target' in snmp:
+ default_values = defaults(base + ['trap-target'])
+ for trap in snmp['trap_target']:
+ snmp['trap_target'][trap] = dict_merge(
+ default_values, snmp['trap_target'][trap])
+
+ if 'v3' in snmp:
+ default_values = defaults(base + ['v3'])
+ # tagNodes need to be merged in individually later on
+ for tmp in ['user', 'group', 'trap_target']:
+ del default_values[tmp]
+ snmp['v3'] = dict_merge(default_values, snmp['v3'])
+
+ for user_group in ['user', 'group']:
+ if user_group in snmp['v3']:
+ default_values = defaults(base + ['v3', user_group])
+ for tmp in snmp['v3'][user_group]:
+ snmp['v3'][user_group][tmp] = dict_merge(
+ default_values, snmp['v3'][user_group][tmp])
+
+ if 'trap_target' in snmp['v3']:
+ default_values = defaults(base + ['v3', 'trap-target'])
+ for trap in snmp['v3']['trap_target']:
+ snmp['v3']['trap_target'][trap] = dict_merge(
+ default_values, snmp['v3']['trap_target'][trap])
return snmp
def verify(snmp):
- if snmp is None:
- # we can not delete SNMP when LLDP is configured with SNMP
- conf = Config()
- if conf.exists('service lldp snmp enable'):
- raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!')
-
+ if not snmp:
return None
+ if {'deleted', 'lldp_snmp'} <= set(snmp):
+ raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!')
+
### check if the configured script actually exist
- if snmp['script_ext']:
- for ext in snmp['script_ext']:
- if not os.path.isfile(ext['script']):
- print ("WARNING: script: {} doesn't exist".format(ext['script']))
+ if 'script_extensions' in snmp and 'extension_name' in snmp['script_extensions']:
+ for extension, extension_opt in snmp['script_extensions']['extension_name'].items():
+ if 'script' not in extension_opt:
+ raise ConfigError(f'Script extension "{extension}" requires an actual script to be configured!')
+
+ tmp = extension_opt['script']
+ if not os.path.isfile(tmp):
+ Warning(f'script "{tmp}" does not exist!')
else:
- chmod_755(ext['script'])
-
- for listen in snmp['listen_address']:
- addr = listen[0]
- port = listen[1]
- protocol = snmp['protocol']
-
- tmp = None
- if is_ipv4(addr):
- # example: udp:127.0.0.1:161
- tmp = f'{protocol}:{addr}:{port}'
- elif snmp['ipv6_enabled']:
- # example: udp6:[::1]:161
- tmp = f'{protocol}6:[{addr}]:{port}'
-
- # We only wan't to configure addresses that exist on the system.
- # Hint the user if they don't exist
- if is_addr_assigned(addr):
- if tmp: snmp['listen_on'].append(tmp)
- else:
- print(f'WARNING: SNMP listen address {addr} not configured!')
+ chmod_755(extension_opt['script'])
+
+ if 'listen_address' in snmp:
+ for address in snmp['listen_address']:
+ # We only wan't to configure addresses that exist on the system.
+ # Hint the user if they don't exist
+ if not is_addr_assigned(address):
+ Warning(f'SNMP listen address "{address}" not configured!')
+
+ if 'trap_target' in snmp:
+ for trap, trap_config in snmp['trap_target'].items():
+ if 'community' not in trap_config:
+ raise ConfigError(f'Trap target "{trap}" requires a community to be set!')
verify_vrf(snmp)
# bail out early if SNMP v3 is not configured
- if not snmp['v3_enabled']:
+ if 'v3' not in snmp:
return None
- if 'v3_groups' in snmp.keys():
- for group in snmp['v3_groups']:
- #
- # A view must exist prior to mapping it into a group
- #
- if 'view' in group.keys():
- error = True
- if 'v3_views' in snmp.keys():
- for view in snmp['v3_views']:
- if view['name'] == group['view']:
- error = False
- if error:
- raise ConfigError('You must create view "{0}" first'.format(group['view']))
- else:
- raise ConfigError('"view" must be specified')
-
- if not 'mode' in group.keys():
- raise ConfigError('"mode" must be specified')
-
- if not 'seclevel' in group.keys():
- raise ConfigError('"seclevel" must be specified')
-
- if 'v3_traps' in snmp.keys():
- for trap in snmp['v3_traps']:
- if trap['authPassword'] and trap['authMasterKey']:
- raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap auth')
-
- if trap['authPassword'] == '' and trap['authMasterKey'] == '':
- raise ConfigError('Must specify encrypted-password or plaintext-key for trap auth')
-
- if trap['privPassword'] and trap['privMasterKey']:
- raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap privacy')
+ if 'user' in snmp['v3']:
+ for user, user_config in snmp['v3']['user'].items():
+ if 'group' not in user_config:
+ raise ConfigError(f'Group membership required for user "{user}"!')
- if trap['privPassword'] == '' and trap['privMasterKey'] == '':
- raise ConfigError('Must specify encrypted-password or plaintext-key for trap privacy')
+ if 'plaintext_password' not in user_config['auth'] and 'encrypted_password' not in user_config['auth']:
+ raise ConfigError(f'Must specify authentication encrypted-password or plaintext-password for user "{user}"!')
- if not 'type' in trap.keys():
- raise ConfigError('v3 trap: "type" must be specified')
+ if 'plaintext_password' not in user_config['privacy'] and 'encrypted_password' not in user_config['privacy']:
+ raise ConfigError(f'Must specify privacy encrypted-password or plaintext-password for user "{user}"!')
- if not 'authPassword' and 'authMasterKey' in trap.keys():
- raise ConfigError('v3 trap: "auth" must be specified')
+ if 'group' in snmp['v3']:
+ for group, group_config in snmp['v3']['group'].items():
+ if 'seclevel' not in group_config:
+ raise ConfigError(f'Must configure "seclevel" for group "{group}"!')
+ if 'view' not in group_config:
+ raise ConfigError(f'Must configure "view" for group "{group}"!')
- if not 'authProtocol' in trap.keys():
- raise ConfigError('v3 trap: "protocol" must be specified')
+ # Check if 'view' exists
+ view = group_config['view']
+ if 'view' not in snmp['v3'] or view not in snmp['v3']['view']:
+ raise ConfigError(f'You must create view "{view}" first!')
- if not 'privPassword' and 'privMasterKey' in trap.keys():
- raise ConfigError('v3 trap: "user" must be specified')
+ if 'view' in snmp['v3']:
+ for view, view_config in snmp['v3']['view'].items():
+ if 'oid' not in view_config:
+ raise ConfigError(f'Must configure an "oid" for view "{view}"!')
- if 'v3_users' in snmp.keys():
- for user in snmp['v3_users']:
- #
- # Group must exist prior to mapping it into a group
- # seclevel will be extracted from group
- #
- if user['group']:
- error = True
- if 'v3_groups' in snmp.keys():
- for group in snmp['v3_groups']:
- if group['name'] == user['group']:
- seclevel = group['seclevel']
- error = False
+ if 'trap_target' in snmp['v3']:
+ for trap, trap_config in snmp['v3']['trap_target'].items():
+ if 'plaintext_password' not in trap_config['auth'] and 'encrypted_password' not in trap_config['auth']:
+ raise ConfigError(f'Must specify one of authentication encrypted-password or plaintext-password for trap "{trap}"!')
- if error:
- raise ConfigError('You must create group "{0}" first'.format(user['group']))
+ if {'plaintext_password', 'encrypted_password'} <= set(trap_config['auth']):
+ raise ConfigError(f'Can not specify both authentication encrypted-password and plaintext-password for trap "{trap}"!')
- # Depending on the configured security level the user has to provide additional info
- if (not user['authPassword'] and not user['authMasterKey']):
- raise ConfigError('Must specify encrypted-password or plaintext-key for user auth')
+ if 'plaintext_password' not in trap_config['privacy'] and 'encrypted_password' not in trap_config['privacy']:
+ raise ConfigError(f'Must specify one of privacy encrypted-password or plaintext-password for trap "{trap}"!')
- if user['privPassword'] == '' and user['privMasterKey'] == '':
- raise ConfigError('Must specify encrypted-password or plaintext-key for user privacy')
+ if {'plaintext_password', 'encrypted_password'} <= set(trap_config['privacy']):
+ raise ConfigError(f'Can not specify both privacy encrypted-password and plaintext-password for trap "{trap}"!')
- if user['mode'] == '':
- raise ConfigError('Must specify user mode ro/rw')
-
- if 'v3_views' in snmp.keys():
- for view in snmp['v3_views']:
- if not view['oids']:
- raise ConfigError('Must configure an oid')
+ if 'type' not in trap_config:
+ raise ConfigError('SNMP v3 trap "type" must be specified!')
return None
def generate(snmp):
+
#
# As we are manipulating the snmpd user database we have to stop it first!
# This is even save if service is going to be removed
- call('systemctl stop snmpd.service')
- config_files = [config_file_client, config_file_daemon, config_file_access,
- config_file_user, systemd_override]
+ call(f'systemctl stop {systemd_service}')
+ # Clean config files
+ config_files = [config_file_client, config_file_daemon,
+ config_file_access, config_file_user, systemd_override]
for file in config_files:
- rmfile(file)
+ if os.path.isfile(file):
+ os.unlink(file)
if not snmp:
return None
- if 'v3_users' in snmp.keys():
+ if 'v3' in snmp:
# net-snmp is now regenerating the configuration file in the background
# thus we need to re-open and re-read the file as the content changed.
# After that we can no read the encrypted password from the config and
# replace the CLI plaintext password with its encrypted version.
- os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos"
+ os.environ['vyos_libexec_dir'] = '/usr/libexec/vyos'
- for user in snmp['v3_users']:
- if user['authProtocol'] == 'sha':
- hash = plaintext_to_sha1
- else:
- hash = plaintext_to_md5
+ if 'user' in snmp['v3']:
+ for user, user_config in snmp['v3']['user'].items():
+ if dict_search('auth.type', user_config) == 'sha':
+ hash = plaintext_to_sha1
+ else:
+ hash = plaintext_to_md5
+
+ if dict_search('auth.plaintext_password', user_config) is not None:
+ tmp = hash(dict_search('auth.plaintext_password', user_config),
+ dict_search('v3.engineid', snmp))
+
+ snmp['v3']['user'][user]['auth']['encrypted_password'] = tmp
+ del snmp['v3']['user'][user]['auth']['plaintext_password']
- if user['authPassword']:
- user['authMasterKey'] = hash(user['authPassword'], snmp['v3_engineid'])
- user['authPassword'] = ''
+ call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" auth encrypted-password "{tmp}" > /dev/null')
+ call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" auth plaintext-password > /dev/null')
- call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" auth encrypted-password "{authMasterKey}" > /dev/null'.format(**user))
- call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" auth plaintext-password > /dev/null'.format(**user))
+ if dict_search('privacy.plaintext_password', user_config) is not None:
+ tmp = hash(dict_search('privacy.plaintext_password', user_config),
+ dict_search('v3.engineid', snmp))
- if user['privPassword']:
- user['privMasterKey'] = hash(user['privPassword'], snmp['v3_engineid'])
- user['privPassword'] = ''
+ snmp['v3']['user'][user]['privacy']['encrypted_password'] = tmp
+ del snmp['v3']['user'][user]['privacy']['plaintext_password']
- call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" privacy encrypted-password "{privMasterKey}" > /dev/null'.format(**user))
- call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" privacy plaintext-password > /dev/null'.format(**user))
+ call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" privacy encrypted-password "{tmp}" > /dev/null')
+ call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" privacy plaintext-password > /dev/null')
# Write client config file
- render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp)
+ render(config_file_client, 'snmp/etc.snmp.conf.j2', snmp)
# Write server config file
- render(config_file_daemon, 'snmp/etc.snmpd.conf.tmpl', snmp)
+ render(config_file_daemon, 'snmp/etc.snmpd.conf.j2', snmp)
# Write access rights config file
- render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp)
+ render(config_file_access, 'snmp/usr.snmpd.conf.j2', snmp)
# Write access rights config file
- render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp)
+ render(config_file_user, 'snmp/var.snmpd.conf.j2', snmp)
# Write daemon configuration file
- render(systemd_override, 'snmp/override.conf.tmpl', snmp)
+ render(systemd_override, 'snmp/override.conf.j2', snmp)
return None
@@ -578,7 +290,7 @@ def apply(snmp):
return None
# start SNMP daemon
- call('systemctl restart snmpd.service')
+ call(f'systemctl restart {systemd_service}')
# Enable AgentX in FRR
# This should be done for each daemon individually because common command
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index 67724b043..28669694b 100755
--- a/src/conf_mode/ssh.py
+++ b/src/conf_mode/ssh.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
@@ -33,6 +33,9 @@ airbag.enable()
config_file = r'/run/sshd/sshd_config'
systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf'
+sshguard_config_file = '/etc/sshguard/sshguard.conf'
+sshguard_whitelist = '/etc/sshguard/whitelist'
+
key_rsa = '/etc/ssh/ssh_host_rsa_key'
key_dsa = '/etc/ssh/ssh_host_dsa_key'
key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
@@ -54,6 +57,11 @@ def get_config(config=None):
# pass config file path - used in override template
ssh['config_file'] = config_file
+ # Ignore default XML values if config doesn't exists
+ # Delete key from dict
+ if not conf.exists(base + ['dynamic-protection']):
+ del ssh['dynamic_protection']
+
return ssh
def verify(ssh):
@@ -84,8 +92,12 @@ def generate(ssh):
syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!')
call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}')
- render(config_file, 'ssh/sshd_config.tmpl', ssh)
- render(systemd_override, 'ssh/override.conf.tmpl', ssh)
+ render(config_file, 'ssh/sshd_config.j2', ssh)
+ render(systemd_override, 'ssh/override.conf.j2', ssh)
+
+ if 'dynamic_protection' in ssh:
+ render(sshguard_config_file, 'ssh/sshguard_config.j2', ssh)
+ render(sshguard_whitelist, 'ssh/sshguard_whitelist.j2', ssh)
# Reload systemd manager configuration
call('systemctl daemon-reload')
@@ -95,7 +107,12 @@ def apply(ssh):
if not ssh:
# SSH access is removed in the commit
call('systemctl stop ssh.service')
+ call('systemctl stop sshguard.service')
return None
+ if 'dynamic_protection' not in ssh:
+ call('systemctl stop sshguard.service')
+ else:
+ call('systemctl restart sshguard.service')
call('systemctl restart ssh.service')
return None
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 32cb2f036..05fc3a97a 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.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
@@ -20,14 +20,13 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.util import call
from vyos.util import dict_search
+from vyos.util import sysctl_write
+from vyos.util import write_file
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def sysctl(name, value):
- call(f'sysctl -wq {name}={value}')
-
def get_config(config=None):
if config:
conf = config
@@ -50,29 +49,29 @@ def generate(opt):
pass
def apply(opt):
+ # Apply ARP threshold values
+ # table_size has a default value - thus the key always exists
size = int(dict_search('arp.table_size', opt))
- if size:
- # apply ARP threshold values
- sysctl('net.ipv4.neigh.default.gc_thresh3', str(size))
- sysctl('net.ipv4.neigh.default.gc_thresh2', str(size // 2))
- sysctl('net.ipv4.neigh.default.gc_thresh1', str(size // 8))
+ # Amount upon reaching which the records begin to be cleared immediately
+ sysctl_write('net.ipv4.neigh.default.gc_thresh3', size)
+ # Amount after which the records begin to be cleaned after 5 seconds
+ sysctl_write('net.ipv4.neigh.default.gc_thresh2', size // 2)
+ # Minimum number of stored records is indicated which is not cleared
+ sysctl_write('net.ipv4.neigh.default.gc_thresh1', size // 8)
# enable/disable IPv4 forwarding
- tmp = '1'
- if 'disable_forwarding' in opt:
- tmp = '0'
- sysctl('net.ipv4.conf.all.forwarding', tmp)
+ tmp = dict_search('disable_forwarding', opt)
+ value = '0' if (tmp != None) else '1'
+ write_file('/proc/sys/net/ipv4/conf/all/forwarding', value)
- tmp = '0'
- # configure multipath - dict_search() returns an empty dict if key was found
- if isinstance(dict_search('multipath.ignore_unreachable_nexthops', opt), dict):
- tmp = '1'
- sysctl('net.ipv4.fib_multipath_use_neigh', tmp)
+ # configure multipath
+ tmp = dict_search('multipath.ignore_unreachable_nexthops', opt)
+ value = '1' if (tmp != None) else '0'
+ sysctl_write('net.ipv4.fib_multipath_use_neigh', value)
- tmp = '0'
- if isinstance(dict_search('multipath.layer4_hashing', opt), dict):
- tmp = '1'
- sysctl('net.ipv4.fib_multipath_hash_policy', tmp)
+ tmp = dict_search('multipath.layer4_hashing', opt)
+ value = '1' if (tmp != None) else '0'
+ sysctl_write('net.ipv4.fib_multipath_hash_policy', value)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index f70ec2631..26aacf46b 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 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
@@ -15,95 +15,68 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import sys
from sys import exit
-from copy import deepcopy
from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.util import dict_search
+from vyos.util import sysctl_write
+from vyos.util import write_file
+from vyos.xml import defaults
from vyos import ConfigError
-from vyos.util import call
-
from vyos import airbag
airbag.enable()
-ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf'
-
-default_config_data = {
- 'reboot_message': False,
- 'ipv6_forward': '1',
- 'disable_addr_assignment': False,
- 'mp_layer4_hashing': '0',
- 'neighbor_cache': 8192,
- 'strict_dad': '1'
-
-}
-
-def sysctl(name, value):
- call('sysctl -wq {}={}'.format(name, value))
-
def get_config(config=None):
- ip_opt = deepcopy(default_config_data)
if config:
conf = config
else:
conf = Config()
- conf.set_level('system ipv6')
- if conf.exists(''):
- ip_opt['disable_addr_assignment'] = conf.exists('disable')
- if conf.exists_effective('disable') != conf.exists('disable'):
- ip_opt['reboot_message'] = True
-
- if conf.exists('disable-forwarding'):
- ip_opt['ipv6_forward'] = '0'
+ base = ['system', 'ipv6']
- if conf.exists('multipath layer4-hashing'):
- ip_opt['mp_layer4_hashing'] = '1'
+ opt = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- if conf.exists('neighbor table-size'):
- ip_opt['neighbor_cache'] = int(conf.return_value('neighbor table-size'))
+ # 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)
+ opt = dict_merge(default_values, opt)
- if conf.exists('strict-dad'):
- ip_opt['strict_dad'] = 2
+ return opt
- return ip_opt
-
-def verify(ip_opt):
+def verify(opt):
pass
-def generate(ip_opt):
+def generate(opt):
pass
-def apply(ip_opt):
- # disable IPv6 address assignment
- if ip_opt['disable_addr_assignment']:
- with open(ipv6_disable_file, 'w') as f:
- f.write('options ipv6 disable_ipv6=1')
- else:
- if os.path.exists(ipv6_disable_file):
- os.unlink(ipv6_disable_file)
-
- if ip_opt['reboot_message']:
- print('Changing IPv6 disable parameter will only take affect\n' \
- 'when the system is rebooted.')
-
+def apply(opt):
# configure multipath
- sysctl('net.ipv6.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing'])
-
- # apply neighbor table threshold values
- sysctl('net.ipv6.neigh.default.gc_thresh3', ip_opt['neighbor_cache'])
- sysctl('net.ipv6.neigh.default.gc_thresh2', ip_opt['neighbor_cache'] // 2)
- sysctl('net.ipv6.neigh.default.gc_thresh1', ip_opt['neighbor_cache'] // 8)
+ tmp = dict_search('multipath.layer4_hashing', opt)
+ value = '1' if (tmp != None) else '0'
+ sysctl_write('net.ipv6.fib_multipath_hash_policy', value)
+
+ # Apply ND threshold values
+ # table_size has a default value - thus the key always exists
+ size = int(dict_search('neighbor.table_size', opt))
+ # Amount upon reaching which the records begin to be cleared immediately
+ sysctl_write('net.ipv6.neigh.default.gc_thresh3', size)
+ # Amount after which the records begin to be cleaned after 5 seconds
+ sysctl_write('net.ipv6.neigh.default.gc_thresh2', size // 2)
+ # Minimum number of stored records is indicated which is not cleared
+ sysctl_write('net.ipv6.neigh.default.gc_thresh1', size // 8)
# enable/disable IPv6 forwarding
- with open('/proc/sys/net/ipv6/conf/all/forwarding', 'w') as f:
- f.write(ip_opt['ipv6_forward'])
+ tmp = dict_search('disable_forwarding', opt)
+ value = '0' if (tmp != None) else '1'
+ write_file('/proc/sys/net/ipv6/conf/all/forwarding', value)
# configure IPv6 strict-dad
+ tmp = dict_search('strict_dad', opt)
+ value = '2' if (tmp != None) else '1'
for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'):
for name in files:
- if name == "accept_dad":
- with open(os.path.join(root, name), 'w') as f:
- f.write(str(ip_opt['strict_dad']))
+ if name == 'accept_dad':
+ write_file(os.path.join(root, name), value)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py
index 2220d7b66..a521c9834 100755
--- a/src/conf_mode/system-login-banner.py
+++ b/src/conf_mode/system-login-banner.py
@@ -15,22 +15,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sys import exit
+from copy import deepcopy
+
from vyos.config import Config
+from vyos.util import write_file
from vyos import ConfigError
-
from vyos import airbag
airbag.enable()
-motd="""
-Check out project news at https://blog.vyos.io
-and feel free to report bugs at https://phabricator.vyos.net
-
-You can change this banner using "set system login banner post-login" command.
-
-VyOS is a free software distribution that includes multiple components,
-you can check individual component licenses under /usr/share/doc/*/copyright
-
-"""
+try:
+ with open('/usr/share/vyos/default_motd') as f:
+ motd = f.read()
+except:
+ # Use an empty banner if the default banner file cannot be read
+ motd = "\n"
PRELOGIN_FILE = r'/etc/issue'
PRELOGIN_NET_FILE = r'/etc/issue.net'
@@ -38,12 +36,12 @@ POSTLOGIN_FILE = r'/etc/motd'
default_config_data = {
'issue': 'Welcome to VyOS - \\n \\l\n\n',
- 'issue_net': 'Welcome to VyOS\n',
+ 'issue_net': '',
'motd': motd
}
def get_config(config=None):
- banner = default_config_data
+ banner = deepcopy(default_config_data)
if config:
conf = config
else:
@@ -92,14 +90,9 @@ def generate(banner):
pass
def apply(banner):
- with open(PRELOGIN_FILE, 'w') as f:
- f.write(banner['issue'])
-
- with open(PRELOGIN_NET_FILE, 'w') as f:
- f.write(banner['issue_net'])
-
- with open(POSTLOGIN_FILE, 'w') as f:
- f.write(banner['motd'])
+ write_file(PRELOGIN_FILE, banner['issue'])
+ write_file(PRELOGIN_NET_FILE, banner['issue_net'])
+ write_file(POSTLOGIN_FILE, banner['motd'])
return None
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 4dd7f936d..c717286ae 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.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
@@ -23,6 +23,7 @@ from pwd import getpwall
from pwd import getpwnam
from spwd import getspnam
from sys import exit
+from time import sleep
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -31,6 +32,7 @@ from vyos.template import render
from vyos.template import is_ipv4
from vyos.util import cmd
from vyos.util import call
+from vyos.util import run
from vyos.util import DEVNULL
from vyos.util import dict_search
from vyos.xml import defaults
@@ -195,7 +197,7 @@ def generate(login):
pass
if 'radius' in login:
- render(radius_config_file, 'login/pam_radius_auth.conf.tmpl', login,
+ render(radius_config_file, 'login/pam_radius_auth.conf.j2', login,
permission=0o600, user='root', group='root')
else:
if os.path.isfile(radius_config_file):
@@ -239,7 +241,7 @@ def apply(login):
#
# XXX: Should we deny using root at all?
home_dir = getpwnam(user).pw_dir
- render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.tmpl',
+ render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.j2',
user_config, permission=0o600,
formater=lambda _: _.replace("&quot;", '"'),
user=user, group='users')
@@ -250,13 +252,22 @@ def apply(login):
if 'rm_users' in login:
for user in login['rm_users']:
try:
+ # Disable user to prevent re-login
+ call(f'usermod -s /sbin/nologin {user}')
+
# Logout user if he is still logged in
if user in list(set([tmp[0] for tmp in users()])):
print(f'{user} is logged in, forcing logout!')
- call(f'pkill -HUP -u {user}')
-
- # Remove user account but leave home directory to be safe
- call(f'userdel --remove {user}', stderr=DEVNULL)
+ # re-run command until user is logged out
+ while run(f'pkill -HUP -u {user}'):
+ sleep(0.250)
+
+ # Remove user account but leave home directory in place. Re-run
+ # command until user is removed - userdel might return 8 as
+ # SSH sessions are not all yet properly cleaned away, thus we
+ # simply re-run the command until the account wen't away
+ while run(f'userdel --remove {user}', stderr=DEVNULL):
+ sleep(0.250)
except Exception as e:
raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
diff --git a/src/conf_mode/system-logs.py b/src/conf_mode/system-logs.py
new file mode 100755
index 000000000..c71938a79
--- /dev/null
+++ b/src/conf_mode/system-logs.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from sys import exit
+
+from vyos import ConfigError
+from vyos import airbag
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.logger import syslog
+from vyos.template import render
+from vyos.util import dict_search
+from vyos.xml import defaults
+airbag.enable()
+
+# path to logrotate configs
+logrotate_atop_file = '/etc/logrotate.d/vyos-atop'
+logrotate_rsyslog_file = '/etc/logrotate.d/vyos-rsyslog'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['system', 'logs']
+ default_values = defaults(base)
+ logs_config = conf.get_config_dict(base,
+ key_mangling=('-', '_'),
+ get_first_key=True)
+ logs_config = dict_merge(default_values, logs_config)
+
+ return logs_config
+
+
+def verify(logs_config):
+ # Nothing to verify here
+ pass
+
+
+def generate(logs_config):
+ # get configuration for logrotate atop
+ logrotate_atop = dict_search('logrotate.atop', logs_config)
+ # generate new config file for atop
+ syslog.debug('Adding logrotate config for atop')
+ render(logrotate_atop_file, 'logs/logrotate/vyos-atop.j2', logrotate_atop)
+
+ # get configuration for logrotate rsyslog
+ logrotate_rsyslog = dict_search('logrotate.messages', logs_config)
+ # generate new config file for rsyslog
+ syslog.debug('Adding logrotate config for rsyslog')
+ render(logrotate_rsyslog_file, 'logs/logrotate/vyos-rsyslog.j2',
+ logrotate_rsyslog)
+
+
+def apply(logs_config):
+ # No further actions needed
+ pass
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py
index b1c63e316..36dbf155b 100755
--- a/src/conf_mode/system-option.py
+++ b/src/conf_mode/system-option.py
@@ -74,8 +74,8 @@ def verify(options):
return None
def generate(options):
- render(curlrc_config, 'system/curlrc.tmpl', options)
- render(ssh_config, 'system/ssh_config.tmpl', options)
+ render(curlrc_config, 'system/curlrc.j2', options)
+ render(ssh_config, 'system/ssh_config.j2', options)
return None
def apply(options):
diff --git a/src/conf_mode/system-proxy.py b/src/conf_mode/system-proxy.py
index 02536c2ab..079c43e7e 100755
--- a/src/conf_mode/system-proxy.py
+++ b/src/conf_mode/system-proxy.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
@@ -13,83 +13,59 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#
-import sys
import os
-import re
-from vyos import ConfigError
-from vyos.config import Config
+from sys import exit
+from vyos.config import Config
+from vyos.template import render
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
proxy_def = r'/etc/profile.d/vyos-system-proxy.sh'
-
-def get_config():
- c = Config()
- if not c.exists('system proxy'):
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['system', 'proxy']
+ if not conf.exists(base):
return None
- c.set_level('system proxy')
+ proxy = conf.get_config_dict(base, get_first_key=True)
+ return proxy
- cnf = {
- 'url': None,
- 'port': None,
- 'usr': None,
- 'passwd': None
- }
+def verify(proxy):
+ if not proxy:
+ return
- if c.exists('url'):
- cnf['url'] = c.return_value('url')
- if c.exists('port'):
- cnf['port'] = c.return_value('port')
- if c.exists('username'):
- cnf['usr'] = c.return_value('username')
- if c.exists('password'):
- cnf['passwd'] = c.return_value('password')
+ if 'url' not in proxy or 'port' not in proxy:
+ raise ConfigError('Proxy URL and port require a value')
- return cnf
+ if ('username' in proxy and 'password' not in proxy) or \
+ ('username' not in proxy and 'password' in proxy):
+ raise ConfigError('Both username and password need to be defined!')
+def generate(proxy):
+ if not proxy:
+ if os.path.isfile(proxy_def):
+ os.unlink(proxy_def)
+ return
-def verify(c):
- if not c:
- return None
- if not c['url'] or not c['port']:
- raise ConfigError("proxy url and port requires a value")
- elif c['usr'] and not c['passwd']:
- raise ConfigError("proxy password requires a value")
- elif not c['usr'] and c['passwd']:
- raise ConfigError("proxy username requires a value")
-
+ render(proxy_def, 'system/proxy.j2', proxy, permission=0o755)
-def generate(c):
- if not c:
- return None
- if not c['usr']:
- return str("export http_proxy={url}:{port}\nexport https_proxy=$http_proxy\nexport ftp_proxy=$http_proxy"
- .format(url=c['url'], port=c['port']))
- else:
- return str("export http_proxy=http://{usr}:{passwd}@{url}:{port}\nexport https_proxy=$http_proxy\nexport ftp_proxy=$http_proxy"
- .format(url=re.sub('http://', '', c['url']), port=c['port'], usr=c['usr'], passwd=c['passwd']))
-
-
-def apply(ln):
- if not ln and os.path.exists(proxy_def):
- os.remove(proxy_def)
- else:
- open(proxy_def, 'w').write(
- "# generated by system-proxy.py\n{}\n".format(ln))
+def apply(proxy):
+ pass
if __name__ == '__main__':
try:
c = get_config()
verify(c)
- ln = generate(c)
- apply(ln)
+ generate(c)
+ apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
index 3d8a51cd8..a9d3bbe31 100755
--- a/src/conf_mode/system-syslog.py
+++ b/src/conf_mode/system-syslog.py
@@ -17,6 +17,7 @@
import os
import re
+from pathlib import Path
from sys import exit
from vyos.config import Config
@@ -89,7 +90,7 @@ def get_config(config=None):
filename: {
'log-file': '/var/log/user/' + filename,
'max-files': '5',
- 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename,
+ 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog-generated-' + filename,
'selectors': '*.err',
'max-size': 262144
}
@@ -203,12 +204,19 @@ def generate(c):
return None
conf = '/etc/rsyslog.d/vyos-rsyslog.conf'
- render(conf, 'syslog/rsyslog.conf.tmpl', c)
+ render(conf, 'syslog/rsyslog.conf.j2', c)
+
+ # cleanup current logrotate config files
+ logrotate_files = Path('/etc/logrotate.d/').glob('vyos-rsyslog-generated-*')
+ for file in logrotate_files:
+ file.unlink()
# eventually write for each file its own logrotate file, since size is
# defined it shouldn't matter
- conf = '/etc/logrotate.d/vyos-rsyslog'
- render(conf, 'syslog/logrotate.tmpl', c)
+ for filename, fileconfig in c.get('files', {}).items():
+ if fileconfig['log-file'].startswith('/var/log/user/'):
+ conf = '/etc/logrotate.d/vyos-rsyslog-generated-' + filename
+ render(conf, 'syslog/logrotate.j2', { 'config_render': fileconfig })
def verify(c):
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 19b252513..86985d765 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -103,7 +103,7 @@ def generate(console):
config_file = base_dir + f'/serial-getty@{device}.service'
getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service'
- render(config_file, 'getty/serial-getty.service.tmpl', device_config)
+ render(config_file, 'getty/serial-getty.service.j2', device_config)
os.symlink(config_file, getty_wants_symlink)
# GRUB
diff --git a/src/conf_mode/system_lcd.py b/src/conf_mode/system_lcd.py
index b5ce32beb..3341dd738 100755
--- a/src/conf_mode/system_lcd.py
+++ b/src/conf_mode/system_lcd.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2020-2022 VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -61,9 +61,9 @@ def generate(lcd):
lcd['device'] = find_device_file(lcd['device'])
# Render config file for daemon LCDd
- render(lcdd_conf, 'lcd/LCDd.conf.tmpl', lcd)
+ render(lcdd_conf, 'lcd/LCDd.conf.j2', lcd)
# Render config file for client lcdproc
- render(lcdproc_conf, 'lcd/lcdproc.conf.tmpl', lcd)
+ render(lcdproc_conf, 'lcd/lcdproc.conf.j2', lcd)
return None
diff --git a/src/conf_mode/system_sysctl.py b/src/conf_mode/system_sysctl.py
index 4f16d1ed6..2e0004ffa 100755
--- a/src/conf_mode/system_sysctl.py
+++ b/src/conf_mode/system_sysctl.py
@@ -50,7 +50,7 @@ def generate(sysctl):
os.unlink(config_file)
return None
- render(config_file, 'system/sysctl.conf.tmpl', sysctl)
+ render(config_file, 'system/sysctl.conf.j2', sysctl)
return None
def apply(sysctl):
diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py
index 2409eec1f..c5daccb7f 100755
--- a/src/conf_mode/tftp_server.py
+++ b/src/conf_mode/tftp_server.py
@@ -22,8 +22,10 @@ from copy import deepcopy
from glob import glob
from sys import exit
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configverify import verify_vrf
from vyos.template import render
from vyos.template import is_ipv4
from vyos.util import call
@@ -65,10 +67,11 @@ def verify(tftpd):
if 'listen_address' not in tftpd:
raise ConfigError('TFTP server listen address must be configured!')
- for address in tftpd['listen_address']:
+ for address, address_config in tftpd['listen_address'].items():
if not is_addr_assigned(address):
- print(f'WARNING: TFTP server listen address "{address}" not ' \
- 'assigned to any interface!')
+ Warning(f'TFTP server listen address "{address}" not ' \
+ 'assigned to any interface!')
+ verify_vrf(address_config)
return None
@@ -83,7 +86,7 @@ def generate(tftpd):
return None
idx = 0
- for address in tftpd['listen_address']:
+ for address, address_config in tftpd['listen_address'].items():
config = deepcopy(tftpd)
port = tftpd['port']
if is_ipv4(address):
@@ -91,8 +94,11 @@ def generate(tftpd):
else:
config['listen_address'] = f'[{address}]:{port} -6'
+ if 'vrf' in address_config:
+ config['vrf'] = address_config['vrf']
+
file = config_file + str(idx)
- render(file, 'tftp-server/default.tmpl', config)
+ render(file, 'tftp-server/default.j2', config)
idx = idx + 1
return None
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 99b82ca2d..bad9cfbd8 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -503,7 +503,7 @@ def generate(ipsec):
charon_radius_conf, interface_conf, swanctl_conf]:
if os.path.isfile(config_file):
os.unlink(config_file)
- render(charon_conf, 'ipsec/charon.tmpl', {'install_routes': default_install_routes})
+ render(charon_conf, 'ipsec/charon.j2', {'install_routes': default_install_routes})
return
if ipsec['dhcp_no_address']:
@@ -553,25 +553,27 @@ def generate(ipsec):
if not local_prefixes or not remote_prefixes:
continue
- passthrough = []
+ passthrough = None
for local_prefix in local_prefixes:
for remote_prefix in remote_prefixes:
local_net = ipaddress.ip_network(local_prefix)
remote_net = ipaddress.ip_network(remote_prefix)
if local_net.overlaps(remote_net):
+ if passthrough is None:
+ passthrough = []
passthrough.append(local_prefix)
ipsec['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough
- render(ipsec_conf, 'ipsec/ipsec.conf.tmpl', ipsec)
- render(ipsec_secrets, 'ipsec/ipsec.secrets.tmpl', ipsec)
- render(charon_conf, 'ipsec/charon.tmpl', ipsec)
- render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.tmpl', ipsec)
- render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.tmpl', ipsec)
- render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', ipsec)
- render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', ipsec)
+ render(ipsec_conf, 'ipsec/ipsec.conf.j2', ipsec)
+ render(ipsec_secrets, 'ipsec/ipsec.secrets.j2', ipsec)
+ render(charon_conf, 'ipsec/charon.j2', ipsec)
+ render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec)
+ render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec)
+ render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec)
+ render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec)
def resync_nhrp(ipsec):
if ipsec and not ipsec['nhrp_exists']:
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index 818e8fa0b..fd5a4acd8 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -358,10 +358,10 @@ def generate(l2tp):
if not l2tp:
return None
- render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp)
+ render(l2tp_conf, 'accel-ppp/l2tp.config.j2', l2tp)
if l2tp['auth_mode'] == 'local':
- render(l2tp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', l2tp)
+ render(l2tp_chap_secrets, 'accel-ppp/chap-secrets.j2', l2tp)
os.chmod(l2tp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
else:
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py
index f6db196dc..8e0e30bbf 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -23,9 +23,12 @@ from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
+from vyos.util import is_systemd_service_running
+from vyos.util import dict_search
from vyos.xml import defaults
from vyos import ConfigError
from crypt import crypt, mksalt, METHOD_SHA512
+from time import sleep
from vyos import airbag
airbag.enable()
@@ -33,6 +36,7 @@ airbag.enable()
cfg_dir = '/run/ocserv'
ocserv_conf = cfg_dir + '/ocserv.conf'
ocserv_passwd = cfg_dir + '/ocpasswd'
+ocserv_otp_usr = cfg_dir + '/users.oath'
radius_cfg = cfg_dir + '/radiusclient.conf'
radius_servers = cfg_dir + '/radius_servers'
@@ -52,6 +56,16 @@ def get_config():
default_values = defaults(base)
ocserv = dict_merge(default_values, ocserv)
+ # workaround a "know limitation" - https://phabricator.vyos.net/T2665
+ del ocserv['authentication']['local_users']['username']['otp']
+ if not ocserv["authentication"]["local_users"]["username"]:
+ raise ConfigError('openconnect mode local required at least one user')
+ default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp']
+ for user, params in ocserv['authentication']['local_users']['username'].items():
+ # Not every configuration requires OTP settings
+ if ocserv['authentication']['local_users']['username'][user].get('otp'):
+ ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp'])
+
if ocserv:
ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
@@ -61,17 +75,34 @@ def get_config():
def verify(ocserv):
if ocserv is None:
return None
-
# Check authentication
if "authentication" in ocserv:
if "mode" in ocserv["authentication"]:
if "local" in ocserv["authentication"]["mode"]:
- if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]:
- raise ConfigError('openconnect mode local required at leat one user')
+ if "radius" in ocserv["authentication"]["mode"]:
+ raise ConfigError('OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration')
+ if not ocserv["authentication"]["local_users"]:
+ raise ConfigError('openconnect mode local required at least one user')
+ if not ocserv["authentication"]["local_users"]["username"]:
+ raise ConfigError('openconnect mode local required at least one user')
else:
- for user in ocserv["authentication"]["local_users"]["username"]:
- if not "password" in ocserv["authentication"]["local_users"]["username"][user]:
- raise ConfigError(f'password required for user {user}')
+ # For OTP mode: verify that each local user has an OTP key
+ if "otp" in ocserv["authentication"]["mode"]["local"]:
+ users_wo_key = []
+ for user, user_config in ocserv["authentication"]["local_users"]["username"].items():
+ # User has no OTP key defined
+ if dict_search('otp.key', user_config) == None:
+ users_wo_key.append(user)
+ if users_wo_key:
+ raise ConfigError(f'OTP enabled, but no OTP key is configured for these users:\n{users_wo_key}')
+ # For password (and default) mode: verify that each local user has password
+ if "password" in ocserv["authentication"]["mode"]["local"] or "otp" not in ocserv["authentication"]["mode"]["local"]:
+ users_wo_pswd = []
+ for user in ocserv["authentication"]["local_users"]["username"]:
+ if not "password" in ocserv["authentication"]["local_users"]["username"][user]:
+ users_wo_pswd.append(user)
+ if users_wo_pswd:
+ raise ConfigError(f'password required for users:\n{users_wo_pswd}')
else:
raise ConfigError('openconnect authentication mode required')
else:
@@ -120,22 +151,57 @@ def verify(ocserv):
else:
raise ConfigError('openconnect network settings required')
-
def generate(ocserv):
if not ocserv:
return None
if "radius" in ocserv["authentication"]["mode"]:
# Render radius client configuration
- render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"])
+ render(radius_cfg, 'ocserv/radius_conf.j2', ocserv["authentication"]["radius"])
# Render radius servers
- render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"])
+ render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"])
+ elif "local" in ocserv["authentication"]["mode"]:
+ # if mode "OTP", generate OTP users file parameters
+ if "otp" in ocserv["authentication"]["mode"]["local"]:
+ if "local_users" in ocserv["authentication"]:
+ for user in ocserv["authentication"]["local_users"]["username"]:
+ # OTP token type from CLI parameters:
+ otp_interval = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("interval"))
+ token_type = ocserv["authentication"]["local_users"]["username"][user]["otp"].get("token_type")
+ otp_length = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("otp_length"))
+ if token_type == "hotp-time":
+ otp_type = "HOTP/T" + otp_interval
+ elif token_type == "hotp-event":
+ otp_type = "HOTP/E"
+ else:
+ otp_type = "HOTP/T" + otp_interval
+ ocserv["authentication"]["local_users"]["username"][user]["otp"]["token_tmpl"] = otp_type + "/" + otp_length
+ # if there is a password, generate hash
+ if "password" in ocserv["authentication"]["mode"]["local"] or not "otp" in ocserv["authentication"]["mode"]["local"]:
+ if "local_users" in ocserv["authentication"]:
+ for user in ocserv["authentication"]["local_users"]["username"]:
+ ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"])
+
+ if "password-otp" in ocserv["authentication"]["mode"]["local"]:
+ # Render local users ocpasswd
+ render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
+ # Render local users OTP keys
+ render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.j2', ocserv["authentication"]["local_users"])
+ elif "password" in ocserv["authentication"]["mode"]["local"]:
+ # Render local users ocpasswd
+ render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
+ elif "otp" in ocserv["authentication"]["mode"]["local"]:
+ # Render local users OTP keys
+ render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.j2', ocserv["authentication"]["local_users"])
+ else:
+ # Render local users ocpasswd
+ render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
else:
if "local_users" in ocserv["authentication"]:
for user in ocserv["authentication"]["local_users"]["username"]:
ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"])
# Render local users
- render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"])
+ render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
if "ssl" in ocserv:
cert_file_path = os.path.join(cfg_dir, 'cert.pem')
@@ -161,17 +227,27 @@ def generate(ocserv):
f.write(wrap_certificate(pki_ca_cert['certificate']))
# Render config
- render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv)
+ render(ocserv_conf, 'ocserv/ocserv_config.j2', ocserv)
def apply(ocserv):
if not ocserv:
call('systemctl stop ocserv.service')
- for file in [ocserv_conf, ocserv_passwd]:
+ for file in [ocserv_conf, ocserv_passwd, ocserv_otp_usr]:
if os.path.exists(file):
os.unlink(file)
else:
call('systemctl restart ocserv.service')
+ counter = 0
+ while True:
+ # exit early when service runs
+ if is_systemd_service_running("ocserv.service"):
+ break
+ sleep(0.250)
+ if counter > 5:
+ raise ConfigError('openconnect failed to start, check the logs for details')
+ break
+ counter += 1
if __name__ == '__main__':
diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
index 30abe4782..7550c411e 100755
--- a/src/conf_mode/vpn_pptp.py
+++ b/src/conf_mode/vpn_pptp.py
@@ -264,10 +264,10 @@ def generate(pptp):
if not pptp:
return None
- render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp)
+ render(pptp_conf, 'accel-ppp/pptp.config.j2', pptp)
if pptp['local_users']:
- render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp)
+ render(pptp_chap_secrets, 'accel-ppp/chap-secrets.j2', pptp)
os.chmod(pptp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
else:
if os.path.exists(pptp_chap_secrets):
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index 68980e5ab..db53463cf 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -114,7 +114,7 @@ def generate(sstp):
return None
# accel-cmd reload doesn't work so any change results in a restart of the daemon
- render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp)
+ render(sstp_conf, 'accel-ppp/sstp.config.j2', sstp)
cert_name = sstp['ssl']['certificate']
pki_cert = sstp['pki']['certificate'][cert_name]
@@ -127,7 +127,7 @@ def generate(sstp):
write_file(ca_cert_file_path, wrap_certificate(pki_ca['certificate']))
if dict_search('authentication.mode', sstp) == 'local':
- render(sstp_chap_secrets, 'accel-ppp/chap-secrets.config_dict.tmpl',
+ render(sstp_chap_secrets, 'accel-ppp/chap-secrets.config_dict.j2',
sstp, permission=0o640)
else:
if os.path.exists(sstp_chap_secrets):
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 38c0c4463..972d0289b 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.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
@@ -29,6 +29,7 @@ from vyos.util import dict_search
from vyos.util import get_interface_config
from vyos.util import popen
from vyos.util import run
+from vyos.util import sysctl_write
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -37,10 +38,16 @@ airbag.enable()
config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf'
nft_vrf_config = '/tmp/nftables-vrf-zones'
-def list_rules():
- command = 'ip -j -4 rule show'
- answer = loads(cmd(command))
- return [_ for _ in answer if _]
+def has_rule(af : str, priority : int, table : str):
+ """ Check if a given ip rule exists """
+ if af not in ['-4', '-6']:
+ raise ValueError()
+ command = f'ip -j {af} rule show'
+ for tmp in loads(cmd(command)):
+ if {'priority', 'table'} <= set(tmp):
+ if tmp['priority'] == priority and tmp['table'] == table:
+ return True
+ return False
def vrf_interfaces(c, match):
matched = []
@@ -69,7 +76,6 @@ def vrf_routing(c, match):
c.set_level(old_level)
return matched
-
def get_config(config=None):
if config:
conf = config
@@ -77,7 +83,8 @@ def get_config(config=None):
conf = Config()
base = ['vrf']
- vrf = conf.get_config_dict(base, get_first_key=True)
+ vrf = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True, get_first_key=True)
# determine which VRF has been removed
for name in node_changed(conf, base + ['name']):
@@ -127,10 +134,10 @@ def verify(vrf):
def generate(vrf):
- render(config_file, 'vrf/vrf.conf.tmpl', vrf)
+ render(config_file, 'vrf/vrf.conf.j2', vrf)
# Render nftables zones config
- render(nft_vrf_config, 'firewall/nftables-vrf-zones.tmpl', vrf)
+ render(nft_vrf_config, 'firewall/nftables-vrf-zones.j2', vrf)
return None
@@ -146,15 +153,13 @@ def apply(vrf):
# set the default VRF global behaviour
bind_all = '0'
- if 'bind-to-all' in vrf:
+ if 'bind_to_all' in vrf:
bind_all = '1'
- call(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}')
- call(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}')
+ sysctl_write('net.ipv4.tcp_l3mdev_accept', bind_all)
+ sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all)
for tmp in (dict_search('vrf_remove', vrf) or []):
if os.path.isdir(f'/sys/class/net/{tmp}'):
- call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272')
- call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272')
call(f'ip link delete dev {tmp}')
# Remove nftables conntrack zone map item
nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}'
@@ -165,31 +170,68 @@ def apply(vrf):
# check if table already exists
_, err = popen('nft list table inet vrf_zones')
# If not, create a table
- if err:
- if os.path.exists(nft_vrf_config):
- cmd(f'nft -f {nft_vrf_config}')
- os.unlink(nft_vrf_config)
+ if err and os.path.exists(nft_vrf_config):
+ cmd(f'nft -f {nft_vrf_config}')
+ os.unlink(nft_vrf_config)
+
+ # Linux routing uses rules to find tables - routing targets are then
+ # looked up in those tables. If the lookup got a matching route, the
+ # process ends.
+ #
+ # TL;DR; first table with a matching entry wins!
+ #
+ # You can see your routing table lookup rules using "ip rule", sadly the
+ # local lookup is hit before any VRF lookup. Pinging an addresses from the
+ # VRF will usually find a hit in the local table, and never reach the VRF
+ # routing table - this is usually not what you want. Thus we will
+ # re-arrange the tables and move the local lookup further down once VRFs
+ # are enabled.
+ #
+ # Thanks to https://stbuehler.de/blog/article/2020/02/29/using_vrf__virtual_routing_and_forwarding__on_linux.html
+
+ for afi in ['-4', '-6']:
+ # move lookup local to pref 32765 (from 0)
+ if not has_rule(afi, 32765, 'local'):
+ call(f'ip {afi} rule add pref 32765 table local')
+ if has_rule(afi, 0, 'local'):
+ call(f'ip {afi} rule del pref 0')
+ # make sure that in VRFs after failed lookup in the VRF specific table
+ # nothing else is reached
+ if not has_rule(afi, 1000, 'l3mdev'):
+ # this should be added by the kernel when a VRF is created
+ # add it here for completeness
+ call(f'ip {afi} rule add pref 1000 l3mdev protocol kernel')
+
+ # add another rule with an unreachable target which only triggers in VRF context
+ # if a route could not be reached
+ if not has_rule(afi, 2000, 'l3mdev'):
+ call(f'ip {afi} rule add pref 2000 l3mdev unreachable')
for name, config in vrf['name'].items():
table = config['table']
-
if not os.path.isdir(f'/sys/class/net/{name}'):
# For each VRF apart from your default context create a VRF
# interface with a separate routing table
call(f'ip link add {name} type vrf table {table}')
- # The kernel Documentation/networking/vrf.txt also recommends
- # adding unreachable routes to the VRF routing tables so that routes
- # afterwards are taken.
- call(f'ip -4 route add vrf {name} unreachable default metric 4278198272')
- call(f'ip -6 route add vrf {name} unreachable default metric 4278198272')
- # We also should add proper loopback IP addresses to the newly
- # created VRFs for services bound to the loopback address (SNMP, NTP)
- call(f'ip -4 addr add 127.0.0.1/8 dev {name}')
- call(f'ip -6 addr add ::1/128 dev {name}')
# set VRF description for e.g. SNMP monitoring
vrf_if = Interface(name)
+ # We also should add proper loopback IP addresses to the newly added
+ # VRF for services bound to the loopback address (SNMP, NTP)
+ vrf_if.add_addr('127.0.0.1/8')
+ vrf_if.add_addr('::1/128')
+ # add VRF description if available
vrf_if.set_alias(config.get('description', ''))
+
+ # Enable/Disable IPv4 forwarding
+ tmp = dict_search('ip.disable_forwarding', config)
+ value = '0' if (tmp != None) else '1'
+ vrf_if.set_ipv4_forwarding(value)
+ # Enable/Disable IPv6 forwarding
+ tmp = dict_search('ipv6.disable_forwarding', config)
+ value = '0' if (tmp != None) else '1'
+ vrf_if.set_ipv6_forwarding(value)
+
# Enable/Disable of an interface must always be done at the end of the
# derived class to make use of the ref-counting set_admin_state()
# function. We will only enable the interface if 'up' was called as
@@ -203,37 +245,9 @@ def apply(vrf):
nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
cmd(f'nft {nft_add_element}')
- # Linux routing uses rules to find tables - routing targets are then
- # looked up in those tables. If the lookup got a matching route, the
- # process ends.
- #
- # TL;DR; first table with a matching entry wins!
- #
- # You can see your routing table lookup rules using "ip rule", sadly the
- # local lookup is hit before any VRF lookup. Pinging an addresses from the
- # VRF will usually find a hit in the local table, and never reach the VRF
- # routing table - this is usually not what you want. Thus we will
- # re-arrange the tables and move the local lookup furhter down once VRFs
- # are enabled.
-
- # get current preference on local table
- local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0]
-
- # change preference when VRFs are enabled and local lookup table is default
- if not local_pref and 'name' in vrf:
- for af in ['-4', '-6']:
- call(f'ip {af} rule add pref 32765 table local')
- call(f'ip {af} rule del pref 0')
# return to default lookup preference when no VRF is configured
if 'name' not in vrf:
- for af in ['-4', '-6']:
- call(f'ip {af} rule add pref 0 table local')
- call(f'ip {af} rule del pref 32765')
-
- # clean out l3mdev-table rule if present
- if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]:
- call(f'ip {af} rule del pref 1000')
# Remove VRF zones table from nftables
tmp = run('nft list table inet vrf_zones')
if tmp == 0:
diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py
index 50d60f0dc..585fdbebf 100755
--- a/src/conf_mode/vrf_vni.py
+++ b/src/conf_mode/vrf_vni.py
@@ -40,20 +40,18 @@ def verify(vrf):
return None
def generate(vrf):
- vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.tmpl', vrf)
+ vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.j2', vrf)
return None
def apply(vrf):
# add configuration to FRR
frr_cfg = frr.FRRConfig()
frr_cfg.load_configuration(frr_daemon)
- frr_cfg.modify_section(f'^vrf .+$', '')
- frr_cfg.add_before(r'(interface .*|line vty)', vrf['new_frr_config'])
+ frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True)
+ if 'new_frr_config' in vrf:
+ frr_cfg.add_before(frr.default_add_before, vrf['new_frr_config'])
frr_cfg.commit_configuration(frr_daemon)
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py
new file mode 100755
index 000000000..070a4deea
--- /dev/null
+++ b/src/conf_mode/zone_policy.py
@@ -0,0 +1,213 @@
+#!/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 json import loads
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import cmd
+from vyos.util import dict_search_args
+from vyos.util import run
+from vyos.xml import defaults
+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)
+
+ zone_policy['firewall'] = conf.get_config_dict(['firewall'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ if 'zone' in zone_policy:
+ # 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 + ['zone'])
+ for zone in zone_policy['zone']:
+ zone_policy['zone'][zone] = dict_merge(default_values,
+ zone_policy['zone'][zone])
+
+ 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.j2', 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/cron.d/check-wwan b/src/etc/cron.d/check-wwan
deleted file mode 100644
index 28190776f..000000000
--- a/src/etc/cron.d/check-wwan
+++ /dev/null
@@ -1 +0,0 @@
-*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
index 74a7e83bf..5d879471d 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
@@ -26,7 +26,7 @@ function iptovtysh () {
local VTYSH_GATEWAY=""
local VTYSH_DEV=""
local VTYSH_TAG="210"
- local VTYSH_DISTANCE=""
+ local VTYSH_DISTANCE=$IF_METRIC
# convert default route to 0.0.0.0/0
if [ "$4" == "default" ] ; then
VTYSH_NETADDR="0.0.0.0/0"
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
index 24090e2a8..b1902b585 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
@@ -1,44 +1,48 @@
-# modified make_resolv_conf () for VyOS
-make_resolv_conf() {
- hostsd_client="/usr/bin/vyos-hostsd-client"
- hostsd_changes=
+# modified make_resolv_conf() for VyOS
+# should be used only if vyos-hostsd is running
- if [ -n "$new_domain_name" ]; then
- logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client"
- $hostsd_client --delete-search-domains --tag "dhcp-$interface"
- logmsg info "Adding domain name \"$new_domain_name\" as search domain with tag \"dhcp-$interface\" via vyos-hostsd-client"
- $hostsd_client --add-search-domains "$new_domain_name" --tag "dhcp-$interface"
- hostsd_changes=y
- fi
+if /usr/bin/systemctl -q is-active vyos-hostsd; then
+ make_resolv_conf() {
+ hostsd_client="/usr/bin/vyos-hostsd-client"
+ hostsd_changes=
- if [ -n "$new_dhcp6_domain_search" ]; then
- logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
- $hostsd_client --delete-search-domains --tag "dhcpv6-$interface"
- logmsg info "Adding search domain \"$new_dhcp6_domain_search\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
- $hostsd_client --add-search-domains "$new_dhcp6_domain_search" --tag "dhcpv6-$interface"
- hostsd_changes=y
- fi
+ if [ -n "$new_domain_name" ]; then
+ logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-search-domains --tag "dhcp-$interface"
+ logmsg info "Adding domain name \"$new_domain_name\" as search domain with tag \"dhcp-$interface\" via vyos-hostsd-client"
+ $hostsd_client --add-search-domains "$new_domain_name" --tag "dhcp-$interface"
+ hostsd_changes=y
+ fi
- if [ -n "$new_domain_name_servers" ]; then
- logmsg info "Deleting nameservers with tag \"dhcp-$interface\" via vyos-hostsd-client"
- $hostsd_client --delete-name-servers --tag "dhcp-$interface"
- logmsg info "Adding nameservers \"$new_domain_name_servers\" with tag \"dhcp-$interface\" via vyos-hostsd-client"
- $hostsd_client --add-name-servers $new_domain_name_servers --tag "dhcp-$interface"
- hostsd_changes=y
- fi
+ if [ -n "$new_dhcp6_domain_search" ]; then
+ logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-search-domains --tag "dhcpv6-$interface"
+ logmsg info "Adding search domain \"$new_dhcp6_domain_search\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --add-search-domains "$new_dhcp6_domain_search" --tag "dhcpv6-$interface"
+ hostsd_changes=y
+ fi
- if [ -n "$new_dhcp6_name_servers" ]; then
- logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
- $hostsd_client --delete-name-servers --tag "dhcpv6-$interface"
- logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
- $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface"
- hostsd_changes=y
- fi
+ if [ -n "$new_domain_name_servers" ]; then
+ logmsg info "Deleting nameservers with tag \"dhcp-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-name-servers --tag "dhcp-$interface"
+ logmsg info "Adding nameservers \"$new_domain_name_servers\" with tag \"dhcp-$interface\" via vyos-hostsd-client"
+ $hostsd_client --add-name-servers $new_domain_name_servers --tag "dhcp-$interface"
+ hostsd_changes=y
+ fi
- if [ $hostsd_changes ]; then
- logmsg info "Applying changes via vyos-hostsd-client"
- $hostsd_client --apply
- else
- logmsg info "No changes to apply via vyos-hostsd-client"
- fi
-}
+ if [ -n "$new_dhcp6_name_servers" ]; then
+ logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-name-servers --tag "dhcpv6-$interface"
+ logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface"
+ hostsd_changes=y
+ fi
+
+ if [ $hostsd_changes ]; then
+ logmsg info "Applying changes via vyos-hostsd-client"
+ $hostsd_client --apply
+ else
+ logmsg info "No changes to apply via vyos-hostsd-client"
+ fi
+ }
+fi
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
index fec792b64..ad6a1d5eb 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -4,14 +4,19 @@
# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state
hostsd_client="/usr/bin/vyos-hostsd-client"
hostsd_changes=
+# check vyos-hostsd status
+/usr/bin/systemctl -q is-active vyos-hostsd
+hostsd_status=$?
if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
- # delete search domains and nameservers via vyos-hostsd
- logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client"
- $hostsd_client --delete-search-domains --tag "dhcp-$interface"
- logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client"
- $hostsd_client --delete-name-servers --tag "dhcp-${interface}"
- hostsd_changes=y
+ if [[ $hostsd_status -eq 0 ]]; then
+ # delete search domains and nameservers via vyos-hostsd
+ logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-search-domains --tag "dhcp-$interface"
+ logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client"
+ $hostsd_client --delete-name-servers --tag "dhcp-${interface}"
+ hostsd_changes=y
+ fi
if_metric="$IF_METRIC"
@@ -92,12 +97,14 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
fi
if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then
- # delete search domains and nameservers via vyos-hostsd
- logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
- $hostsd_client --delete-search-domains --tag "dhcpv6-$interface"
- logmsg info "Deleting nameservers with tag \"dhcpv6-${interface}\" via vyos-hostsd-client"
- $hostsd_client --delete-name-servers --tag "dhcpv6-${interface}"
- hostsd_changes=y
+ if [[ $hostsd_status -eq 0 ]]; then
+ # delete search domains and nameservers via vyos-hostsd
+ logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --delete-search-domains --tag "dhcpv6-$interface"
+ logmsg info "Deleting nameservers with tag \"dhcpv6-${interface}\" via vyos-hostsd-client"
+ $hostsd_client --delete-name-servers --tag "dhcpv6-${interface}"
+ hostsd_changes=y
+ fi
fi
if [ $hostsd_changes ]; then
diff --git a/src/etc/logrotate.d/conntrackd b/src/etc/logrotate.d/conntrackd
new file mode 100644
index 000000000..b0b09dec1
--- /dev/null
+++ b/src/etc/logrotate.d/conntrackd
@@ -0,0 +1,9 @@
+/var/log/conntrackd-stats.log {
+ weekly
+ rotate 2
+ missingok
+
+ postrotate
+ systemctl restart conntrackd.service > /dev/null
+ endscript
+}
diff --git a/src/etc/logrotate.d/vyos-rsyslog b/src/etc/logrotate.d/vyos-rsyslog
new file mode 100644
index 000000000..3c087b94e
--- /dev/null
+++ b/src/etc/logrotate.d/vyos-rsyslog
@@ -0,0 +1,12 @@
+/var/log/messages {
+ create
+ missingok
+ nomail
+ notifempty
+ rotate 10
+ size 1M
+ postrotate
+ # inform rsyslog service about rotation
+ /usr/lib/rsyslog/rsyslog-rotate
+ endscript
+}
diff --git a/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback b/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback
index bb918a468..fa1917ab1 100755
--- a/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback
+++ b/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback
@@ -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
@@ -23,14 +23,9 @@
from sys import argv
from sys import exit
-from syslog import syslog
-from syslog import openlog
-from syslog import LOG_PID
-from syslog import LOG_INFO
-
from vyos.configquery import ConfigTreeQuery
+from vyos.configdict import get_interface_dict
from vyos.ifconfig import PPPoEIf
-from vyos.util import read_file
# When the ppp link comes up, this script is called with the following
# parameters
@@ -45,15 +40,10 @@ if (len(argv) < 7):
exit(1)
interface = argv[6]
-dialer_pid = read_file(f'/var/run/{interface}.pid')
-
-openlog(ident=f'pppd[{dialer_pid}]', facility=LOG_INFO)
-syslog('executing ' + argv[0])
conf = ConfigTreeQuery()
-pppoe = conf.get_config_dict(['interfaces', 'pppoe', argv[6]],
- get_first_key=True, key_mangling=('-', '_'))
-pppoe['ifname'] = argv[6]
+_, pppoe = get_interface_dict(conf.config, ['interfaces', 'pppoe'], interface)
-p = PPPoEIf(pppoe['ifname'])
+# Update the config
+p = PPPoEIf(interface)
p.update(pppoe)
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/system-versions-foot.py b/src/helpers/system-versions-foot.py
index c33e41d79..2aa687221 100755
--- a/src/helpers/system-versions-foot.py
+++ b/src/helpers/system-versions-foot.py
@@ -21,7 +21,7 @@ import vyos.systemversions as systemversions
import vyos.defaults
import vyos.version
-sys_versions = systemversions.get_system_versions()
+sys_versions = systemversions.get_system_component_version()
component_string = formatversions.format_versions_string(sys_versions)
diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py
index c5bf22f10..b9cc87bfa 100755
--- a/src/helpers/vyos-boot-config-loader.py
+++ b/src/helpers/vyos-boot-config-loader.py
@@ -23,12 +23,12 @@ import grp
import traceback
from datetime import datetime
-from vyos.defaults import directories
+from vyos.defaults import directories, config_status
from vyos.configsession import ConfigSession, ConfigSessionError
from vyos.configtree import ConfigTree
from vyos.util import cmd
-STATUS_FILE = '/tmp/vyos-config-status'
+STATUS_FILE = config_status
TRACE_FILE = '/tmp/boot-config-trace'
CFG_GROUP = 'vyattacfg'
diff --git a/src/helpers/vyos-check-wwan.py b/src/helpers/vyos-check-wwan.py
index c6e6c54b7..2ff9a574f 100755
--- a/src/helpers/vyos-check-wwan.py
+++ b/src/helpers/vyos-check-wwan.py
@@ -18,7 +18,6 @@ from vyos.configquery import VbashOpRun
from vyos.configquery import ConfigTreeQuery
from vyos.util import is_wwan_connected
-from vyos.util import call
conf = ConfigTreeQuery()
dict = conf.get_config_dict(['interfaces', 'wwan'], key_mangling=('-', '_'),
@@ -30,8 +29,7 @@ for interface, interface_config in dict.items():
# do not restart this interface as it's disabled by the user
continue
- #op = VbashOpRun()
- #op.run(['connect', 'interface', interface])
- call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py')
+ op = VbashOpRun()
+ op.run(['connect', 'interface', interface])
exit(0)
diff --git a/src/helpers/vyos-vrrp-conntracksync.sh b/src/helpers/vyos-vrrp-conntracksync.sh
index 4501aa63e..0cc718938 100755
--- a/src/helpers/vyos-vrrp-conntracksync.sh
+++ b/src/helpers/vyos-vrrp-conntracksync.sh
@@ -14,12 +14,10 @@
# Modified by : Mohit Mehta <mohit@vyatta.com>
# Slight modifications were made to this script for running with Vyatta
# The original script came from 0.9.14 debian conntrack-tools package
-#
-#
CONNTRACKD_BIN=/usr/sbin/conntrackd
CONNTRACKD_LOCK=/var/lock/conntrack.lock
-CONNTRACKD_CONFIG=/etc/conntrackd/conntrackd.conf
+CONNTRACKD_CONFIG=/run/conntrackd/conntrackd.conf
FACILITY=daemon
LEVEL=notice
TAG=conntrack-tools
@@ -29,6 +27,10 @@ FAILOVER_STATE="/var/run/vyatta-conntrackd-failover-state"
$LOGCMD "vyatta-vrrp-conntracksync invoked at `date`"
+if ! systemctl is-active --quiet conntrackd.service; then
+ echo "conntrackd service not running"
+ exit 1
+fi
if [ ! -e $FAILOVER_STATE ]; then
mkdir -p /var/run
diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name
index e21d8c9ff..1798e92db 100755
--- a/src/helpers/vyos_net_name
+++ b/src/helpers/vyos_net_name
@@ -20,19 +20,20 @@ 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
+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'
vyos_log_file = os.path.join(vyos_log_dir, 'vyos-net-name')
config_path = '/opt/vyatta/etc/config/config.boot'
-config_status = '/tmp/vyos-config-status'
lock = threading.Lock()
@@ -43,13 +44,6 @@ except FileExistsError:
logging.basicConfig(filename=vyos_log_file, level=logging.DEBUG)
-def boot_configuration_complete() -> bool:
- """ Check if vyos-router has completed, hence hotplug event
- """
- if os.path.isfile(config_status):
- return True
- return False
-
def is_available(intfs: dict, intf_name: str) -> bool:
""" Check if interface name is already assigned
"""
@@ -147,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}")
@@ -254,4 +254,3 @@ if not boot_configuration_complete():
else:
logging.debug("boot configuration complete")
lock.release()
-
diff --git a/src/migration-scripts/bgp/0-to-1 b/src/migration-scripts/bgp/0-to-1
index b1d5a6514..5e9dffe1f 100755
--- a/src/migration-scripts/bgp/0-to-1
+++ b/src/migration-scripts/bgp/0-to-1
@@ -33,7 +33,7 @@ with open(file_name, 'r') as f:
base = ['protocols', 'bgp']
config = ConfigTree(config_file)
-if not config.exists(base):
+if not config.exists(base) or not config.is_tag(base):
# Nothing to do
exit(0)
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/flow-accounting/0-to-1 b/src/migration-scripts/flow-accounting/0-to-1
new file mode 100755
index 000000000..72cce77b0
--- /dev/null
+++ b/src/migration-scripts/flow-accounting/0-to-1
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# T4099: flow-accounting: sync "source-ip" and "source-address" between netflow
+# and sflow ion CLI
+# T4105: flow-accounting: drop "sflow agent-address auto"
+
+from sys import argv
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['system', 'flow-accounting']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+# T4099
+tmp = base + ['netflow', 'source-ip']
+if config.exists(tmp):
+ config.rename(tmp, 'source-address')
+
+# T4105
+tmp = base + ['sflow', 'agent-address']
+if config.exists(tmp):
+ value = config.return_value(tmp)
+ if value == 'auto':
+ # delete the "auto"
+ config.delete(tmp)
+
+ # 1) check if BGP router-id is set
+ # 2) check if OSPF router-id is set
+ # 3) check if OSPFv3 router-id is set
+ router_id = None
+ for protocol in ['bgp', 'ospf', 'ospfv3']:
+ if config.exists(['protocols', protocol, 'parameters', 'router-id']):
+ router_id = config.return_value(['protocols', protocol, 'parameters', 'router-id'])
+ break
+ if router_id:
+ config.set(tmp, value=router_id)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/interfaces/25-to-26 b/src/migration-scripts/interfaces/25-to-26
new file mode 100755
index 000000000..a8936235e
--- /dev/null
+++ b/src/migration-scripts/interfaces/25-to-26
@@ -0,0 +1,54 @@
+#!/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/>.
+
+# T4384: pppoe: replace default-route CLI option with common CLI nodes already
+# present for DHCP
+
+from sys import argv
+
+from vyos.ethtool import Ethtool
+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 = ['interfaces', 'pppoe']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ exit(0)
+
+for ifname in config.list_nodes(base):
+ tmp_config = base + [ifname, 'default-route']
+ if config.exists(tmp_config):
+ # Retrieve current config value
+ value = config.return_value(tmp_config)
+ # Delete old Config node
+ config.delete(tmp_config)
+ if value == 'none':
+ config.set(base + [ifname, 'no-default-route'])
+
+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/ipsec/8-to-9 b/src/migration-scripts/ipsec/8-to-9
new file mode 100755
index 000000000..eb44b6216
--- /dev/null
+++ b/src/migration-scripts/ipsec/8-to-9
@@ -0,0 +1,48 @@
+#!/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 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 = ['vpn', 'ipsec', 'ike-group']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+ for ike_group in config.list_nodes(base):
+ base_closeaction = base + [ike_group, 'close-action']
+ if config.exists(base_closeaction) and config.return_value(base_closeaction) == 'clear':
+ config.set(base_closeaction, 'none', replace=True)
+
+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/openconnect/1-to-2 b/src/migration-scripts/openconnect/1-to-2
new file mode 100755
index 000000000..7031fb252
--- /dev/null
+++ b/src/migration-scripts/openconnect/1-to-2
@@ -0,0 +1,54 @@
+#!/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/>.
+
+# Delete depricated outside-nexthop address
+
+import sys
+
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+cfg_base = ['vpn', 'openconnect']
+
+if not config.exists(cfg_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ if config.exists(cfg_base + ['authentication', 'mode']):
+ if config.return_value(cfg_base + ['authentication', 'mode']) == 'radius':
+ # if "mode value radius", change to "tag node mode + valueless node radius"
+ config.delete(cfg_base + ['authentication','mode', 'radius'])
+ config.set(cfg_base + ['authentication', 'mode', 'radius'], value=None, replace=True)
+ elif not config.exists(cfg_base + ['authentication', 'mode', 'local']):
+ # if "mode local", change to "tag node mode + node local value password"
+ config.delete(cfg_base + ['authentication', 'mode', 'local'])
+ config.set(cfg_base + ['authentication', 'mode', 'local'], value='password', replace=True)
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/ospf/0-to-1 b/src/migration-scripts/ospf/0-to-1
new file mode 100755
index 000000000..678569d9e
--- /dev/null
+++ b/src/migration-scripts/ospf/0-to-1
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# T3753: upgrade to FRR8 and move CLI options to better fit with the new FRR CLI
+
+from sys import argv
+from vyos.configtree import ConfigTree
+
+def ospf_passive_migration(config, ospf_base):
+ if config.exists(ospf_base):
+ if config.exists(ospf_base + ['passive-interface']):
+ default = False
+ for interface in config.return_values(ospf_base + ['passive-interface']):
+ if interface == 'default':
+ default = True
+ continue
+ config.set(ospf_base + ['interface', interface, 'passive'])
+
+ config.delete(ospf_base + ['passive-interface'])
+ config.set(ospf_base + ['passive-interface'], value='default')
+
+ if config.exists(ospf_base + ['passive-interface-exclude']):
+ for interface in config.return_values(ospf_base + ['passive-interface-exclude']):
+ config.set(ospf_base + ['interface', interface, 'passive', 'disable'])
+ config.delete(ospf_base + ['passive-interface-exclude'])
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+ospfv3_base = ['protocols', 'ospfv3']
+if config.exists(ospfv3_base):
+ area_base = ospfv3_base + ['area']
+ if config.exists(area_base):
+ for area in config.list_nodes(area_base):
+ if not config.exists(area_base + [area, 'interface']):
+ continue
+
+ for interface in config.return_values(area_base + [area, 'interface']):
+ config.set(ospfv3_base + ['interface', interface, 'area'], value=area)
+ config.set_tag(ospfv3_base + ['interface'])
+
+ config.delete(area_base + [area, 'interface'])
+
+# Migrate OSPF syntax in default VRF
+ospf_base = ['protocols', 'ospf']
+ospf_passive_migration(config, ospf_base)
+
+vrf_base = ['vrf', 'name']
+if config.exists(vrf_base):
+ for vrf in config.list_nodes(vrf_base):
+ vrf_ospf_base = vrf_base + [vrf, 'protocols', 'ospf']
+ if config.exists(vrf_ospf_base):
+ ospf_passive_migration(config, vrf_ospf_base)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print(f'Failed to save the modified config: {e}')
+ exit(1)
diff --git a/src/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/migration-scripts/quagga/9-to-10 b/src/migration-scripts/quagga/9-to-10
new file mode 100755
index 000000000..249738822
--- /dev/null
+++ b/src/migration-scripts/quagga/9-to-10
@@ -0,0 +1,62 @@
+#!/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/>.
+
+# re-organize route-map as-path
+
+from sys import argv
+from sys import exit
+
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 2):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['policy', 'route-map']
+
+config = ConfigTree(config_file)
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+for route_map in config.list_nodes(base):
+ # Bail out Early
+ if not config.exists(base + [route_map, 'rule']):
+ continue
+
+ for rule in config.list_nodes(base + [route_map, 'rule']):
+ rule_base = base + [route_map, 'rule', rule]
+ if config.exists(rule_base + ['set', 'as-path-exclude']):
+ tmp = config.return_value(rule_base + ['set', 'as-path-exclude'])
+ config.delete(rule_base + ['set', 'as-path-exclude'])
+ config.set(rule_base + ['set', 'as-path', 'exclude'], value=tmp)
+
+ if config.exists(rule_base + ['set', 'as-path-prepend']):
+ tmp = config.return_value(rule_base + ['set', 'as-path-prepend'])
+ config.delete(rule_base + ['set', 'as-path-prepend'])
+ config.set(rule_base + ['set', 'as-path', 'prepend'], value=tmp)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/ssh/1-to-2 b/src/migration-scripts/ssh/1-to-2
index bc8815753..31c40df16 100755
--- a/src/migration-scripts/ssh/1-to-2
+++ b/src/migration-scripts/ssh/1-to-2
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 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
@@ -30,26 +30,52 @@ file_name = argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
-base = ['service', 'ssh', 'loglevel']
+base = ['service', 'ssh']
config = ConfigTree(config_file)
if not config.exists(base):
# Nothing to do
exit(0)
-else:
- # red in configured loglevel and convert it to lower case
- tmp = config.return_value(base).lower()
+path_loglevel = base + ['loglevel']
+if config.exists(path_loglevel):
+ # red in configured loglevel and convert it to lower case
+ tmp = config.return_value(path_loglevel).lower()
# VyOS 1.2 had no proper value validation on the CLI thus the
# user could use any arbitrary values - sanitize them
if tmp not in ['quiet', 'fatal', 'error', 'info', 'verbose']:
tmp = 'info'
+ config.set(path_loglevel, value=tmp)
+
+# T4273: migrate ssh cipher list to multi node
+path_ciphers = base + ['ciphers']
+if config.exists(path_ciphers):
+ tmp = []
+ # get curtrent cipher list - comma delimited
+ for cipher in config.return_values(path_ciphers):
+ tmp.extend(cipher.split(','))
+ # delete old cipher suite representation
+ config.delete(path_ciphers)
- config.set(base, value=tmp)
+ for cipher in tmp:
+ config.set(path_ciphers, value=cipher, replace=False)
- 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)
+# T4273: migrate ssh key-exchange list to multi node
+path_kex = base + ['key-exchange']
+if config.exists(path_kex):
+ tmp = []
+ # get curtrent cipher list - comma delimited
+ for kex in config.return_values(path_kex):
+ tmp.extend(kex.split(','))
+ # delete old cipher suite representation
+ config.delete(path_kex)
+
+ for kex in tmp:
+ config.set(path_kex, value=kex, replace=False)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/system/22-to-23 b/src/migration-scripts/system/22-to-23
new file mode 100755
index 000000000..7f832e48a
--- /dev/null
+++ b/src/migration-scripts/system/22-to-23
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit, argv
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['system', 'ipv6']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+# T4346: drop support to disbale IPv6 address family within the OS Kernel
+if config.exists(base + ['disable']):
+ config.delete(base + ['disable'])
+ # IPv6 address family disable was the only CLI option set - we can cleanup
+ # the entire tree
+ if len(config.list_nodes(base)) == 0:
+ config.delete(base)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print(f'Failed to save the modified config: {e}')
+ exit(1)
diff --git a/src/migration-scripts/system/23-to-24 b/src/migration-scripts/system/23-to-24
new file mode 100755
index 000000000..5ea71d51a
--- /dev/null
+++ b/src/migration-scripts/system/23-to-24
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from ipaddress import ip_interface
+from ipaddress import ip_address
+from sys import exit, argv
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['protocols', 'static', 'arp']
+tmp_base = ['protocols', 'static', 'arp-tmp']
+config = ConfigTree(config_file)
+
+def fixup_cli(config, path, interface):
+ if config.exists(path + ['address']):
+ for address in config.return_values(path + ['address']):
+ tmp = ip_interface(address)
+ if ip_address(host) in tmp.network.hosts():
+ mac = config.return_value(tmp_base + [host, 'hwaddr'])
+ iface_path = ['protocols', 'static', 'arp', 'interface']
+ config.set(iface_path + [interface, 'address', host, 'mac'], value=mac)
+ config.set_tag(iface_path)
+ config.set_tag(iface_path + [interface, 'address'])
+ continue
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+# We need a temporary copy of the config tree as the original one needs to be
+# deleted first due to a change iun thge tagNode structure.
+config.copy(base, tmp_base)
+config.delete(base)
+
+for host in config.list_nodes(tmp_base):
+ for type in config.list_nodes(['interfaces']):
+ for interface in config.list_nodes(['interfaces', type]):
+ if_base = ['interfaces', type, interface]
+ fixup_cli(config, if_base, interface)
+
+ if config.exists(if_base + ['vif']):
+ for vif in config.list_nodes(if_base + ['vif']):
+ vif_base = ['interfaces', type, interface, 'vif', vif]
+ fixup_cli(config, vif_base, f'{interface}.{vif}')
+
+ if config.exists(if_base + ['vif-s']):
+ for vif_s in config.list_nodes(if_base + ['vif-s']):
+ vif_s_base = ['interfaces', type, interface, 'vif-s', vif_s]
+ fixup_cli(config, vif_s_base, f'{interface}.{vif_s}')
+
+ if config.exists(if_base + ['vif-s', vif_s, 'vif-c']):
+ for vif_c in config.list_nodes(if_base + ['vif-s', vif_s, 'vif-c']):
+ vif_c_base = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c', vif_c]
+ fixup_cli(config, vif_c_base, f'{interface}.{vif_s}.{vif_c}')
+
+config.delete(tmp_base)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print(f'Failed to save the modified config: {e}')
+ exit(1)
diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py
index 66ecf8439..e45c38f07 100755
--- a/src/op_mode/conntrack_sync.py
+++ b/src/op_mode/conntrack_sync.py
@@ -20,12 +20,15 @@ import xmltodict
from argparse import ArgumentParser
from vyos.configquery import CliShellApiConfigQuery
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import call
from vyos.util import cmd
from vyos.util import run
from vyos.template import render_to_string
conntrackd_bin = '/usr/sbin/conntrackd'
conntrackd_config = '/run/conntrackd/conntrackd.conf'
+failover_state_file = '/var/run/vyatta-conntrackd-failover-state'
parser = ArgumentParser(description='Conntrack Sync')
group = parser.add_mutually_exclusive_group()
@@ -36,6 +39,8 @@ group.add_argument('--show-internal', help='Show internal (main) tracking cache'
group.add_argument('--show-external', help='Show external (main) tracking cache', action='store_true')
group.add_argument('--show-internal-expect', help='Show internal (expect) tracking cache', action='store_true')
group.add_argument('--show-external-expect', help='Show external (expect) tracking cache', action='store_true')
+group.add_argument('--show-statistics', help='Show connection syncing statistics', action='store_true')
+group.add_argument('--show-status', help='Show conntrack-sync status', action='store_true')
def is_configured():
""" Check if conntrack-sync service is configured """
@@ -72,7 +77,7 @@ def xml_to_stdout(xml):
parsed = xmltodict.parse(line)
out.append(parsed)
- print(render_to_string('conntrackd/conntrackd.op-mode.tmpl', {'data' : out}))
+ print(render_to_string('conntrackd/conntrackd.op-mode.j2', {'data' : out}))
if __name__ == '__main__':
args = parser.parse_args()
@@ -131,6 +136,46 @@ if __name__ == '__main__':
out = cmd(f'sudo {conntrackd_bin} -C {conntrackd_config} {opt} -x')
xml_to_stdout(out)
+ elif args.show_statistics:
+ is_configured()
+ config = ConfigTreeQuery()
+ print('\nMain Table Statistics:\n')
+ call(f'sudo {conntrackd_bin} -C {conntrackd_config} -s')
+ print()
+ if config.exists(['service', 'conntrack-sync', 'expect-sync']):
+ print('\nExpect Table Statistics:\n')
+ call(f'sudo {conntrackd_bin} -C {conntrackd_config} -s exp')
+ print()
+
+ elif args.show_status:
+ is_configured()
+ config = ConfigTreeQuery()
+ ct_sync_intf = config.list_nodes(['service', 'conntrack-sync', 'interface'])
+ ct_sync_intf = ', '.join(ct_sync_intf)
+ failover_state = "no transition yet!"
+ expect_sync_protocols = "disabled"
+
+ if config.exists(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp']):
+ failover_mechanism = "vrrp"
+ vrrp_sync_grp = config.value(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group'])
+
+ if os.path.isfile(failover_state_file):
+ with open(failover_state_file, "r") as f:
+ failover_state = f.readline()
+
+ if config.exists(['service', 'conntrack-sync', 'expect-sync']):
+ expect_sync_protocols = config.values(['service', 'conntrack-sync', 'expect-sync'])
+ if 'all' in expect_sync_protocols:
+ expect_sync_protocols = ["ftp", "sip", "h323", "nfs", "sqlnet"]
+ expect_sync_protocols = ', '.join(expect_sync_protocols)
+
+ show_status = (f'\nsync-interface : {ct_sync_intf}\n'
+ f'failover-mechanism : {failover_mechanism} [sync-group {vrrp_sync_grp}]\n'
+ f'last state transition : {failover_state}'
+ f'ExpectationSync : {expect_sync_protocols}')
+
+ print(show_status)
+
else:
parser.print_help()
exit(1)
diff --git a/src/op_mode/containers_op.py b/src/op_mode/containers_op.py
deleted file mode 100755
index bc317029c..000000000
--- a/src/op_mode/containers_op.py
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/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
-
-from getpass import getuser
-from vyos.configquery import ConfigTreeQuery
-from vyos.util import cmd
-
-parser = argparse.ArgumentParser()
-parser.add_argument("-a", "--all", action="store_true", help="Show all containers")
-parser.add_argument("-i", "--image", action="store_true", help="Show container images")
-parser.add_argument("-n", "--networks", action="store_true", help="Show container images")
-parser.add_argument("-p", "--pull", action="store", help="Pull image for container")
-parser.add_argument("-d", "--remove", action="store", help="Delete container image")
-parser.add_argument("-u", "--update", action="store", help="Update given container image")
-
-config = ConfigTreeQuery()
-base = ['container']
-if not config.exists(base):
- print('Containers not configured')
- exit(0)
-
-if getuser() != 'root':
- raise OSError('This functions needs to be run as root to return correct results!')
-
-if __name__ == '__main__':
- args = parser.parse_args()
-
- if args.all:
- print(cmd('podman ps --all'))
-
- elif args.image:
- print(cmd('podman image ls'))
-
- elif args.networks:
- print(cmd('podman network ls'))
-
- elif args.pull:
- image = args.pull
- try:
- print(cmd(f'podman image pull {image}'))
- except:
- print(f'Can\'t find or download image "{image}"')
-
- elif args.remove:
- image = args.remove
- try:
- print(cmd(f'podman image rm {image}'))
- except:
- print(f'Can\'t delete image "{image}"')
-
- elif args.update:
- tmp = config.get_config_dict(base + ['name', args.update],
- key_mangling=('-', '_'), get_first_key=True)
- try:
- image = tmp['image']
- print(cmd(f'podman image pull {image}'))
- except:
- print(f'Can\'t find or download image "{image}"')
- else:
- parser.print_help()
- exit(1)
-
- exit(0)
diff --git a/src/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py
index cfd321522..3bdf5a718 100755
--- a/src/op_mode/cpu_summary.py
+++ b/src/op_mode/cpu_summary.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
@@ -19,18 +19,30 @@ from vyos.util import colon_separated_to_dict
FILE_NAME = '/proc/cpuinfo'
-with open(FILE_NAME, 'r') as f:
- data_raw = f.read()
+def get_raw_data():
+ with open(FILE_NAME, 'r') as f:
+ data_raw = f.read()
-data = colon_separated_to_dict(data_raw)
+ data = colon_separated_to_dict(data_raw)
-# Accumulate all data in a dict for future support for machine-readable output
-cpu_data = {}
-cpu_data['cpu_number'] = len(data['processor'])
-cpu_data['models'] = list(set(data['model name']))
+ # Accumulate all data in a dict for future support for machine-readable output
+ cpu_data = {}
+ cpu_data['cpu_number'] = len(data['processor'])
+ cpu_data['models'] = list(set(data['model name']))
-# Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that
-cpu_data['models'] = map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models'])
+ # Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that
+ cpu_data['models'] = list(map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models']))
+
+ return cpu_data
+
+def get_formatted_output():
+ cpu_data = get_raw_data()
+
+ out = "CPU(s): {0}\n".format(cpu_data['cpu_number'])
+ out += "CPU model(s): {0}".format(",".join(cpu_data['models']))
+
+ return out
+
+if __name__ == '__main__':
+ print(get_formatted_output())
-print("CPU(s): {0}".format(cpu_data['cpu_number']))
-print("CPU model(s): {0}".format(",".join(cpu_data['models'])))
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/format_disk.py b/src/op_mode/format_disk.py
index df4486bce..b3ba44e87 100755
--- a/src/op_mode/format_disk.py
+++ b/src/op_mode/format_disk.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -17,11 +17,10 @@
import argparse
import os
import re
-import sys
+
from datetime import datetime
-from time import sleep
-from vyos.util import is_admin, ask_yes_no
+from vyos.util import ask_yes_no
from vyos.util import call
from vyos.util import cmd
from vyos.util import DEVNULL
@@ -38,16 +37,17 @@ def list_disks():
def is_busy(disk: str):
"""Check if given disk device is busy by re-reading it's partition table"""
- return call(f'sudo blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0
+ return call(f'blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0
def backup_partitions(disk: str):
"""Save sfdisk partitions output to a backup file"""
- device_path = '/dev/' + disk
- backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M')
- backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts)
- cmd(f'sudo /sbin/sfdisk -d {device_path} > {backup_file}')
+ device_path = f'/dev/{disk}'
+ backup_ts = datetime.now().strftime('%Y%m%d-%H%M')
+ backup_file = f'/var/tmp/backup_{disk}.{backup_ts}'
+ call(f'sfdisk -d {device_path} > {backup_file}')
+ print(f'Partition table backup saved to {backup_file}')
def list_partitions(disk: str):
@@ -65,11 +65,11 @@ def list_partitions(disk: str):
def delete_partition(disk: str, partition_idx: int):
- cmd(f'sudo /sbin/parted /dev/{disk} rm {partition_idx}')
+ cmd(f'parted /dev/{disk} rm {partition_idx}')
def format_disk_like(target: str, proto: str):
- cmd(f'sudo /sbin/sfdisk -d /dev/{proto} | sudo /sbin/sfdisk --force /dev/{target}')
+ cmd(f'sfdisk -d /dev/{proto} | sfdisk --force /dev/{target}')
if __name__ == '__main__':
@@ -79,10 +79,6 @@ if __name__ == '__main__':
group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference')
args = parser.parse_args()
- if not is_admin():
- print('Must be admin or root to format disk')
- sys.exit(1)
-
target_disk = args.target
eligible_target_disks = list_disks()
@@ -90,54 +86,48 @@ if __name__ == '__main__':
eligible_proto_disks = eligible_target_disks.copy()
eligible_proto_disks.remove(target_disk)
- fmt = {
- 'target_disk': target_disk,
- 'proto_disk': proto_disk,
- }
-
if proto_disk == target_disk:
print('The two disk drives must be different.')
- sys.exit(1)
+ exit(1)
- if not os.path.exists('/dev/' + proto_disk):
- print('Device /dev/{proto_disk} does not exist'.format_map(fmt))
- sys.exit(1)
+ if not os.path.exists(f'/dev/{proto_disk}'):
+ print(f'Device /dev/{proto_disk} does not exist')
+ exit(1)
if not os.path.exists('/dev/' + target_disk):
- print('Device /dev/{target_disk} does not exist'.format_map(fmt))
- sys.exit(1)
+ print(f'Device /dev/{target_disk} does not exist')
+ exit(1)
if target_disk not in eligible_target_disks:
- print('Device {target_disk} can not be formatted'.format_map(fmt))
- sys.exit(1)
+ print(f'Device {target_disk} can not be formatted')
+ exit(1)
if proto_disk not in eligible_proto_disks:
- print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt))
- sys.exit(1)
+ print(f'Device {proto_disk} can not be used as a prototype for {target_disk}')
+ exit(1)
if is_busy(target_disk):
- print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt))
- sys.exit(1)
+ print(f'Disk device {target_disk} is busy, unable to format')
+ exit(1)
- print('This will re-format disk {target_disk} so that it has the same disk\n'
- 'partion sizes and offsets as {proto_disk}. This will not copy\n'
- 'data from {proto_disk} to {target_disk}. But this will erase all\n'
- 'data on {target_disk}.\n'.format_map(fmt))
+ print(f'\nThis will re-format disk {target_disk} so that it has the same disk'
+ f'\npartion sizes and offsets as {proto_disk}. This will not copy'
+ f'\ndata from {proto_disk} to {target_disk}. But this will erase all'
+ f'\ndata on {target_disk}.\n')
- if not ask_yes_no("Do you wish to proceed?"):
- print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt))
- sys.exit(0)
+ if not ask_yes_no('Do you wish to proceed?'):
+ print(f'Disk drive {target_disk} will not be re-formated')
+ exit(0)
- print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt))
+ print(f'Re-formating disk drive {target_disk}...')
print('Making backup copy of partitions...')
backup_partitions(target_disk)
- sleep(1)
print('Deleting old partitions...')
for p in list_partitions(target_disk):
delete_partition(disk=target_disk, partition_idx=p)
- print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt))
+ print(f'Creating new partitions on {target_disk} based on {proto_disk}...')
format_disk_like(target=target_disk, proto=proto_disk)
- print('Done.')
+ print('Done!')
diff --git a/src/op_mode/generate_openconnect_otp_key.py b/src/op_mode/generate_openconnect_otp_key.py
new file mode 100755
index 000000000..363bcf3ea
--- /dev/null
+++ b/src/op_mode/generate_openconnect_otp_key.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import os
+
+from vyos.util import popen
+from secrets import token_hex
+from base64 import b32encode
+
+if os.geteuid() != 0:
+ exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.")
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-u", "--username", type=str, help='Username used for authentication', required=True)
+ parser.add_argument("-i", "--interval", type=str, help='Duration of single time interval', default="30", required=False)
+ parser.add_argument("-d", "--digits", type=str, help='The number of digits in the one-time password', default="6", required=False)
+ args = parser.parse_args()
+
+ hostname = os.uname()[1]
+ username = args.username
+ digits = args.digits
+ period = args.interval
+
+ # check variables:
+ if int(digits) < 6 or int(digits) > 8:
+ print("")
+ quit("The number of digits in the one-time password must be between '6' and '8'")
+
+ if int(period) < 5 or int(period) > 86400:
+ print("")
+ quit("Time token interval must be between '5' and '86400' seconds")
+
+ # generate OTP key, URL & QR:
+ key_hex = token_hex(20)
+ key_base32 = b32encode(bytes.fromhex(key_hex)).decode()
+
+ otp_url=''.join(["otpauth://totp/",username,"@",hostname,"?secret=",key_base32,"&digits=",digits,"&period=",period])
+ qrcode,err = popen('qrencode -t ansiutf8', input=otp_url)
+
+ print("# You can share it with the user, he just needs to scan the QR in his OTP app")
+ print("# username: ", username)
+ print("# OTP KEY: ", key_base32)
+ print("# OTP URL: ", otp_url)
+ print(qrcode)
+ print('# To add this OTP key to configuration, run the following commands:')
+ print(f"set vpn openconnect authentication local-users username {username} otp key '{key_hex}'")
+ if period != "30":
+ print(f"set vpn openconnect authentication local-users username {username} otp interval '{period}'")
+ if digits != "6":
+ print(f"set vpn openconnect authentication local-users username {username} otp otp-length '{digits}'")
diff --git a/src/op_mode/generate_ovpn_client_file.py b/src/op_mode/generate_ovpn_client_file.py
new file mode 100755
index 000000000..0628e6135
--- /dev/null
+++ b/src/op_mode/generate_ovpn_client_file.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import os
+
+from jinja2 import Template
+from textwrap import fill
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.ifconfig import Section
+from vyos.util import cmd
+
+
+client_config = """
+
+client
+nobind
+remote {{ remote_host }} {{ port }}
+remote-cert-tls server
+proto {{ 'tcp-client' if protocol == 'tcp-active' else 'udp' }}
+dev {{ device }}
+dev-type {{ device }}
+persist-key
+persist-tun
+verb 3
+
+# Encryption options
+{% if encryption is defined and encryption is not none %}
+{% if encryption.cipher is defined and encryption.cipher is not none %}
+cipher {{ encryption.cipher }}
+{% if encryption.cipher == 'bf128' %}
+keysize 128
+{% elif encryption.cipher == 'bf256' %}
+keysize 256
+{% endif %}
+{% endif %}
+{% if encryption.ncp_ciphers is defined and encryption.ncp_ciphers is not none %}
+data-ciphers {{ encryption.ncp_ciphers }}
+{% endif %}
+{% endif %}
+
+{% if hash is defined and hash is not none %}
+auth {{ hash }}
+{% endif %}
+keysize 256
+comp-lzo {{ '' if use_lzo_compression is defined else 'no' }}
+
+<ca>
+-----BEGIN CERTIFICATE-----
+{{ ca }}
+-----END CERTIFICATE-----
+
+</ca>
+
+<cert>
+-----BEGIN CERTIFICATE-----
+{{ cert }}
+-----END CERTIFICATE-----
+
+</cert>
+
+<key>
+-----BEGIN PRIVATE KEY-----
+{{ key }}
+-----END PRIVATE KEY-----
+
+</key>
+
+"""
+
+config = ConfigTreeQuery()
+base = ['interfaces', 'openvpn']
+
+if not config.exists(base):
+ print('OpenVPN not configured')
+ exit(0)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-i", "--interface", type=str, help='OpenVPN interface the client is connecting to', required=True)
+ parser.add_argument("-a", "--ca", type=str, help='OpenVPN CA cerificate', required=True)
+ parser.add_argument("-c", "--cert", type=str, help='OpenVPN client cerificate', required=True)
+ parser.add_argument("-k", "--key", type=str, help='OpenVPN client cerificate key', action="store")
+ args = parser.parse_args()
+
+ interface = args.interface
+ ca = args.ca
+ cert = args.cert
+ key = args.key
+ if not key:
+ key = args.cert
+
+ if interface not in Section.interfaces('openvpn'):
+ exit(f'OpenVPN interface "{interface}" does not exist!')
+
+ if not config.exists(['pki', 'ca', ca, 'certificate']):
+ exit(f'OpenVPN CA certificate "{ca}" does not exist!')
+
+ if not config.exists(['pki', 'certificate', cert, 'certificate']):
+ exit(f'OpenVPN certificate "{cert}" does not exist!')
+
+ if not config.exists(['pki', 'certificate', cert, 'private', 'key']):
+ exit(f'OpenVPN certificate key "{key}" does not exist!')
+
+ ca = config.value(['pki', 'ca', ca, 'certificate'])
+ ca = fill(ca, width=64)
+ cert = config.value(['pki', 'certificate', cert, 'certificate'])
+ cert = fill(cert, width=64)
+ key = config.value(['pki', 'certificate', key, 'private', 'key'])
+ key = fill(key, width=64)
+ remote_host = config.value(base + [interface, 'local-host'])
+
+ ovpn_conf = config.get_config_dict(base + [interface], key_mangling=('-', '_'), get_first_key=True)
+
+ port = '1194' if 'local_port' not in ovpn_conf else ovpn_conf['local_port']
+ proto = 'udp' if 'protocol' not in ovpn_conf else ovpn_conf['protocol']
+ device = 'tun' if 'device_type' not in ovpn_conf else ovpn_conf['device_type']
+
+ config = {
+ 'interface' : interface,
+ 'ca' : ca,
+ 'cert' : cert,
+ 'key' : key,
+ 'device' : device,
+ 'port' : port,
+ 'proto' : proto,
+ 'remote_host' : remote_host,
+ 'address' : [],
+ }
+
+# Clear out terminal first
+print('\x1b[2J\x1b[H')
+client = Template(client_config, trim_blocks=True).render(config)
+print(client)
diff --git a/src/op_mode/generate_public_key_command.py b/src/op_mode/generate_public_key_command.py
index 7a7b6c923..f071ae350 100755
--- a/src/op_mode/generate_public_key_command.py
+++ b/src/op_mode/generate_public_key_command.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# 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
@@ -29,8 +29,12 @@ def get_key(path):
key_string = vyos.remote.get_remote_config(path)
return key_string.split()
-username = sys.argv[1]
-algorithm, key, identifier = get_key(sys.argv[2])
+try:
+ username = sys.argv[1]
+ algorithm, key, identifier = get_key(sys.argv[2])
+except Exception as e:
+ print("Failed to retrieve the public key: {}".format(e))
+ sys.exit(1)
print('# To add this key as an embedded key, run the following commands:')
print('configure')
@@ -39,3 +43,4 @@ print(f'set system login user {username} authentication public-keys {identifier}
print('commit')
print('save')
print('exit')
+
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
index 990b06c12..21561d16f 100755
--- a/src/op_mode/ikev2_profile_generator.py
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -222,9 +222,9 @@ except KeyboardInterrupt:
print('\n\n==== <snip> ====')
if args.os == 'ios':
- print(render_to_string('ipsec/ios_profile.tmpl', data))
+ print(render_to_string('ipsec/ios_profile.j2', data))
print('==== </snip> ====\n')
print('Save the XML from above to a new file named "vyos.mobileconfig" and E-Mail it to your phone.')
elif args.os == 'windows':
- print(render_to_string('ipsec/windows_profile.tmpl', data))
+ print(render_to_string('ipsec/windows_profile.j2', data))
print('==== </snip> ====\n')
diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py
index 731e71891..17f6bf552 100755
--- a/src/op_mode/lldp_op.py
+++ b/src/op_mode/lldp_op.py
@@ -54,12 +54,15 @@ def parse_data(data, interface):
for local_if, values in neighbor.items():
if interface is not None and local_if != interface:
continue
+ cap = ''
for chassis, c_value in values.get('chassis', {}).items():
+ # bail out early if no capabilities found
+ if 'capability' not in c_value:
+ continue
capabilities = c_value['capability']
if isinstance(capabilities, dict):
capabilities = [capabilities]
- cap = ''
for capability in capabilities:
if capability['enabled']:
if capability['type'] == 'Router':
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/powerctrl.py b/src/op_mode/powerctrl.py
index 679b03c0b..fd4f86d88 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -33,10 +33,12 @@ def utc2local(datetime):
def parse_time(s):
try:
- if re.match(r'^\d{1,2}$', s):
- if (int(s) > 59):
+ if re.match(r'^\d{1,9999}$', s):
+ if (int(s) > 59) and (int(s) < 1440):
s = str(int(s)//60) + ":" + str(int(s)%60)
return datetime.strptime(s, "%H:%M").time()
+ if (int(s) >= 1440):
+ return s.split()
else:
return datetime.strptime(s, "%M").time()
else:
@@ -141,7 +143,7 @@ def execute_shutdown(time, reboot=True, ask=True):
cmd(f'/usr/bin/wall "{wall_msg}"')
else:
if not ts:
- exit(f'Invalid time "{time[0]}". The valid format is HH:MM')
+ exit(f'Invalid time "{time[0]}". Uses 24 Hour Clock format')
else:
exit(f'Invalid date "{time[1]}". A valid format is YYYY-MM-DD [HH:MM]')
else:
@@ -172,7 +174,12 @@ def main():
action.add_argument("--reboot", "-r",
help="Reboot the system",
nargs="*",
- metavar="Minutes|HH:MM")
+ metavar="HH:MM")
+
+ action.add_argument("--reboot_in", "-i",
+ help="Reboot the system",
+ nargs="*",
+ metavar="Minutes")
action.add_argument("--poweroff", "-p",
help="Poweroff the system",
@@ -190,7 +197,17 @@ def main():
try:
if args.reboot is not None:
+ for r in args.reboot:
+ if ':' not in r and '/' not in r and '.' not in r:
+ print("Incorrect format! Use HH:MM")
+ exit(1)
execute_shutdown(args.reboot, reboot=True, ask=args.yes)
+ if args.reboot_in is not None:
+ for i in args.reboot_in:
+ if ':' in i:
+ print("Incorrect format! Use Minutes")
+ exit(1)
+ execute_shutdown(args.reboot_in, reboot=True, ask=args.yes)
if args.poweroff is not None:
execute_shutdown(args.poweroff, reboot=False, ask=args.yes)
if args.cancel:
diff --git a/src/op_mode/ppp-server-ctrl.py b/src/op_mode/ppp-server-ctrl.py
index 670cdf879..e93963fdd 100755
--- a/src/op_mode/ppp-server-ctrl.py
+++ b/src/op_mode/ppp-server-ctrl.py
@@ -60,7 +60,7 @@ def main():
output, err = popen(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][args.proto]) + args.action + ses_pattern, stderr=DEVNULL, decode='utf-8')
if not err:
try:
- print(output)
+ print(f' {output}')
except:
sys.exit(0)
else:
diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py
index 109c8dd7b..91b25567a 100755
--- a/src/op_mode/restart_frr.py
+++ b/src/op_mode/restart_frr.py
@@ -22,6 +22,7 @@ import psutil
from logging.handlers import SysLogHandler
from shutil import rmtree
+from vyos.base import Warning
from vyos.util import call
from vyos.util import ask_yes_no
from vyos.util import process_named_running
@@ -138,7 +139,7 @@ def _reload_config(daemon):
# define program arguments
cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons')
cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons')
-cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons')
+cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ldpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons')
# parse arguments
cmd_args = cmd_args_parser.parse_args()
@@ -163,7 +164,7 @@ if cmd_args.action == 'restart':
if cmd_args.daemon != ['']:
for daemon in cmd_args.daemon:
if not process_named_running(daemon):
- print('WARNING: some of listed daemons are not running!')
+ Warning('some of listed daemons are not running!')
# run command to restart daemon
for daemon in cmd_args.daemon:
diff --git a/src/op_mode/show_configuration_json.py b/src/op_mode/show_configuration_json.py
new file mode 100755
index 000000000..fdece533b
--- /dev/null
+++ b/src/op_mode/show_configuration_json.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import json
+
+from vyos.configquery import ConfigTreeQuery
+
+
+config = ConfigTreeQuery()
+c = config.get_config_dict()
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-p", "--pretty", action="store_true", help="Show pretty configuration in JSON format")
+
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ if args.pretty:
+ print(json.dumps(c, indent=4))
+ else:
+ print(json.dumps(c))
diff --git a/src/op_mode/show_cpu.py b/src/op_mode/show_cpu.py
index 0040e950d..9973d9789 100755
--- a/src/op_mode/show_cpu.py
+++ b/src/op_mode/show_cpu.py
@@ -21,7 +21,7 @@ from sys import exit
from vyos.util import popen, DEVNULL
OUT_TMPL_SRC = """
-{% if cpu %}
+{%- if cpu -%}
{% if 'vendor' in cpu %}CPU Vendor: {{cpu.vendor}}{% endif %}
{% if 'model' in cpu %}Model: {{cpu.model}}{% endif %}
{% if 'cpus' in cpu %}Total CPUs: {{cpu.cpus}}{% endif %}
@@ -31,31 +31,42 @@ OUT_TMPL_SRC = """
{% if 'mhz' in cpu %}Current MHz: {{cpu.mhz}}{% endif %}
{% if 'mhz_min' in cpu %}Minimum MHz: {{cpu.mhz_min}}{% endif %}
{% if 'mhz_max' in cpu %}Maximum MHz: {{cpu.mhz_max}}{% endif %}
-{% endif %}
+{%- endif -%}
"""
-cpu = {}
-cpu_json, code = popen('lscpu -J', stderr=DEVNULL)
-
-if code == 0:
- cpu_info = json.loads(cpu_json)
- if len(cpu_info) > 0 and 'lscpu' in cpu_info:
- for prop in cpu_info['lscpu']:
- if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data']
- if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data']
- if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data']
- if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data']
- if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data']
- if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data']
- if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data']
- if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data']
- if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data']
-
-if len(cpu) > 0:
- tmp = { 'cpu':cpu }
+def get_raw_data():
+ cpu = {}
+ cpu_json, code = popen('lscpu -J', stderr=DEVNULL)
+
+ if code == 0:
+ cpu_info = json.loads(cpu_json)
+ if len(cpu_info) > 0 and 'lscpu' in cpu_info:
+ for prop in cpu_info['lscpu']:
+ if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data']
+ if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data']
+ if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data']
+ if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data']
+ if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data']
+ if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data']
+ if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data']
+ if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data']
+ if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data']
+
+ return cpu
+
+def get_formatted_output():
+ cpu = get_raw_data()
+
+ tmp = {'cpu':cpu}
tmpl = Template(OUT_TMPL_SRC)
- print(tmpl.render(tmp))
- exit(0)
-else:
- print('CPU information could not be determined\n')
- exit(1)
+ return tmpl.render(tmp)
+
+if __name__ == '__main__':
+ cpu = get_raw_data()
+
+ if len(cpu) > 0:
+ print(get_formatted_output())
+ else:
+ print('CPU information could not be determined\n')
+ exit(1)
+
diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py
index cd6e8ed43..4b1758eea 100755
--- a/src/op_mode/show_dhcp.py
+++ b/src/op_mode/show_dhcp.py
@@ -26,6 +26,7 @@ from datetime import datetime
from isc_dhcp_leases import Lease, IscDhcpLeases
+from vyos.base import Warning
from vyos.config import Config
from vyos.util import is_systemd_service_running
@@ -213,7 +214,7 @@ if __name__ == '__main__':
# if dhcp server is down, inactive leases may still be shown as active, so warn the user.
if not is_systemd_service_running('isc-dhcp-server.service'):
- print("WARNING: DHCP server is configured but not started. Data may be stale.")
+ Warning('DHCP server is configured but not started. Data may be stale.')
if args.leases:
leases = get_leases(conf, lease_file, args.state, args.pool, args.sort)
diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py
index 1f987ff7b..b34b730e6 100755
--- a/src/op_mode/show_dhcpv6.py
+++ b/src/op_mode/show_dhcpv6.py
@@ -26,6 +26,7 @@ from datetime import datetime
from isc_dhcp_leases import Lease, IscDhcpLeases
+from vyos.base import Warning
from vyos.config import Config
from vyos.util import is_systemd_service_running
@@ -203,7 +204,7 @@ if __name__ == '__main__':
# if dhcp server is down, inactive leases may still be shown as active, so warn the user.
if not is_systemd_service_running('isc-dhcp-server6.service'):
- print("WARNING: DHCPv6 server is configured but not started. Data may be stale.")
+ Warning('DHCPv6 server is configured but not started. Data may be stale.')
if args.leases:
leases = get_leases(conf, lease_file, args.state, args.pool, args.sort)
diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py
index e72f0f965..5b8f00dba 100755
--- a/src/op_mode/show_ipsec_sa.py
+++ b/src/op_mode/show_ipsec_sa.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -14,119 +14,117 @@
# 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
+from re import split as re_split
+from sys import exit
-import vici
-import tabulate
-import hurry.filesize
+from hurry import filesize
+from tabulate import tabulate
+from vici import Session as vici_session
+
+from vyos.util import seconds_to_human
-import vyos.util
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 [convert(c) for c in re_split('([0-9]+)', str(key))]
-def format_output(conns, sas):
+
+def format_output(sas):
sa_data = []
- for peer, parent_conn in conns.items():
- if peer not in sas:
- continue
-
- parent_sa = sas[peer]
- child_sas = parent_sa['child-sas']
- installed_sas = {v['name'].decode(): v for k, v in child_sas.items() if v["state"] == b"INSTALLED"}
-
- # parent_sa["state"] = IKE state, child_sas["state"] = ESP state
- state = 'down'
- uptime = 'N/A'
-
- if parent_sa["state"] == b"ESTABLISHED" and installed_sas:
- state = "up"
-
- remote_host = parent_sa["remote-host"].decode()
- remote_id = parent_sa["remote-id"].decode()
-
- if remote_host == remote_id:
- remote_id = "N/A"
-
- # The counters can only be obtained from the child SAs
- for child_conn in parent_conn['children']:
- if child_conn not in installed_sas:
- data = [child_conn, "down", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"]
- sa_data.append(data)
- continue
-
- isa = installed_sas[child_conn]
- csa_name = isa['name']
- csa_name = csa_name.decode()
-
- bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode()))
- bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode()))
- bytes_str = "{0}/{1}".format(bytes_in, bytes_out)
-
- pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si)
- pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si)
- pkts_str = "{0}/{1}".format(pkts_in, pkts_out)
- # Remove B from <1K values
- pkts_str = re.sub(r'B', r'', pkts_str)
-
- uptime = vyos.util.seconds_to_human(isa['install-time'].decode())
-
- enc = isa["encr-alg"].decode()
- if "encr-keysize" in isa:
- key_size = isa["encr-keysize"].decode()
- else:
- key_size = ""
- if "integ-alg" in isa:
- hash = isa["integ-alg"].decode()
- else:
- hash = ""
- if "dh-group" in isa:
- dh_group = isa["dh-group"].decode()
- else:
- dh_group = ""
-
- proposal = enc
- if key_size:
- proposal = "{0}_{1}".format(proposal, key_size)
- if hash:
- proposal = "{0}/{1}".format(proposal, hash)
- if dh_group:
- proposal = "{0}/{1}".format(proposal, dh_group)
-
- data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal]
- sa_data.append(data)
+ for sa in sas:
+ for parent_sa in sa.values():
+ # create an item for each child-sa
+ for child_sa in parent_sa.get('child-sas', {}).values():
+ # prepare a list for output data
+ sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = sa_out_packets = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A'
+
+ # collect raw data
+ sa_name = child_sa.get('name')
+ sa_state = child_sa.get('state')
+ sa_uptime = child_sa.get('install-time')
+ sa_bytes_in = child_sa.get('bytes-in')
+ sa_bytes_out = child_sa.get('bytes-out')
+ sa_packets_in = child_sa.get('packets-in')
+ sa_packets_out = child_sa.get('packets-out')
+ sa_remote_addr = parent_sa.get('remote-host')
+ sa_remote_id = parent_sa.get('remote-id')
+ sa_proposal_encr_alg = child_sa.get('encr-alg')
+ sa_proposal_integ_alg = child_sa.get('integ-alg')
+ sa_proposal_encr_keysize = child_sa.get('encr-keysize')
+ sa_proposal_dh_group = child_sa.get('dh-group')
+
+ # format data to display
+ if sa_name:
+ sa_out_name = sa_name.decode()
+ if sa_state:
+ if sa_state == b'INSTALLED':
+ sa_out_state = 'up'
+ else:
+ sa_out_state = 'down'
+ if sa_uptime:
+ sa_out_uptime = seconds_to_human(sa_uptime.decode())
+ if sa_bytes_in and sa_bytes_out:
+ bytes_in = filesize.size(int(sa_bytes_in.decode()))
+ bytes_out = filesize.size(int(sa_bytes_out.decode()))
+ sa_out_bytes = f'{bytes_in}/{bytes_out}'
+ if sa_packets_in and sa_packets_out:
+ packets_in = filesize.size(int(sa_packets_in.decode()),
+ system=filesize.si)
+ packets_out = filesize.size(int(sa_packets_out.decode()),
+ system=filesize.si)
+ sa_out_packets = f'{packets_in}/{packets_out}'
+ if sa_remote_addr:
+ sa_out_remote_addr = sa_remote_addr.decode()
+ if sa_remote_id:
+ sa_out_remote_id = sa_remote_id.decode()
+ # format proposal
+ if sa_proposal_encr_alg:
+ sa_out_proposal = sa_proposal_encr_alg.decode()
+ if sa_proposal_encr_keysize:
+ sa_proposal_encr_keysize_str = sa_proposal_encr_keysize.decode()
+ sa_out_proposal = f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}'
+ if sa_proposal_integ_alg:
+ sa_proposal_integ_alg_str = sa_proposal_integ_alg.decode()
+ sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_integ_alg_str}'
+ if sa_proposal_dh_group:
+ sa_proposal_dh_group_str = sa_proposal_dh_group.decode()
+ sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_dh_group_str}'
+
+ # add a new item to output data
+ sa_data.append([
+ sa_out_name, sa_out_state, sa_out_uptime, sa_out_bytes,
+ sa_out_packets, sa_out_remote_addr, sa_out_remote_id,
+ sa_out_proposal
+ ])
+
+ # return output data
return sa_data
+
if __name__ == '__main__':
try:
- session = vici.Session()
- conns = {}
- sas = {}
+ session = vici_session()
+ sas = list(session.list_sas())
- for conn in session.list_conns():
- for key in conn:
- conns[key] = conn[key]
-
- for sa in session.list_sas():
- for key in sa:
- sas[key] = sa[key]
-
- headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"]
- sa_data = format_output(conns, sas)
+ sa_data = format_output(sas)
sa_data = sorted(sa_data, key=alphanum_key)
- output = tabulate.tabulate(sa_data, headers)
+
+ headers = [
+ "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out",
+ "Remote address", "Remote ID", "Proposal"
+ ]
+ output = tabulate(sa_data, headers)
print(output)
except PermissionError:
print("You do not have a permission to connect to the IPsec daemon")
- sys.exit(1)
+ exit(1)
except ConnectionRefusedError:
print("IPsec is not runing")
- sys.exit(1)
+ exit(1)
except Exception as e:
print("An error occured: {0}".format(e))
- sys.exit(1)
+ exit(1)
diff --git a/src/op_mode/show_nat_rules.py b/src/op_mode/show_nat_rules.py
index d68def26a..98adb31dd 100755
--- a/src/op_mode/show_nat_rules.py
+++ b/src/op_mode/show_nat_rules.py
@@ -32,7 +32,7 @@ args = parser.parse_args()
if args.source or args.destination:
tmp = cmd('sudo nft -j list table ip nat')
tmp = json.loads(tmp)
-
+
format_nat_rule = '{0: <10} {1: <50} {2: <50} {3: <10}'
print(format_nat_rule.format("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface"))
print(format_nat_rule.format("----", "------" if args.source else "-----------", "-----------", "------------------" if args.source else "-----------------"))
@@ -40,7 +40,7 @@ if args.source or args.destination:
data_json = jmespath.search('nftables[?rule].rule[?chain]', tmp)
for idx in range(0, len(data_json)):
data = data_json[idx]
-
+
# The following key values must exist
# When the rule JSON does not have some keys, this is not a rule we can work with
continue_rule = False
@@ -50,9 +50,9 @@ if args.source or args.destination:
continue
if continue_rule:
continue
-
+
comment = data['comment']
-
+
# Check the annotation to see if the annotation format is created by VYOS
continue_rule = True
for comment_prefix in ['SRC-NAT-', 'DST-NAT-']:
@@ -60,7 +60,7 @@ if args.source or args.destination:
continue_rule = False
if continue_rule:
continue
-
+
rule = int(''.join(list(filter(str.isdigit, comment))))
chain = data['chain']
if not ((args.source and chain == 'POSTROUTING') or (not args.source and chain == 'PREROUTING')):
@@ -88,7 +88,7 @@ if args.source or args.destination:
else:
port_range = srcdest_json['set'][0]['range']
srcdest += 'port ' + str(port_range[0]) + '-' + str(port_range[1]) + ' '
-
+
tran_addr_json = dict_search('snat' if args.source else 'dnat', data['expr'][i])
if tran_addr_json:
if isinstance(tran_addr_json['addr'],str):
@@ -98,10 +98,10 @@ if args.source or args.destination:
len_tmp = dict_search('snat.addr.prefix.len' if args.source else 'dnat.addr.prefix.len', data['expr'][3])
if addr_tmp and len_tmp:
tran_addr += addr_tmp + '/' + str(len_tmp) + ' '
-
+
if isinstance(tran_addr_json['port'],int):
- tran_addr += 'port ' + tran_addr_json['port']
-
+ tran_addr += 'port ' + str(tran_addr_json['port'])
+
else:
if 'masquerade' in data['expr'][i]:
tran_addr = 'masquerade'
@@ -112,10 +112,10 @@ if args.source or args.destination:
srcdests.append(srcdest)
srcdest = ''
print(format_nat_rule.format(rule, srcdests[0], tran_addr, interface))
-
+
for i in range(1, len(srcdests)):
print(format_nat_rule.format(' ', srcdests[i], ' ', ' '))
-
+
exit(0)
else:
parser.print_help()
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
index f7b99cc0d..9a5adcffb 100755
--- a/src/op_mode/show_openvpn.py
+++ b/src/op_mode/show_openvpn.py
@@ -26,10 +26,10 @@ outp_tmpl = """
{% if clients %}
OpenVPN status on {{ intf }}
-Client CN Remote Host Local Host TX bytes RX bytes Connected Since
---------- ----------- ---------- -------- -------- ---------------
+Client CN Remote Host Tunnel IP Local Host TX bytes RX bytes Connected Since
+--------- ----------- --------- ---------- -------- -------- ---------------
{% for c in clients %}
-{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-21s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.rx_bytes) }} {{ c.online_since }}
+{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-15s"|format(c.tunnel) }} {{ "%-21s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.rx_bytes) }} {{ c.online_since }}
{% endfor %}
{% endif %}
"""
@@ -50,6 +50,19 @@ def bytes2HR(size):
output="{0:.1f} {1}".format(size, suff[suffIdx])
return output
+def get_vpn_tunnel_address(peer, interface):
+ lst = []
+ status_file = '/var/run/openvpn/{}.status'.format(interface)
+
+ with open(status_file, 'r') as f:
+ lines = f.readlines()
+ for line in lines:
+ if peer in line:
+ lst.append(line)
+ tunnel_ip = lst[1].split(',')[0]
+
+ return tunnel_ip
+
def get_status(mode, interface):
status_file = '/var/run/openvpn/{}.status'.format(interface)
# this is an empirical value - I assume we have no more then 999999
@@ -110,7 +123,7 @@ def get_status(mode, interface):
'tx_bytes': bytes2HR(line.split(',')[3]),
'online_since': line.split(',')[4]
}
-
+ client["tunnel"] = get_vpn_tunnel_address(client['remote'], interface)
data['clients'].append(client)
continue
else:
@@ -173,5 +186,7 @@ if __name__ == '__main__':
if len(remote_host) >= 1:
client['remote'] = str(remote_host[0]) + ':' + remote_port
+ client['tunnel'] = 'N/A'
+
tmpl = jinja2.Template(outp_tmpl)
print(tmpl.render(data))
diff --git a/src/op_mode/show_ram.py b/src/op_mode/show_ram.py
index 5818ec132..2b0be3965 100755
--- a/src/op_mode/show_ram.py
+++ b/src/op_mode/show_ram.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# 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
@@ -55,10 +55,17 @@ def get_system_memory_human():
return mem
-if __name__ == '__main__':
- mem = get_system_memory_human()
+def get_raw_data():
+ return get_system_memory_human()
+
+def get_formatted_output():
+ mem = get_raw_data()
- print("Total: {}".format(mem["total"]))
- print("Free: {}".format(mem["free"]))
- print("Used: {}".format(mem["used"]))
+ out = "Total: {}\n".format(mem["total"])
+ out += "Free: {}\n".format(mem["free"])
+ out += "Used: {}".format(mem["used"])
+ return out
+
+if __name__ == '__main__':
+ print(get_formatted_output())
diff --git a/src/op_mode/show_uptime.py b/src/op_mode/show_uptime.py
index c3dea52e6..b70c60cf8 100755
--- a/src/op_mode/show_uptime.py
+++ b/src/op_mode/show_uptime.py
@@ -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 as
@@ -26,25 +26,41 @@ def get_uptime_seconds():
def get_load_averages():
from re import search
from vyos.util import cmd
+ from vyos.cpu import get_core_count
data = cmd("uptime")
matches = search(r"load average:\s*(?P<one>[0-9\.]+)\s*,\s*(?P<five>[0-9\.]+)\s*,\s*(?P<fifteen>[0-9\.]+)\s*", data)
+ core_count = get_core_count()
+
res = {}
- res[1] = float(matches["one"])
- res[5] = float(matches["five"])
- res[15] = float(matches["fifteen"])
+ res[1] = float(matches["one"]) / core_count
+ res[5] = float(matches["five"]) / core_count
+ res[15] = float(matches["fifteen"]) / core_count
return res
-if __name__ == '__main__':
+def get_raw_data():
from vyos.util import seconds_to_human
- print("Uptime: {}\n".format(seconds_to_human(get_uptime_seconds())))
+ res = {}
+ res["uptime_seconds"] = get_uptime_seconds()
+ res["uptime"] = seconds_to_human(get_uptime_seconds())
+ res["load_average"] = get_load_averages()
+
+ return res
+
+def get_formatted_output():
+ data = get_raw_data()
- avgs = get_load_averages()
+ out = "Uptime: {}\n\n".format(data["uptime"])
+ avgs = data["load_average"]
+ out += "Load averages:\n"
+ out += "1 minute: {:.01f}%\n".format(avgs[1]*100)
+ out += "5 minutes: {:.01f}%\n".format(avgs[5]*100)
+ out += "15 minutes: {:.01f}%\n".format(avgs[15]*100)
- print("Load averages:")
- print("1 minute: {:.02f}%".format(avgs[1]*100))
- print("5 minutes: {:.02f}%".format(avgs[5]*100))
- print("15 minutes: {:.02f}%".format(avgs[15]*100))
+ return out
+
+if __name__ == '__main__':
+ print(get_formatted_output())
diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py
index 7962e1e7b..b82ab6eca 100755
--- a/src/op_mode/show_version.py
+++ b/src/op_mode/show_version.py
@@ -26,10 +26,6 @@ from jinja2 import Template
from sys import exit
from vyos.util import call
-parser = argparse.ArgumentParser()
-parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
-parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
-
version_output_tmpl = """
Version: VyOS {{version}}
Release train: {{release_train}}
@@ -51,7 +47,20 @@ Hardware UUID: {{hardware_uuid}}
Copyright: VyOS maintainers and contributors
"""
+def get_raw_data():
+ version_data = vyos.version.get_full_version_data()
+ return version_data
+
+def get_formatted_output():
+ version_data = get_raw_data()
+ tmpl = Template(version_output_tmpl)
+ return tmpl.render(version_data)
+
if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
+ parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
+
args = parser.parse_args()
version_data = vyos.version.get_full_version_data()
@@ -60,9 +69,8 @@ if __name__ == '__main__':
import json
print(json.dumps(version_data))
exit(0)
-
- tmpl = Template(version_output_tmpl)
- print(tmpl.render(version_data))
+ else:
+ print(get_formatted_output())
if args.funny:
print(vyos.limericks.get_random())
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/traceroute.py b/src/op_mode/traceroute.py
new file mode 100755
index 000000000..4299d6e5f
--- /dev/null
+++ b/src/op_mode/traceroute.py
@@ -0,0 +1,207 @@
+#! /usr/bin/env python3
+
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import socket
+import ipaddress
+
+options = {
+ 'backward-hops': {
+ 'traceroute': '{command} --back',
+ 'type': 'noarg',
+ 'help': 'Display number of backward hops when they different from the forwarded path'
+ },
+ 'bypass': {
+ 'traceroute': '{command} -r',
+ 'type': 'noarg',
+ 'help': 'Bypass the normal routing tables and send directly to a host on an attached network'
+ },
+ 'do-not-fragment': {
+ 'traceroute': '{command} -F',
+ 'type': 'noarg',
+ 'help': 'Do not fragment probe packets.'
+ },
+ 'first-ttl': {
+ 'traceroute': '{command} -f {value}',
+ 'type': '<ttl>',
+ 'help': 'Specifies with what TTL to start. Defaults to 1.'
+ },
+ 'icmp': {
+ 'traceroute': '{command} -I',
+ 'type': 'noarg',
+ 'help': 'Use ICMP ECHO for tracerouting'
+ },
+ 'interface': {
+ 'traceroute': '{command} -i {value}',
+ 'type': '<interface>',
+ 'help': 'Source interface'
+ },
+ 'lookup-as': {
+ 'traceroute': '{command} -A',
+ 'type': 'noarg',
+ 'help': 'Perform AS path lookups'
+ },
+ 'mark': {
+ 'traceroute': '{command} --fwmark={value}',
+ 'type': '<fwmark>',
+ 'help': 'Set the firewall mark for outgoing packets'
+ },
+ 'no-resolve': {
+ 'traceroute': '{command} -n',
+ 'type': 'noarg',
+ 'help': 'Do not resolve hostnames'
+ },
+ 'port': {
+ 'traceroute': '{command} -p {value}',
+ 'type': '<port>',
+ 'help': 'Destination port'
+ },
+ 'source-address': {
+ 'traceroute': '{command} -s {value}',
+ 'type': '<x.x.x.x> <h:h:h:h:h:h:h:h>',
+ 'help': 'Specify source IP v4/v6 address'
+ },
+ 'tcp': {
+ 'traceroute': '{command} -T',
+ 'type': 'noarg',
+ 'help': 'Use TCP SYN for tracerouting (default port is 80)'
+ },
+ 'tos': {
+ 'traceroute': '{commad} -t {value}',
+ 'type': '<tos>',
+ 'help': 'Mark packets with specified TOS'
+ },
+ 'ttl': {
+ 'traceroute': '{command} -m {value}',
+ 'type': '<ttl>',
+ 'help': 'Maximum number of hops'
+ },
+ 'udp': {
+ 'traceroute': '{command} -U',
+ 'type': 'noarg',
+ 'help': 'Use UDP to particular port for tracerouting (default port is 53)'
+ },
+ 'vrf': {
+ 'traceroute': 'sudo ip vrf exec {value} {command}',
+ 'type': '<vrf>',
+ 'help': 'Use specified VRF table',
+ 'dflt': 'default'}
+}
+
+traceroute = {
+ 4: '/bin/traceroute -4',
+ 6: '/bin/traceroute -6',
+}
+
+
+class List (list):
+ def first (self):
+ return self.pop(0) if self else ''
+
+ def last(self):
+ return self.pop() if self else ''
+
+ def prepend(self,value):
+ self.insert(0,value)
+
+
+def expension_failure(option, completions):
+ reason = 'Ambiguous' if completions else 'Invalid'
+ sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option))
+ if completions:
+ sys.stderr.write(' Possible completions:\n ')
+ sys.stderr.write('\n '.join(completions))
+ sys.stderr.write('\n')
+ sys.stdout.write('<nocomps>')
+ sys.exit(1)
+
+
+def complete(prefix):
+ return [o for o in options if o.startswith(prefix)]
+
+
+def convert(command, args):
+ while args:
+ shortname = args.first()
+ longnames = complete(shortname)
+ if len(longnames) != 1:
+ expension_failure(shortname, longnames)
+ longname = longnames[0]
+ if options[longname]['type'] == 'noarg':
+ command = options[longname]['traceroute'].format(
+ command=command, value='')
+ elif not args:
+ sys.exit(f'traceroute: missing argument for {longname} option')
+ else:
+ command = options[longname]['traceroute'].format(
+ command=command, value=args.first())
+ return command
+
+
+if __name__ == '__main__':
+ args = List(sys.argv[1:])
+ host = args.first()
+
+ if not host:
+ sys.exit("traceroute: Missing host")
+
+ if host == '--get-options':
+ args.first() # pop traceroute
+ args.first() # pop IP
+ while args:
+ option = args.first()
+
+ matched = complete(option)
+ if not args:
+ sys.stdout.write(' '.join(matched))
+ sys.exit(0)
+
+ if len(matched) > 1 :
+ sys.stdout.write(' '.join(matched))
+ sys.exit(0)
+
+ if options[matched[0]]['type'] == 'noarg':
+ continue
+
+ value = args.first()
+ if not args:
+ matched = complete(option)
+ sys.stdout.write(options[matched[0]]['type'])
+ sys.exit(0)
+
+ for name,option in options.items():
+ if 'dflt' in option and name not in args:
+ args.append(name)
+ args.append(option['dflt'])
+
+ try:
+ ip = socket.gethostbyname(host)
+ except UnicodeError:
+ sys.exit(f'tracroute: Unknown host: {host}')
+ except socket.gaierror:
+ ip = host
+
+ try:
+ version = ipaddress.ip_address(ip).version
+ except ValueError:
+ sys.exit(f'traceroute: Unknown host: {host}')
+
+ command = convert(traceroute[version],args)
+
+ # print(f'{command} {host}')
+ os.system(f'{command} {host}')
+
diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py
index 40854fa8f..8955e5a59 100755
--- a/src/op_mode/vpn_ipsec.py
+++ b/src/op_mode/vpn_ipsec.py
@@ -88,7 +88,22 @@ def reset_profile(profile, tunnel):
def debug_peer(peer, tunnel):
if not peer or peer == "all":
- call('sudo /usr/sbin/ipsec statusall')
+ debug_commands = [
+ "sudo ipsec statusall",
+ "sudo swanctl -L",
+ "sudo swanctl -l",
+ "sudo swanctl -P",
+ "sudo ip x sa show",
+ "sudo ip x policy show",
+ "sudo ip tunnel show",
+ "sudo ip address",
+ "sudo ip rule show",
+ "sudo ip route | head -100",
+ "sudo ip route show table 220"
+ ]
+ for debug_cmd in debug_commands:
+ print(f'\n### {debug_cmd} ###')
+ call(debug_cmd)
return
if not tunnel or tunnel == 'all':
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/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql
index 29f58f709..1133d79ed 100644
--- a/src/services/api/graphql/README.graphql
+++ b/src/services/api/graphql/README.graphql
@@ -1,7 +1,12 @@
+The following examples are in the form as entered in the GraphQL
+'playground', which is found at:
+
+https://{{ host_address }}/graphql
+
Example using GraphQL mutations to configure a DHCP server:
-This assumes that the http-api is running:
+All examples assume that the http-api is running:
'set service https api'
@@ -58,8 +63,8 @@ N.B. fileName can be empty (fileName: "") or data can be empty (data: {}) to
save to /config/config.boot; to save to an alternative path, specify
fileName.
-Similarly, using the same 'endpoint' (meaning the form of the request and
-resolver; the actual enpoint for all GraphQL requests is
+Similarly, using an analogous 'endpoint' (meaning the form of the request
+and resolver; the actual enpoint for all GraphQL requests is
https://hostname/graphql), one can load an arbitrary config file from a
path.
@@ -73,17 +78,73 @@ mutation {
}
}
+Op-mode 'show' commands may be requested by path, e.g.:
+
+query {
+ Show (data: {path: ["interfaces", "ethernet", "detail"]}) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+}
-The GraphQL playground will be found at:
+N.B. to see the output the 'data' field 'result' must be present in the
+request.
-https://{{ host_address }}/graphql
+Mutations to manipulate firewall address groups:
-An equivalent curl command to the first example above would be:
+mutation {
+ CreateFirewallAddressGroup (data: {name: "ADDR-GRP", address: "10.0.0.1"}) {
+ success
+ errors
+ }
+}
+
+mutation {
+ UpdateFirewallAddressGroupMembers (data: {name: "ADDR-GRP",
+ address: ["10.0.0.1-10.0.0.8", "192.168.0.1"]}) {
+ success
+ errors
+ }
+}
+
+mutation {
+ RemoveFirewallAddressGroupMembers (data: {name: "ADDR-GRP",
+ address: "192.168.0.1"}) {
+ success
+ errors
+ }
+}
+
+N.B. The schema for the above specify that 'address' be of the form 'list of
+strings' (SDL type [String!]! for UpdateFirewallAddressGroupMembers, where
+the ! indicates that the input is required; SDL type [String] in
+CreateFirewallAddressGroup, since a group may be created without any
+addresses). However, notice that a single string may be passed without being
+a member of a list, in which case the specification allows for 'input
+coercion':
+
+http://spec.graphql.org/October2021/#sec-Scalars.Input-Coercion
+
+Similarly, IPv6 versions of the above:
+
+CreateFirewallAddressIpv6Group
+UpdateFirewallAddressIpv6GroupMembers
+RemoveFirewallAddressIpv6GroupMembers
+
+
+Instead of using the GraphQL playground, an equivalent curl command to the
+first example above would be:
curl -k 'https://192.168.100.168/graphql' -H 'Content-Type: application/json' --data-binary '{"query": "mutation {createInterfaceEthernet (data: {interface: \"eth1\", address: \"192.168.0.1/24\", description: \"BOB\"}) {success errors data {address}}}"}'
Note that the 'mutation' term is prefaced by 'query' in the curl command.
+Curl equivalents may be read from within the GraphQL playground at the 'copy
+curl' button.
+
What's here:
services
@@ -97,15 +158,22 @@ services
│   │   └── schema
│   │   ├── config_file.graphql
│   │   ├── dhcp_server.graphql
+│   │   ├── firewall_group.graphql
│   │   ├── interface_ethernet.graphql
-│   │   └── schema.graphql
+│   │   ├── schema.graphql
+│   │   ├── show_config.graphql
+│   │   └── show.graphql
│   ├── README.graphql
│   ├── recipes
│   │   ├── __init__.py
+│   │   ├── remove_firewall_address_group_members.py
│   │   ├── session.py
│   │   └── templates
│   │   ├── create_dhcp_server.tmpl
-│   │   └── create_interface_ethernet.tmpl
+│   │   ├── create_firewall_address_group.tmpl
+│   │   ├── create_interface_ethernet.tmpl
+│   │   ├── remove_firewall_address_group_members.tmpl
+│   │   └── update_firewall_address_group_members.tmpl
│   └── state.py
├── vyos-configd
├── vyos-hostsd
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py
index 1fbe13d0c..84d719fda 100644
--- a/src/services/api/graphql/bindings.py
+++ b/src/services/api/graphql/bindings.py
@@ -1,4 +1,20 @@
+# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
import vyos.defaults
+from . graphql.queries import query
from . graphql.mutations import mutation
from . graphql.directives import directives_dict
from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
@@ -8,6 +24,6 @@ def generate_schema():
type_defs = load_schema_from_path(api_schema_dir)
- schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives=directives_dict)
+ schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers, directives=directives_dict)
return schema
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
index f5cd88acd..0a9298f55 100644
--- a/src/services/api/graphql/graphql/directives.py
+++ b/src/services/api/graphql/graphql/directives.py
@@ -1,5 +1,21 @@
+# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
from ariadne import SchemaDirectiveVisitor, ObjectType
-from . mutations import make_configure_resolver, make_config_file_resolver
+from . queries import *
+from . mutations import *
def non(arg):
pass
@@ -19,19 +35,45 @@ class VyosDirective(SchemaDirectiveVisitor):
class ConfigureDirective(VyosDirective):
"""
Class providing implementation of 'configure' directive in schema.
-
"""
def visit_field_definition(self, field, object_type):
super().visit_field_definition(field, object_type,
make_resolver=make_configure_resolver)
+class ShowConfigDirective(VyosDirective):
+ """
+ Class providing implementation of 'show' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_show_config_resolver)
+
class ConfigFileDirective(VyosDirective):
"""
Class providing implementation of 'configfile' directive in schema.
-
"""
def visit_field_definition(self, field, object_type):
super().visit_field_definition(field, object_type,
make_resolver=make_config_file_resolver)
-directives_dict = {"configure": ConfigureDirective, "configfile": ConfigFileDirective}
+class ShowDirective(VyosDirective):
+ """
+ Class providing implementation of 'show' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_show_resolver)
+
+class ImageDirective(VyosDirective):
+ """
+ Class providing implementation of 'image' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_image_resolver)
+
+directives_dict = {"configure": ConfigureDirective,
+ "showconfig": ShowConfigDirective,
+ "configfile": ConfigFileDirective,
+ "show": ShowDirective,
+ "image": ImageDirective}
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
index 8a28b13d7..0c3eb702a 100644
--- a/src/services/api/graphql/graphql/mutations.py
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -1,3 +1,17 @@
+# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
from importlib import import_module
from typing import Any, Dict
@@ -10,7 +24,7 @@ from api.graphql.recipes.session import Session
mutation = ObjectType("Mutation")
-def make_resolver(mutation_name, class_name, session_func):
+def make_mutation_resolver(mutation_name, class_name, session_func):
"""Dynamically generate a resolver for the mutation named in the
schema by 'mutation_name'.
@@ -51,7 +65,8 @@ def make_resolver(mutation_name, class_name, session_func):
klass = type(class_name, (Session,), {})
k = klass(session, data)
method = getattr(k, session_func)
- method()
+ result = method()
+ data['result'] = result
return {
"success": True,
@@ -65,16 +80,20 @@ def make_resolver(mutation_name, class_name, session_func):
return func_impl
+def make_prefix_resolver(mutation_name, prefix=[]):
+ for pre in prefix:
+ Pre = pre.capitalize()
+ if Pre in mutation_name:
+ class_name = mutation_name.replace(Pre, '', 1)
+ return make_mutation_resolver(mutation_name, class_name, pre)
+ raise Exception
+
def make_configure_resolver(mutation_name):
class_name = mutation_name
- return make_resolver(mutation_name, class_name, 'configure')
+ return make_mutation_resolver(mutation_name, class_name, 'configure')
def make_config_file_resolver(mutation_name):
- if 'Save' in mutation_name:
- class_name = mutation_name.replace('Save', '', 1)
- return make_resolver(mutation_name, class_name, 'save')
- elif 'Load' in mutation_name:
- class_name = mutation_name.replace('Load', '', 1)
- return make_resolver(mutation_name, class_name, 'load')
- else:
- raise Exception
+ return make_prefix_resolver(mutation_name, prefix=['save', 'load'])
+
+def make_image_resolver(mutation_name):
+ return make_prefix_resolver(mutation_name, prefix=['add', 'delete'])
diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py
new file mode 100644
index 000000000..e1868091e
--- /dev/null
+++ b/src/services/api/graphql/graphql/queries.py
@@ -0,0 +1,89 @@
+# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from importlib import import_module
+from typing import Any, Dict
+from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake
+from graphql import GraphQLResolveInfo
+from makefun import with_signature
+
+from .. import state
+from api.graphql.recipes.session import Session
+
+query = ObjectType("Query")
+
+def make_query_resolver(query_name, class_name, session_func):
+ """Dynamically generate a resolver for the query named in the
+ schema by 'query_name'.
+
+ Dynamic generation is provided using the package 'makefun' (via the
+ decorator 'with_signature'), which provides signature-preserving
+ function wrappers; it provides several improvements over, say,
+ functools.wraps.
+
+ :raise Exception:
+ raising ConfigErrors, or internal errors
+ """
+
+ func_base_name = convert_camel_case_to_snake(class_name)
+ resolver_name = f'resolve_{func_base_name}'
+ func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'
+
+ @query.field(query_name)
+ @convert_kwargs_to_snake_case
+ @with_signature(func_sig, func_name=resolver_name)
+ async def func_impl(*args, **kwargs):
+ try:
+ if 'data' not in kwargs:
+ return {
+ "success": False,
+ "errors": ['missing data']
+ }
+
+ data = kwargs['data']
+ session = state.settings['app'].state.vyos_session
+
+ # one may override the session functions with a local subclass
+ try:
+ mod = import_module(f'api.graphql.recipes.{func_base_name}')
+ klass = getattr(mod, class_name)
+ except ImportError:
+ # otherwise, dynamically generate subclass to invoke subclass
+ # name based templates
+ klass = type(class_name, (Session,), {})
+ k = klass(session, data)
+ method = getattr(k, session_func)
+ result = method()
+ data['result'] = result
+
+ return {
+ "success": True,
+ "data": data
+ }
+ except Exception as error:
+ return {
+ "success": False,
+ "errors": [str(error)]
+ }
+
+ return func_impl
+
+def make_show_config_resolver(query_name):
+ class_name = query_name
+ return make_query_resolver(query_name, class_name, 'show_config')
+
+def make_show_resolver(query_name):
+ class_name = query_name
+ return make_query_resolver(query_name, class_name, 'show')
diff --git a/src/services/api/graphql/graphql/schema/firewall_group.graphql b/src/services/api/graphql/graphql/schema/firewall_group.graphql
index efe7de632..d89904b9e 100644
--- a/src/services/api/graphql/graphql/schema/firewall_group.graphql
+++ b/src/services/api/graphql/graphql/schema/firewall_group.graphql
@@ -45,3 +45,51 @@ type RemoveFirewallAddressGroupMembersResult {
success: Boolean!
errors: [String]
}
+
+input CreateFirewallAddressIpv6GroupInput {
+ name: String!
+ address: [String]
+}
+
+type CreateFirewallAddressIpv6Group {
+ name: String!
+ address: [String]
+}
+
+type CreateFirewallAddressIpv6GroupResult {
+ data: CreateFirewallAddressIpv6Group
+ success: Boolean!
+ errors: [String]
+}
+
+input UpdateFirewallAddressIpv6GroupMembersInput {
+ name: String!
+ address: [String!]!
+}
+
+type UpdateFirewallAddressIpv6GroupMembers {
+ name: String!
+ address: [String!]!
+}
+
+type UpdateFirewallAddressIpv6GroupMembersResult {
+ data: UpdateFirewallAddressIpv6GroupMembers
+ success: Boolean!
+ errors: [String]
+}
+
+input RemoveFirewallAddressIpv6GroupMembersInput {
+ name: String!
+ address: [String!]!
+}
+
+type RemoveFirewallAddressIpv6GroupMembers {
+ name: String!
+ address: [String!]!
+}
+
+type RemoveFirewallAddressIpv6GroupMembersResult {
+ data: RemoveFirewallAddressIpv6GroupMembers
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/image.graphql b/src/services/api/graphql/graphql/schema/image.graphql
new file mode 100644
index 000000000..7d1b4f9d0
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/image.graphql
@@ -0,0 +1,29 @@
+input AddSystemImageInput {
+ location: String!
+}
+
+type AddSystemImage {
+ location: String
+ result: String
+}
+
+type AddSystemImageResult {
+ data: AddSystemImage
+ success: Boolean!
+ errors: [String]
+}
+
+input DeleteSystemImageInput {
+ name: String!
+}
+
+type DeleteSystemImage {
+ name: String
+ result: String
+}
+
+type DeleteSystemImageResult {
+ data: DeleteSystemImage
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
index 9e97a0d60..952e46f34 100644
--- a/src/services/api/graphql/graphql/schema/schema.graphql
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -3,12 +3,16 @@ schema {
mutation: Mutation
}
-type Query {
- _dummy: String
-}
-
directive @configure on FIELD_DEFINITION
directive @configfile on FIELD_DEFINITION
+directive @show on FIELD_DEFINITION
+directive @showconfig on FIELD_DEFINITION
+directive @image on FIELD_DEFINITION
+
+type Query {
+ Show(data: ShowInput) : ShowResult @show
+ ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig
+}
type Mutation {
CreateDhcpServer(data: DhcpServerConfigInput) : CreateDhcpServerResult @configure
@@ -16,6 +20,11 @@ type Mutation {
CreateFirewallAddressGroup(data: CreateFirewallAddressGroupInput) : CreateFirewallAddressGroupResult @configure
UpdateFirewallAddressGroupMembers(data: UpdateFirewallAddressGroupMembersInput) : UpdateFirewallAddressGroupMembersResult @configure
RemoveFirewallAddressGroupMembers(data: RemoveFirewallAddressGroupMembersInput) : RemoveFirewallAddressGroupMembersResult @configure
+ CreateFirewallAddressIpv6Group(data: CreateFirewallAddressIpv6GroupInput) : CreateFirewallAddressIpv6GroupResult @configure
+ UpdateFirewallAddressIpv6GroupMembers(data: UpdateFirewallAddressIpv6GroupMembersInput) : UpdateFirewallAddressIpv6GroupMembersResult @configure
+ RemoveFirewallAddressIpv6GroupMembers(data: RemoveFirewallAddressIpv6GroupMembersInput) : RemoveFirewallAddressIpv6GroupMembersResult @configure
SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configfile
LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configfile
+ AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @image
+ DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @image
}
diff --git a/src/services/api/graphql/graphql/schema/show.graphql b/src/services/api/graphql/graphql/schema/show.graphql
new file mode 100644
index 000000000..c7709e48b
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/show.graphql
@@ -0,0 +1,14 @@
+input ShowInput {
+ path: [String!]!
+}
+
+type Show {
+ path: [String]
+ result: String
+}
+
+type ShowResult {
+ data: Show
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/show_config.graphql b/src/services/api/graphql/graphql/schema/show_config.graphql
new file mode 100644
index 000000000..34afd2aa9
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/show_config.graphql
@@ -0,0 +1,21 @@
+"""
+Use 'scalar Generic' for show config output, to avoid attempts to
+JSON-serialize in case of JSON output.
+"""
+scalar Generic
+
+input ShowConfigInput {
+ path: [String!]!
+ configFormat: String
+}
+
+type ShowConfig {
+ path: [String]
+ result: Generic
+}
+
+type ShowConfigResult {
+ data: ShowConfig
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py
index cde30c27a..b91932e14 100644
--- a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py
+++ b/src/services/api/graphql/recipes/remove_firewall_address_group_members.py
@@ -1,3 +1,17 @@
+# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
from . session import Session
diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py
index b96cc1753..1f844ff70 100644
--- a/src/services/api/graphql/recipes/session.py
+++ b/src/services/api/graphql/recipes/session.py
@@ -1,9 +1,33 @@
+# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+
from ariadne import convert_camel_case_to_snake
+
import vyos.defaults
from vyos.config import Config
+from vyos.configtree import ConfigTree
from vyos.template import render
-class Session(object):
+class Session:
+ """
+ Wrapper for calling configsession functions based on GraphQL requests.
+ Non-nullable fields in the respective schema allow avoiding a key check
+ in 'data'.
+ """
def __init__(self, session, data):
self._session = session
self._data = data
@@ -43,6 +67,21 @@ class Session(object):
session.delete(path)
session.commit()
+ def show_config(self):
+ session = self._session
+ data = self._data
+ out = ''
+
+ try:
+ out = session.show_config(data['path'])
+ if data.get('config_format', '') == 'json':
+ config_tree = vyos.configtree.ConfigTree(out)
+ out = json.loads(config_tree.to_json())
+ except Exception as error:
+ raise error
+
+ return out
+
def save(self):
session = self._session
data = self._data
@@ -63,3 +102,37 @@ class Session(object):
session.commit()
except Exception as error:
raise error
+
+ def show(self):
+ session = self._session
+ data = self._data
+ out = ''
+
+ try:
+ out = session.show(data['path'])
+ except Exception as error:
+ raise error
+
+ return out
+
+ def add(self):
+ session = self._session
+ data = self._data
+
+ try:
+ res = session.install_image(data['location'])
+ except Exception as error:
+ raise error
+
+ return res
+
+ def delete(self):
+ session = self._session
+ data = self._data
+
+ try:
+ res = session.remove_image(data['name'])
+ except Exception as error:
+ raise error
+
+ return res
diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl b/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl
new file mode 100644
index 000000000..e9b660722
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl
@@ -0,0 +1,4 @@
+set firewall group ipv6-address-group {{ name }}
+{% for add in address %}
+set firewall group ipv6-address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl
new file mode 100644
index 000000000..0efa0b226
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl
@@ -0,0 +1,3 @@
+{% for add in address %}
+delete firewall group ipv6-address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl
new file mode 100644
index 000000000..f98a5517c
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl
@@ -0,0 +1,3 @@
+{% for add in address %}
+set firewall group ipv6-address-group {{ name }} address {{ add }}
+{% endfor %}
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index 670b6e66a..48c9135e2 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -28,6 +28,7 @@ import zmq
from contextlib import contextmanager
from vyos.defaults import directories
+from vyos.util import boot_configuration_complete
from vyos.configsource import ConfigSourceString, ConfigSourceError
from vyos.config import Config
from vyos import ConfigError
@@ -186,7 +187,7 @@ def initialization(socket):
session_out = None
# if not a 'live' session, for example on boot, write to file
- if not session_out or not os.path.isfile('/tmp/vyos-config-status'):
+ if not session_out or not boot_configuration_complete():
session_out = script_stdout_log
session_mode = 'a'
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
index f4b1d0fc2..9ae7b1ea9 100755
--- a/src/services/vyos-hostsd
+++ b/src/services/vyos-hostsd
@@ -139,6 +139,27 @@
# }
#
#
+### authoritative_zones
+## Additional zones hosted authoritatively by pdns-recursor.
+## We add NTAs for these zones but do not do much else here.
+#
+# { 'type': 'authoritative_zones',
+# 'op': 'add',
+# 'data': ['<str zone>', ...]
+# }
+#
+# { 'type': 'authoritative_zones',
+# 'op': 'delete',
+# 'data': ['<str zone>', ...]
+# }
+#
+# { 'type': 'authoritative_zones',
+# 'op': 'get',
+# }
+# response:
+# { 'data': ['<str zone>', ...] }
+#
+#
### search_domains
#
# { 'type': 'search_domains',
@@ -255,6 +276,7 @@ STATE = {
"name_server_tags_recursor": [],
"name_server_tags_system": [],
"forward_zones": {},
+ "authoritative_zones": [],
"hosts": {},
"host_name": "vyos",
"domain_name": "",
@@ -267,7 +289,8 @@ base_schema = Schema({
Required('op'): Any('add', 'delete', 'set', 'get', 'apply'),
'type': Any('name_servers',
'name_server_tags_recursor', 'name_server_tags_system',
- 'forward_zones', 'search_domains', 'hosts', 'host_name'),
+ 'forward_zones', 'authoritative_zones', 'search_domains',
+ 'hosts', 'host_name'),
'data': Any(list, dict),
'tag': str,
'tag_regex': str
@@ -347,6 +370,11 @@ msg_schema_map = {
'delete': data_list_schema,
'get': op_type_schema
},
+ 'authoritative_zones': {
+ 'add': data_list_schema,
+ 'delete': data_list_schema,
+ 'get': op_type_schema
+ },
'search_domains': {
'add': data_dict_list_schema,
'delete': data_list_schema,
@@ -393,12 +421,12 @@ def pdns_rec_control(command):
def make_resolv_conf(state):
logger.info(f"Writing {RESOLV_CONF_FILE}")
- render(RESOLV_CONF_FILE, 'vyos-hostsd/resolv.conf.tmpl', state,
+ render(RESOLV_CONF_FILE, 'vyos-hostsd/resolv.conf.j2', state,
user='root', group='root')
def make_hosts(state):
logger.info(f"Writing {HOSTS_FILE}")
- render(HOSTS_FILE, 'vyos-hostsd/hosts.tmpl', state,
+ render(HOSTS_FILE, 'vyos-hostsd/hosts.j2', state,
user='root', group='root')
def make_pdns_rec_conf(state):
@@ -409,12 +437,12 @@ def make_pdns_rec_conf(state):
chmod_755(PDNS_REC_RUN_DIR)
render(PDNS_REC_LUA_CONF_FILE,
- 'dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl',
+ 'dns-forwarding/recursor.vyos-hostsd.conf.lua.j2',
state, user=PDNS_REC_USER, group=PDNS_REC_GROUP)
logger.info(f"Writing {PDNS_REC_ZONES_FILE}")
render(PDNS_REC_ZONES_FILE,
- 'dns-forwarding/recursor.forward-zones.conf.tmpl',
+ 'dns-forwarding/recursor.forward-zones.conf.j2',
state, user=PDNS_REC_USER, group=PDNS_REC_GROUP)
def set_host_name(state, data):
@@ -522,7 +550,7 @@ def handle_message(msg):
data = get_option(msg, 'data')
if _type in ['name_servers', 'forward_zones', 'search_domains', 'hosts']:
delete_items_from_dict(STATE[_type], data)
- elif _type in ['name_server_tags_recursor', 'name_server_tags_system']:
+ elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'authoritative_zones']:
delete_items_from_list(STATE[_type], data)
else:
raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
@@ -534,7 +562,7 @@ def handle_message(msg):
elif _type in ['forward_zones', 'hosts']:
add_items_to_dict(STATE[_type], data)
# maybe we need to rec_control clear-nta each domain that was removed here?
- elif _type in ['name_server_tags_recursor', 'name_server_tags_system']:
+ elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'authoritative_zones']:
add_items_to_list(STATE[_type], data)
else:
raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
@@ -550,7 +578,7 @@ def handle_message(msg):
if _type in ['name_servers', 'search_domains', 'hosts']:
tag_regex = get_option(msg, 'tag_regex')
result = get_items_from_dict_regex(STATE[_type], tag_regex)
- elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'forward_zones']:
+ elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'forward_zones', 'authoritative_zones']:
result = STATE[_type]
else:
raise ValueError(f'Operation "{op}" unknown data type "{_type}"')
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index aa7ac6708..c1b595412 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -32,6 +32,7 @@ from fastapi.responses import HTMLResponse
from fastapi.exceptions import RequestValidationError
from fastapi.routing import APIRoute
from pydantic import BaseModel, StrictStr, validator
+from starlette.middleware.cors import CORSMiddleware
from starlette.datastructures import FormData
from starlette.formparsers import FormParser, MultiPartParser
from multipart.multipart import parse_options_header
@@ -351,7 +352,7 @@ class MultipartRoute(APIRoute):
return error(e.status_code, e.detail)
except Exception as e:
if request.ERR_MISSING_KEY:
- return error(422, "Valid API key is required")
+ return error(401, "Valid API key is required")
if request.ERR_MISSING_DATA:
return error(422, "Non-empty data field is required")
if request.ERR_NOT_JSON:
@@ -610,13 +611,19 @@ def show_op(data: ShowModel):
# GraphQL integration
###
-from api.graphql.bindings import generate_schema
+def graphql_init(fast_api_app):
+ from api.graphql.bindings import generate_schema
-api.graphql.state.init()
+ api.graphql.state.init()
+ api.graphql.state.settings['app'] = app
-schema = generate_schema()
+ schema = generate_schema()
-app.add_route('/graphql', GraphQL(schema, debug=True))
+ if app.state.vyos_origins:
+ origins = app.state.vyos_origins
+ app.add_route('/graphql', CORSMiddleware(GraphQL(schema, debug=True), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS")))
+ else:
+ app.add_route('/graphql', GraphQL(schema, debug=True))
###
@@ -640,15 +647,22 @@ if __name__ == '__main__':
app.state.vyos_session = config_session
app.state.vyos_keys = server_config['api_keys']
- app.state.vyos_debug = bool(server_config['debug'] == 'true')
- app.state.vyos_strict = bool(server_config['strict'] == 'true')
+ app.state.vyos_debug = server_config['debug']
+ app.state.vyos_gql = server_config['gql']
+ app.state.vyos_strict = server_config['strict']
+ app.state.vyos_origins = server_config.get('cors', {}).get('origins', [])
- api.graphql.state.settings['app'] = app
+ if app.state.vyos_gql:
+ graphql_init(app)
try:
- uvicorn.run(app, host=server_config["listen_address"],
- port=int(server_config["port"]),
- proxy_headers=True)
+ if not server_config['socket']:
+ uvicorn.run(app, host=server_config["listen_address"],
+ port=int(server_config["port"]),
+ proxy_headers=True)
+ else:
+ uvicorn.run(app, uds="/run/api.sock",
+ proxy_headers=True)
except OSError as err:
logger.critical(f"OSError {err}")
sys.exit(1)
diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
index 1fba0d75b..a8df232ae 100755
--- a/src/system/keepalived-fifo.py
+++ b/src/system/keepalived-fifo.py
@@ -29,6 +29,7 @@ from logging.handlers import SysLogHandler
from vyos.ifconfig.vrrp import VRRP
from vyos.configquery import ConfigTreeQuery
from vyos.util import cmd
+from vyos.util import dict_search
# configure logging
logger = logging.getLogger(__name__)
@@ -69,22 +70,11 @@ class KeepalivedFifo:
raise ValueError()
# Read VRRP configuration directly from CLI
- vrrp_config_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True)
- self.vrrp_config = {'vrrp_groups': {}, 'sync_groups': {}}
- for key in ['group', 'sync_group']:
- if key not in vrrp_config_dict:
- continue
- for group, group_config in vrrp_config_dict[key].items():
- if 'transition_script' not in group_config:
- continue
- self.vrrp_config['vrrp_groups'][group] = {
- 'STOP': group_config['transition_script'].get('stop'),
- 'FAULT': group_config['transition_script'].get('fault'),
- 'BACKUP': group_config['transition_script'].get('backup'),
- 'MASTER': group_config['transition_script'].get('master'),
- }
- logger.info(f'Loaded configuration: {self.vrrp_config}')
+ self.vrrp_config_dict = conf.get_config_dict(base,
+ key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ logger.debug(f'Loaded configuration: {self.vrrp_config_dict}')
except Exception as err:
logger.error(f'Unable to load configuration: {err}')
@@ -129,20 +119,17 @@ class KeepalivedFifo:
if os.path.exists(mdns_running_file):
cmd(mdns_update_command)
- if n_name in self.vrrp_config['vrrp_groups'] and n_state in self.vrrp_config['vrrp_groups'][n_name]:
- n_script = self.vrrp_config['vrrp_groups'][n_name].get(n_state)
- if n_script:
- self._run_command(n_script)
+ tmp = dict_search(f'group.{n_name}.transition_script.{n_state.lower()}', self.vrrp_config_dict)
+ if tmp != None:
+ self._run_command(tmp)
# check and run commands for VRRP sync groups
- # currently, this is not available in VyOS CLI
- if n_type == 'GROUP':
+ elif n_type == 'GROUP':
if os.path.exists(mdns_running_file):
cmd(mdns_update_command)
- if n_name in self.vrrp_config['sync_groups'] and n_state in self.vrrp_config['sync_groups'][n_name]:
- n_script = self.vrrp_config['sync_groups'][n_name].get(n_state)
- if n_script:
- self._run_command(n_script)
+ tmp = dict_search(f'sync_group.{n_name}.transition_script.{n_state.lower()}', self.vrrp_config_dict)
+ if tmp != None:
+ self._run_command(tmp)
# mark task in queue as done
self.message_queue.task_done()
except Exception as err:
diff --git a/src/systemd/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/systemd/tftpd@.service b/src/systemd/tftpd@.service
index 266bc0962..a674bf598 100644
--- a/src/systemd/tftpd@.service
+++ b/src/systemd/tftpd@.service
@@ -7,7 +7,7 @@ RequiresMountsFor=/run
Type=forking
#NotifyAccess=main
EnvironmentFile=-/etc/default/tftpd%I
-ExecStart=/usr/sbin/in.tftpd "$DAEMON_ARGS"
+ExecStart=/bin/sh -c "${VRF_ARGS} /usr/sbin/in.tftpd ${DAEMON_ARGS}"
Restart=on-failure
[Install]
diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service
index b77335778..4da55f518 100644
--- a/src/systemd/vyos-hostsd.service
+++ b/src/systemd/vyos-hostsd.service
@@ -7,7 +7,7 @@ DefaultDependencies=no
# Seemingly sensible way to say "as early as the system is ready"
# All vyos-hostsd needs is read/write mounted root
-After=systemd-remount-fs.service
+After=systemd-remount-fs.service cloud-init.service
[Service]
WorkingDirectory=/run/vyos-hostsd
diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service
deleted file mode 100644
index ba5df5984..000000000
--- a/src/systemd/vyos-http-api.service
+++ /dev/null
@@ -1,23 +0,0 @@
-[Unit]
-Description=VyOS HTTP API service
-After=auditd.service systemd-user-sessions.service time-sync.target vyos-router.service
-Requires=vyos-router.service
-
-[Service]
-ExecStartPre=/usr/libexec/vyos/init/vyos-config
-ExecStart=/usr/libexec/vyos/services/vyos-http-api-server
-Type=idle
-
-SyslogIdentifier=vyos-http-api
-SyslogFacility=daemon
-
-Restart=on-failure
-
-# Does't work but leave it here
-User=root
-Group=vyattacfg
-
-[Install]
-# Installing in a earlier target leaves ExecStartPre waiting
-WantedBy=getty.target
-
diff --git a/src/tests/test_util.py b/src/tests/test_util.py
index 9bd27adc0..8ac9a500a 100644
--- a/src/tests/test_util.py
+++ b/src/tests/test_util.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
@@ -23,3 +23,6 @@ class TestVyOSUtil(TestCase):
expected_data = {"foo_bar": {"baz_quux": None}}
new_data = mangle_dict_keys(data, '-', '_')
self.assertEqual(new_data, expected_data)
+
+ def test_sysctl_read(self):
+ self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1')
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/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client
index d4d38315a..a0515951a 100755
--- a/src/utils/vyos-hostsd-client
+++ b/src/utils/vyos-hostsd-client
@@ -129,7 +129,8 @@ try:
params = h.split(",")
if len(params) < 2:
raise ValueError("Malformed host entry")
- entry['address'] = params[1]
+ # Address needs to be a list because of changes made in T2683
+ entry['address'] = [params[1]]
entry['aliases'] = params[2:]
data[params[0]] = entry
client.add_hosts({args.tag: data})
diff --git a/src/validators/as-number-list b/src/validators/as-number-list
new file mode 100755
index 000000000..432d44180
--- /dev/null
+++ b/src/validators/as-number-list
@@ -0,0 +1,29 @@
+#!/bin/sh
+#
+# 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/>.
+
+if [ $# -lt 1 ]; then
+ echo "Illegal number of parameters"
+ exit 1
+fi
+
+for var in "$@"; do
+ ${vyos_validators_dir}/numeric --range 1-4294967294 $var
+ if [ $? -ne 0 ]; then
+ exit 1
+ fi
+done
+
+exit 0
diff --git a/src/validators/bgp-route-target b/src/validators/bgp-rd-rt
index e7e4d403f..b2b69c9be 100755
--- a/src/validators/bgp-route-target
+++ b/src/validators/bgp-rd-rt
@@ -19,29 +19,37 @@ from vyos.template import is_ipv4
parser = ArgumentParser()
group = parser.add_mutually_exclusive_group()
-group.add_argument('--single', action='store', help='Validate and allow only one route-target')
-group.add_argument('--multi', action='store', help='Validate multiple, whitespace separated route-targets')
+group.add_argument('--route-distinguisher', action='store', help='Validate BGP route distinguisher')
+group.add_argument('--route-target', action='store', help='Validate one BGP route-target')
+group.add_argument('--route-target-multi', action='store', help='Validate multiple, whitespace separated BGP route-targets')
args = parser.parse_args()
-def is_valid_rt(rt):
- # every route target needs to have a colon and must consists of two parts
+def is_valid(rt):
+ """ Verify BGP RD/RT - both can be verified using the same logic """
+ # every RD/RT (route distinguisher/route target) needs to have a colon and
+ # must consists of two parts
value = rt.split(':')
if len(value) != 2:
return False
- # A route target must either be only numbers, or the first part must be an
- # IPv4 address
+
+ # An RD/RT must either be only numbers, or the first part must be an IPv4
+ # address
if (is_ipv4(value[0]) or value[0].isdigit()) and value[1].isdigit():
return True
return False
if __name__ == '__main__':
- if args.single:
- if not is_valid_rt(args.single):
+ if args.route_distinguisher:
+ if not is_valid(args.route_distinguisher):
+ exit(1)
+
+ elif args.route_target:
+ if not is_valid(args.route_target):
exit(1)
- elif args.multi:
- for rt in args.multi.split(' '):
- if not is_valid_rt(rt):
+ elif args.route_target_multi:
+ for rt in args.route_target_multi.split(' '):
+ if not is_valid(rt):
exit(1)
else:
diff --git a/src/validators/ip-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 e5cbc9532..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-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 66cd90c9c..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-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..7080860c4 100755
--- a/src/validators/ipv6-range
+++ b/src/validators/ipv6-range
@@ -1,16 +1,20 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
-import sys
-import re
-from vyos.template import is_ipv6
+from ipaddress import IPv6Address
+from sys import argv, exit
if __name__ == '__main__':
- if len(sys.argv)>1:
- ipv6_range = sys.argv[1]
- # Regex for ipv6-ipv6 https://regexr.com/
- 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):
- sys.exit(1)
-
- sys.exit(0)
+ if len(argv) > 1:
+ # try to pass validation and raise an error if failed
+ try:
+ ipv6_range = argv[1]
+ range_left = ipv6_range.split('-')[0]
+ range_right = ipv6_range.split('-')[1]
+ if not IPv6Address(range_left) < IPv6Address(range_right):
+ raise ValueError(f'left element {range_left} must be less than right element {range_right}')
+ except Exception as err:
+ print(f'Error: {ipv6_range} is not a valid IPv6 range: {err}')
+ exit(1)
+ else:
+ print('Error: an IPv6 range argument must be provided')
+ exit(1)
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..bd6f0ef60
--- /dev/null
+++ b/src/validators/port-multi
@@ -0,0 +1,52 @@
+#!/usr/bin/python3
+
+from sys import argv
+from sys import exit
+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
+ tmp = line.split()
+ names.append(tmp[0])
+ if len(tmp) > 2:
+ # Add port aliases to service list, too
+ names.extend(tmp[2:])
+ # remove duplicate entries (e.g. echo) from list
+ names = list(dict.fromkeys(names))
+ return names
+
+if __name__ == '__main__':
+ if len(argv)>1:
+ ports = 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')
+ exit(1)
+ if int(port_1) > int(port_2):
+ print(f'Error: {port} is not a valid port range')
+ exit(1)
+ elif port.isnumeric():
+ if int(port) not in range(1, 65536):
+ print(f'Error: {port} is not a valid port')
+ exit(1)
+ elif port not in services:
+ print(f'Error: {port} is not a valid service name')
+ exit(1)
+ else:
+ exit(2)
+
+ 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/range b/src/validators/range
new file mode 100755
index 000000000..d4c25f3c4
--- /dev/null
+++ b/src/validators/range
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import sys
+import argparse
+
+class MalformedRange(Exception):
+ pass
+
+def validate_range(value, min=None, max=None):
+ try:
+ lower, upper = re.match(r'^(\d+)-(\d+)$', value).groups()
+
+ lower, upper = int(lower), int(upper)
+
+ if int(lower) > int(upper):
+ raise MalformedRange("the lower bound exceeds the upper bound".format(value))
+
+ if min is not None:
+ if lower < min:
+ raise MalformedRange("the lower bound must not be less than {}".format(min))
+
+ if max is not None:
+ if upper > max:
+ raise MalformedRange("the upper bound must not be greater than {}".format(max))
+
+ except (AttributeError, ValueError):
+ raise MalformedRange("range syntax error")
+
+parser = argparse.ArgumentParser(description='Range validator.')
+parser.add_argument('--min', type=int, action='store')
+parser.add_argument('--max', type=int, action='store')
+parser.add_argument('value', action='store')
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ try:
+ validate_range(args.value, min=args.min, max=args.max)
+ except MalformedRange as e:
+ print("Incorrect range '{}': {}".format(args.value, e))
+ sys.exit(1)
diff --git a/src/validators/script b/src/validators/script
index 1d8a27e5c..4ffdeb2a0 100755
--- a/src/validators/script
+++ b/src/validators/script
@@ -36,7 +36,7 @@ if __name__ == '__main__':
# File outside the config dir is just a warning
if not vyos.util.file_is_persistent(script):
- sys.exit(
- f'Warning: file {path} is outside the / config directory\n'
+ sys.exit(0)(
+ f'Warning: file {script} is outside the "/config" directory\n'
'It will not be automatically migrated to a new image on system update'
)
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)