summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/completion/list_disks.py21
-rwxr-xr-xsrc/completion/list_protocols.sh3
-rwxr-xr-xsrc/completion/list_sysctl_parameters.sh20
-rwxr-xr-xsrc/conf_mode/conntrack.py140
-rwxr-xr-xsrc/conf_mode/conntrack_sync.py135
-rwxr-xr-xsrc/conf_mode/dhcp_server.py45
-rwxr-xr-xsrc/conf_mode/firewall.py73
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py2
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py3
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py8
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py7
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py8
-rwxr-xr-xsrc/conf_mode/interfaces-vti.py97
-rwxr-xr-xsrc/conf_mode/protocols_isis.py8
-rwxr-xr-xsrc/conf_mode/protocols_nhrp.py122
-rwxr-xr-xsrc/conf_mode/service_router-advert.py12
-rwxr-xr-xsrc/conf_mode/system_sysctl.py73
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py358
-rwxr-xr-xsrc/conf_mode/vpn_rsa-keys.py111
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook46
-rw-r--r--src/etc/ipsec.d/key-pair.template67
-rwxr-xr-xsrc/etc/ipsec.d/vti-up-down47
-rwxr-xr-xsrc/etc/opennhrp/opennhrp-script.py136
-rw-r--r--src/etc/systemd/system/conntrackd.service.d/override.conf8
-rwxr-xr-xsrc/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py29
-rwxr-xr-xsrc/helpers/vyos-vrrp-conntracksync.sh154
-rwxr-xr-xsrc/migration-scripts/conntrack-sync/1-to-266
-rwxr-xr-xsrc/migration-scripts/interfaces/20-to-2160
-rwxr-xr-xsrc/migration-scripts/interfaces/5-to-612
-rwxr-xr-xsrc/migration-scripts/ipsec/4-to-514
-rwxr-xr-xsrc/migration-scripts/ipsec/5-to-668
-rwxr-xr-xsrc/migration-scripts/ntp/0-to-12
-rwxr-xr-xsrc/migration-scripts/system/20-to-2157
-rwxr-xr-xsrc/op_mode/conntrack_sync.py136
-rwxr-xr-xsrc/op_mode/dynamic_dns.py13
-rwxr-xr-xsrc/op_mode/monitor_bandwidth_test.sh2
-rwxr-xr-xsrc/op_mode/openconnect-control.py2
-rwxr-xr-xsrc/op_mode/show_nat_rules.py6
-rwxr-xr-xsrc/op_mode/vpn_ike_sa.py68
-rwxr-xr-xsrc/op_mode/vpn_ipsec.py206
-rw-r--r--src/systemd/opennhrp.service13
-rwxr-xr-xsrc/validators/ipv43
-rwxr-xr-xsrc/validators/ipv4-multicast3
-rwxr-xr-xsrc/validators/ipv6-exclude7
-rwxr-xr-xsrc/validators/ipv6-multicast3
-rwxr-xr-xsrc/validators/ipv6-range16
-rwxr-xr-xsrc/validators/ipv6-range-exclude7
-rwxr-xr-xsrc/validators/sysctl24
48 files changed, 2448 insertions, 73 deletions
diff --git a/src/completion/list_disks.py b/src/completion/list_disks.py
index ff1135e23..0aa872abb 100755
--- a/src/completion/list_disks.py
+++ b/src/completion/list_disks.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
@@ -23,11 +23,20 @@ parser.add_argument("-e", "--exclude", type=str, help="Exclude specified device
args = parser.parse_args()
disks = set()
-with open('/proc/partitions') as partitions_file:
- for line in partitions_file:
- fields = line.strip().split()
- if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name':
- disks.add(fields[3])
+with open('/proc/partitions') as f:
+ table = f.read()
+
+for line in table.splitlines()[1:]:
+ fields = line.strip().split()
+ # probably an empty line at the top
+ if len(fields) == 0:
+ continue
+ disks.add(fields[3])
+
+if 'loop0' in disks:
+ disks.remove('loop0')
+if 'sr0' in disks:
+ disks.remove('sr0')
if args.exclude:
disks.remove(args.exclude)
diff --git a/src/completion/list_protocols.sh b/src/completion/list_protocols.sh
new file mode 100755
index 000000000..e9d50a70f
--- /dev/null
+++ b/src/completion/list_protocols.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+grep -v '^#' /etc/protocols | awk 'BEGIN {ORS=""} {if ($3) {print TRS $1; TRS=" "}}'
diff --git a/src/completion/list_sysctl_parameters.sh b/src/completion/list_sysctl_parameters.sh
new file mode 100755
index 000000000..c111716bb
--- /dev/null
+++ b/src/completion/list_sysctl_parameters.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+#
+# 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/>.
+
+declare -a vals
+eval "vals=($(/sbin/sysctl -N -a))"
+echo ${vals[@]}
+exit 0
diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py
new file mode 100755
index 000000000..4e6e39c0f
--- /dev/null
+++ b/src/conf_mode/conntrack.py
@@ -0,0 +1,140 @@
+#!/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 vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.util import cmd
+from vyos.util import run
+from vyos.util import process_named_running
+from vyos.util import dict_search
+from vyos.template import render
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf'
+sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf'
+
+# Every ALG (Application Layer Gateway) consists of either a Kernel Object
+# also called a Kernel Module/Driver or some rules present in iptables
+module_map = {
+ 'ftp' : {
+ 'ko' : ['nf_nat_ftp', 'nf_conntrack_ftp'],
+ },
+ 'h323' : {
+ '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'],
+ },
+ 'pptp' : {
+ 'ko' : ['nf_nat_pptp', 'nf_conntrack_pptp'],
+ },
+ 'sip' : {
+ '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'],
+ },
+ 'tftp' : {
+ 'ko' : ['nf_nat_tftp', 'nf_conntrack_tftp'],
+ },
+}
+
+def resync_conntrackd():
+ tmp = run('/usr/libexec/vyos/conf_mode/conntrack_sync.py')
+ if tmp > 0:
+ print('ERROR: error restarting conntrackd!')
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['system', 'conntrack']
+
+ conntrack = 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)
+ conntrack = dict_merge(default_values, conntrack)
+
+ return conntrack
+
+def verify(conntrack):
+ return None
+
+def generate(conntrack):
+ render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.tmpl', conntrack)
+ render(sysctl_file, 'conntrack/sysctl.conf.tmpl', conntrack)
+
+ return None
+
+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.
+ for module, module_config in module_map.items():
+ if dict_search(f'modules.{module}.disable', conntrack) != None:
+ if 'ko' in module_config:
+ for mod in module_config['ko']:
+ # 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']:
+ print(f'iptables --delete {rule}')
+ cmd(f'iptables --delete {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 process_named_running('conntrackd'):
+ # Reload conntrack-sync daemon to fetch new sysctl values
+ resync_conntrackd()
+
+ # We silently ignore all errors
+ # See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080
+ cmd(f'sysctl -f {sysctl_file}')
+
+ 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/conntrack_sync.py b/src/conf_mode/conntrack_sync.py
new file mode 100755
index 000000000..7f22fa2dd
--- /dev/null
+++ b/src/conf_mode/conntrack_sync.py
@@ -0,0 +1,135 @@
+#!/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 vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_interface_exists
+from vyos.util import call
+from vyos.util import dict_search
+from vyos.util import process_named_running
+from vyos.util import read_file
+from vyos.util import run
+from vyos.template import render
+from vyos.template import get_ipv4
+from vyos.validate import is_addr_assigned
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = '/run/conntrackd/conntrackd.conf'
+
+def resync_vrrp():
+ tmp = run('/usr/libexec/vyos/conf_mode/vrrp.py')
+ if tmp > 0:
+ print('ERROR: error restarting VRRP daemon!')
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['service', 'conntrack-sync']
+ if not conf.exists(base):
+ return None
+
+ conntrack = 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)
+ conntrack = dict_merge(default_values, conntrack)
+
+ conntrack['hash_size'] = read_file('/sys/module/nf_conntrack/parameters/hashsize')
+ conntrack['table_size'] = read_file('/proc/sys/net/netfilter/nf_conntrack_max')
+
+ conntrack['vrrp'] = conf.get_config_dict(['high-availability', 'vrrp', 'sync-group'],
+ get_first_key=True)
+
+ return conntrack
+
+def verify(conntrack):
+ if not conntrack:
+ return None
+
+ if 'interface' not in conntrack:
+ raise ConfigError('Interface not defined!')
+
+ for interface in conntrack['interface']:
+ verify_interface_exists(interface)
+ # Interface must not only exist, it must also carry an IP address
+ if len(get_ipv4(interface)) < 1:
+ raise ConfigError(f'Interface {interface} requires an IP address!')
+
+ if 'expect_sync' in conntrack:
+ if len(conntrack['expect_sync']) > 1 and 'all' in conntrack['expect_sync']:
+ raise ConfigError('Cannot configure all with other protocol')
+
+ 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!')
+
+ vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack)
+ if vrrp_group == None:
+ raise ConfigError(f'No VRRP sync-group defined!')
+ if vrrp_group not in conntrack['vrrp']:
+ raise ConfigError(f'VRRP sync-group {vrrp_group} not configured!')
+
+ return None
+
+def generate(conntrack):
+ if not conntrack:
+ if os.path.isfile(config_file):
+ os.unlink(config_file)
+ return None
+
+ render(config_file, 'conntrackd/conntrackd.conf.tmpl', conntrack)
+
+ return None
+
+def apply(conntrack):
+ if not conntrack:
+ # Failover mechanism daemon should be indicated that it no longer needs
+ # to execute conntrackd actions on transition. This is only required
+ # once when conntrackd is stopped and taken out of service!
+ if process_named_running('conntrackd'):
+ resync_vrrp()
+
+ call('systemctl stop conntrackd.service')
+ return None
+
+ # Failover mechanism daemon should be indicated that it needs to execute
+ # conntrackd actions on transition. This is only required once when conntrackd
+ # is started the first time!
+ if not process_named_running('conntrackd'):
+ resync_vrrp()
+
+ call('systemctl restart conntrackd.service')
+ 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/dhcp_server.py b/src/conf_mode/dhcp_server.py
index 84a8736e8..cdee72e09 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -18,6 +18,8 @@ import os
from ipaddress import ip_address
from ipaddress import ip_network
+from netaddr import IPAddress
+from netaddr import IPRange
from sys import exit
from vyos.config import Config
@@ -25,6 +27,7 @@ from vyos.configdict import dict_merge
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
+from vyos.util import run
from vyos.validate import is_subnet_connected
from vyos.validate import is_addr_assigned
from vyos.xml import defaults
@@ -162,8 +165,7 @@ def verify(dhcp):
# Check if DHCP address range is inside configured subnet declaration
if 'range' in subnet_config:
- range_start = []
- range_stop = []
+ networks = []
for range, range_config in subnet_config['range'].items():
if not {'start', 'stop'} <= set(range_config):
raise ConfigError(f'DHCP range "{range}" start and stop address must be defined!')
@@ -178,18 +180,16 @@ def verify(dhcp):
raise ConfigError(f'DHCP range "{range}" stop address must be greater or equal\n' \
'to the ranges start address!')
- # Range start address must be unique
- if range_config['start'] in range_start:
- raise ConfigError('Conflicting DHCP lease range: Pool start\n' \
- 'address "{start}" defined multipe times!'.format(range_config))
+ for network in networks:
+ start = range_config['start']
+ stop = range_config['stop']
+ if start in network:
+ raise ConfigError(f'Range "{range}" start address "{start}" already part of another range!')
+ if stop in network:
+ raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another range!')
- # Range stop address must be unique
- if range_config['stop'] in range_start:
- raise ConfigError('Conflicting DHCP lease range: Pool stop\n' \
- 'address "{stop}" defined multipe times!'.format(range_config))
-
- range_start.append(range_config['start'])
- range_stop.append(range_config['stop'])
+ tmp = IPRange(range_config['start'], range_config['stop'])
+ networks.append(tmp)
if 'failover' in subnet_config:
for key in ['local_address', 'peer_address', 'name', 'status']:
@@ -272,10 +272,25 @@ def generate(dhcp):
if not dhcp or 'disable' in dhcp:
return None
- # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters
- # we can pass to ISC DHCPd
+ # 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,
+ 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
+ # configuration
+ tmp = run(f'/usr/sbin/dhcpd -4 -q -t -cf {tmp_file}')
+ if tmp > 0:
+ if os.path.exists(tmp_file):
+ os.unlink(tmp_file)
+ raise ConfigError('Configuration file errors encountered - check your options!')
+
+ # 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,
formater=lambda _: _.replace("&quot;", '"'))
+
return None
def apply(dhcp):
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
new file mode 100755
index 000000000..8e6ce5b14
--- /dev/null
+++ b/src/conf_mode/firewall.py
@@ -0,0 +1,73 @@
+#!/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 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.template import render
+from vyos.util import call
+from vyos import ConfigError
+from vyos import airbag
+from pprint import pprint
+airbag.enable()
+
+
+def get_config(config=None):
+
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['nfirewall']
+ firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ pprint(firewall)
+ return firewall
+
+def verify(firewall):
+ # bail out early - looks like removal from running config
+ if not firewall:
+ return None
+
+ return None
+
+def generate(firewall):
+ if not firewall:
+ return None
+
+ return None
+
+def apply(firewall):
+ if not firewall:
+ return None
+
+ 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/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 0727b47a8..9cae29481 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -43,7 +43,7 @@ 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 = 'POSTROUTING'
+egress_iptables_nflog_chain = 'FORWARD'
# helper functions
# check if node exists and return True if this is true
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 1a549f27d..431d65f1f 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -83,6 +83,9 @@ def get_config(config=None):
tmp = leaf_node_changed(conf, ['mode'])
if tmp: bond.update({'shutdown_required': {}})
+ tmp = leaf_node_changed(conf, ['lacp-rate'])
+ if tmp: bond.update({'shutdown_required': {}})
+
# determine which members have been removed
interfaces_removed = leaf_node_changed(conf, ['member', 'interface'])
if interfaces_removed:
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index 44fc9cb9e..55c783f38 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,8 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import os
-
from sys import exit
from vyos.config import Config
@@ -42,7 +40,7 @@ def get_config(config=None):
return dummy
def verify(dummy):
- if 'deleted' in dummy.keys():
+ if 'deleted' in dummy:
verify_bridge_delete(dummy)
return None
@@ -58,7 +56,7 @@ def apply(dummy):
d = DummyIf(dummy['ifname'])
# Remove dummy interface
- if 'deleted' in dummy.keys():
+ if 'deleted' in dummy:
d.remove()
else:
d.update(dummy)
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index 34a054837..945a2ea9c 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -24,6 +24,7 @@ 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.ifconfig import MACVLANIf
from vyos import ConfigError
@@ -45,6 +46,9 @@ def get_config(config=None):
mode = leaf_node_changed(conf, ['mode'])
if mode: peth.update({'mode_old' : mode})
+ if 'source_interface' in peth:
+ peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'],
+ peth['source_interface'])
return peth
def verify(peth):
@@ -55,9 +59,10 @@ def verify(peth):
verify_source_interface(peth)
verify_vrf(peth)
verify_address(peth)
-
+ verify_mtu_parent(peth, peth['parent'])
# use common function to verify VLAN configuration
verify_vlan_config(peth)
+
return None
def generate(peth):
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 4e6c8a9ab..1575c83ef 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -109,6 +109,14 @@ def verify(tunnel):
if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']:
raise ConfigError('Can not disable PMTU discovery for given encapsulation')
+ if dict_search('parameters.ip.ignore_df', tunnel) != None:
+ if tunnel['encapsulation'] not in ['gretap']:
+ raise ConfigError('Option ignore-df can only be used on GRETAP tunnels!')
+
+ if dict_search('parameters.ip.no_pmtu_discovery', tunnel) == None:
+ raise ConfigError('Option ignore-df path MTU discovery to be disabled!')
+
+
def generate(tunnel):
return None
diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py
new file mode 100755
index 000000000..6ff23ae59
--- /dev/null
+++ b/src/conf_mode/interfaces-vti.py
@@ -0,0 +1,97 @@
+#!/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 netifaces import interfaces
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.ifconfig import VTIIf
+from vyos.util import dict_search
+from vyos import ConfigError
+from vyos import airbag
+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
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['interfaces', 'vti']
+ vti = get_interface_dict(conf, base)
+
+ # VTI is more then an interface - we retrieve the "real" configuration from
+ # the IPsec peer configuration which binds this VTI
+ conf.set_level([])
+ vti['ipsec'] = conf.get_config_dict(['vpn', 'ipsec', 'site-to-site', 'peer'],
+ key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ for peer, peer_config in vti['ipsec'].items():
+ if dict_search('vti.bind', peer_config) == vti['ifname']:
+ vti['remote'] = peer
+ if 'local_address' in peer_config:
+ vti['source_address'] = peer_config['local_address']
+ # we also need to "calculate" a per vti individual key
+ base = 0x900000
+ vti['key'] = base + int(vti['ifname'].lstrip('vti'))
+
+ return vti
+
+def verify(vti):
+ if 'deleted' in vti:
+ return None
+
+ ifname = vti['ifname']
+ found = False
+ for peer, peer_config in vti['ipsec'].items():
+ if dict_search('vti.bind', peer_config) == ifname:
+ found = True
+ # we can now stop processing the for loop
+ break
+ if not found:
+ tmp = vti['ifname']
+ raise ConfigError(f'Interface "{ifname}" not referenced in any VPN configuration!')
+
+ return None
+
+def generate(vti):
+ return None
+
+def apply(vti):
+ if vti['ifname'] in interfaces():
+ # Always delete the VTI interface in advance
+ VTIIf(**vti).remove()
+
+ if 'deleted' not in vti:
+ tmp = VTIIf(**vti)
+ tmp.update(vti)
+
+ 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/protocols_isis.py b/src/conf_mode/protocols_isis.py
index ef21e0055..c3a444f16 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -128,9 +128,11 @@ def verify(isis):
raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!')
# If md5 and plaintext-password set at the same time
- if 'area_password' in isis:
- if {'md5', 'plaintext_password'} <= set(isis['encryption']):
- raise ConfigError('Can not use both md5 and plaintext-password for ISIS area-password!')
+ for password in ['area_password', 'domain_password']:
+ if password in isis:
+ if {'md5', 'plaintext_password'} <= set(isis[password]):
+ tmp = password.replace('_', '-')
+ raise ConfigError(f'Can use either md5 or plaintext-password for {tmp}!')
# If one param from delay set, but not set others
if 'spf_delay_ietf' in isis:
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
new file mode 100755
index 000000000..12dacdba0
--- /dev/null
+++ b/src/conf_mode/protocols_nhrp.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.config import Config
+from vyos.configdict import node_changed
+from vyos.template import render
+from vyos.util import process_named_running
+from vyos.util import run
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+opennhrp_conf = '/run/opennhrp/opennhrp.conf'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['protocols', 'nhrp']
+
+ nhrp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ nhrp['del_tunnels'] = node_changed(conf, base + ['tunnel'], key_mangling=('-', '_'))
+
+ if not conf.exists(base):
+ return nhrp
+
+ nhrp['if_tunnel'] = conf.get_config_dict(['interfaces', 'tunnel'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ nhrp['profile_map'] = {}
+ profile = conf.get_config_dict(['vpn', 'ipsec', 'profile'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ for name, profile_conf in profile.items():
+ if 'bind' in profile_conf and 'tunnel' in profile_conf['bind']:
+ interfaces = profile_conf['bind']['tunnel']
+ if isinstance(interfaces, str):
+ interfaces = [interfaces]
+ for interface in interfaces:
+ nhrp['profile_map'][interface] = name
+
+ return nhrp
+
+def verify(nhrp):
+ if 'tunnel' in nhrp:
+ for name, nhrp_conf in nhrp['tunnel'].items():
+ if not nhrp['if_tunnel'] or name not in nhrp['if_tunnel']:
+ raise ConfigError(f'Tunnel interface "{name}" does not exist')
+
+ tunnel_conf = nhrp['if_tunnel'][name]
+
+ if 'encapsulation' not in tunnel_conf or tunnel_conf['encapsulation'] != 'gre':
+ raise ConfigError(f'Tunnel "{name}" is not an mGRE tunnel')
+
+ if 'remote' in tunnel_conf:
+ raise ConfigError(f'Tunnel "{name}" cannot have a remote address defined')
+
+ if 'map' in nhrp_conf:
+ for map_name, map_conf in nhrp_conf['map'].items():
+ if 'nbma_address' not in map_conf:
+ raise ConfigError(f'nbma-address missing on map {map_name} on tunnel {name}')
+
+ if 'dynamic_map' in nhrp_conf:
+ 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}')
+ return None
+
+def generate(nhrp):
+ render(opennhrp_conf, 'nhrp/opennhrp.conf.tmpl', 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']
+
+ 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}')
+
+ 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}')
+
+ action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop'
+ run(f'systemctl {action} opennhrp')
+ 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/service_router-advert.py b/src/conf_mode/service_router-advert.py
index 65eb11ce3..9afcdd63e 100755
--- a/src/conf_mode/service_router-advert.py
+++ b/src/conf_mode/service_router-advert.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2019 VyOS maintainers and contributors
+# Copyright (C) 2018-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -40,11 +40,14 @@ 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_interface_values = defaults(base + ['interface'])
- # we deal with prefix defaults later on
+ # we deal with prefix, route defaults later on
if 'prefix' in default_interface_values:
del default_interface_values['prefix']
+ if 'route' in default_interface_values:
+ del default_interface_values['route']
default_prefix_values = defaults(base + ['interface', 'prefix'])
+ default_route_values = defaults(base + ['interface', 'route'])
if 'interface' in rtradv:
for interface in rtradv['interface']:
@@ -56,6 +59,11 @@ def get_config(config=None):
rtradv['interface'][interface]['prefix'][prefix] = dict_merge(
default_prefix_values, rtradv['interface'][interface]['prefix'][prefix])
+ if 'route' in rtradv['interface'][interface]:
+ for route in rtradv['interface'][interface]['route']:
+ rtradv['interface'][interface]['route'][route] = dict_merge(
+ default_route_values, rtradv['interface'][interface]['route'][route])
+
if 'name_server' in rtradv['interface'][interface]:
# always use a list when dealing with nameservers - eases the template generation
if isinstance(rtradv['interface'][interface]['name_server'], str):
diff --git a/src/conf_mode/system_sysctl.py b/src/conf_mode/system_sysctl.py
new file mode 100755
index 000000000..4f16d1ed6
--- /dev/null
+++ b/src/conf_mode/system_sysctl.py
@@ -0,0 +1,73 @@
+#!/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 vyos.config import Config
+from vyos.template import render
+from vyos.util import cmd
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/sysctl/99-vyos-sysctl.conf'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['system', 'sysctl']
+ if not conf.exists(base):
+ return None
+
+ sysctl = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return sysctl
+
+def verify(sysctl):
+ return None
+
+def generate(sysctl):
+ if not sysctl:
+ if os.path.isfile(config_file):
+ os.unlink(config_file)
+ return None
+
+ render(config_file, 'system/sysctl.conf.tmpl', sysctl)
+ return None
+
+def apply(sysctl):
+ if not sysctl:
+ return None
+
+ # We silently ignore all errors
+ # See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080
+ cmd(f'sysctl -f {config_file}')
+ 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/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 969266c30..4efedd995 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# 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
@@ -17,51 +17,383 @@
import os
from sys import exit
+from time import sleep
from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_interface_exists
+from vyos.ifconfig import Interface
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
+from vyos.util import get_interface_address
+from vyos.util import process_named_running
+from vyos.util import run
+from vyos.util import cidr_fit
from vyos import ConfigError
from vyos import airbag
-from pprint import pprint
airbag.enable()
+authby_translate = {
+ 'pre-shared-secret': 'secret',
+ 'rsa': 'rsasig',
+ 'x509': 'rsasig'
+}
+default_pfs = 'dh-group2'
+pfs_translate = {
+ 'dh-group1': 'modp768',
+ 'dh-group2': 'modp1024',
+ 'dh-group5': 'modp1536',
+ 'dh-group14': 'modp2048',
+ 'dh-group15': 'modp3072',
+ 'dh-group16': 'modp4096',
+ 'dh-group17': 'modp6144',
+ 'dh-group18': 'modp8192',
+ 'dh-group19': 'ecp256',
+ 'dh-group20': 'ecp384',
+ 'dh-group21': 'ecp512',
+ 'dh-group22': 'modp1024s160',
+ 'dh-group23': 'modp2048s224',
+ 'dh-group24': 'modp2048s256',
+ 'dh-group25': 'ecp192',
+ 'dh-group26': 'ecp224',
+ 'dh-group27': 'ecp224bp',
+ 'dh-group28': 'ecp256bp',
+ 'dh-group29': 'ecp384bp',
+ 'dh-group30': 'ecp512bp',
+ 'dh-group31': 'curve25519',
+ 'dh-group32': 'curve448'
+}
+
+any_log_modes = [
+ 'dmn', 'mgr', 'ike', 'chd','job', 'cfg', 'knl', 'net', 'asn',
+ 'enc', 'lib', 'esp', 'tls', 'tnc', 'imc', 'imv', 'pts'
+]
+
+ike_ciphers = {}
+esp_ciphers = {}
+
+mark_base = 0x900000
+
+CA_PATH = "/etc/ipsec.d/cacerts/"
+CRL_PATH = "/etc/ipsec.d/crls/"
+
+DHCP_BASE = "/var/lib/dhcp/dhclient"
+
+LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/']
+X509_PATH = '/config/auth/'
+
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['vpn', 'nipsec']
+ base = ['vpn', 'ipsec']
if not conf.exists(base):
return None
# retrieve common dictionary keys
- ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces', 'interface'])
+ ipsec['l2tp_exists'] = conf.exists('vpn l2tp remote-access ipsec-settings ')
+ ipsec['nhrp_exists'] = conf.exists('protocols nhrp tunnel')
+ ipsec['rsa_keys'] = conf.get_config_dict(['vpn', 'rsa-keys'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ default_ike_pfs = None
+
+ if 'ike_group' in ipsec:
+ for group, ike_conf in ipsec['ike_group'].items():
+ if 'proposal' in ike_conf:
+ ciphers = []
+ for i in ike_conf['proposal']:
+ proposal = ike_conf['proposal'][i]
+ enc = proposal['encryption'] if 'encryption' in proposal else None
+ hash = proposal['hash'] if 'hash' in proposal else None
+ pfs = ('dh-group' + proposal['dh_group']) if 'dh_group' in proposal else default_pfs
+
+ if not default_ike_pfs:
+ default_ike_pfs = pfs
+
+ if enc and hash:
+ ciphers.append(f"{enc}-{hash}-{pfs_translate[pfs]}" if pfs else f"{enc}-{hash}")
+ ike_ciphers[group] = ','.join(ciphers) + '!'
+
+ if 'esp_group' in ipsec:
+ for group, esp_conf in ipsec['esp_group'].items():
+ pfs = esp_conf['pfs'] if 'pfs' in esp_conf else 'enable'
+
+ if pfs == 'disable':
+ pfs = None
+
+ if pfs == 'enable':
+ pfs = default_ike_pfs
+
+ if 'proposal' in esp_conf:
+ ciphers = []
+ for i in esp_conf['proposal']:
+ proposal = esp_conf['proposal'][i]
+ enc = proposal['encryption'] if 'encryption' in proposal else None
+ hash = proposal['hash'] if 'hash' in proposal else None
+ if enc and hash:
+ ciphers.append(f"{enc}-{hash}-{pfs_translate[pfs]}" if pfs else f"{enc}-{hash}")
+ esp_ciphers[group] = ','.join(ciphers) + '!'
+
return ipsec
+def get_rsa_local_key(ipsec):
+ return dict_search('local_key.file', ipsec['rsa_keys'])
+
+def verify_rsa_local_key(ipsec):
+ file = get_rsa_local_key(ipsec)
+
+ if not file:
+ return False
+
+ for path in LOCAL_KEY_PATHS:
+ full_path = os.path.join(path, file)
+ if os.path.exists(full_path):
+ return full_path
+
+ return False
+
+def verify_rsa_key(ipsec, key_name):
+ return dict_search(f'rsa_key_name.{key_name}.rsa_key', ipsec['rsa_keys'])
+
def verify(ipsec):
if not ipsec:
return None
+ if 'ipsec_interfaces' in ipsec and 'interface' in ipsec['ipsec_interfaces']:
+ interfaces = ipsec['ipsec_interfaces']['interface']
+ if isinstance(interfaces, str):
+ interfaces = [interfaces]
+
+ for ifname in interfaces:
+ verify_interface_exists(ifname)
+
+ if 'profile' in ipsec:
+ for profile, profile_conf in ipsec['profile'].items():
+ if 'esp_group' in profile_conf:
+ if 'esp_group' not in ipsec or profile_conf['esp_group'] not in ipsec['esp_group']:
+ raise ConfigError(f"Invalid esp-group on {profile} profile")
+ else:
+ raise ConfigError(f"Missing esp-group on {profile} profile")
+
+ if 'ike_group' in profile_conf:
+ if 'ike_group' not in ipsec or profile_conf['ike_group'] not in ipsec['ike_group']:
+ raise ConfigError(f"Invalid ike-group on {profile} profile")
+ else:
+ raise ConfigError(f"Missing ike-group on {profile} profile")
+
+ if 'authentication' not in profile_conf:
+ raise ConfigError(f"Missing authentication on {profile} profile")
+
+ if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
+ for peer, peer_conf in ipsec['site_to_site']['peer'].items():
+ has_default_esp = False
+ if 'default_esp_group' in peer_conf:
+ has_default_esp = True
+ if 'esp_group' not in ipsec or peer_conf['default_esp_group'] not in ipsec['esp_group']:
+ raise ConfigError(f"Invalid esp-group on site-to-site peer {peer}")
+
+ if 'ike_group' in peer_conf:
+ if 'ike_group' not in ipsec or peer_conf['ike_group'] not in ipsec['ike_group']:
+ raise ConfigError(f"Invalid ike-group on site-to-site peer {peer}")
+ else:
+ raise ConfigError(f"Missing ike-group on site-to-site peer {peer}")
+
+ if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']:
+ raise ConfigError(f"Missing authentication on site-to-site peer {peer}")
+
+ if peer_conf['authentication']['mode'] == 'x509':
+ if 'x509' not in peer_conf['authentication']:
+ raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}")
+
+ if 'key' not in peer_conf['authentication']['x509']:
+ raise ConfigError(f"Missing x509 key on site-to-site peer {peer}")
+
+ if 'ca_cert_file' not in peer_conf['authentication']['x509'] or 'cert_file' not in peer_conf['authentication']['x509']:
+ raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}")
+
+ if 'file' not in peer_conf['authentication']['x509']['key']:
+ raise ConfigError(f"Missing x509 key file on site-to-site peer {peer}")
+
+ for key in ['ca_cert_file', 'cert_file', 'crl_file']:
+ if key in peer_conf['authentication']['x509']:
+ path = os.path.join(X509_PATH, peer_conf['authentication']['x509'][key])
+ if not os.path.exists(path):
+ raise ConfigError(f"File not found for {key} on site-to-site peer {peer}")
+
+ key_path = os.path.join(X509_PATH, peer_conf['authentication']['x509']['key']['file'])
+ if not os.path.exists(key_path):
+ raise ConfigError(f"Private key not found on site-to-site peer {peer}")
+
+ if peer_conf['authentication']['mode'] == 'rsa':
+ if not verify_rsa_local_key(ipsec):
+ raise ConfigError(f"Invalid key on rsa-keys local-key")
+
+ if 'rsa_key_name' not in peer_conf['authentication']:
+ raise ConfigError(f"Missing rsa-key-name on site-to-site peer {peer}")
+
+ if not verify_rsa_key(ipsec, peer_conf['authentication']['rsa_key_name']):
+ raise ConfigError(f"Invalid rsa-key-name on site-to-site peer {peer}")
+
+ if 'local_address' not in peer_conf and 'dhcp_interface' not in peer_conf:
+ raise ConfigError(f"Missing local-address or dhcp-interface on site-to-site peer {peer}")
+
+ if 'dhcp_interface' in peer_conf:
+ dhcp_interface = peer_conf['dhcp_interface']
+
+ verify_interface_exists(dhcp_interface)
+
+ if not os.path.exists(f'{DHCP_BASE}_{dhcp_interface}.conf'):
+ raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}")
+
+ address = Interface(dhcp_interface).get_addr()
+ if not address:
+ raise ConfigError(f"Failed to get address from dhcp-interface on site-to-site peer {peer}")
+
+ if 'vti' in peer_conf:
+ if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf:
+ raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}")
+
+ if 'bind' in peer_conf['vti']:
+ vti_interface = peer_conf['vti']['bind']
+ if not os.path.exists(f'/sys/class/net/{vti_interface}'):
+ raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!')
+
+ if 'vti' not in peer_conf and 'tunnel' not in peer_conf:
+ raise ConfigError(f"No VTI or tunnel specified on site-to-site peer {peer}")
+
+ if 'tunnel' in peer_conf:
+ for tunnel, tunnel_conf in peer_conf['tunnel'].items():
+ if 'esp_group' not in tunnel_conf and not has_default_esp:
+ raise ConfigError(f"Missing esp-group on tunnel {tunnel} for site-to-site peer {peer}")
+
+ esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group']
+
+ if esp_group_name not in ipsec['esp_group']:
+ raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}")
+
+ esp_group = ipsec['esp_group'][esp_group_name]
+
+ if 'mode' in esp_group and esp_group['mode'] == 'transport':
+ if 'protocol' in tunnel_conf and ((peer in ['any', '0.0.0.0']) or ('local_address' not in peer_conf or peer_conf['local_address'] in ['any', '0.0.0.0'])):
+ raise ConfigError(f"Fixed local-address or peer required when a protocol is defined with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}")
+
+ if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']):
+ raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}")
+
def generate(ipsec):
- if not ipsec:
- return None
+ data = {}
- return ipsec
+ if ipsec:
+ data = ipsec
+ data['authby'] = authby_translate
+ data['ciphers'] = {'ike': ike_ciphers, 'esp': esp_ciphers}
+ data['marks'] = {}
+ data['rsa_local_key'] = verify_rsa_local_key(ipsec)
+ data['x509_path'] = X509_PATH
+
+ if 'site_to_site' in data and 'peer' in data['site_to_site']:
+ for peer, peer_conf in ipsec['site_to_site']['peer'].items():
+ if peer_conf['authentication']['mode'] == 'x509':
+ ca_cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['ca_cert_file'])
+ call(f'cp -f {ca_cert_file} {CA_PATH}')
+
+ if 'crl_file' in peer_conf['authentication']['x509']:
+ crl_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['crl_file'])
+ call(f'cp -f {crl_file} {CRL_PATH}')
+
+ local_ip = ''
+ if 'local_address' in peer_conf:
+ local_ip = peer_conf['local_address']
+ elif 'dhcp_interface' in peer_conf:
+ local_ip = Interface(peer_conf['dhcp_interface']).get_addr()
+
+ data['site_to_site']['peer'][peer]['local_address'] = local_ip
+
+ if 'vti' in peer_conf and 'bind' in peer_conf['vti']:
+ vti_interface = peer_conf['vti']['bind']
+ data['marks'][vti_interface] = get_mark(vti_interface)
+ else:
+ for tunnel, tunnel_conf in peer_conf['tunnel'].items():
+ local_prefix = dict_search('local.prefix', tunnel_conf)
+ remote_prefix = dict_search('remote.prefix', tunnel_conf)
+
+ if not local_prefix or not remote_prefix:
+ continue
+
+ passthrough = cidr_fit(local_prefix, remote_prefix)
+ data['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough
+
+ if 'logging' in ipsec and 'log_modes' in ipsec['logging']:
+ modes = ipsec['logging']['log_modes']
+ level = ipsec['logging']['log_level'] if 'log_level' in ipsec['logging'] else '1'
+ if isinstance(modes, str):
+ modes = [modes]
+ if 'any' in modes:
+ modes = any_log_modes
+ data['charondebug'] = f' {level}, '.join(modes) + ' ' + level
+
+ render("/etc/ipsec.conf", "ipsec/ipsec.conf.tmpl", data)
+ render("/etc/ipsec.secrets", "ipsec/ipsec.secrets.tmpl", data)
+ render("/etc/strongswan.d/interfaces_use.conf", "ipsec/interfaces_use.conf.tmpl", data)
+ render("/etc/swanctl/swanctl.conf", "ipsec/swanctl.conf.tmpl", data)
+
+def resync_l2tp(ipsec):
+ if ipsec and not ipsec['l2tp_exists']:
+ return
+
+ tmp = run('/usr/libexec/vyos/conf_mode/ipsec-settings.py')
+ if tmp > 0:
+ print('ERROR: failed to reapply L2TP IPSec settings!')
+
+def resync_nhrp(ipsec):
+ if ipsec and not ipsec['nhrp_exists']:
+ return
+
+ tmp = run('/usr/libexec/vyos/conf_mode/protocols_nhrp.py')
+ if tmp > 0:
+ print('ERROR: failed to reapply NHRP settings!')
def apply(ipsec):
if not ipsec:
- return None
+ call('sudo /usr/sbin/ipsec stop')
+ else:
+ should_start = ('profile' in ipsec or dict_search('site_to_site.peer', ipsec))
+
+ if not process_named_running('charon') and should_start:
+ args = f'--auto-update {ipsec["auto_update"]}' if 'auto_update' in ipsec else ''
+ call(f'sudo /usr/sbin/ipsec start {args}')
+ elif not should_start:
+ call('sudo /usr/sbin/ipsec stop')
+ elif ipsec['interface_change']:
+ call('sudo /usr/sbin/ipsec restart')
+ else:
+ call('sudo /usr/sbin/ipsec rereadall')
+ call('sudo /usr/sbin/ipsec reload')
+
+ if should_start:
+ sleep(2) # Give charon enough time to start
+ call('sudo /usr/sbin/swanctl -q')
+
+ resync_l2tp(ipsec)
+ resync_nhrp(ipsec)
- pprint(ipsec)
+def get_mark(vti_interface):
+ vti_num = int(vti_interface.lstrip('vti'))
+ return mark_base + vti_num
if __name__ == '__main__':
try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
+ ipsec = get_config()
+ verify(ipsec)
+ generate(ipsec)
+ apply(ipsec)
except ConfigError as e:
print(e)
exit(1)
diff --git a/src/conf_mode/vpn_rsa-keys.py b/src/conf_mode/vpn_rsa-keys.py
new file mode 100755
index 000000000..6cf7eba6e
--- /dev/null
+++ b/src/conf_mode/vpn_rsa-keys.py
@@ -0,0 +1,111 @@
+#!/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 base64
+import os
+import struct
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.util import call
+from vyos import ConfigError
+from vyos import airbag
+from Crypto.PublicKey.RSA import construct
+
+airbag.enable()
+
+LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/']
+LOCAL_OUTPUT = '/etc/ipsec.d/certs/localhost.pub'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['vpn', 'rsa-keys']
+ if not conf.exists(base):
+ return None
+
+ return conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
+
+def verify(conf):
+ if not conf:
+ return
+
+ if 'local_key' in conf and 'file' in conf['local_key']:
+ local_key = conf['local_key']['file']
+ if not local_key:
+ raise ConfigError(f'Invalid local-key')
+
+ if not get_local_key(local_key):
+ raise ConfigError(f'File not found for local-key: {local_key}')
+
+def get_local_key(local_key):
+ for path in LOCAL_KEY_PATHS:
+ full_path = os.path.join(path, local_key)
+ if os.path.exists(full_path):
+ return full_path
+ return False
+
+def generate(conf):
+ if not conf:
+ return
+
+ if 'local_key' in conf and 'file' in conf['local_key']:
+ local_key = conf['local_key']['file']
+ local_key_path = get_local_key(local_key)
+ call(f'sudo /usr/bin/openssl rsa -in {local_key_path} -pubout -out {LOCAL_OUTPUT}')
+
+ if 'rsa_key_name' in conf:
+ for key_name, key_conf in conf['rsa_key_name'].items():
+ if 'rsa_key' not in key_conf:
+ continue
+
+ remote_key = key_conf['rsa_key']
+
+ if remote_key[:2] == "0s": # Vyatta format
+ remote_key = migrate_from_vyatta_key(remote_key)
+ else:
+ remote_key = bytes('-----BEGIN PUBLIC KEY-----\n' + remote_key + '\n-----END PUBLIC KEY-----\n', 'utf-8')
+
+ with open(f'/etc/ipsec.d/certs/{key_name}.pub', 'wb') as f:
+ f.write(remote_key)
+
+def migrate_from_vyatta_key(data):
+ data = base64.b64decode(data[2:])
+ length = struct.unpack('B', data[:1])[0]
+ e = int.from_bytes(data[1:1+length], 'big')
+ n = int.from_bytes(data[1+length:], 'big')
+ pubkey = construct((n, e))
+ return pubkey.exportKey(format='PEM')
+
+def apply(conf):
+ if not conf:
+ return
+
+ call('sudo /usr/sbin/ipsec rereadall')
+ call('sudo /usr/sbin/ipsec reload')
+
+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/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook
new file mode 100644
index 000000000..36edf04f3
--- /dev/null
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+
+from vyos.util import call
+
+IPSEC_CONF="/etc/ipsec.conf"
+IPSEC_SECRETS="/etc/ipsec.secrets"
+
+def getlines(file):
+ with open(file, 'r') as f:
+ return f.readlines()
+
+def writelines(file, lines):
+ with open(file, 'w') as f:
+ f.writelines(lines)
+
+if __name__ == '__main__':
+ interface = os.getenv('interface')
+ new_ip = os.getenv('new_ip_address')
+ old_ip = os.getenv('old_ip_address')
+ reason = os.getenv('reason')
+
+ if (old_ip == new_ip and reason != 'BOUND') or reason in ['REBOOT', 'EXPIRE']:
+ sys.exit(0)
+
+ conf_lines = getlines(IPSEC_CONF)
+ secrets_lines = getlines(IPSEC_SECRETS)
+ found = False
+ to_match = f'# dhcp:{interface}'
+
+ for i, line in enumerate(conf_lines):
+ if line.find(to_match) > 0:
+ conf_lines[i] = line.replace(old_ip, new_ip)
+ found = True
+
+ for i, line in enumerate(secrets_lines):
+ if line.find(to_match) > 0:
+ secrets_lines[i] = line.replace(old_ip, new_ip)
+
+ if found:
+ writelines(IPSEC_CONF, conf_lines)
+ writelines(IPSEC_SECRETS, secrets_lines)
+ call('sudo /usr/sbin/ipsec rereadall')
+ call('sudo /usr/sbin/ipsec reload')
diff --git a/src/etc/ipsec.d/key-pair.template b/src/etc/ipsec.d/key-pair.template
new file mode 100644
index 000000000..56be97516
--- /dev/null
+++ b/src/etc/ipsec.d/key-pair.template
@@ -0,0 +1,67 @@
+[ req ]
+ default_bits = 2048
+ default_keyfile = privkey.pem
+ distinguished_name = req_distinguished_name
+ string_mask = utf8only
+ attributes = req_attributes
+ dirstring_type = nobmp
+# SHA-1 is deprecated, so use SHA-2 instead.
+ default_md = sha256
+# Extension to add when the -x509 option is used.
+ x509_extensions = v3_ca
+
+[ req_distinguished_name ]
+ countryName = Country Name (2 letter code)
+ countryName_min = 2
+ countryName_max = 2
+ ST = State Name
+ localityName = Locality Name (eg, city)
+ organizationName = Organization Name (eg, company)
+ organizationalUnitName = Organizational Unit Name (eg, department)
+ commonName = Common Name (eg, Device hostname)
+ commonName_max = 64
+ emailAddress = Email Address
+ emailAddress_max = 40
+[ req_attributes ]
+ challengePassword = A challenge password (optional)
+ challengePassword_min = 4
+ challengePassword_max = 20
+[ v3_ca ]
+ subjectKeyIdentifier=hash
+ authorityKeyIdentifier=keyid:always,issuer:always
+ basicConstraints = critical, CA:true
+ keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+[ v3_intermediate_ca ]
+# Extensions for a typical intermediate CA (`man x509v3_config`).
+ subjectKeyIdentifier = hash
+ authorityKeyIdentifier = keyid:always,issuer
+ basicConstraints = critical, CA:true, pathlen:0
+ keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+[ usr_cert ]
+# Extensions for client certificates (`man x509v3_config`).
+ basicConstraints = CA:FALSE
+ nsCertType = client, email
+ nsComment = "OpenSSL Generated Client Certificate"
+ subjectKeyIdentifier = hash
+ authorityKeyIdentifier = keyid,issuer
+ keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
+ extendedKeyUsage = clientAuth, emailProtection
+[ server_cert ]
+# Extensions for server certificates (`man x509v3_config`).
+ basicConstraints = CA:FALSE
+ nsCertType = server
+ nsComment = "OpenSSL Generated Server Certificate"
+ subjectKeyIdentifier = hash
+ authorityKeyIdentifier = keyid,issuer:always
+ keyUsage = critical, digitalSignature, keyEncipherment
+ extendedKeyUsage = serverAuth
+[ crl_ext ]
+# Extension for CRLs (`man x509v3_config`).
+ authorityKeyIdentifier=keyid:always
+[ ocsp ]
+# Extension for OCSP signing certificates (`man ocsp`).
+ basicConstraints = CA:FALSE
+ subjectKeyIdentifier = hash
+ authorityKeyIdentifier = keyid,issuer
+ keyUsage = critical, digitalSignature
+ extendedKeyUsage = critical, OCSPSigning
diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down
new file mode 100755
index 000000000..0e1cd7753
--- /dev/null
+++ b/src/etc/ipsec.d/vti-up-down
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+## Script called up strongswan to bring the vti interface up/down based on the state of the IPSec tunnel.
+## Called as vti_up_down vti_intf_name
+
+import os
+import sys
+
+from vyos.util import call, get_interface_config, get_interface_address
+
+def get_dhcp_address(interface):
+ addr = get_interface_address(interface)
+ if not addr:
+ return None
+ if len(addr['addr_info']) == 0:
+ return None
+ return addr['addr_info'][0]['local']
+
+if __name__ == '__main__':
+ verb = os.getenv('PLUTO_VERB')
+ connection = os.getenv('PLUTO_CONNECTION')
+ interface = sys.argv[1]
+ dhcp_interface = sys.argv[2]
+
+ print(f'vti-up-down: start: {verb} {connection} {interface}')
+
+ if verb in ['up-client', 'up-host']:
+ call('sudo ip route delete default table 220')
+
+ vti_link = get_interface_config(interface)
+
+ if not vti_link:
+ print('vti-up-down: interface not found')
+ sys.exit(0)
+
+ vti_link_up = (vti_link['operstate'] == 'UP' if 'operstate' in vti_link else False)
+
+ if verb in ['up-client', 'up-host']:
+ if not vti_link_up:
+ if dhcp_interface != 'no':
+ local_ip = get_dhcp_address(dhcp_interface)
+ call(f'sudo ip tunnel change {interface} local {local_ip}')
+ call(f'sudo ip link set {interface} up')
+ elif verb in ['down-client', 'down-host']:
+ if vti_link_up:
+ call(f'sudo ip link set {interface} down')
+
+ print('vti-up-down: finish') \ No newline at end of file
diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py
new file mode 100755
index 000000000..f7487ee5f
--- /dev/null
+++ b/src/etc/opennhrp/opennhrp-script.py
@@ -0,0 +1,136 @@
+#!/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 pprint import pprint
+import os
+import re
+import sys
+import vici
+
+from vyos.util import cmd
+from vyos.util import process_named_running
+
+NHRP_CONFIG="/run/opennhrp/opennhrp.conf"
+
+def parse_type_ipsec(interface):
+ with open(NHRP_CONFIG, 'r') as f:
+ lines = f.readlines()
+ match = rf'^interface {interface} #(hub|spoke)(?:\s([\w-]+))?$'
+ for line in lines:
+ m = re.match(match, line)
+ if m:
+ return m[1], m[2]
+ return None, None
+
+def vici_initiate(conn, child_sa, src_addr, dest_addr):
+ try:
+ session = vici.Session()
+ logs = session.initiate({
+ 'ike': conn,
+ 'child': child_sa,
+ 'timeout': '-1',
+ 'my-host': src_addr,
+ 'other-host': dest_addr
+ })
+ for log in logs:
+ message = log['msg'].decode('ascii')
+ print('INIT LOG:', message)
+ return True
+ except:
+ return None
+
+def vici_terminate(conn, child_sa, src_addr, dest_addr):
+ try:
+ session = vici.Session()
+ logs = session.terminate({
+ 'ike': conn,
+ 'child': child_sa,
+ 'timeout': '-1',
+ 'my-host': src_addr,
+ 'other-host': dest_addr
+ })
+ for log in logs:
+ message = log['msg'].decode('ascii')
+ print('TERM LOG:', message)
+ return True
+ except:
+ return None
+
+def iface_up(interface):
+ cmd(f'sudo ip route flush proto 42 dev {interface}')
+ cmd(f'sudo ip neigh flush dev {interface}')
+
+def peer_up(dmvpn_type, conn):
+ src_addr = os.getenv('NHRP_SRCADDR')
+ src_nbma = os.getenv('NHRP_SRCNBMA')
+ dest_addr = os.getenv('NHRP_DESTADDR')
+ dest_nbma = os.getenv('NHRP_DESTNBMA')
+ dest_mtu = os.getenv('NHRP_DESTMTU')
+
+ if dest_mtu:
+ args = cmd(f'sudo ip route get {dest_nbma} from {src_nbma}')
+ cmd(f'sudo ip route add {args} proto 42 mtu {dest_mtu}')
+
+ if conn and dmvpn_type == 'spoke' and process_named_running('charon'):
+ vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma)
+ vici_initiate(conn, 'dmvpn', src_nbma, dest_nbma)
+
+def peer_down(dmvpn_type, conn):
+ src_nbma = os.getenv('NHRP_SRCNBMA')
+ dest_nbma = os.getenv('NHRP_DESTNBMA')
+
+ if conn and dmvpn_type == 'spoke' and process_named_running('charon'):
+ vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma)
+
+ cmd(f'sudo ip route del {dest_nbma} src {src_nbma} proto 42')
+
+def route_up(interface):
+ dest_addr = os.getenv('NHRP_DESTADDR')
+ dest_prefix = os.getenv('NHRP_DESTPREFIX')
+ next_hop = os.getenv('NHRP_NEXTHOP')
+
+ cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 via {next_hop} dev {interface}')
+ cmd('sudo ip route flush cache')
+
+def route_down(interface):
+ dest_addr = os.getenv('NHRP_DESTADDR')
+ dest_prefix = os.getenv('NHRP_DESTPREFIX')
+
+ cmd(f'sudo ip route del {dest_addr}/{dest_prefix} proto 42')
+ cmd('sudo ip route flush cache')
+
+if __name__ == '__main__':
+ action = sys.argv[1]
+ interface = os.getenv('NHRP_INTERFACE')
+ dmvpn_type, profile_name = parse_type_ipsec(interface)
+
+ dmvpn_conn = None
+
+ if profile_name:
+ dmvpn_conn = f'dmvpn-{profile_name}-{interface}'
+
+ if action == 'interface-up':
+ iface_up(interface)
+ elif action == 'peer-register':
+ pass
+ elif action == 'peer-up':
+ peer_up(dmvpn_type, dmvpn_conn)
+ elif action == 'peer-down':
+ peer_down(dmvpn_type, dmvpn_conn)
+ elif action == 'route-up':
+ route_up(interface)
+ elif action == 'route-down':
+ route_down(interface)
diff --git a/src/etc/systemd/system/conntrackd.service.d/override.conf b/src/etc/systemd/system/conntrackd.service.d/override.conf
new file mode 100644
index 000000000..eb611e0d9
--- /dev/null
+++ b/src/etc/systemd/system/conntrackd.service.d/override.conf
@@ -0,0 +1,8 @@
+[Unit]
+After=
+After=vyos-router.service
+ConditionPathExists=/run/conntrackd/conntrackd.conf
+
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/conntrackd -C /run/conntrackd/conntrackd.conf
diff --git a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
index dc751c45c..ec33906ba 100755
--- a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
+++ b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -15,48 +15,47 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
-import syslog as sl
+import syslog
from vyos.config import Config
from vyos import ConfigError
from vyos.util import run
-
def get_config():
c = Config()
interfaces = dict()
for intf in c.list_effective_nodes('interfaces ethernet'):
# skip interfaces that are disabled or is configured for dhcp
- check_disable = "interfaces ethernet {} disable".format(intf)
- check_dhcp = "interfaces ethernet {} address dhcp".format(intf)
+ check_disable = f'interfaces ethernet {intf} disable'
+ check_dhcp = f'interfaces ethernet {intf} address dhcp'
if c.exists_effective(check_disable):
continue
# get addresses configured on the interface
intf_addresses = c.return_effective_values(
- "interfaces ethernet {} address".format(intf)
- )
+ f'interfaces ethernet {intf} address')
interfaces[intf] = [addr.strip("'") for addr in intf_addresses]
return interfaces
-
def apply(config):
+ syslog.openlog(ident='ether-resume', logoption=syslog.LOG_PID,
+ facility=syslog.LOG_INFO)
+
for intf, addresses in config.items():
# bring the interface up
- cmd = ["ip", "link", "set", "dev", intf, "up"]
- sl.syslog(sl.LOG_NOTICE, " ".join(cmd))
+ cmd = f'ip link set dev {intf} up'
+ syslog.syslog(cmd)
run(cmd)
# add configured addresses to interface
for addr in addresses:
- if addr == "dhcp":
- cmd = ["dhclient", intf]
+ if addr == 'dhcp':
+ cmd = ['dhclient', intf]
else:
- cmd = ["ip", "address", "add", addr, "dev", intf]
- sl.syslog(sl.LOG_NOTICE, " ".join(cmd))
+ cmd = f'ip address add {addr} dev {intf}'
+ syslog.syslog(cmd)
run(cmd)
-
if __name__ == '__main__':
try:
config = get_config()
diff --git a/src/helpers/vyos-vrrp-conntracksync.sh b/src/helpers/vyos-vrrp-conntracksync.sh
new file mode 100755
index 000000000..4501aa63e
--- /dev/null
+++ b/src/helpers/vyos-vrrp-conntracksync.sh
@@ -0,0 +1,154 @@
+#!/bin/sh
+#
+# (C) 2008 by Pablo Neira Ayuso <pablo@netfilter.org>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+#
+# Description:
+#
+# This is the script for primary-backup setups for keepalived
+# (http://www.keepalived.org). You may adapt it to make it work with other
+# high-availability managers.
+#
+# 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
+FACILITY=daemon
+LEVEL=notice
+TAG=conntrack-tools
+LOGCMD="logger -t $TAG -p $FACILITY.$LEVEL"
+VRRP_GRP="VRRP sync-group [$2]"
+FAILOVER_STATE="/var/run/vyatta-conntrackd-failover-state"
+
+$LOGCMD "vyatta-vrrp-conntracksync invoked at `date`"
+
+
+if [ ! -e $FAILOVER_STATE ]; then
+ mkdir -p /var/run
+ touch $FAILOVER_STATE
+fi
+
+case "$1" in
+ master)
+ echo MASTER at `date` > $FAILOVER_STATE
+ $LOGCMD "`uname -n` transitioning to MASTER state for $VRRP_GRP"
+ #
+ # commit the external cache into the kernel table
+ #
+ $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -c
+ if [ $? -eq 1 ]
+ then
+ $LOGCMD "ERROR: failed to invoke conntrackd -c"
+ fi
+
+ #
+ # commit the expect entries to the kernel
+ #
+ $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -c exp
+ if [ $? -eq 1 ]
+ then
+ $LOGCMD "ERROR: failed to invoke conntrackd -ce exp"
+ fi
+
+ #
+ # flush the internal and the external caches
+ #
+ $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -f
+ if [ $? -eq 1 ]
+ then
+ $LOGCMD "ERROR: failed to invoke conntrackd -f"
+ fi
+
+ #
+ # resynchronize my internal cache to the kernel table
+ #
+ $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -R
+ if [ $? -eq 1 ]
+ then
+ $LOGCMD "ERROR: failed to invoke conntrackd -R"
+ fi
+
+ #
+ # send a bulk update to backups
+ #
+ $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -B
+ if [ $? -eq 1 ]
+ then
+ $LOGCMD "ERROR: failed to invoke conntrackd -B"
+ fi
+ ;;
+ backup)
+ echo BACKUP at `date` > $FAILOVER_STATE
+ $LOGCMD "`uname -n` transitioning to BACKUP state for $VRRP_GRP"
+ #
+ # is conntrackd running? request some statistics to check it
+ #
+ $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -s
+ if [ $? -eq 1 ]
+ then
+ #
+ # something's wrong, do we have a lock file?
+ #
+ if [ -f $CONNTRACKD_LOCK ]
+ then
+ $LOGCMD "WARNING: conntrackd was not cleanly stopped."
+ $LOGCMD "If you suspect that it has crashed:"
+ $LOGCMD "1) Enable coredumps"
+ $LOGCMD "2) Try to reproduce the problem"
+ $LOGCMD "3) Post the coredump to netfilter-devel@vger.kernel.org"
+ rm -f $CONNTRACKD_LOCK
+ fi
+ $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -d
+ if [ $? -eq 1 ]
+ then
+ $LOGCMD "ERROR: cannot launch conntrackd"
+ exit 1
+ fi
+ fi
+ #
+ # shorten kernel conntrack timers to remove the zombie entries.
+ #
+ $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -t
+ if [ $? -eq 1 ]
+ then
+ $LOGCMD "ERROR: failed to invoke conntrackd -t"
+ fi
+
+ #
+ # request resynchronization with master firewall replica (if any)
+ # Note: this does nothing in the alarm approach.
+ #
+ $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -n
+ if [ $? -eq 1 ]
+ then
+ $LOGCMD "ERROR: failed to invoke conntrackd -n"
+ fi
+ ;;
+ fault)
+ echo FAULT at `date` > $FAILOVER_STATE
+ $LOGCMD "`uname -n` transitioning to FAULT state for $VRRP_GRP"
+ #
+ # shorten kernel conntrack timers to remove the zombie entries.
+ #
+ $CONNTRACKD_BIN -C $CONNTRACKD_CONFIG -t
+ if [ $? -eq 1 ]
+ then
+ $LOGCMD "ERROR: failed to invoke conntrackd -t"
+ fi
+ ;;
+ *)
+ echo UNKNOWN at `date` > $FAILOVER_STATE
+ $LOGCMD "ERROR: `uname -n` unknown state transition for $VRRP_GRP"
+ echo "Usage: vyatta-vrrp-conntracksync.sh {master|backup|fault}"
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/src/migration-scripts/conntrack-sync/1-to-2 b/src/migration-scripts/conntrack-sync/1-to-2
new file mode 100755
index 000000000..ebbd8c35a
--- /dev/null
+++ b/src/migration-scripts/conntrack-sync/1-to-2
@@ -0,0 +1,66 @@
+#!/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/>.
+
+# VyOS 1.2 crux allowed configuring a lower or upper case loglevel. This
+# is no longer supported as the input data is validated and will lead to
+# an error. If user specifies an upper case logleve, make it lowercase
+
+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 = ['service', 'conntrack-sync']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+ base_accept_proto = base + ['accept-protocol']
+ if config.exists(base_accept_proto):
+ tmp = config.return_value(base_accept_proto)
+ config.delete(base_accept_proto)
+ for protocol in tmp.split(','):
+ config.set(base_accept_proto, value=protocol, replace=False)
+
+ base_ignore_addr = base + ['ignore-address', 'ipv4']
+ if config.exists(base_ignore_addr):
+ tmp = config.return_values(base_ignore_addr)
+ config.delete(base_ignore_addr)
+ for address in tmp:
+ config.set(base + ['ignore-address'], value=address, replace=False)
+
+ # we no longer support cluster mode
+ base_cluster = base + ['failover-mechanism', 'cluster']
+ if config.exists(base_cluster):
+ config.delete(base_cluster)
+
+ 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/20-to-21 b/src/migration-scripts/interfaces/20-to-21
new file mode 100755
index 000000000..d1ec2ad3e
--- /dev/null
+++ b/src/migration-scripts/interfaces/20-to-21
@@ -0,0 +1,60 @@
+#!/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/>.
+
+# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported
+# having a VTI interface in the CLI but no IPSec configuration - drop VTI
+# configuration if this is the case for VyOS 1.4
+
+import sys
+from vyos.configtree import ConfigTree
+
+if __name__ == '__main__':
+ 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)
+ base = ['interfaces', 'vti']
+ if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+ ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer']
+ for interface in config.list_nodes(base):
+ found = False
+ if config.exists(ipsec_base):
+ for peer in config.list_nodes(ipsec_base):
+ if config.exists(ipsec_base + [peer, 'vti', 'bind']):
+ tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind'])
+ if tmp == interface:
+ # Interface was found and we no longer need to search
+ # for it in our IPSec peers
+ found = True
+ break
+ if not found:
+ config.delete(base + [interface])
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/interfaces/5-to-6 b/src/migration-scripts/interfaces/5-to-6
index 1291751d8..ae79c1d1b 100755
--- a/src/migration-scripts/interfaces/5-to-6
+++ b/src/migration-scripts/interfaces/5-to-6
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -55,6 +55,16 @@ def copy_rtradv(c, old_base, interface):
min_max = interval.split('-')[0]
c.set(new_base + ['interval', min_max], value=tmp)
+ # cleanup boolean nodes in individual route
+ route_base = new_base + ['route']
+ if c.exists(route_base):
+ for route in config.list_nodes(route_base):
+ if c.exists(route_base + [route, 'remove-route']):
+ tmp = c.return_value(route_base + [route, 'remove-route'])
+ c.delete(route_base + [route, 'remove-route'])
+ if tmp == 'false':
+ c.set(route_base + [route, 'no-remove-route'])
+
# cleanup boolean nodes in individual prefix
prefix_base = new_base + ['prefix']
if c.exists(prefix_base):
diff --git a/src/migration-scripts/ipsec/4-to-5 b/src/migration-scripts/ipsec/4-to-5
index b64aa8462..4e959a7bf 100755
--- a/src/migration-scripts/ipsec/4-to-5
+++ b/src/migration-scripts/ipsec/4-to-5
@@ -1,4 +1,18 @@
#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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/>.
# log-modes have changed, keyword all to any
diff --git a/src/migration-scripts/ipsec/5-to-6 b/src/migration-scripts/ipsec/5-to-6
new file mode 100755
index 000000000..86be55d13
--- /dev/null
+++ b/src/migration-scripts/ipsec/5-to-6
@@ -0,0 +1,68 @@
+#!/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/>.
+
+# Remove deprecated strongSwan options from VyOS CLI
+# - vpn ipsec nat-traversal enable
+# - vpn ipsec nat-networks allowed-network
+
+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']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+# Delete CLI nodes whose config options got removed by strongSwan
+for cli_node in ['nat-traversal', 'nat-networks']:
+ if config.exists(base + [cli_node]):
+ config.delete(base + [cli_node])
+
+# Remove options only valid in Openswan
+if config.exists(base + ['site-to-site', 'peer']):
+ for peer in config.list_nodes(base + ['site-to-site', 'peer']):
+ if not config.exists(base + ['site-to-site', 'peer', peer, 'tunnel']):
+ continue
+ for tunnel in config.list_nodes(base + ['site-to-site', 'peer', peer, 'tunnel']):
+ # allow-public-networks - Sets a value in ipsec.conf that was only ever valid in Openswan on kernel 2.6
+ nat_networks = base + ['site-to-site', 'peer', peer, 'tunnel', tunnel, 'allow-nat-networks']
+ if config.exists(nat_networks):
+ config.delete(nat_networks)
+
+ # allow-nat-networks - Also sets a value only valid in Openswan
+ public_networks = base + ['site-to-site', 'peer', peer, 'tunnel', tunnel, 'allow-public-networks']
+ if config.exists(public_networks):
+ config.delete(public_networks)
+
+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/ntp/0-to-1 b/src/migration-scripts/ntp/0-to-1
index 9c66f3109..294964580 100755
--- a/src/migration-scripts/ntp/0-to-1
+++ b/src/migration-scripts/ntp/0-to-1
@@ -17,7 +17,7 @@ with open(file_name, 'r') as f:
config = ConfigTree(config_file)
-if not config.exists(['system', 'ntp']):
+if not config.exists(['system', 'ntp', 'server']):
# Nothing to do
sys.exit(0)
else:
diff --git a/src/migration-scripts/system/20-to-21 b/src/migration-scripts/system/20-to-21
new file mode 100755
index 000000000..ad41be646
--- /dev/null
+++ b/src/migration-scripts/system/20-to-21
@@ -0,0 +1,57 @@
+#!/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, 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', 'sysctl']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+for all_custom in ['all', 'custom']:
+ if config.exists(base + [all_custom]):
+ for key in config.list_nodes(base + [all_custom]):
+ tmp = config.return_value(base + [all_custom, key, 'value'])
+ config.set(base + ['parameter', key, 'value'], value=tmp)
+ config.set_tag(base + ['parameter'])
+ config.delete(base + [all_custom])
+
+for ipv4_param in ['net.ipv4.igmp_max_memberships', 'net.ipv4.ipfrag_time']:
+ if config.exists(base + [ipv4_param]):
+ tmp = config.return_value(base + [ipv4_param])
+ config.set(base + ['parameter', ipv4_param, 'value'], value=tmp)
+ config.set_tag(base + ['parameter'])
+ config.delete(base + [ipv4_param])
+
+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/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py
new file mode 100755
index 000000000..66ecf8439
--- /dev/null
+++ b/src/op_mode/conntrack_sync.py
@@ -0,0 +1,136 @@
+#!/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 syslog
+import xmltodict
+
+from argparse import ArgumentParser
+from vyos.configquery import CliShellApiConfigQuery
+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'
+
+parser = ArgumentParser(description='Conntrack Sync')
+group = parser.add_mutually_exclusive_group()
+group.add_argument('--restart', help='Restart connection tracking synchronization service', action='store_true')
+group.add_argument('--reset-cache-internal', help='Reset internal cache', action='store_true')
+group.add_argument('--reset-cache-external', help='Reset external cache', action='store_true')
+group.add_argument('--show-internal', help='Show internal (main) tracking cache', action='store_true')
+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')
+
+def is_configured():
+ """ Check if conntrack-sync service is configured """
+ config = CliShellApiConfigQuery()
+ if not config.exists(['service', 'conntrack-sync']):
+ print('Service conntrackd-sync not configured!')
+ exit(1)
+
+def send_bulk_update():
+ """ send bulk update of internal-cache to other systems """
+ tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -B')
+ if tmp > 0:
+ print('ERROR: failed to send bulk update to other conntrack-sync systems')
+
+def request_sync():
+ """ request resynchronization with other systems """
+ tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -n')
+ if tmp > 0:
+ print('ERROR: failed to request resynchronization of external cache')
+
+def flush_cache(direction):
+ """ flush conntrackd cache (internal or external) """
+ if direction not in ['internal', 'external']:
+ raise ValueError()
+ tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -f {direction}')
+ if tmp > 0:
+ print('ERROR: failed to clear {direction} cache')
+
+def xml_to_stdout(xml):
+ out = []
+ for line in xml.splitlines():
+ if line == '\n':
+ continue
+ parsed = xmltodict.parse(line)
+ out.append(parsed)
+
+ print(render_to_string('conntrackd/conntrackd.op-mode.tmpl', {'data' : out}))
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+ syslog.openlog(ident='conntrack-tools', logoption=syslog.LOG_PID,
+ facility=syslog.LOG_INFO)
+
+ if args.restart:
+ is_configured()
+
+ syslog.syslog('Restarting conntrack sync service...')
+ cmd('systemctl restart conntrackd.service')
+ # request resynchronization with other systems
+ request_sync()
+ # send bulk update of internal-cache to other systems
+ send_bulk_update()
+
+ elif args.reset_cache_external:
+ is_configured()
+ syslog.syslog('Resetting external cache of conntrack sync service...')
+
+ # flush the external cache
+ flush_cache('external')
+ # request resynchronization with other systems
+ request_sync()
+
+ elif args.reset_cache_internal:
+ is_configured()
+ syslog.syslog('Resetting internal cache of conntrack sync service...')
+ # flush the internal cache
+ flush_cache('internal')
+
+ # request resynchronization of internal cache with kernel conntrack table
+ tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -R')
+ if tmp > 0:
+ print('ERROR: failed to resynchronize internal cache with kernel conntrack table')
+
+ # send bulk update of internal-cache to other systems
+ send_bulk_update()
+
+ elif args.show_external or args.show_internal or args.show_external_expect or args.show_internal_expect:
+ is_configured()
+ opt = ''
+ if args.show_external:
+ opt = '-e ct'
+ elif args.show_external_expect:
+ opt = '-e expect'
+ elif args.show_internal:
+ opt = '-i ct'
+ elif args.show_internal_expect:
+ opt = '-i expect'
+
+ if args.show_external or args.show_internal:
+ print('Main Table Entries:')
+ else:
+ print('Expect Table Entries:')
+ out = cmd(f'sudo {conntrackd_bin} -C {conntrackd_config} {opt} -x')
+ xml_to_stdout(out)
+
+ else:
+ parser.print_help()
+ exit(1)
diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py
index 962943896..263a3b6a5 100755
--- a/src/op_mode/dynamic_dns.py
+++ b/src/op_mode/dynamic_dns.py
@@ -36,6 +36,10 @@ update-status: {{ entry.status }}
"""
def show_status():
+ # A ddclient status file must not always exist
+ if not os.path.exists(cache_file):
+ sys.exit(0)
+
data = {
'hosts': []
}
@@ -61,11 +65,10 @@ def show_status():
if ip:
outp['ip'] = ip.split(',')[0]
- if 'atime=' in line:
- atime = line.split('atime=')[1]
- if atime:
- tmp = atime.split(',')[0]
- outp['time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(int(tmp, base=10)))
+ if 'mtime=' in line:
+ mtime = line.split('mtime=')[1]
+ if mtime:
+ outp['time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(int(mtime.split(',')[0], base=10)))
if 'status=' in line:
status = line.split('status=')[1]
diff --git a/src/op_mode/monitor_bandwidth_test.sh b/src/op_mode/monitor_bandwidth_test.sh
index 6da0291c5..900223bca 100755
--- a/src/op_mode/monitor_bandwidth_test.sh
+++ b/src/op_mode/monitor_bandwidth_test.sh
@@ -26,5 +26,5 @@ elif [[ $(dig $1 AAAA +short | grep -v '\.$' | wc -l) -gt 0 ]]; then
OPT="-V"
fi
-/usr/bin/iperf $OPT -c $1
+/usr/bin/iperf $OPT -c $1 $2
diff --git a/src/op_mode/openconnect-control.py b/src/op_mode/openconnect-control.py
index ef9fe618c..c3cd25186 100755
--- a/src/op_mode/openconnect-control.py
+++ b/src/op_mode/openconnect-control.py
@@ -58,7 +58,7 @@ def main():
is_ocserv_configured()
if args.action == "restart":
- run("systemctl restart ocserv")
+ run("sudo systemctl restart ocserv.service")
sys.exit(0)
elif args.action == "show_sessions":
show_sessions()
diff --git a/src/op_mode/show_nat_rules.py b/src/op_mode/show_nat_rules.py
index 68cff61c8..4b7e40d1f 100755
--- a/src/op_mode/show_nat_rules.py
+++ b/src/op_mode/show_nat_rules.py
@@ -34,8 +34,8 @@ if args.source or args.destination:
tmp = json.loads(tmp)
format_nat66_rule = '{0: <10} {1: <50} {2: <50} {3: <10}'
- print(format_nat66_rule.format("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface"))
- print(format_nat66_rule.format("----", "------" if args.source else "-----------", "-----------", "------------------" if args.source else "-----------------"))
+ 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 "-----------------"))
data_json = jmespath.search('nftables[?rule].rule[?chain]', tmp)
for idx in range(0, len(data_json)):
@@ -86,7 +86,7 @@ if args.source or args.destination:
else:
tran_addr = dict_search('snat.addr' if args.source else 'dnat.addr', data['expr'][3])
- print(format_nat66_rule.format(rule, srcdest, tran_addr, interface))
+ print(format_nat_rule.format(rule, srcdest, tran_addr, interface))
exit(0)
else:
diff --git a/src/op_mode/vpn_ike_sa.py b/src/op_mode/vpn_ike_sa.py
new file mode 100755
index 000000000..28da9f8dc
--- /dev/null
+++ b/src/op_mode/vpn_ike_sa.py
@@ -0,0 +1,68 @@
+#!/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 vici
+
+ike_sa_peer_prefix = """\
+Peer ID / IP Local ID / IP
+------------ -------------"""
+
+ike_sa_tunnel_prefix = """
+
+ State IKEVer Encrypt Hash D-H Group NAT-T A-Time L-Time
+ ----- ------ ------- ---- --------- ----- ------ ------"""
+
+def s(byte_string):
+ return str(byte_string, 'utf-8')
+
+def ike_sa(peer, nat):
+ session = vici.Session()
+ sas = session.list_sas()
+ peers = []
+ for conn in sas:
+ for name, sa in conn.items():
+ if peer and not name.startswith('peer-' + peer):
+ continue
+ if name.startswith('peer-') and name in peers:
+ continue
+ if nat and 'nat-local' not in sa:
+ continue
+ peers.append(name)
+ remote_str = f'{s(sa["remote-host"])} {s(sa["remote-id"])}' if s(sa['remote-id']) != '%any' else s(sa["remote-host"])
+ local_str = f'{s(sa["local-host"])} {s(sa["local-id"])}' if s(sa['local-id']) != '%any' else s(sa["local-host"])
+ print(ike_sa_peer_prefix)
+ print('%-39s %-39s' % (remote_str, local_str))
+ state = 'up' if 'state' in sa and s(sa['state']) == 'ESTABLISHED' else 'down'
+ version = 'IKEv' + s(sa['version'])
+ encryption = f'{s(sa["encr-alg"])}_{s(sa["encr-keysize"])}' if 'encr-alg' in sa else 'n/a'
+ integrity = s(sa['integ-alg']) if 'integ-alg' in sa else 'n/a'
+ dh_group = s(sa['dh-group']) if 'dh-group' in sa else 'n/a'
+ natt = 'yes' if 'nat-local' in sa and s(sa['nat-local']) == 'yes' else 'no'
+ atime = s(sa['established']) if 'established' in sa else '0'
+ ltime = s(sa['rekey-time']) if 'rekey_time' in sa else '0'
+ print(ike_sa_tunnel_prefix)
+ print(' %-6s %-6s %-12s %-13s %-14s %-6s %-7s %-7s\n' % (state, version, encryption, integrity, dh_group, natt, atime, ltime))
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--peer', help='Peer name', required=False)
+ parser.add_argument('--nat', help='NAT Traversal', required=False)
+
+ args = parser.parse_args()
+
+ ike_sa(args.peer, args.nat) \ No newline at end of file
diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py
new file mode 100755
index 000000000..434186abb
--- /dev/null
+++ b/src/op_mode/vpn_ipsec.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import base64
+import os
+import re
+import struct
+import sys
+import argparse
+from subprocess import TimeoutExpired
+
+from vyos.util import ask_yes_no, call, cmd, process_named_running
+from Crypto.PublicKey.RSA import importKey
+
+RSA_LOCAL_KEY_PATH = '/config/ipsec.d/rsa-keys/localhost.key'
+RSA_LOCAL_PUB_PATH = '/etc/ipsec.d/certs/localhost.pub'
+RSA_KEY_PATHS = ['/config/auth', '/config/ipsec.d/rsa-keys']
+
+X509_CONFIG_PATH = '/etc/ipsec.d/key-pair.template'
+X509_PATH = '/config/auth/'
+
+IPSEC_CONF = '/etc/ipsec.conf'
+SWANCTL_CONF = '/etc/swanctl.conf'
+
+def migrate_to_vyatta_key(path):
+ with open(path, 'r') as f:
+ key = importKey(f.read())
+ e = key.e.to_bytes((key.e.bit_length() + 7) // 8, 'big')
+ n = key.n.to_bytes((key.n.bit_length() + 7) // 8, 'big')
+ return '0s' + str(base64.b64encode(struct.pack('B', len(e)) + e + n), 'ascii')
+ return None
+
+def find_rsa_keys():
+ keys = []
+ for path in RSA_KEY_PATHS:
+ if not os.path.exists(path):
+ continue
+ for filename in os.listdir(path):
+ full_path = os.path.join(path, filename)
+ if os.path.isfile(full_path) and full_path.endswith(".key"):
+ keys.append(full_path)
+ return keys
+
+def show_rsa_keys():
+ for key_path in find_rsa_keys():
+ print('Private key: ' + os.path.basename(key_path))
+ print('Public key: ' + migrate_to_vyatta_key(key_path) + '\n')
+
+def generate_rsa_key(bits = 2192):
+ if (bits < 16 or bits > 4096) or bits % 16 != 0:
+ print('Invalid bit length')
+ return
+
+ if os.path.exists(RSA_LOCAL_KEY_PATH):
+ if not ask_yes_no("A local RSA key file already exists and will be overwritten. Continue?"):
+ return
+
+ print(f'Generating rsa-key to {RSA_LOCAL_KEY_PATH}')
+
+ directory = os.path.dirname(RSA_LOCAL_KEY_PATH)
+ call(f'sudo mkdir -p {directory}')
+ result = call(f'sudo /usr/bin/openssl genrsa -out {RSA_LOCAL_KEY_PATH} {bits}')
+
+ if result != 0:
+ print(f'Could not generate RSA key: {result}')
+ return
+
+ call(f'sudo /usr/bin/openssl rsa -inform PEM -in {RSA_LOCAL_KEY_PATH} -pubout -out {RSA_LOCAL_PUB_PATH}')
+
+ print('Your new local RSA key has been generated')
+ print('The public portion of the key is:\n')
+ print(migrate_to_vyatta_key(RSA_LOCAL_KEY_PATH))
+
+def generate_x509_pair(name):
+ if os.path.exists(X509_PATH + name):
+ if not ask_yes_no("A certificate request with this name already exists and will be overwritten. Continue?"):
+ return
+
+ result = os.system(f'openssl req -new -nodes -keyout {X509_PATH}{name}.key -out {X509_PATH}{name}.csr -config {X509_CONFIG_PATH}')
+
+ if result != 0:
+ print(f'Could not generate x509 key-pair: {result}')
+ return
+
+ print('Private key and certificate request has been generated')
+ print(f'CSR: {X509_PATH}{name}.csr')
+ print(f'Private key: {X509_PATH}{name}.key')
+
+def get_peer_connections(peer, tunnel, return_all = False):
+ search = rf'^conn (peer-{peer}-(tunnel-[\d]+|vti))$'
+ matches = []
+ with open(IPSEC_CONF, 'r') as f:
+ for line in f.readlines():
+ result = re.match(search, line)
+ if result:
+ suffix = f'tunnel-{tunnel}' if tunnel.isnumeric() else tunnel
+ if return_all or (result[2] == suffix):
+ matches.append(result[1])
+ return matches
+
+def reset_peer(peer, tunnel):
+ if not peer:
+ print('Invalid peer, aborting')
+ return
+
+ conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all'))
+
+ if not conns:
+ print('Tunnel(s) not found, aborting')
+ return
+
+ result = True
+ for conn in conns:
+ try:
+ call(f'sudo /usr/sbin/ipsec down {conn}', timeout = 10)
+ call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10)
+ except TimeoutExpired as e:
+ print(f'Timed out while resetting {conn}')
+ result = False
+
+
+ print('Peer reset result: ' + ('success' if result else 'failed'))
+
+def get_profile_connection(profile, tunnel = None):
+ search = rf'(dmvpn-{profile}-[\w]+)' if tunnel == 'all' else rf'(dmvpn-{profile}-{tunnel})'
+ with open(SWANCTL_CONF, 'r') as f:
+ for line in f.readlines():
+ result = re.search(search, line)
+ if result:
+ return result[1]
+ return None
+
+def reset_profile(profile, tunnel):
+ if not profile:
+ print('Invalid profile, aborting')
+ return
+
+ if not tunnel:
+ print('Invalid tunnel, aborting')
+ return
+
+ conn = get_profile_connection(profile)
+
+ if not conn:
+ print('Profile not found, aborting')
+ return
+
+ call(f'sudo /usr/sbin/ipsec down {conn}')
+ result = call(f'sudo /usr/sbin/ipsec up {conn}')
+
+ print('Profile reset result: ' + ('success' if result == 0 else 'failed'))
+
+def debug_peer(peer, tunnel):
+ if not peer or peer == "all":
+ call('sudo /usr/sbin/ipsec statusall')
+ return
+
+ if not tunnel or tunnel == 'all':
+ tunnel = ''
+
+ conn = get_peer_connection(peer, tunnel)
+
+ if not conn:
+ print('Peer not found, aborting')
+ return
+
+ call(f'sudo /usr/sbin/ipsec statusall | grep {conn}')
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--action', help='Control action', required=True)
+ parser.add_argument('--bits', help='Bits for rsa-key', required=False)
+ parser.add_argument('--name', help='Name for x509 key-pair, peer for reset', required=False)
+ parser.add_argument('--tunnel', help='Specific tunnel of peer', required=False)
+
+ args = parser.parse_args()
+
+ if args.action == 'rsa-key':
+ bits = int(args.bits) if args.bits else 2192
+ generate_rsa_key(bits)
+ elif args.action == 'rsa-key-show':
+ show_rsa_keys()
+ elif args.action == 'x509':
+ if not args.name:
+ print('Invalid name for key-pair, aborting.')
+ sys.exit(0)
+ generate_x509_pair(args.name)
+ elif args.action == 'reset-peer':
+ reset_peer(args.name, args.tunnel)
+ elif args.action == "reset-profile":
+ reset_profile(args.name, args.tunnel)
+ elif args.action == "vpn-debug":
+ debug_peer(args.name, args.tunnel)
diff --git a/src/systemd/opennhrp.service b/src/systemd/opennhrp.service
new file mode 100644
index 000000000..70235f89d
--- /dev/null
+++ b/src/systemd/opennhrp.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=OpenNHRP
+After=vyos-router.service
+ConditionPathExists=/run/opennhrp/opennhrp.conf
+StartLimitIntervalSec=0
+
+[Service]
+Type=forking
+ExecStart=/usr/sbin/opennhrp -d -v -a /run/opennhrp.socket -c /run/opennhrp/opennhrp.conf -s /etc/opennhrp/opennhrp-script.py -p /run/opennhrp.pid
+ExecReload=/usr/bin/kill -HUP $MAINPID
+PIDFile=/run/opennhrp.pid
+Restart=on-failure
+RestartSec=20
diff --git a/src/validators/ipv4 b/src/validators/ipv4
new file mode 100755
index 000000000..53face090
--- /dev/null
+++ b/src/validators/ipv4
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4 $1
diff --git a/src/validators/ipv4-multicast b/src/validators/ipv4-multicast
new file mode 100755
index 000000000..e5cbc9532
--- /dev/null
+++ b/src/validators/ipv4-multicast
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4-multicast $1
diff --git a/src/validators/ipv6-exclude b/src/validators/ipv6-exclude
new file mode 100755
index 000000000..893eeab09
--- /dev/null
+++ b/src/validators/ipv6-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv6 "${arg:1}"
diff --git a/src/validators/ipv6-multicast b/src/validators/ipv6-multicast
new file mode 100755
index 000000000..66cd90c9c
--- /dev/null
+++ b/src/validators/ipv6-multicast
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv6-multicast $1
diff --git a/src/validators/ipv6-range b/src/validators/ipv6-range
new file mode 100755
index 000000000..033b6461b
--- /dev/null
+++ b/src/validators/ipv6-range
@@ -0,0 +1,16 @@
+#!/usr/bin/python3
+
+import sys
+import re
+from vyos.template import is_ipv6
+
+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)
diff --git a/src/validators/ipv6-range-exclude b/src/validators/ipv6-range-exclude
new file mode 100755
index 000000000..912b55ae3
--- /dev/null
+++ b/src/validators/ipv6-range-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv6-range "${arg:1}"
diff --git a/src/validators/sysctl b/src/validators/sysctl
new file mode 100755
index 000000000..9b5bba3e1
--- /dev/null
+++ b/src/validators/sysctl
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# 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/>.
+
+declare -a array
+eval "array=($(/sbin/sysctl -N -a))"
+
+if [[ ! " ${array[@]} " =~ " $1 " ]]; then
+ # passed sysctl option is invalid
+ exit 1
+fi
+exit 0