summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/completion/list_disks.py36
-rwxr-xr-xsrc/completion/list_disks.sh5
-rwxr-xr-xsrc/completion/list_interfaces.py14
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py85
-rwxr-xr-xsrc/conf_mode/host_name.py196
-rwxr-xr-xsrc/conf_mode/http-api.py3
-rwxr-xr-xsrc/conf_mode/https.py59
-rwxr-xr-xsrc/conf_mode/interface-bonding.py469
-rwxr-xr-xsrc/conf_mode/interface-bridge.py234
-rwxr-xr-xsrc/conf_mode/interface-dummy.py115
-rwxr-xr-xsrc/conf_mode/interface-loopback.py101
-rwxr-xr-xsrc/conf_mode/interface-openvpn.py88
-rwxr-xr-xsrc/conf_mode/interface-vxlan.py208
-rwxr-xr-xsrc/conf_mode/interface-wireguard.py541
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py2
-rwxr-xr-xsrc/conf_mode/syslog.py418
-rwxr-xr-xsrc/helpers/vyos-boot-config-loader.py101
-rwxr-xr-xsrc/helpers/vyos-bridge-sync.py53
-rwxr-xr-xsrc/helpers/vyos-sudo.py9
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/0-to-150
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/1-to-278
-rwxr-xr-xsrc/migration-scripts/interfaces/0-to-126
-rwxr-xr-xsrc/migration-scripts/interfaces/1-to-263
-rwxr-xr-xsrc/op_mode/clear_conntrack.py26
-rwxr-xr-xsrc/op_mode/format_disk.py148
-rwxr-xr-xsrc/op_mode/generate_ssh_server_key.py27
-rwxr-xr-xsrc/op_mode/powerctrl.py17
-rwxr-xr-xsrc/op_mode/show_openvpn.py169
-rwxr-xr-xsrc/op_mode/toggle_help_binding.sh25
-rwxr-xr-xsrc/op_mode/wireguard.py188
-rwxr-xr-xsrc/services/vyos-hostsd284
-rwxr-xr-xsrc/system/unpriv-ip2
-rw-r--r--src/systemd/vyos-hostsd.service31
-rwxr-xr-xsrc/utils/vyos-hostsd-client82
34 files changed, 2906 insertions, 1047 deletions
diff --git a/src/completion/list_disks.py b/src/completion/list_disks.py
new file mode 100755
index 000000000..ff1135e23
--- /dev/null
+++ b/src/completion/list_disks.py
@@ -0,0 +1,36 @@
+#!/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/>.
+
+# Completion script used by show disks to collect physical disk
+
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-e", "--exclude", type=str, help="Exclude specified device from the result list")
+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])
+
+if args.exclude:
+ disks.remove(args.exclude)
+
+for disk in disks:
+ print(disk)
diff --git a/src/completion/list_disks.sh b/src/completion/list_disks.sh
deleted file mode 100755
index f32e558fd..000000000
--- a/src/completion/list_disks.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/bash
-
-# Completion script used by show disks to collect physical disk
-
-awk 'NR > 2 && $4 !~ /[0-9]$/ { print $4 }' </proc/partitions
diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py
index 66432af19..5e444ef78 100755
--- a/src/completion/list_interfaces.py
+++ b/src/completion/list_interfaces.py
@@ -2,15 +2,14 @@
import sys
import argparse
-
import vyos.interfaces
-
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-t", "--type", type=str, help="List interfaces of specific type")
group.add_argument("-b", "--broadcast", action="store_true", help="List all broadcast interfaces")
group.add_argument("-br", "--bridgeable", action="store_true", help="List all bridgeable interfaces")
+group.add_argument("-bo", "--bondable", action="store_true", help="List all bondable interfaces")
args = parser.parse_args()
@@ -21,11 +20,13 @@ if args.type:
except ValueError as e:
print(e, file=sys.stderr)
print("")
+
elif args.broadcast:
eth = vyos.interfaces.list_interfaces_of_type("ethernet")
bridge = vyos.interfaces.list_interfaces_of_type("bridge")
bond = vyos.interfaces.list_interfaces_of_type("bonding")
interfaces = eth + bridge + bond
+
elif args.bridgeable:
eth = vyos.interfaces.list_interfaces_of_type("ethernet")
bond = vyos.interfaces.list_interfaces_of_type("bonding")
@@ -34,6 +35,15 @@ elif args.bridgeable:
vxlan = vyos.interfaces.list_interfaces_of_type("vxlan")
wireless = vyos.interfaces.list_interfaces_of_type("wireless")
interfaces = eth + bond + l2tpv3 + openvpn + vxlan + wireless
+
+elif args.bondable:
+ eth = vyos.interfaces.list_interfaces_of_type("ethernet")
+ # we need to filter out VLAN interfaces identified by a dot (.) in their name
+ for intf in eth:
+ if '.' in intf:
+ eth.remove(intf)
+ interfaces = eth
+
else:
interfaces = vyos.interfaces.list_interfaces()
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 3ca77adee..38f3cb4de 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -24,6 +24,7 @@ import jinja2
import netifaces
import vyos.util
+import vyos.hostsd_client
from vyos.config import Config
from vyos import ConfigError
@@ -44,7 +45,7 @@ config_tmpl = """
# Non-configurable defaults
daemon=yes
threads=1
-allow-from=0.0.0.0/0, ::/0
+allow-from={{ allow_from | join(',') }}
log-common-errors=yes
non-local-bind=yes
query-local-address=0.0.0.0
@@ -83,10 +84,10 @@ dnssec={{ dnssec }}
"""
default_config_data = {
+ 'allow_from': [],
'cache_size': 10000,
'export_hosts_file': 'yes',
'listen_on': [],
- 'interfaces': [],
'name_servers': [],
'negative_ttl': 3600,
'domains': [],
@@ -94,19 +95,6 @@ default_config_data = {
}
-# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX!
-def get_resolvers(file):
- try:
- with open(file, 'r') as resolvconf:
- lines = [line.split('#', 1)[0].rstrip()
- for line in resolvconf.readlines()]
- resolvers = [line.split()[1]
- for line in lines if 'nameserver' in line]
- return resolvers
- except IOError:
- return []
-
-
def get_config(arguments):
dns = default_config_data
conf = Config()
@@ -121,6 +109,9 @@ def get_config(arguments):
conf.set_level('service dns forwarding')
+ if conf.exists('allow-from'):
+ dns['allow_from'] = conf.return_values('allow-from')
+
if conf.exists('cache-size'):
cache_size = conf.return_value('cache-size')
dns['cache_size'] = cache_size
@@ -164,64 +155,27 @@ def get_config(arguments):
if conf.exists('dnssec'):
dns['dnssec'] = conf.return_value('dnssec')
- ## Hacks and tricks
-
- # The old VyOS syntax that comes from dnsmasq was "listen-on $interface".
- # pdns wants addresses instead, so we emulate it by looking up all addresses
- # of a given interface and writing them to the config
- if conf.exists('listen-on'):
- print("WARNING: since VyOS 1.2.0, \"service dns forwarding listen-on\" is a limited compatibility option.")
- print("It will only make DNS forwarder listen on addresses assigned to the interface at the time of commit")
- print("which means it will NOT work properly with VRRP/clustering or addresses received from DHCP.")
- print("Please reconfigure your system with \"service dns forwarding listen-address\" instead.")
-
- interfaces = conf.return_values('listen-on')
-
- listen4 = []
- listen6 = []
- for interface in interfaces:
- try:
- addrs = netifaces.ifaddresses(interface)
- except ValueError:
- print(
- "WARNING: interface {0} does not exist".format(interface))
- continue
-
- if netifaces.AF_INET in addrs.keys():
- for ip4 in addrs[netifaces.AF_INET]:
- listen4.append(ip4['addr'])
-
- if netifaces.AF_INET6 in addrs.keys():
- for ip6 in addrs[netifaces.AF_INET6]:
- listen6.append(ip6['addr'])
-
- if (not listen4) and (not (listen6)):
- print(
- "WARNING: interface {0} has no configured addresses".format(interface))
-
- dns['listen_on'] = dns['listen_on'] + listen4 + listen6
-
- # Save interfaces in the dict for the reference
- dns['interfaces'] = interfaces
-
# Add name servers received from DHCP
if conf.exists('dhcp'):
interfaces = []
interfaces = conf.return_values('dhcp')
+ hc = vyos.hostsd_client.Client()
+
for interface in interfaces:
- dhcp_resolvers = get_resolvers(
- "/etc/resolv.conf.dhclient-new-{0}".format(interface))
+ dhcp_resolvers = hc.get_name_servers("dhcp-{0}".format(interface))
+ dhcpv6_resolvers = hc.get_name_servers("dhcpv6-{0}".format(interface))
+
if dhcp_resolvers:
dns['name_servers'] = dns['name_servers'] + dhcp_resolvers
+ if dhcpv6_resolvers:
+ dns['name_servers'] = dns['name_servers'] + dhcpv6_resolvers
return dns
-
def bracketize_ipv6_addrs(addrs):
"""Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched."""
return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs]
-
def verify(dns):
# bail out early - looks like removal from running config
if dns is None:
@@ -231,6 +185,10 @@ def verify(dns):
raise ConfigError(
"Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option")
+ if not dns['allow_from']:
+ raise ConfigError(
+ "Error: DNS forwarding requires an allow-from network")
+
if dns['domains']:
for domain in dns['domains']:
if not domain['servers']:
@@ -239,7 +197,6 @@ def verify(dns):
return None
-
def generate(dns):
# bail out early - looks like removal from running config
if dns is None:
@@ -251,16 +208,14 @@ def generate(dns):
f.write(config_text)
return None
-
def apply(dns):
- if dns is not None:
- os.system("systemctl restart pdns-recursor")
- else:
+ if dns is None:
# DNS forwarding is removed in the commit
os.system("systemctl stop pdns-recursor")
if os.path.isfile(config_file):
os.unlink(config_file)
-
+ else:
+ os.system("systemctl restart pdns-recursor")
if __name__ == '__main__':
args = parser.parse_args()
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index 2fad57db6..bb1ec9597 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -30,57 +30,12 @@ import argparse
import jinja2
import vyos.util
+import vyos.hostsd_client
from vyos.config import Config
from vyos import ConfigError
-parser = argparse.ArgumentParser()
-parser.add_argument("--dhclient", action="store_true",
- help="Started from dhclient-script")
-
-config_file_hosts = '/etc/hosts'
-config_file_resolv = '/etc/resolv.conf'
-
-config_tmpl_hosts = """
-### Autogenerated by host_name.py ###
-127.0.0.1 localhost
-127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }} {{ hostname }}{% endif %}
-
-# The following lines are desirable for IPv6 capable hosts
-::1 localhost ip6-localhost ip6-loopback
-fe00::0 ip6-localnet
-ff00::0 ip6-mcastprefix
-ff02::1 ip6-allnodes
-ff02::2 ip6-allrouters
-
-# static hostname mappings
-{%- if static_host_mapping['hostnames'] %}
-{% for hn in static_host_mapping['hostnames'] -%}
-{{static_host_mapping['hostnames'][hn]['ipaddr']}}\t{{static_host_mapping['hostnames'][hn]['alias']}}\t{{hn}}
-{% endfor -%}
-{%- endif %}
-
-### modifications from other scripts should be added below
-
-"""
-
-config_tmpl_resolv = """
-### Autogenerated by host_name.py ###
-{% for ns in nameserver -%}
-nameserver {{ ns }}
-{% endfor -%}
-
-{%- if domain_name %}
-domain {{ domain_name }}
-{%- endif %}
-
-{%- if domain_search %}
-search {{ domain_search | join(" ") }}
-{%- endif %}
-
-"""
-
default_config_data = {
'hostname': 'vyos',
'domain_name': '',
@@ -89,32 +44,10 @@ default_config_data = {
'no_dhcp_ns': False
}
-# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX!
-def get_resolvers(file):
- resolv = {}
- try:
- with open(file, 'r') as resolvconf:
- lines = [line.split('#', 1)[0].rstrip()
- for line in resolvconf.readlines()]
- resolvers = [line.split()[1]
- for line in lines if 'nameserver' in line]
- domains = [line.split()[1] for line in lines if 'search' in line]
- resolv['resolvers'] = resolvers
- resolv['domains'] = domains
- return resolv
- except IOError:
- return []
-
-
-def get_config(arguments):
+def get_config():
conf = Config()
hosts = copy.deepcopy(default_config_data)
- if arguments.dhclient:
- conf.exists = conf.exists_effective
- conf.return_value = conf.return_effective_value
- conf.return_values = conf.return_effective_values
-
if conf.exists("system host-name"):
hosts['hostname'] = conf.return_value("system host-name")
# This may happen if the config is not loaded yet,
@@ -136,19 +69,15 @@ def get_config(arguments):
hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers')
# system static-host-mapping
- hosts['static_host_mapping'] = {'hostnames': {}}
+ hosts['static_host_mapping'] = []
if conf.exists('system static-host-mapping host-name'):
for hn in conf.list_nodes('system static-host-mapping host-name'):
- hosts['static_host_mapping']['hostnames'][hn] = {
- 'ipaddr': conf.return_value('system static-host-mapping host-name ' + hn + ' inet'),
- 'alias': ''
- }
-
- if conf.exists('system static-host-mapping host-name ' + hn + ' alias'):
- a = conf.return_values(
- 'system static-host-mapping host-name ' + hn + ' alias')
- hosts['static_host_mapping']['hostnames'][hn]['alias'] = " ".join(a)
+ mapping = {}
+ mapping['host'] = hn
+ mapping['address'] = conf.return_value('system static-host-mapping host-name {0} inet'.format(hn))
+ mapping['aliases'] = conf.return_values('system static-host-mapping host-name {0} alias'.format(hn))
+ hosts['static_host_mapping'].append(mapping)
return hosts
@@ -180,83 +109,43 @@ def verify(config):
'The search list is currently limited to 256 characters')
# static mappings alias hostname
- if config['static_host_mapping']['hostnames']:
- for hn in config['static_host_mapping']['hostnames']:
- if not config['static_host_mapping']['hostnames'][hn]['ipaddr']:
- raise ConfigError('IP address required for ' + hn)
- for hn_alias in config['static_host_mapping']['hostnames'][hn]['alias'].split(' '):
- if not hostname_regex.match(hn_alias) and len(hn_alias) != 0:
- raise ConfigError('Invalid hostname alias ' + hn_alias)
+ if config['static_host_mapping']:
+ for m in config['static_host_mapping']:
+ if not m['address']:
+ raise ConfigError('IP address required for ' + m['host'])
+ for a in m['aliases']:
+ if not hostname_regex.match(a) and len(a) != 0:
+ raise ConfigError('Invalid alias \'{0}\' in mapping {1}'.format(a, m['host']))
return None
def generate(config):
+ pass
+
+def apply(config):
if config is None:
return None
- # If "system disable-dhcp-nameservers" is __configured__ all DNS resolvers
- # received via dhclient should not be added into the final 'resolv.conf'.
- #
- # We iterate over every resolver file and retrieve the received nameservers
- # for later adjustment of the system nameservers
- dhcp_ns = []
- dhcp_sd = []
- for file in glob.glob('/etc/resolv.conf.dhclient-new*'):
- for key, value in get_resolvers(file).items():
- ns = [r for r in value if key == 'resolvers']
- dhcp_ns.extend(ns)
- sd = [d for d in value if key == 'domains']
- dhcp_sd.extend(sd)
-
- if not config['no_dhcp_ns']:
- config['nameserver'] += dhcp_ns
- config['domain_search'] += dhcp_sd
-
- # Prune duplicate values
- # Not order preserving, but then when multiple DHCP clients are used,
- # there can't be guarantees about the order anyway
- dhcp_ns = list(set(dhcp_ns))
- dhcp_sd = list(set(dhcp_sd))
-
- # We have third party scripts altering /etc/hosts, too.
- # One example are the DHCP hostname update scripts thus we need to cache in
- # every modification first - so changing domain-name, domain-search or hostname
- # during runtime works
- old_hosts = ""
- with open(config_file_hosts, 'r') as f:
- # Skips text before the beginning of our marker.
- # NOTE: Marker __MUST__ match the one specified in config_tmpl_hosts
- for line in f:
- if line.strip() == '### modifications from other scripts should be added below':
- break
-
- for line in f:
- # This additional line.strip() filters empty lines
- if line.strip():
- old_hosts += line
-
- # Add an additional newline
- old_hosts += '\n'
-
- tmpl = jinja2.Template(config_tmpl_hosts)
- config_text = tmpl.render(config)
-
- with open(config_file_hosts, 'w') as f:
- f.write(config_text)
- f.write(old_hosts)
-
- tmpl = jinja2.Template(config_tmpl_resolv)
- config_text = tmpl.render(config)
- with open(config_file_resolv, 'w') as f:
- f.write(config_text)
+ ## Send the updated data to vyos-hostsd
- return None
+ # vyos-hostsd uses "tags" to identify data sources
+ tag = "static"
+ try:
+ client = vyos.hostsd_client.Client()
-def apply(config):
- if config is None:
- return None
+ client.set_host_name(config['hostname'], config['domain_name'], config['domain_search'])
+
+ client.delete_name_servers(tag)
+ client.add_name_servers(tag, config['nameserver'])
+
+ client.delete_hosts(tag)
+ client.add_hosts(tag, config['static_host_mapping'])
+ except vyos.hostsd_client.VyOSHostsdError as e:
+ raise ConfigError(str(e))
+
+ ## Actually update the hostname -- vyos-hostsd doesn't do that
# No domain name -- the Debian way.
hostname_new = config['hostname']
@@ -283,22 +172,9 @@ def apply(config):
if __name__ == '__main__':
- args = parser.parse_args()
-
- if args.dhclient:
- # There's a big chance it was triggered by a commit still in progress
- # so we need to wait until the new values are in the running config
- vyos.util.wait_for_commit_lock()
-
-
try:
- c = get_config(args)
- # If it's called from dhclient, then either:
- # a) verification was already done at commit time
- # b) it's run on an unconfigured system, e.g. by cloud-init
- # Therefore, verification is either redundant or useless
- if not args.dhclient:
- verify(c)
+ c = get_config()
+ verify(c)
generate(c)
apply(c)
except ConfigError as e:
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 1f91ac582..9c062f0aa 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -69,6 +69,9 @@ def generate(http_api):
if http_api is None:
return None
+ if not os.path.exists('/etc/vyos'):
+ os.mkdir('/etc/vyos')
+
with open(config_file, 'w') as f:
json.dump(http_api, f, indent=2)
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index 289eacf69..f948063e9 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -40,12 +40,21 @@ server {
return 302 https://$server_name$request_uri;
}
+{% for addr, names in listen_addresses.items() %}
server {
# SSL configuration
#
+{% if addr == '*' %}
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
+{% else %}
+ listen {{ addr }}:443 ssl;
+{% endif %}
+
+{% for name in names %}
+ server_name {{ name }};
+{% endfor %}
{% if vyos_cert %}
include {{ vyos_cert.conf }};
@@ -57,9 +66,42 @@ server {
include snippets/snakeoil.conf;
{% endif %}
-{% for l_addr in listen_address %}
- server_name {{ l_addr }};
-{% endfor %}
+ # proxy settings for HTTP API, if enabled; 503, if not
+ location ~ /(retrieve|configure) {
+{% if api %}
+ proxy_pass http://localhost:{{ api.port }};
+ proxy_buffering off;
+{% else %}
+ return 503;
+{% endif %}
+ }
+
+ error_page 501 502 503 =200 @50*_json;
+
+ location @50*_json {
+ default_type application/json;
+ return 200 '{"error": "Start service in configuration mode: set service https api"}';
+ }
+
+}
+{% else %}
+server {
+ # SSL configuration
+ #
+ listen 443 ssl default_server;
+ listen [::]:443 ssl default_server;
+
+ server_name _;
+
+{% if vyos_cert %}
+ include {{ vyos_cert.conf }};
+{% else %}
+ #
+ # Self signed certs generated by the ssl-cert package
+ # Don't use them in a production server!
+ #
+ include snippets/snakeoil.conf;
+{% endif %}
# proxy settings for HTTP API, if enabled; 503, if not
location ~ /(retrieve|configure) {
@@ -79,6 +121,8 @@ server {
}
}
+
+{% endfor %}
"""
def get_config():
@@ -90,8 +134,13 @@ def get_config():
conf.set_level('service https')
if conf.exists('listen-address'):
- addrs = conf.return_values('listen-address')
- https['listen_address'] = addrs[:]
+ addrs = {}
+ for addr in conf.list_nodes('listen-address'):
+ addrs[addr] = ['_']
+ if conf.exists('listen-address {0} server-name'.format(addr)):
+ names = conf.return_values('listen-address {0} server-name'.format(addr))
+ addrs[addr] = names[:]
+ https['listen_addresses'] = addrs
if conf.exists('certificates'):
if conf.exists('certificates system-generated-certificate'):
diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py
new file mode 100755
index 000000000..dc0363fb7
--- /dev/null
+++ b/src/conf_mode/interface-bonding.py
@@ -0,0 +1,469 @@
+#!/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/>.
+#
+#
+
+import os
+
+from copy import deepcopy
+from sys import exit
+from netifaces import interfaces
+
+from vyos.ifconfig import BondIf, EthernetIf
+from vyos.configdict import list_diff, vlan_to_dict
+from vyos.config import Config
+from vyos import ConfigError
+
+default_config_data = {
+ 'address': [],
+ 'address_remove': [],
+ 'arp_mon_intvl': 0,
+ 'arp_mon_tgt': [],
+ 'description': '',
+ 'deleted': False,
+ 'dhcp_client_id': '',
+ 'dhcp_hostname': '',
+ 'dhcpv6_prm_only': False,
+ 'dhcpv6_temporary': False,
+ 'disable': False,
+ 'disable_link_detect': 1,
+ 'hash_policy': 'layer2',
+ 'ip_arp_cache_tmo': 30,
+ 'ip_proxy_arp': 0,
+ 'ip_proxy_arp_pvlan': 0,
+ 'intf': '',
+ 'mac': '',
+ 'mode': '802.3ad',
+ 'member': [],
+ 'mtu': 1500,
+ 'primary': '',
+ 'vif_s': [],
+ 'vif_s_remove': [],
+ 'vif': [],
+ 'vif_remove': []
+}
+
+
+def get_bond_mode(mode):
+ if mode == 'round-robin':
+ return 'balance-rr'
+ elif mode == 'active-backup':
+ return 'active-backup'
+ elif mode == 'xor-hash':
+ return 'balance-xor'
+ elif mode == 'broadcast':
+ return 'broadcast'
+ elif mode == '802.3ad':
+ return '802.3ad'
+ elif mode == 'transmit-load-balance':
+ return 'balance-tlb'
+ elif mode == 'adaptive-load-balance':
+ return 'balance-alb'
+ else:
+ raise ConfigError('invalid bond mode "{}"'.format(mode))
+
+
+def apply_vlan_config(vlan, config):
+ """
+ Generic function to apply a VLAN configuration from a dictionary
+ to a VLAN interface
+ """
+
+ if type(vlan) != type(EthernetIf("lo")):
+ raise TypeError()
+
+ # update interface description used e.g. within SNMP
+ vlan.ifalias = config['description']
+ # ignore link state changes
+ vlan.link_detect = config['disable_link_detect']
+ # Maximum Transmission Unit (MTU)
+ vlan.mtu = config['mtu']
+ # Change VLAN interface MAC address
+ if config['mac']:
+ vlan.mac = config['mac']
+
+ # enable/disable VLAN interface
+ if config['disable']:
+ vlan.state = 'down'
+ else:
+ vlan.state = 'up'
+
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in config['address_remove']:
+ vlan.del_addr(addr)
+ for addr in config['address']:
+ vlan.add_addr(addr)
+
+
+def get_config():
+ # initialize kernel module if not loaded
+ if not os.path.isfile('/sys/class/net/bonding_masters'):
+ import syslog
+ syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module")
+ if os.system('modprobe bonding max_bonds=0 miimon=250') != 0:
+ syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module")
+ raise ConfigError("failed loading bonding kernel module")
+
+ bond = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ try:
+ bond['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ except KeyError as E:
+ print("Interface not specified")
+
+ # check if bond has been removed
+ cfg_base = 'interfaces bonding ' + bond['intf']
+ if not conf.exists(cfg_base):
+ bond['deleted'] = True
+ return bond
+
+ # set new configuration level
+ conf.set_level(cfg_base)
+
+ # retrieve configured interface addresses
+ if conf.exists('address'):
+ bond['address'] = conf.return_values('address')
+
+ # get interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('address')
+ bond['address_remove'] = list_diff(eff_addr, bond['address'])
+
+ # ARP link monitoring frequency in milliseconds
+ if conf.exists('arp-monitor interval'):
+ bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval'))
+
+ # IP address to use for ARP monitoring
+ if conf.exists('arp-monitor target'):
+ bond['arp_mon_tgt'] = conf.return_values('arp-monitor target')
+
+ # retrieve interface description
+ if conf.exists('description'):
+ bond['description'] = conf.return_value('description')
+ else:
+ bond['description'] = bond['intf']
+
+ # get DHCP client identifier
+ if conf.exists('dhcp-options client-id'):
+ bond['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
+
+ # DHCP client host name (overrides the system host name)
+ if conf.exists('dhcp-options host-name'):
+ bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
+
+ # DHCPv6 only acquire config parameters, no address
+ if conf.exists('dhcpv6-options parameters-only'):
+ bond['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only')
+
+ # DHCPv6 temporary IPv6 address
+ if conf.exists('dhcpv6-options temporary'):
+ bond['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary')
+
+ # ignore link state changes
+ if conf.exists('disable-link-detect'):
+ bond['disable_link_detect'] = 2
+
+ # disable bond interface
+ if conf.exists('disable'):
+ bond['disable'] = True
+
+ # Bonding transmit hash policy
+ if conf.exists('hash-policy'):
+ bond['hash_policy'] = conf.return_value('hash-policy')
+
+ # ARP cache entry timeout in seconds
+ if conf.exists('ip arp-cache-timeout'):
+ bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
+
+ # Enable proxy-arp on this interface
+ if conf.exists('ip enable-proxy-arp'):
+ bond['ip_proxy_arp'] = 1
+
+ # Enable private VLAN proxy ARP on this interface
+ if conf.exists('ip proxy-arp-pvlan'):
+ bond['ip_proxy_arp_pvlan'] = 1
+
+ # Media Access Control (MAC) address
+ if conf.exists('mac'):
+ bond['mac'] = conf.return_value('mac')
+
+ # Bonding mode
+ if conf.exists('mode'):
+ bond['mode'] = get_bond_mode(conf.return_value('mode'))
+
+ # Maximum Transmission Unit (MTU)
+ if conf.exists('mtu'):
+ bond['mtu'] = int(conf.return_value('mtu'))
+
+ # determine bond member interfaces (currently configured)
+ if conf.exists('member interface'):
+ bond['member'] = conf.return_values('member interface')
+
+ # Primary device interface
+ if conf.exists('primary'):
+ bond['primary'] = conf.return_value('primary')
+
+ # re-set configuration level and retrieve vif-s interfaces
+ conf.set_level(cfg_base)
+ # get vif-s interfaces (currently effective) - to determine which vif-s
+ # interface is no longer present and needs to be removed
+ eff_intf = conf.list_effective_nodes('vif-s')
+ act_intf = conf.list_nodes('vif-s')
+ bond['vif_s_remove'] = list_diff(eff_intf, act_intf)
+
+ if conf.exists('vif-s'):
+ for vif_s in conf.list_nodes('vif-s'):
+ # set config level to vif-s interface
+ conf.set_level(cfg_base + ' vif-s ' + vif_s)
+ bond['vif_s'].append(vlan_to_dict(conf))
+
+ # re-set configuration level and retrieve vif-s interfaces
+ conf.set_level(cfg_base)
+ # Determine vif interfaces (currently effective) - to determine which
+ # vif interface is no longer present and needs to be removed
+ eff_intf = conf.list_effective_nodes('vif')
+ act_intf = conf.list_nodes('vif')
+ bond['vif_remove'] = list_diff(eff_intf, act_intf)
+
+ if conf.exists('vif'):
+ for vif in conf.list_nodes('vif'):
+ # set config level to vif interface
+ conf.set_level(cfg_base + ' vif ' + vif)
+ bond['vif'].append(vlan_to_dict(conf))
+
+ return bond
+
+
+def verify(bond):
+ if len (bond['arp_mon_tgt']) > 16:
+ raise ConfigError('The maximum number of targets that can be specified is 16')
+
+ if bond['primary']:
+ if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('Mode dependency failed, primary not supported ' \
+ 'in this mode.'.format())
+
+ if bond['primary'] not in bond['member']:
+ raise ConfigError('Interface "{}" is not part of the bond' \
+ .format(bond['primary']))
+
+ for vif_s in bond['vif_s']:
+ for vif in bond['vif']:
+ if vif['id'] == vif_s['id']:
+ raise ConfigError('Can not use identical ID on vif and vif-s interface')
+
+
+ conf = Config()
+ for intf in bond['member']:
+ # a bonding member interface is only allowed to be assigned to one bond!
+ all_bonds = conf.list_nodes('interfaces bonding')
+ # We do not need to check our own bond
+ all_bonds.remove(bond['intf'])
+ for tmp in all_bonds:
+ if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf):
+ raise ConfigError('can not enslave interface {} which already ' \
+ 'belongs to {}'.format(intf, tmp))
+
+ # we can not add disabled slave interfaces to our bond
+ if conf.exists('interfaces ethernet ' + intf + ' disable'):
+ raise ConfigError('can not enslave disabled interface {}' \
+ .format(intf))
+
+ # can not add interfaces with an assigned address to a bond
+ if conf.exists('interfaces ethernet ' + intf + ' address'):
+ raise ConfigError('can not enslave interface {} which has an address ' \
+ 'assigned'.format(intf))
+
+ # bond members are not allowed to be bridge members, too
+ for tmp in conf.list_nodes('interfaces bridge'):
+ if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf):
+ raise ConfigError('can not enslave interface {} which belongs to ' \
+ 'bridge {}'.format(intf, tmp))
+
+ # bond members are not allowed to be vrrp members, too
+ for tmp in conf.list_nodes('high-availability vrrp group'):
+ if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf):
+ raise ConfigError('can not enslave interface {} which belongs to ' \
+ 'VRRP group {}'.format(intf, tmp))
+
+ # bond members are not allowed to be underlaying psuedo-ethernet devices
+ for tmp in conf.list_nodes('interfaces pseudo-ethernet'):
+ if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf):
+ raise ConfigError('can not enslave interface {} which belongs to ' \
+ 'pseudo-ethernet {}'.format(intf, tmp))
+
+ # bond members are not allowed to be underlaying vxlan devices
+ for tmp in conf.list_nodes('interfaces vxlan'):
+ if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf):
+ raise ConfigError('can not enslave interface {} which belongs to ' \
+ 'vxlan {}'.format(intf, tmp))
+
+
+ if bond['primary']:
+ if bond['primary'] not in bond['member']:
+ raise ConfigError('primary interface must be a member interface of {}' \
+ .format(bond['intf']))
+
+ if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('primary interface only works for mode active-backup, ' \
+ 'transmit-load-balance or adaptive-load-balance')
+
+ if bond['arp_mon_intvl'] > 0:
+ if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
+ 'transmit-load-balance or adaptive-load-balance')
+
+ return None
+
+
+def generate(bond):
+ return None
+
+
+def apply(bond):
+ b = BondIf(bond['intf'])
+
+ if bond['deleted']:
+ #
+ # delete bonding interface
+ b.remove()
+ else:
+ # Some parameters can not be changed when the bond is up.
+ # Always disable the bond prior changing anything
+ b.state = 'down'
+
+ # The bonding mode can not be changed when there are interfaces enslaved
+ # to this bond, thus we will free all interfaces from the bond first!
+ for intf in b.get_slaves():
+ b.del_port(intf)
+
+ # ARP link monitoring frequency
+ b.arp_interval = bond['arp_mon_intvl']
+ # reset miimon on arp-montior deletion
+ if bond['arp_mon_intvl'] == 0:
+ # reset miimon to default
+ b.bond_miimon = 250
+
+ # ARP monitor targets need to be synchronized between sysfs and CLI.
+ # Unfortunately an address can't be send twice to sysfs as this will
+ # result in the following exception: OSError: [Errno 22] Invalid argument.
+ #
+ # We remove ALL adresses prior adding new ones, this will remove addresses
+ # added manually by the user too - but as we are limited to 16 adresses
+ # from the kernel side this looks valid to me. We won't run into an error
+ # when a user added manual adresses which would result in having more
+ # then 16 adresses in total.
+ arp_tgt_addr = list(map(str, b.arp_ip_target.split()))
+ for addr in arp_tgt_addr:
+ b.arp_ip_target = '-' + addr
+
+ # Add configured ARP target addresses
+ for addr in bond['arp_mon_tgt']:
+ b.arp_ip_target = '+' + addr
+
+ # update interface description used e.g. within SNMP
+ b.ifalias = bond['description']
+
+ #
+ # missing DHCP/DHCPv6 options go here
+ #
+
+ # ignore link state changes
+ b.link_detect = bond['disable_link_detect']
+ # Bonding transmit hash policy
+ b.xmit_hash_policy = bond['hash_policy']
+ # configure ARP cache timeout in milliseconds
+ b.arp_cache_tmp = bond['ip_arp_cache_tmo']
+ # Enable proxy-arp on this interface
+ b.proxy_arp = bond['ip_proxy_arp']
+ # Enable private VLAN proxy ARP on this interface
+ b.proxy_arp_pvlan = bond['ip_proxy_arp_pvlan']
+
+ # Change interface MAC address
+ if bond['mac']:
+ b.mac = bond['mac']
+
+ # Bonding policy
+ b.mode = bond['mode']
+ # Maximum Transmission Unit (MTU)
+ b.mtu = bond['mtu']
+
+ # Primary device interface
+ if bond['primary']:
+ b.primary = bond['primary']
+
+ # Add (enslave) interfaces to bond
+ for intf in bond['member']:
+ b.add_port(intf)
+
+ # As the bond interface is always disabled first when changing
+ # parameters we will only re-enable the interface if it is not
+ # administratively disabled
+ if not bond['disable']:
+ b.state = 'up'
+
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in bond['address_remove']:
+ b.del_addr(addr)
+ for addr in bond['address']:
+ b.add_addr(addr)
+
+ # remove no longer required service VLAN interfaces (vif-s)
+ for vif_s in bond['vif_s_remove']:
+ b.del_vlan(vif_s)
+
+ # create service VLAN interfaces (vif-s)
+ for vif_s in bond['vif_s']:
+ s_vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype'])
+ apply_vlan_config(s_vlan, vif_s)
+
+ # remove no longer required client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c in vif_s['vif_c_remove']:
+ s_vlan.del_vlan(vif_c)
+
+ # create client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c in vif_s['vif_c']:
+ c_vlan = s_vlan.add_vlan(vif_c['id'])
+ apply_vlan_config(c_vlan, vif_c)
+
+ # remove no longer required VLAN interfaces (vif)
+ for vif in bond['vif_remove']:
+ b.del_vlan(vif)
+
+ # create VLAN interfaces (vif)
+ for vif in bond['vif']:
+ vlan = b.add_vlan(vif['id'])
+ apply_vlan_config(vlan, vif)
+
+ 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/interface-bridge.py b/src/conf_mode/interface-bridge.py
index 543349e7b..401182a0d 100755
--- a/src/conf_mode/interface-bridge.py
+++ b/src/conf_mode/interface-bridge.py
@@ -17,51 +17,39 @@
#
import os
-import sys
-import copy
-import subprocess
-import vyos.configinterface as VyIfconfig
+from copy import deepcopy
+from sys import exit
+from netifaces import interfaces
+from vyos.ifconfig import BridgeIf, Interface
+from vyos.configdict import list_diff
from vyos.config import Config
from vyos import ConfigError
default_config_data = {
'address': [],
'address_remove': [],
- 'aging': '300',
- 'arp_cache_timeout_ms': '30000',
+ 'aging': 300,
+ 'arp_cache_tmo': 30,
'description': '',
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcpv6_parameters_only': False,
- 'dhcpv6_temporary': False,
'disable': False,
- 'disable_link_detect': False,
- 'forwarding_delay': '15',
- 'hello_time': '2',
+ 'disable_link_detect': 1,
+ 'forwarding_delay': 14,
+ 'hello_time': 2,
'igmp_querier': 0,
'intf': '',
'mac' : '',
- 'max_age': '20',
+ 'max_age': 20,
'member': [],
'member_remove': [],
- 'priority': '32768',
- 'stp': 'off'
+ 'priority': 32768,
+ 'stp': 0
}
-def subprocess_cmd(command):
- process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True)
- proc_stdout = process.communicate()[0].strip()
- pass
-
-def diff(first, second):
- second = set(second)
- return [item for item in first if item not in second]
-
def get_config():
- bridge = copy.deepcopy(default_config_data)
+ bridge = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
@@ -82,45 +70,34 @@ def get_config():
if conf.exists('address'):
bridge['address'] = conf.return_values('address')
+ # Determine interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('address')
+ bridge['address_remove'] = list_diff(eff_addr, bridge['address'])
+
# retrieve aging - how long addresses are retained
if conf.exists('aging'):
- bridge['aging'] = conf.return_value('aging')
+ bridge['aging'] = int(conf.return_value('aging'))
# retrieve interface description
if conf.exists('description'):
bridge['description'] = conf.return_value('description')
- # DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
-
- # DHCP client hostname
- if conf.exists('dhcp-options host-name'):
- bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
-
- # DHCPv6 acquire only config parameters, no address
- if conf.exists('dhcpv6-options parameters-only'):
- bridge['dhcpv6_parameters_only'] = True
-
- # DHCPv6 IPv6 "temporary" address
- if conf.exists('dhcpv6-options temporary'):
- bridge['dhcpv6_temporary'] = True
-
# Disable this bridge interface
if conf.exists('disable'):
bridge['disable'] = True
# Ignore link state changes
if conf.exists('disable-link-detect'):
- bridge['disable_link_detect'] = True
+ bridge['disable_link_detect'] = 2
# Forwarding delay
if conf.exists('forwarding-delay'):
- bridge['forwarding_delay'] = conf.return_value('forwarding-delay')
+ bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay'))
# Hello packet advertisment interval
if conf.exists('hello-time'):
- bridge['hello_time'] = conf.return_value('hello-time')
+ bridge['hello_time'] = int(conf.return_value('hello-time'))
# Enable Internet Group Management Protocol (IGMP) querier
if conf.exists('igmp querier'):
@@ -128,8 +105,7 @@ def get_config():
# ARP cache entry timeout in seconds
if conf.exists('ip arp-cache-timeout'):
- tmp = 1000 * int(conf.return_value('ip arp-cache-timeout'))
- bridge['arp_cache_timeout_ms'] = str(tmp)
+ bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
# Media Access Control (MAC) address
if conf.exists('mac'):
@@ -137,21 +113,24 @@ def get_config():
# Interval at which neighbor bridges are removed
if conf.exists('max-age'):
- bridge['max_age'] = conf.return_value('max-age')
+ bridge['max_age'] = int(conf.return_value('max-age'))
# Determine bridge member interface (currently configured)
for intf in conf.list_nodes('member interface'):
+ # cost and priority initialized with linux defaults
+ # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority}
+ # after adding interface to bridge after reboot
iface = {
'name': intf,
- 'cost': '',
- 'priority': ''
+ 'cost': 100,
+ 'priority': 32
}
if conf.exists('member interface {} cost'.format(intf)):
- iface['cost'] = conf.return_value('member interface {} cost'.format(intf))
+ iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf)))
if conf.exists('member interface {} priority'.format(intf)):
- iface['priority'] = conf.return_value('member interface {} priority'.format(intf))
+ iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf)))
bridge['member'].append(iface)
@@ -159,28 +138,19 @@ def get_config():
# interfaces is no longer assigend to the bridge and thus can be removed
eff_intf = conf.list_effective_nodes('member interface')
act_intf = conf.list_nodes('member interface')
- bridge['member_remove'] = diff(eff_intf, act_intf)
-
- # Determine interface addresses (currently effective) - to determine which
- # address is no longer valid and needs to be removed from the bridge
- eff_addr = conf.return_effective_values('address')
- act_addr = conf.return_values('address')
- bridge['address_remove'] = diff(eff_addr, act_addr)
+ bridge['member_remove'] = list_diff(eff_intf, act_intf)
# Priority for this bridge
if conf.exists('priority'):
- bridge['priority'] = conf.return_value('priority')
+ bridge['priority'] = int(conf.return_value('priority'))
# Enable spanning tree protocol
if conf.exists('stp'):
- bridge['stp'] = 'on'
+ bridge['stp'] = 1
return bridge
def verify(bridge):
- if bridge is None:
- return None
-
conf = Config()
for br in conf.list_nodes('interfaces bridge'):
# it makes no sense to verify ourself in this case
@@ -190,108 +160,88 @@ def verify(bridge):
for intf in bridge['member']:
tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br))
if intf['name'] in tmp:
- raise ConfigError('{} can be assigned to any one bridge only'.format(intf['name']))
+ raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf']))
+
+ # the interface must exist prior adding it to a bridge
+ for intf in bridge['member']:
+ if intf['name'] not in interfaces():
+ raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf']))
+
+ # bridge members are not allowed to be bond members, too
+ for intf in bridge['member']:
+ for bond in conf.list_nodes('interfaces bonding'):
+ if conf.exists('interfaces bonding ' + bond + ' member interface'):
+ if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'):
+ raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf']))
return None
def generate(bridge):
- if bridge is None:
- return None
-
return None
def apply(bridge):
- if bridge is None:
- return None
+ br = BridgeIf(bridge['intf'])
- cmd = ''
if bridge['deleted']:
- # bridges need to be shutdown first
- cmd += 'ip link set dev "{}" down'.format(bridge['intf'])
- cmd += ' && '
- # delete bridge
- cmd += 'brctl delbr "{}"'.format(bridge['intf'])
- subprocess_cmd(cmd)
-
+ # delete bridge interface
+ # DHCP is stopped inside remove()
+ br.remove()
else:
- # create bridge if it does not exist
- if not os.path.exists("/sys/class/net/" + bridge['intf']):
- # create bridge interface
- cmd += 'brctl addbr "{}"'.format(bridge['intf'])
- cmd += ' && '
- # activate "UP" the interface
- cmd += 'ip link set dev "{}" up'.format(bridge['intf'])
- cmd += ' && '
-
+ # enable interface
+ br.state = 'up'
# set ageing time
- cmd += 'brctl setageing "{}" "{}"'.format(bridge['intf'], bridge['aging'])
- cmd += ' && '
-
+ br.ageing_time = bridge['aging']
# set bridge forward delay
- cmd += 'brctl setfd "{}" "{}"'.format(bridge['intf'], bridge['forwarding_delay'])
- cmd += ' && '
-
+ br.forward_delay = bridge['forwarding_delay']
# set hello time
- cmd += 'brctl sethello "{}" "{}"'.format(bridge['intf'], bridge['hello_time'])
- cmd += ' && '
-
+ br.hello_time = bridge['hello_time']
# set max message age
- cmd += 'brctl setmaxage "{}" "{}"'.format(bridge['intf'], bridge['max_age'])
- cmd += ' && '
-
+ br.max_age = bridge['max_age']
# set bridge priority
- cmd += 'brctl setbridgeprio "{}" "{}"'.format(bridge['intf'], bridge['priority'])
- cmd += ' && '
-
+ br.priority = bridge['priority']
# turn stp on/off
- cmd += 'brctl stp "{}" "{}"'.format(bridge['intf'], bridge['stp'])
-
- for intf in bridge['member_remove']:
- # remove interface from bridge
- cmd += ' && '
- cmd += 'brctl delif "{}" "{}"'.format(bridge['intf'], intf)
-
- for intf in bridge['member']:
- # add interface to bridge
- # but only if it is not yet member of this bridge
- if not os.path.exists('/sys/devices/virtual/net/' + bridge['intf'] + '/brif/' + intf['name']):
- cmd += ' && '
- cmd += 'brctl addif "{}" "{}"'.format(bridge['intf'], intf['name'])
-
- # set bridge port cost
- if intf['cost']:
- cmd += ' && '
- cmd += 'brctl setpathcost "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['cost'])
-
- # set bridge port priority
- if intf['priority']:
- cmd += ' && '
- cmd += 'brctl setportprio "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['priority'])
-
- subprocess_cmd(cmd)
+ br.stp_state = bridge['stp']
+ # enable or disable IGMP querier
+ br.multicast_querier = bridge['igmp_querier']
+ # update interface description used e.g. within SNMP
+ br.ifalias = bridge['description']
# Change interface MAC address
if bridge['mac']:
- VyIfconfig.set_mac_address(bridge['intf'], bridge['mac'])
-
- # update interface description used e.g. within SNMP
- VyIfconfig.set_description(bridge['intf'], bridge['description'])
+ br.mac = bridge['mac']
- # Ignore link state changes?
- VyIfconfig.set_link_detect(bridge['intf'], bridge['disable_link_detect'])
+ # remove interface from bridge
+ for intf in bridge['member_remove']:
+ br.del_port( intf['name'] )
- # enable or disable IGMP querier
- VyIfconfig.set_multicast_querier(bridge['intf'], bridge['igmp_querier'])
+ # add interfaces to bridge
+ for member in bridge['member']:
+ br.add_port(member['name'])
- # ARP cache entry timeout in seconds
- VyIfconfig.set_arp_cache_timeout(bridge['intf'], bridge['arp_cache_timeout_ms'])
+ # up/down interface
+ if bridge['disable']:
+ br.state = 'down'
# Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
for addr in bridge['address_remove']:
- VyIfconfig.remove_interface_address(bridge['intf'], addr)
-
+ br.del_addr(addr)
for addr in bridge['address']:
- VyIfconfig.add_interface_address(bridge['intf'], addr)
+ br.add_addr(addr)
+
+ # configure additional bridge member options
+ for member in bridge['member']:
+ # set bridge port cost
+ br.set_cost(member['name'], member['cost'])
+ # set bridge port priority
+ br.set_priority(member['name'], member['priority'])
+
+ i = Interface(member['name'])
+ # configure ARP cache timeout
+ i.arp_cache_tmo = bridge['arp_cache_tmo']
+ # ignore link state changes
+ i.link_detect = bridge['disable_link_detect']
return None
@@ -303,4 +253,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py
new file mode 100755
index 000000000..614fe08db
--- /dev/null
+++ b/src/conf_mode/interface-dummy.py
@@ -0,0 +1,115 @@
+#!/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/>.
+#
+#
+
+from os import environ
+from copy import deepcopy
+from sys import exit
+
+from vyos.ifconfig import DummyIf
+from vyos.configdict import list_diff
+from vyos.config import Config
+from vyos import ConfigError
+
+default_config_data = {
+ 'address': [],
+ 'address_remove': [],
+ 'deleted': False,
+ 'description': '',
+ 'disable': False,
+ 'intf': ''
+}
+
+def get_config():
+ dummy = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ try:
+ dummy['intf'] = environ['VYOS_TAGNODE_VALUE']
+ except KeyError as E:
+ print("Interface not specified")
+
+ # Check if interface has been removed
+ if not conf.exists('interfaces dummy ' + dummy['intf']):
+ dummy['deleted'] = True
+ return dummy
+
+ # set new configuration level
+ conf.set_level('interfaces dummy ' + dummy['intf'])
+
+ # retrieve configured interface addresses
+ if conf.exists('address'):
+ dummy['address'] = conf.return_values('address')
+
+ # retrieve interface description
+ if conf.exists('description'):
+ dummy['description'] = conf.return_value('description')
+
+ # Disable this interface
+ if conf.exists('disable'):
+ dummy['disable'] = True
+
+ # Determine interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed from the interface
+ eff_addr = conf.return_effective_values('address')
+ act_addr = conf.return_values('address')
+ dummy['address_remove'] = list_diff(eff_addr, act_addr)
+
+ return dummy
+
+def verify(dummy):
+ return None
+
+def generate(dummy):
+ return None
+
+def apply(dummy):
+ du = DummyIf(dummy['intf'])
+
+ # Remove dummy interface
+ if dummy['deleted']:
+ du.remove()
+ else:
+ # enable interface
+ du.state = 'up'
+ # update interface description used e.g. within SNMP
+ du.ifalias = dummy['description']
+
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in dummy['address_remove']:
+ du.del_addr(addr)
+ for addr in dummy['address']:
+ du.add_addr(addr)
+
+ # disable interface on demand
+ if dummy['disable']:
+ du.state = 'down'
+
+ 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/interface-loopback.py b/src/conf_mode/interface-loopback.py
new file mode 100755
index 000000000..a1a807868
--- /dev/null
+++ b/src/conf_mode/interface-loopback.py
@@ -0,0 +1,101 @@
+#!/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/>.
+#
+
+from os import environ
+from sys import exit
+from copy import deepcopy
+
+from vyos.ifconfig import LoopbackIf
+from vyos.configdict import list_diff
+from vyos.config import Config
+from vyos import ConfigError
+
+default_config_data = {
+ 'address': [],
+ 'address_remove': [],
+ 'deleted': False,
+ 'description': '',
+}
+
+
+def get_config():
+ loopback = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ try:
+ loopback['intf'] = environ['VYOS_TAGNODE_VALUE']
+ except KeyError as E:
+ print("Interface not specified")
+
+ # Check if interface has been removed
+ if not conf.exists('interfaces loopback ' + loopback['intf']):
+ loopback['deleted'] = True
+
+ # set new configuration level
+ conf.set_level('interfaces loopback ' + loopback['intf'])
+
+ # retrieve configured interface addresses
+ if conf.exists('address'):
+ loopback['address'] = conf.return_values('address')
+
+ # retrieve interface description
+ if conf.exists('description'):
+ loopback['description'] = conf.return_value('description')
+
+ # Determine interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed from the interface
+ eff_addr = conf.return_effective_values('address')
+ act_addr = conf.return_values('address')
+ loopback['address_remove'] = list_diff(eff_addr, act_addr)
+
+ return loopback
+
+def verify(loopback):
+ return None
+
+def generate(loopback):
+ return None
+
+def apply(loopback):
+ lo = LoopbackIf(loopback['intf'])
+ if not loopback['deleted']:
+ # update interface description used e.g. within SNMP
+ # update interface description used e.g. within SNMP
+ lo.ifalias = loopback['description']
+
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in loopback['address']:
+ lo.add_addr(addr)
+
+ # remove interface address(es)
+ for addr in loopback['address_remove']:
+ lo.del_addr(addr)
+
+ 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/interface-openvpn.py b/src/conf_mode/interface-openvpn.py
index 4e5915d4e..548c78535 100755
--- a/src/conf_mode/interface-openvpn.py
+++ b/src/conf_mode/interface-openvpn.py
@@ -18,24 +18,25 @@
import os
import re
-import pwd
-import grp
import sys
import stat
-import copy
import jinja2
-import psutil
-from ipaddress import ip_address,ip_network,IPv4Interface
-from signal import SIGUSR1
+from copy import deepcopy
+from grp import getgrnam
+from ipaddress import ip_address,ip_network,IPv4Interface
+from netifaces import interfaces
+from psutil import pid_exists
+from pwd import getpwnam
from subprocess import Popen, PIPE
+from time import sleep
from vyos.config import Config
from vyos import ConfigError
from vyos.validate import is_addr_assigned
-user = 'nobody'
-group = 'nogroup'
+user = 'openvpn'
+group = 'openvpn'
# Please be careful if you edit the template.
config_tmpl = """
@@ -58,6 +59,7 @@ dev {{ intf }}
user {{ uid }}
group {{ gid }}
persist-key
+iproute /usr/libexec/vyos/system/unpriv-ip
proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %}
@@ -301,8 +303,8 @@ def openvpn_mkdir(directory):
# fix permissions - corresponds to mode 755
os.chmod(directory, stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
- uid = pwd.getpwnam(user).pw_uid
- gid = grp.getgrnam(group).gr_gid
+ uid = getpwnam(user).pw_uid
+ gid = getgrnam(group).gr_gid
os.chown(directory, uid, gid)
def fixup_permission(filename, permission=stat.S_IRUSR):
@@ -314,8 +316,8 @@ def fixup_permission(filename, permission=stat.S_IRUSR):
os.chmod(filename, permission)
# make file owned by root / vyattacfg
- uid = pwd.getpwnam('root').pw_uid
- gid = grp.getgrnam('vyattacfg').gr_gid
+ uid = getpwnam('root').pw_uid
+ gid = getgrnam('vyattacfg').gr_gid
os.chown(filename, uid, gid)
def checkCertHeader(header, filename):
@@ -334,7 +336,7 @@ def checkCertHeader(header, filename):
return False
def get_config():
- openvpn = copy.deepcopy(default_config_data)
+ openvpn = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
@@ -792,8 +794,8 @@ def generate(openvpn):
fixup_permission(auth_file)
# get numeric uid/gid
- uid = pwd.getpwnam(user).pw_uid
- gid = grp.getgrnam(group).gr_gid
+ uid = getpwnam(user).pw_uid
+ gid = getgrnam(group).gr_gid
# Generate client specific configuration
for client in openvpn['client']:
@@ -806,6 +808,11 @@ def generate(openvpn):
tmpl = jinja2.Template(config_tmpl)
config_text = tmpl.render(openvpn)
+
+ # we need to support quoting of raw parameters from OpenVPN CLI
+ # see https://phabricator.vyos.net/T1632
+ config_text = config_text.replace("&quot;",'"')
+
with open(get_config_name(interface), 'w') as f:
f.write(config_text)
os.chown(get_config_name(interface), uid, gid)
@@ -813,47 +820,46 @@ def generate(openvpn):
return None
def apply(openvpn):
- interface = openvpn['intf']
-
pid = 0
- pidfile = '/var/run/openvpn/{}.pid'.format(interface)
+ pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf'])
if os.path.isfile(pidfile):
pid = 0
with open(pidfile, 'r') as f:
pid = int(f.read())
- # If tunnel interface has been deleted - stop service
- if openvpn['deleted'] or openvpn['disable']:
- directory = os.path.dirname(get_config_name(interface))
+ # Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the
+ # service as the configuration is not re-read. Stop daemon only if it's
+ # running - it could have died or killed by someone evil
+ if pid_exists(pid):
+ cmd = 'start-stop-daemon --stop --quiet'
+ cmd += ' --pidfile ' + pidfile
+ subprocess_cmd(cmd)
- # we only need to stop the demon if it's running
- # daemon could have died or killed by someone
- if psutil.pid_exists(pid):
- cmd = 'start-stop-daemon --stop --quiet'
- cmd += ' --pidfile ' + pidfile
- subprocess_cmd(cmd)
-
- # cleanup old PID file
- if os.path.isfile(pidfile):
- os.remove(pidfile)
+ # cleanup old PID file
+ if os.path.isfile(pidfile):
+ os.remove(pidfile)
+ # Do some cleanup when OpenVPN is disabled/deleted
+ if openvpn['deleted'] or openvpn['disable']:
# cleanup old configuration file
- if os.path.isfile(get_config_name(interface)):
- os.remove(get_config_name(interface))
+ if os.path.isfile(get_config_name(openvpn['intf'])):
+ os.remove(get_config_name(openvpn['intf']))
# cleanup client config dir
- if os.path.isdir(directory + '/ccd/' + interface):
+ directory = os.path.dirname(get_config_name(openvpn['intf']))
+ if os.path.isdir(directory + '/ccd/' + openvpn['intf']):
try:
- os.remove(directory + '/ccd/' + interface + '/*')
+ os.remove(directory + '/ccd/' + openvpn['intf'] + '/*')
except:
pass
return None
- # Send SIGUSR1 to the process instead of creating a new process
- if psutil.pid_exists(pid):
- os.kill(pid, SIGUSR1)
- return None
+ # On configuration change we need to wait for the 'old' interface to
+ # vanish from the Kernel, if it is not gone, OpenVPN will report:
+ # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16)
+ while openvpn['intf'] in interfaces():
+ sleep(0.250) # 250ms
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
@@ -862,13 +868,13 @@ def apply(openvpn):
cmd += ' --exec /usr/sbin/openvpn'
# now pass arguments to openvpn binary
cmd += ' --'
- cmd += ' --config ' + get_config_name(interface)
+ cmd += ' --config ' + get_config_name(openvpn['intf'])
# execute assembled command
subprocess_cmd(cmd)
-
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/interface-vxlan.py b/src/conf_mode/interface-vxlan.py
new file mode 100755
index 000000000..59022238e
--- /dev/null
+++ b/src/conf_mode/interface-vxlan.py
@@ -0,0 +1,208 @@
+#!/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/>.
+#
+
+from os import environ
+from sys import exit
+from copy import deepcopy
+
+from vyos.configdict import list_diff
+from vyos.config import Config
+from vyos.ifconfig import VXLANIf, Interface
+from vyos.interfaces import get_type_of_interface
+from vyos import ConfigError
+from netifaces import interfaces
+
+default_config_data = {
+ 'address': [],
+ 'address_remove': [],
+ 'deleted': False,
+ 'description': '',
+ 'disable': False,
+ 'group': '',
+ 'intf': '',
+ 'ip_arp_cache_tmo': 30,
+ 'ip_proxy_arp': 0,
+ 'link': '',
+ 'mtu': 1450,
+ 'remote': '',
+ 'remote_port': 8472 # The Linux implementation of VXLAN pre-dates
+ # the IANA's selection of a standard destination port
+}
+
+
+def get_config():
+ vxlan = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ try:
+ vxlan['intf'] = environ['VYOS_TAGNODE_VALUE']
+ except KeyError as E:
+ print("Interface not specified")
+
+ # Check if interface has been removed
+ if not conf.exists('interfaces vxlan ' + vxlan['intf']):
+ vxlan['deleted'] = True
+ return vxlan
+
+ # set new configuration level
+ conf.set_level('interfaces vxlan ' + vxlan['intf'])
+
+ # retrieve configured interface addresses
+ if conf.exists('address'):
+ vxlan['address'] = conf.return_values('address')
+
+ # Determine interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed from the interface
+ eff_addr = conf.return_effective_values('address')
+ act_addr = conf.return_values('address')
+ vxlan['address_remove'] = list_diff(eff_addr, act_addr)
+
+ # retrieve interface description
+ if conf.exists('description'):
+ vxlan['description'] = conf.return_value('description')
+
+ # Disable this interface
+ if conf.exists('disable'):
+ vxlan['disable'] = True
+
+ # VXLAN multicast grou
+ if conf.exists('group'):
+ vxlan['group'] = conf.return_value('group')
+
+ # ARP cache entry timeout in seconds
+ if conf.exists('ip arp-cache-timeout'):
+ vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
+
+ # Enable proxy-arp on this interface
+ if conf.exists('ip enable-proxy-arp'):
+ vxlan['ip_proxy_arp'] = 1
+
+ # VXLAN underlay interface
+ if conf.exists('link'):
+ vxlan['link'] = conf.return_value('link')
+
+ # Maximum Transmission Unit (MTU)
+ if conf.exists('mtu'):
+ vxlan['mtu'] = int(conf.return_value('mtu'))
+
+ # Remote address of VXLAN tunnel
+ if conf.exists('remote'):
+ vxlan['remote'] = conf.return_value('remote')
+
+ # Remote port of VXLAN tunnel
+ if conf.exists('port'):
+ vxlan['remote_port'] = int(conf.return_value('port'))
+
+ # Virtual Network Identifier
+ if conf.exists('vni'):
+ vxlan['vni'] = conf.return_value('vni')
+
+ return vxlan
+
+
+def verify(vxlan):
+ if vxlan['deleted']:
+ # bail out early
+ return None
+
+ if vxlan['mtu'] < 1500:
+ print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
+
+ if vxlan['group'] and not vxlan['link']:
+ raise ConfigError('Multicast VXLAN requires an underlaying interface ')
+
+ if not (vxlan['group'] or vxlan['remote']):
+ raise ConfigError('Group or remote must be configured')
+
+ if not vxlan['vni']:
+ raise ConfigError('Must configure VNI for VXLAN')
+
+ if vxlan['link']:
+ # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU
+ # if our configured MTU is at least 50 bytes less
+ underlay_mtu = int(Interface(vxlan['link']).mtu)
+ if underlay_mtu < (vxlan['mtu'] + 50):
+ raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
+ 'MTU is to small ({})'.format(underlay_mtu))
+
+ return None
+
+
+def generate(vxlan):
+ return None
+
+
+def apply(vxlan):
+ # Check if the VXLAN interface already exists
+ if vxlan['intf'] in interfaces():
+ v = VXLANIf(vxlan['intf'])
+ # VXLAN is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ v.remove()
+
+ if not vxlan['deleted']:
+ # VXLAN interface needs to be created on-block
+ # instead of passing a ton of arguments, I just use a dict
+ # that is managed by vyos.ifconfig
+ conf = deepcopy(VXLANIf.get_config())
+
+ # Assign VXLAN instance configuration parameters to config dict
+ conf['vni'] = vxlan['vni']
+ conf['group'] = vxlan['group']
+ conf['dev'] = vxlan['link']
+ conf['remote'] = vxlan['remote']
+ conf['port'] = vxlan['remote_port']
+
+ # Finally create the new interface
+ v = VXLANIf(vxlan['intf'], config=conf)
+ # update interface description used e.g. by SNMP
+ v.ifalias = vxlan['description']
+ # Maximum Transfer Unit (MTU)
+ v.mtu = vxlan['mtu']
+
+ # configure ARP cache timeout in milliseconds
+ v.arp_cache_tmp = vxlan['ip_arp_cache_tmo']
+ # Enable proxy-arp on this interface
+ v.proxy_arp = vxlan['ip_proxy_arp']
+
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in vxlan['address_remove']:
+ v.del_addr(addr)
+ for addr in vxlan['address']:
+ v.add_addr(addr)
+
+ # As the bond interface is always disabled first when changing
+ # parameters we will only re-enable the interface if it is not
+ # administratively disabled
+ if not vxlan['disable']:
+ v.state='up'
+
+ 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/interface-wireguard.py b/src/conf_mode/interface-wireguard.py
index 8234fad0b..d51a7a08d 100755
--- a/src/conf_mode/interface-wireguard.py
+++ b/src/conf_mode/interface-wireguard.py
@@ -24,350 +24,243 @@ import subprocess
from vyos.config import Config
from vyos import ConfigError
+from vyos.ifconfig import WireGuardIf
-dir = r'/config/auth/wireguard'
-pk = dir + '/private.key'
-pub = dir + '/public.key'
-psk_file = r'/tmp/psk'
+ifname = str(os.environ['VYOS_TAGNODE_VALUE'])
+intfc = WireGuardIf(ifname)
+
+kdir = r'/config/auth/wireguard'
def check_kmod():
- if not os.path.exists('/sys/module/wireguard'):
- sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
- if os.system('sudo modprobe wireguard') != 0:
- sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed")
- raise ConfigError("modprobe wireguard failed")
+ if not os.path.exists('/sys/module/wireguard'):
+ sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
+ if os.system('sudo modprobe wireguard') != 0:
+ sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed")
+ raise ConfigError("modprobe wireguard failed")
+
def get_config():
- c = Config()
- if not c.exists('interfaces wireguard'):
- return None
-
- c.set_level('interfaces')
- intfcs = c.list_nodes('wireguard')
- intfcs_eff = c.list_effective_nodes('wireguard')
- new_lst = list(set(intfcs) - set(intfcs_eff))
- del_lst = list(set(intfcs_eff) - set(intfcs))
-
- config_data = {
- 'interfaces' : {}
- }
- ### setting defaults and determine status of the config
- for intfc in intfcs:
- cnf = 'wireguard ' + intfc
- # default data struct
- config_data['interfaces'].update(
- {
- intfc : {
- 'addr' : '',
- 'descr' : intfc, ## snmp ifAlias
- 'lport' : '',
- 'status' : 'exists',
- 'state' : 'enabled',
- 'fwmark' : 0x00,
- 'mtu' : '1420',
- 'peer' : {}
- }
+ c = Config()
+ if not c.exists('interfaces wireguard'):
+ return None
+
+ config_data = {
+ ifname: {
+ 'addr': '',
+ 'descr': ifname,
+ 'lport': None,
+ 'status': 'exists',
+ 'state': 'enabled',
+ 'fwmark': 0x00,
+ 'mtu': 1420,
+ 'peer': {},
+ 'pk' : '{}/default/private.key'.format(kdir)
}
- )
-
- ### determine status either delete or create
- for i in new_lst:
- config_data['interfaces'][i]['status'] = 'create'
-
- for i in del_lst:
- config_data['interfaces'].update(
- {
- i : {
- 'status': 'delete'
- }
- }
- )
-
- ### based on the status, setup conf values
- for intfc in intfcs:
- cnf = 'wireguard ' + intfc
- if config_data['interfaces'][intfc]['status'] != 'delete':
- ### addresses
- if c.exists(cnf + ' address'):
- config_data['interfaces'][intfc]['addr'] = c.return_values(cnf + ' address')
- ### interface up/down
- if c.exists(cnf + ' disable'):
- config_data['interfaces'][intfc]['state'] = 'disable'
- ### listen port
- if c.exists(cnf + ' port'):
- config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port')
- ### fwmark
- if c.exists(cnf + ' fwmark'):
- config_data['interfaces'][intfc]['fwmark'] = c.return_value(cnf + ' fwmark')
- ### description
- if c.exists(cnf + ' description'):
- config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description')
- ### mtu
- if c.exists(cnf + ' mtu'):
- config_data['interfaces'][intfc]['mtu'] = c.return_value(cnf + ' mtu')
- ### peers
- if c.exists(cnf + ' peer'):
- for p in c.list_nodes(cnf + ' peer'):
- if not c.exists(cnf + ' peer ' + p + ' disable'):
- config_data['interfaces'][intfc]['peer'].update(
- {
- p : {
- 'allowed-ips' : [],
- 'endpoint' : '',
- 'pubkey' : ''
- }
- }
- )
- if c.exists(cnf + ' peer ' + p + ' pubkey'):
- config_data['interfaces'][intfc]['peer'][p]['pubkey'] = c.return_value(cnf + ' peer ' + p + ' pubkey')
- if c.exists(cnf + ' peer ' + p + ' allowed-ips'):
- config_data['interfaces'][intfc]['peer'][p]['allowed-ips'] = c.return_values(cnf + ' peer ' + p + ' allowed-ips')
- if c.exists(cnf + ' peer ' + p + ' endpoint'):
- config_data['interfaces'][intfc]['peer'][p]['endpoint'] = c.return_value(cnf + ' peer ' + p + ' endpoint')
- if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'):
- config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive')
- if c.exists(cnf + ' peer ' + p + ' preshared-key'):
- config_data['interfaces'][intfc]['peer'][p]['psk'] = c.return_value(cnf + ' peer ' + p + ' preshared-key')
-
- return config_data
+ }
+
+ c.set_level('interfaces wireguard')
+ if not c.exists_effective(ifname):
+ config_data[ifname]['status'] = 'create'
+
+ if not c.exists(ifname) and c.exists_effective(ifname):
+ config_data[ifname]['status'] = 'delete'
+
+ if config_data[ifname]['status'] != 'delete':
+ if c.exists(ifname + ' address'):
+ config_data[ifname]['addr'] = c.return_values(ifname + ' address')
+ if c.exists(ifname + ' disable'):
+ config_data[ifname]['state'] = 'disable'
+ if c.exists(ifname + ' port'):
+ config_data[ifname]['lport'] = c.return_value(ifname + ' port')
+ if c.exists(ifname + ' fwmark'):
+ config_data[ifname]['fwmark'] = c.return_value(ifname + ' fwmark')
+ if c.exists(ifname + ' description'):
+ config_data[ifname]['descr'] = c.return_value(
+ ifname + ' description')
+ if c.exists(ifname + ' mtu'):
+ config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu')
+ if c.exists(ifname + ' private-key'):
+ config_data[ifname]['pk'] = "{0}/{1}/private.key".format(kdir,c.return_value(ifname + ' private-key'))
+ if c.exists(ifname + ' peer'):
+ for p in c.list_nodes(ifname + ' peer'):
+ if not c.exists(ifname + ' peer ' + p + ' disable'):
+ config_data[ifname]['peer'].update(
+ {
+ p: {
+ 'allowed-ips': [],
+ 'endpoint': '',
+ 'pubkey': ''
+ }
+ }
+ )
+ if c.exists(ifname + ' peer ' + p + ' pubkey'):
+ config_data[ifname]['peer'][p]['pubkey'] = c.return_value(
+ ifname + ' peer ' + p + ' pubkey')
+ if c.exists(ifname + ' peer ' + p + ' allowed-ips'):
+ config_data[ifname]['peer'][p]['allowed-ips'] = c.return_values(
+ ifname + ' peer ' + p + ' allowed-ips')
+ if c.exists(ifname + ' peer ' + p + ' endpoint'):
+ config_data[ifname]['peer'][p]['endpoint'] = c.return_value(
+ ifname + ' peer ' + p + ' endpoint')
+ if c.exists(ifname + ' peer ' + p + ' persistent-keepalive'):
+ config_data[ifname]['peer'][p]['persistent-keepalive'] = c.return_value(
+ ifname + ' peer ' + p + ' persistent-keepalive')
+ if c.exists(ifname + ' peer ' + p + ' preshared-key'):
+ config_data[ifname]['peer'][p]['psk'] = c.return_value(
+ ifname + ' peer ' + p + ' preshared-key')
+
+ return config_data
def verify(c):
- if not c:
- return None
+ if not c:
+ return None
- for i in c['interfaces']:
- if c['interfaces'][i]['status'] != 'delete':
- if not c['interfaces'][i]['addr']:
- raise ConfigError("address required for interface " + i)
- if not c['interfaces'][i]['peer']:
- raise ConfigError("peer required on interface " + i)
+ if not os.path.exists(c[ifname]['pk']):
+ raise ConfigError(
+ "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'")
- for p in c['interfaces'][i]['peer']:
- if not c['interfaces'][i]['peer'][p]['allowed-ips']:
- raise ConfigError("allowed-ips required on interface " + i + " for peer " + p)
- if not c['interfaces'][i]['peer'][p]['pubkey']:
- raise ConfigError("pubkey from your peer is mandatory on " + i + " for peer " + p)
+ if c[ifname]['status'] != 'delete':
+ if not c[ifname]['addr']:
+ raise ConfigError("ERROR: IP address required")
+ if not c[ifname]['peer']:
+ raise ConfigError("ERROR: peer required")
+ for p in c[ifname]['peer']:
+ if not c[ifname]['peer'][p]['allowed-ips']:
+ raise ConfigError("ERROR: allowed-ips required for peer " + p)
+ if not c[ifname]['peer'][p]['pubkey']:
+ raise ConfigError("peer pubkey required for peer " + p)
def apply(c):
- ### no wg config left, delete all wireguard devices on the os
- if not c:
- net_devs = os.listdir('/sys/class/net/')
- for dev in net_devs:
- if os.path.isdir('/sys/class/net/' + dev):
- buf = open('/sys/class/net/' + dev + '/uevent', 'r').read()
- if re.search("DEVTYPE=wireguard", buf, re.I|re.M):
- wg_intf = re.sub("INTERFACE=", "", re.search("INTERFACE=.*", buf, re.I|re.M).group(0))
- sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf)
- subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True)
- return None
-
- ###
- ## find the diffs between effective config an new config
- ###
- c_eff = Config()
- c_eff.set_level('interfaces wireguard')
-
- ### link status up/down aka interface disable
-
- for intf in c['interfaces']:
- if not c['interfaces'][intf]['status'] == 'delete':
- if c['interfaces'][intf]['state'] == 'disable':
- sl.syslog(sl.LOG_NOTICE, "disable interface " + intf)
- subprocess.call(['ip l s dev ' + intf + ' down ' + ' &>/dev/null'], shell=True)
- else:
- sl.syslog(sl.LOG_NOTICE, "enable interface " + intf)
- subprocess.call(['ip l s dev ' + intf + ' up ' + ' &>/dev/null'], shell=True)
-
- ### deletion of a specific interface
- for intf in c['interfaces']:
- if c['interfaces'][intf]['status'] == 'delete':
- sl.syslog(sl.LOG_NOTICE, "removing interface " + intf)
- subprocess.call(['ip l d dev ' + intf + ' &>/dev/null'], shell=True)
-
- ### peer deletion
- peer_eff = c_eff.list_effective_nodes( intf + ' peer')
+ # no wg config left, delete all wireguard devices, if any
+ if not c:
+ net_devs = os.listdir('/sys/class/net/')
+ for dev in net_devs:
+ if os.path.isdir('/sys/class/net/' + dev):
+ buf = open('/sys/class/net/' + dev + '/uevent', 'r').read()
+ if re.search("DEVTYPE=wireguard", buf, re.I | re.M):
+ wg_intf = re.sub("INTERFACE=", "", re.search(
+ "INTERFACE=.*", buf, re.I | re.M).group(0))
+ sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf)
+ subprocess.call(
+ ['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True)
+ return None
+
+ # interface removal
+ if c[ifname]['status'] == 'delete':
+ sl.syslog(sl.LOG_NOTICE, "removing interface " + ifname)
+ intfc.remove()
+ return None
+
+ c_eff = Config()
+ c_eff.set_level('interfaces wireguard')
+
+ # interface state
+ if c[ifname]['state'] == 'disable':
+ sl.syslog(sl.LOG_NOTICE, "disable interface " + ifname)
+ intfc.state = 'down'
+ else:
+ if not intfc.state == 'up':
+ sl.syslog(sl.LOG_NOTICE, "enable interface " + ifname)
+ intfc.state = 'up'
+
+ # IP address
+ if not c_eff.exists_effective(ifname + ' address'):
+ for ip in c[ifname]['addr']:
+ intfc.add_addr(ip)
+ else:
+ addr_eff = c_eff.return_effective_values(ifname + ' address')
+ addr_rem = list(set(addr_eff) - set(c[ifname]['addr']))
+ addr_add = list(set(c[ifname]['addr']) - set(addr_eff))
+
+ if len(addr_rem) != 0:
+ for ip in addr_rem:
+ sl.syslog(
+ sl.LOG_NOTICE, "remove IP address {0} from {1}".format(ip, ifname))
+ intfc.del_addr(ip)
+
+ if len(addr_add) != 0:
+ for ip in addr_add:
+ sl.syslog(
+ sl.LOG_NOTICE, "add IP address {0} to {1}".format(ip, ifname))
+ intfc.add_addr(ip)
+
+ # interface MTU
+ if c[ifname]['mtu'] != 1420:
+ intfc.mtu = int(c[ifname]['mtu'])
+ else:
+ # default is set to 1420 in config_data
+ intfc.mtu = int(c[ifname]['mtu'])
+
+ # ifalias for snmp from description
+ descr_eff = c_eff.return_effective_value(ifname + ' description')
+ if descr_eff != c[ifname]['descr']:
+ intfc.ifalias = str(c[ifname]['descr'])
+
+ # peer deletion
+ peer_eff = c_eff.list_effective_nodes(ifname + ' peer')
peer_cnf = []
+
try:
- for p in c['interfaces'][intf]['peer']:
- peer_cnf.append(p)
+ for p in c[ifname]['peer']:
+ peer_cnf.append(p)
except KeyError:
- pass
+ pass
peer_rem = list(set(peer_eff) - set(peer_cnf))
for p in peer_rem:
- pkey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey')
- remove_peer(intf, pkey)
+ pkey = c_eff.return_effective_value(ifname + ' peer ' + p + ' pubkey')
+ intfc.remove_peer(pkey)
- ### peer pubkey update
- ### wg identifies peers by its pubky, so we have to remove the peer first
- ### it will recreated it then below with the new key from the cli config
+ # peer key update
for p in peer_eff:
- if p in peer_cnf:
- ekey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey')
- nkey = c['interfaces'][intf]['peer'][p]['pubkey']
- if nkey != ekey:
- sl.syslog(sl.LOG_NOTICE, "peer " + p + ' changed pubkey from ' + ekey + 'to key ' + nkey + ' on interface ' + intf)
- remove_peer(intf, ekey)
-
- ### new config
- if c['interfaces'][intf]['status'] == 'create':
- if not os.path.exists(pk):
- raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'")
-
- subprocess.call(['ip l a dev ' + intf + ' type wireguard 2>/dev/null'], shell=True)
- for addr in c['interfaces'][intf]['addr']:
- add_addr(intf, addr)
-
- subprocess.call(['ip l set up dev ' + intf + ' mtu ' + c['interfaces'][intf]['mtu'] + ' &>/dev/null'], shell=True)
- configure_interface(c, intf)
-
- ### config updates
- if c['interfaces'][intf]['status'] == 'exists':
- ### IP address change
- addr_eff = c_eff.return_effective_values(intf + ' address')
- addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr']))
- addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff))
-
- if len(addr_rem) != 0:
- for addr in addr_rem:
- del_addr(intf, addr)
-
- if len(addr_add) != 0:
- for addr in addr_add:
- add_addr(intf, addr)
-
- ## mtu update
- mtu = c['interfaces'][intf]['mtu']
- if mtu != 1420:
- sl.syslog(sl.LOG_NOTICE, "setting mtu to " + mtu + " on " + intf)
- subprocess.call(['ip l set mtu ' + mtu + ' dev ' + intf + ' &>/dev/null'], shell=True)
-
-
- ### persistent-keepalive
- for p in c['interfaces'][intf]['peer']:
- val_eff = ""
- val = ""
-
- try:
- val = c['interfaces'][intf]['peer'][p]['persistent-keepalive']
- except KeyError:
- pass
-
- if c_eff.exists_effective(intf + ' peer ' + p + ' persistent-keepalive'):
- val_eff = c_eff.return_effective_value(intf + ' peer ' + p + ' persistent-keepalive')
-
- ### disable keepalive
- if val_eff and not val:
- c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = 0
-
- ### set new keepalive value
- if not val_eff and val:
- c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = val
-
- ## wg command call
- configure_interface(c, intf)
-
- ### ifalias for snmp from description
- if c['interfaces'][intf]['status'] != 'delete':
- descr_eff = c_eff.return_effective_value(intf + ' description')
- cnf_descr = c['interfaces'][intf]['descr']
- if descr_eff != cnf_descr:
- with open('/sys/class/net/' + str(intf) + '/ifalias', 'w') as fh:
- fh.write(str(cnf_descr))
-
-def configure_interface(c, intf):
- for p in c['interfaces'][intf]['peer']:
- ## config init for wg call
- wg_config = {
- 'interface' : intf,
- 'port' : 0,
- 'private-key' : pk,
- 'pubkey' : '',
- 'psk' : '/dev/null',
- 'allowed-ips' : [],
- 'fwmark' : 0x00,
- 'endpoint' : None,
- 'keepalive' : 0
- }
-
- ## mandatory settings
- wg_config['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey']
- wg_config['allowed-ips'] = c['interfaces'][intf]['peer'][p]['allowed-ips']
-
- ## optional settings
- # listen-port
- if c['interfaces'][intf]['lport']:
- wg_config['port'] = c['interfaces'][intf]['lport']
-
- ## fwmark
- if c['interfaces'][intf]['fwmark']:
- wg_config['fwmark'] = c['interfaces'][intf]['fwmark']
-
- ## endpoint
- if c['interfaces'][intf]['peer'][p]['endpoint']:
- wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint']
-
- ## persistent-keepalive
- if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]:
- wg_config['keepalive'] = c['interfaces'][intf]['peer'][p]['persistent-keepalive']
-
- ## preshared-key - is only read from a file, it's called via sudo redirection doesn't work either
- if 'psk' in c['interfaces'][intf]['peer'][p]:
- old_umask = os.umask(0o077)
- open(psk_file, 'w').write(str(c['interfaces'][intf]['peer'][p]['psk']))
- os.umask(old_umask)
- wg_config['psk'] = psk_file
-
- ### assemble wg command
- cmd = "sudo wg set " + intf
- cmd += " listen-port " + str(wg_config['port'])
- cmd += " fwmark " + str(wg_config['fwmark'])
- cmd += " private-key " + wg_config['private-key']
- cmd += " peer " + wg_config['pubkey']
- cmd += " preshared-key " + wg_config['psk']
- cmd += " allowed-ips "
- for ap in wg_config['allowed-ips']:
- if ap != wg_config['allowed-ips'][-1]:
- cmd += ap + ","
- else:
- cmd += ap
-
- if wg_config['endpoint']:
- cmd += " endpoint " + wg_config['endpoint']
-
- if wg_config['keepalive'] != 0:
- cmd += " persistent-keepalive " + wg_config['keepalive']
- else:
- cmd += " persistent-keepalive 0"
-
- sl.syslog(sl.LOG_NOTICE, cmd)
- #print (cmd)
- subprocess.call([cmd], shell=True)
- """ remove psk_file """
- if os.path.exists(psk_file):
- os.remove(psk_file)
-
-def add_addr(intf, addr):
- # see https://phabricator.vyos.net/T949
- ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True)
- sl.syslog(sl.LOG_NOTICE, "ip a a dev " + intf + " " + addr)
-
-def del_addr(intf, addr):
- ret = subprocess.call(['ip a d dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True)
- sl.syslog(sl.LOG_NOTICE, "ip a d dev " + intf + " " + addr)
-
-def remove_peer(intf, peer_key):
- cmd = 'sudo wg set ' + str(intf) + ' peer ' + peer_key + ' remove &>/dev/null'
- ret = subprocess.call([cmd], shell=True)
- sl.syslog(sl.LOG_NOTICE, "peer " + peer_key + " removed from " + intf)
+ if p in peer_cnf:
+ ekey = c_eff.return_effective_value(
+ ifname + ' peer ' + p + ' pubkey')
+ nkey = c[ifname]['peer'][p]['pubkey']
+ if nkey != ekey:
+ sl.syslog(
+ sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname))
+ intfc.remove_peer(ekey)
+
+ intfc.config['private-key'] = c[ifname]['pk']
+ for p in c[ifname]['peer']:
+ intfc.config['pubkey'] = str(c[ifname]['peer'][p]['pubkey'])
+ intfc.config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips'])
+
+ # listen-port
+ if c[ifname]['lport']:
+ intfc.config['port'] = c[ifname]['lport']
+
+ # fwmark
+ if c[ifname]['fwmark']:
+ intfc.config['fwmark'] = c[ifname]['fwmark']
+
+ # endpoint
+ if c[ifname]['peer'][p]['endpoint']:
+ intfc.config['endpoint'] = c[ifname]['peer'][p]['endpoint']
+
+ # persistent-keepalive
+ if 'persistent-keepalive' in c[ifname]['peer'][p]:
+ intfc.config['keepalive'] = c[ifname][
+ 'peer'][p]['persistent-keepalive']
+
+ # preshared-key - needs to be read from a file
+ if 'psk' in c[ifname]['peer'][p]:
+ psk_file = '/config/auth/wireguard/psk'
+ old_umask = os.umask(0o077)
+ open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk']))
+ os.umask(old_umask)
+ intfc.config['psk'] = psk_file
+
+ intfc.update()
if __name__ == '__main__':
- try:
- check_kmod()
- c = get_config()
- verify(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
+ try:
+ check_kmod()
+ c = get_config()
+ verify(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 8d25e7abd..156bb2edd 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -62,7 +62,7 @@ conn {{ra_conn_name}}
left={{outside_addr}}
leftsubnet=%dynamic[/1701]
rightsubnet=%dynamic
- mark=%unique
+ mark_in=%unique
auto=add
ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024!
dpddelay=15
diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py
index 7b79c701b..c4f3d2c9c 100755
--- a/src/conf_mode/syslog.py
+++ b/src/conf_mode/syslog.py
@@ -24,16 +24,16 @@ import jinja2
from vyos.config import Config
from vyos import ConfigError
-########### config templates
+# config templates
-#### /etc/rsyslog.d/vyos-rsyslog.conf ###
+# /etc/rsyslog.d/vyos-rsyslog.conf ###
configs = '''
## generated by syslog.py ##
## file based logging
{% if files['global']['marker'] -%}
$ModLoad immark
{% if files['global']['marker-interval'] %}
-$MarkMessagePeriod {{files['global']['marker-interval']}}
+$MarkMessagePeriod {{files['global']['marker-interval']}}
{% endif %}
{% endif -%}
{% if files['global']['preserver_fqdn'] -%}
@@ -80,217 +80,241 @@ logrotate_configs = '''
}
{% endfor %}
'''
-############# config templates end
+# config templates end
+
def get_config():
- c = Config()
- if not c.exists('system syslog'):
- return None
- c.set_level('system syslog')
-
- config_data = {
- 'files' : {},
- 'console' : {},
- 'hosts' : {},
- 'user' : {}
- }
-
- #####
- # /etc/rsyslog.d/vyos-rsyslog.conf
- # 'set system syslog global'
- #####
- config_data['files'].update(
- {
- 'global' : {
- 'log-file' : '/var/log/messages',
- 'max-size' : 262144,
- 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog',
- 'selectors' : '*.notice;local7.debug',
- 'max-files' : '5',
- 'preserver_fqdn' : False
- }
- }
- )
-
- if c.exists('global marker'):
- config_data['files']['global']['marker'] = True
- if c.exists('global marker interval'):
- config_data['files']['global']['marker-interval'] = c.return_value('global marker interval')
- if c.exists('global facility'):
- config_data['files']['global']['selectors'] = generate_selectors(c, 'global facility')
- if c.exists('global archive size'):
- config_data['files']['global']['max-size'] = int(c.return_value('global archive size'))* 1024
- if c.exists('global archive file'):
- config_data['files']['global']['max-files'] = c.return_value('global archive file')
- if c.exists('global preserve-fqdn'):
- config_data['files']['global']['preserver_fqdn'] = True
-
- ###
- # set system syslog file
- ###
-
- if c.exists('file'):
- filenames = c.list_nodes('file')
- for filename in filenames:
- config_data['files'].update(
- {
- filename : {
- 'log-file' : '/var/log/user/' + filename,
- 'max-files' : '5',
- 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/' + filename,
- 'selectors' : '*.err',
- 'max-size' : 262144
- }
- }
- )
-
- if c.exists('file ' + filename + ' facility'):
- config_data['files'][filename]['selectors'] = generate_selectors(c, 'file ' + filename + ' facility')
- if c.exists('file ' + filename + ' archive size'):
- config_data['files'][filename]['max-size'] = int(c.return_value('file ' + filename + ' archive size'))* 1024
- if c.exists('file ' + filename + ' archive files'):
- config_data['files'][filename]['max-files'] = c.return_value('file ' + filename + ' archive files')
-
- ## set system syslog console
- if c.exists('console'):
- config_data['console'] = {
- '/dev/console' : {
- 'selectors' : '*.err'
- }
+ c = Config()
+ if not c.exists('system syslog'):
+ return None
+ c.set_level('system syslog')
+
+ config_data = {
+ 'files': {},
+ 'console': {},
+ 'hosts': {},
+ 'user': {}
}
-
- for f in c.list_nodes('console facility'):
- if c.exists('console facility ' + f + ' level'):
- config_data['console'] = {
- '/dev/console' : {
- 'selectors' : generate_selectors(c, 'console facility')
- }
- }
-
- ## set system syslog host
- if c.exists('host'):
- proto = 'udp'
- rhosts = c.list_nodes('host')
- for rhost in rhosts:
- for fac in c.list_nodes('host ' + rhost + ' facility'):
- if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'):
- proto = c.return_value('host ' + rhost + ' facility ' + fac + ' protocol')
-
- config_data['hosts'].update(
+
+ #
+ # /etc/rsyslog.d/vyos-rsyslog.conf
+ # 'set system syslog global'
+ #
+ config_data['files'].update(
{
- rhost : {
- 'selectors' : generate_selectors(c, 'host ' + rhost + ' facility'),
- 'proto' : proto
- }
+ 'global': {
+ 'log-file': '/var/log/messages',
+ 'max-size': 262144,
+ 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog',
+ 'selectors': '*.notice;local7.debug',
+ 'max-files': '5',
+ 'preserver_fqdn': False
+ }
}
- )
+ )
- ## set system syslog user
- if c.exists('user'):
- usrs = c.list_nodes('user')
- for usr in usrs:
- config_data['user'].update(
- {
- usr : {
- 'selectors' : generate_selectors(c, 'user ' + usr + ' facility')
- }
+ if c.exists('global marker'):
+ config_data['files']['global']['marker'] = True
+ if c.exists('global marker interval'):
+ config_data['files']['global'][
+ 'marker-interval'] = c.return_value('global marker interval')
+ if c.exists('global facility'):
+ config_data['files']['global'][
+ 'selectors'] = generate_selectors(c, 'global facility')
+ if c.exists('global archive size'):
+ config_data['files']['global']['max-size'] = int(
+ c.return_value('global archive size')) * 1024
+ if c.exists('global archive file'):
+ config_data['files']['global'][
+ 'max-files'] = c.return_value('global archive file')
+ if c.exists('global preserve-fqdn'):
+ config_data['files']['global']['preserver_fqdn'] = True
+
+ #
+ # set system syslog file
+ #
+
+ if c.exists('file'):
+ filenames = c.list_nodes('file')
+ for filename in filenames:
+ config_data['files'].update(
+ {
+ filename: {
+ 'log-file': '/var/log/user/' + filename,
+ 'max-files': '5',
+ 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename,
+ 'selectors': '*.err',
+ 'max-size': 262144
+ }
+ }
+ )
+
+ if c.exists('file ' + filename + ' facility'):
+ config_data['files'][filename]['selectors'] = generate_selectors(
+ c, 'file ' + filename + ' facility')
+ if c.exists('file ' + filename + ' archive size'):
+ config_data['files'][filename]['max-size'] = int(
+ c.return_value('file ' + filename + ' archive size')) * 1024
+ if c.exists('file ' + filename + ' archive files'):
+ config_data['files'][filename]['max-files'] = c.return_value(
+ 'file ' + filename + ' archive files')
+
+ # set system syslog console
+ if c.exists('console'):
+ config_data['console'] = {
+ '/dev/console': {
+ 'selectors': '*.err'
+ }
}
- )
-
- return config_data
+
+ for f in c.list_nodes('console facility'):
+ if c.exists('console facility ' + f + ' level'):
+ config_data['console'] = {
+ '/dev/console': {
+ 'selectors': generate_selectors(c, 'console facility')
+ }
+ }
+
+ # set system syslog host
+ if c.exists('host'):
+ proto = 'udp'
+ rhosts = c.list_nodes('host')
+ for rhost in rhosts:
+ for fac in c.list_nodes('host ' + rhost + ' facility'):
+ if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'):
+ proto = c.return_value(
+ 'host ' + rhost + ' facility ' + fac + ' protocol')
+
+ config_data['hosts'].update(
+ {
+ rhost: {
+ 'selectors': generate_selectors(c, 'host ' + rhost + ' facility'),
+ 'proto': proto
+ }
+ }
+ )
+
+ # set system syslog user
+ if c.exists('user'):
+ usrs = c.list_nodes('user')
+ for usr in usrs:
+ config_data['user'].update(
+ {
+ usr: {
+ 'selectors': generate_selectors(c, 'user ' + usr + ' facility')
+ }
+ }
+ )
+
+ return config_data
+
def generate_selectors(c, config_node):
-## protocols and security are being mapped here
-## for backward compatibility with old configs
-## security and protocol mappings can be removed later
- if c.is_tag(config_node):
- nodes = c.list_nodes(config_node)
- selectors = ""
- for node in nodes:
- lvl = c.return_value( config_node + ' ' + node + ' level')
- if lvl == None:
- lvl = "err"
- if lvl == 'all':
- lvl = '*'
- if node == 'all' and node != nodes[-1]:
- selectors += "*." + lvl + ";"
- elif node == 'all':
- selectors += "*." + lvl
- elif node != nodes[-1]:
- if node == 'protocols':
- node = 'local7'
- if node == 'security':
- node = 'auth'
- selectors += node + "." + lvl + ";"
- else:
- if node == 'protocols':
- node = 'local7'
- if node == 'security':
- node = 'auth'
- selectors += node + "." + lvl
- return selectors
+# protocols and security are being mapped here
+# for backward compatibility with old configs
+# security and protocol mappings can be removed later
+ if c.is_tag(config_node):
+ nodes = c.list_nodes(config_node)
+ selectors = ""
+ for node in nodes:
+ lvl = c.return_value(config_node + ' ' + node + ' level')
+ if lvl == None:
+ lvl = "err"
+ if lvl == 'all':
+ lvl = '*'
+ if node == 'all' and node != nodes[-1]:
+ selectors += "*." + lvl + ";"
+ elif node == 'all':
+ selectors += "*." + lvl
+ elif node != nodes[-1]:
+ if node == 'protocols':
+ node = 'local7'
+ if node == 'security':
+ node = 'auth'
+ selectors += node + "." + lvl + ";"
+ else:
+ if node == 'protocols':
+ node = 'local7'
+ if node == 'security':
+ node = 'auth'
+ selectors += node + "." + lvl
+ return selectors
+
def generate(c):
- if c == None:
- return None
+ if c == None:
+ return None
- tmpl = jinja2.Template(configs, trim_blocks=True)
- config_text = tmpl.render(c)
- with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f:
- f.write(config_text)
+ tmpl = jinja2.Template(configs, trim_blocks=True)
+ config_text = tmpl.render(c)
+ with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f:
+ f.write(config_text)
+
+ # eventually write for each file its own logrotate file, since size is
+ # defined it shouldn't matter
+ tmpl = jinja2.Template(logrotate_configs, trim_blocks=True)
+ config_text = tmpl.render(c)
+ with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f:
+ f.write(config_text)
- ## eventually write for each file its own logrotate file, since size is defined it shouldn't matter
- tmpl = jinja2.Template(logrotate_configs, trim_blocks=True)
- config_text = tmpl.render(c)
- with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f:
- f.write(config_text)
def verify(c):
- if c == None:
- return None
- #
- # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf)
- # it interferes with the global logging, to make sure we are using a single base, template is enforced here
- #
- if not os.path.islink('/etc/rsyslog.conf'):
- os.remove('/etc/rsyslog.conf')
- os.symlink('/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf')
-
- # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there
- # is a chance that someone still needs it, so I don't automatically remove them
-
- if c == None:
- return None
-
- fac = ['*','auth','authpriv','cron','daemon','kern','lpr','mail','mark','news','protocols','security',\
- 'syslog','user','uucp','local0','local1','local2','local3','local4','local5','local6','local7']
- lvl = ['emerg','alert','crit','err','warning','notice','info','debug','*']
-
- for conf in c:
- if c[conf]:
- for item in c[conf]:
- for s in c[conf][item]['selectors'].split(";"):
- f = re.sub("\..*$","",s)
- if f not in fac:
- print (c[conf])
- raise ConfigError('Invalid facility ' + s + ' set in '+ conf + ' ' + item)
- l = re.sub("^.+\.","",s)
- if l not in lvl:
- raise ConfigError('Invalid logging level ' + s + ' set in '+ conf + ' ' + item)
+ if c == None:
+ return None
+ #
+ # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf)
+ # it interferes with the global logging, to make sure we are using a single base, template is enforced here
+ #
+ if not os.path.islink('/etc/rsyslog.conf'):
+ os.remove('/etc/rsyslog.conf')
+ os.symlink(
+ '/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf')
+
+ # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there
+ # is a chance that someone still needs it, so I don't automatically remove
+ # them
+
+ if c == None:
+ return None
+
+ fac = [
+ '*', 'auth', 'authpriv', 'cron', 'daemon', 'kern', 'lpr', 'mail', 'mark', 'news', 'protocols', 'security',
+ 'syslog', 'user', 'uucp', 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7']
+ lvl = ['emerg', 'alert', 'crit', 'err',
+ 'warning', 'notice', 'info', 'debug', '*']
+
+ for conf in c:
+ if c[conf]:
+ for item in c[conf]:
+ for s in c[conf][item]['selectors'].split(";"):
+ f = re.sub("\..*$", "", s)
+ if f not in fac:
+ print (c[conf])
+ raise ConfigError(
+ 'Invalid facility ' + s + ' set in ' + conf + ' ' + item)
+ l = re.sub("^.+\.", "", s)
+ if l not in lvl:
+ raise ConfigError(
+ 'Invalid logging level ' + s + ' set in ' + conf + ' ' + item)
+
def apply(c):
- if not os.path.exists('/var/run/rsyslogd.pid'):
- os.system("sudo systemctl start rsyslog >/dev/null")
- else:
- os.system("sudo systemctl restart rsyslog >/dev/null")
+ if not c and os.path.exists('/var/run/rsyslogd.pid'):
+ os.system("sudo systemctl stop syslog.socket")
+ os.system("sudo systemctl stop rsyslog")
+ else:
+ if not os.path.exists('/var/run/rsyslogd.pid'):
+ os.system("sudo systemctl start rsyslog >/dev/null")
+ else:
+ os.system("sudo systemctl restart rsyslog >/dev/null")
if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py
new file mode 100755
index 000000000..06c95765f
--- /dev/null
+++ b/src/helpers/vyos-boot-config-loader.py
@@ -0,0 +1,101 @@
+#!/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/>.
+#
+#
+
+import os
+import sys
+import subprocess
+import traceback
+
+from vyos.configsession import ConfigSession, ConfigSessionError
+from vyos.configtree import ConfigTree
+
+STATUS_FILE = '/tmp/vyos-config-status'
+TRACE_FILE = '/tmp/boot-config-trace'
+
+session = ConfigSession(os.getpid(), 'vyos-boot-config-loader')
+env = session.get_session_env()
+
+default_file_name = env['vyatta_sysconfdir'] + '/config.boot.default'
+
+if len(sys.argv) < 1:
+ print("Must be called with argument.")
+ sys.exit(1)
+else:
+ file_name = sys.argv[1]
+
+def write_config_status(status):
+ with open(STATUS_FILE, 'w') as f:
+ f.write('{0}\n'.format(status))
+
+def trace_to_file(trace_file_name):
+ with open(trace_file_name, 'w') as trace_file:
+ traceback.print_exc(file=trace_file)
+
+def failsafe():
+ try:
+ with open(default_file_name, 'r') as f:
+ config_file = f.read()
+ except Exception as e:
+ print("Catastrophic: no default config file "
+ "'{0}'".format(default_file_name))
+ sys.exit(1)
+
+ config = ConfigTree(config_file)
+ if not config.exists(['system', 'login', 'user', 'vyos',
+ 'authentication', 'encrypted-password']):
+ print("No password entry in default config file;")
+ print("unable to recover password for user 'vyos'.")
+ sys.exit(1)
+ else:
+ passwd = config.return_value(['system', 'login', 'user', 'vyos',
+ 'authentication',
+ 'encrypted-password'])
+
+ cmd = ("useradd -s /bin/bash -G 'users,sudo' -m -N -p '{0}' "
+ "vyos".format(passwd))
+ try:
+ subprocess.check_call(cmd, shell=True)
+ except subprocess.CalledProcessError as e:
+ sys.exit("{0}".format(e))
+
+ with open('/etc/motd', 'a+') as f:
+ f.write('\n\n')
+ f.write('!!!!!\n')
+ f.write('There were errors loading the initial configuration;\n')
+ f.write('please examine the errors in {0} and correct.'
+ '\n'.format(TRACE_FILE))
+ f.write('!!!!!\n\n')
+
+try:
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+except Exception as e:
+ write_config_status(1)
+ failsafe()
+ trace_to_file(TRACE_FILE)
+ sys.exit("{0}".format(e))
+
+try:
+ session.load_config(file_name)
+ session.commit()
+ write_config_status(0)
+except ConfigSessionError as e:
+ write_config_status(1)
+ failsafe()
+ trace_to_file(TRACE_FILE)
+ sys.exit(1)
diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py
new file mode 100755
index 000000000..495eb5d40
--- /dev/null
+++ b/src/helpers/vyos-bridge-sync.py
@@ -0,0 +1,53 @@
+#!/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/>.
+#
+
+# Script is used to synchronize configured bridge interfaces.
+# one can add a non existing interface to a bridge group (e.g. VLAN)
+# but the vlan interface itself does yet not exist. It should be added
+# to the bridge automatically once it's available
+
+import argparse
+import subprocess
+
+from sys import exit
+from time import sleep
+from vyos.config import Config
+
+def subprocess_cmd(command):
+ process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True)
+ proc_stdout = process.communicate()[0].strip()
+ pass
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-i', '--interface', action='store', help='Interface name which should be added to bridge it is configured for', required=True)
+ args, unknownargs = parser.parse_known_args()
+
+ conf = Config()
+ if not conf.list_nodes('interfaces bridge'):
+ # no bridge interfaces exist .. bail out early
+ exit(0)
+ else:
+ for bridge in conf.list_nodes('interfaces bridge'):
+ for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)):
+ if args.interface == member_if:
+ cmd = 'brctl addif "{}" "{}"'.format(bridge, args.interface)
+ # let interfaces etc. settle - especially required for OpenVPN bridged interfaces
+ sleep(4)
+ subprocess_cmd(cmd)
+
+ exit(0)
diff --git a/src/helpers/vyos-sudo.py b/src/helpers/vyos-sudo.py
index 0101a0c95..3e4c196d9 100755
--- a/src/helpers/vyos-sudo.py
+++ b/src/helpers/vyos-sudo.py
@@ -15,17 +15,10 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-import getpass
-import grp
import os
import sys
-
-def is_admin() -> bool:
- """Look if current user is in sudo group"""
- current_user = getpass.getuser()
- (_, _, _, admin_group_members) = grp.getgrnam('sudo')
- return current_user in admin_group_members
+from vyos.util import is_admin
if __name__ == '__main__':
diff --git a/src/migration-scripts/dns-forwarding/0-to-1 b/src/migration-scripts/dns-forwarding/0-to-1
new file mode 100755
index 000000000..6e8720eef
--- /dev/null
+++ b/src/migration-scripts/dns-forwarding/0-to-1
@@ -0,0 +1,50 @@
+#!/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/>.
+#
+
+# This migration script will check if there is a allow-from directive configured
+# for the dns forwarding service - if not, the node will be created with the old
+# default values of 0.0.0.0/0 and ::/0
+
+import sys
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+base = ['service', 'dns', 'forwarding']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ if not config.exists(base + ['allow-from']):
+ config.set(base + ['allow-from'], value='0.0.0.0/0', replace=False)
+ config.set(base + ['allow-from'], value='::/0', replace=False)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2
new file mode 100755
index 000000000..31ba5573f
--- /dev/null
+++ b/src/migration-scripts/dns-forwarding/1-to-2
@@ -0,0 +1,78 @@
+#!/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/>.
+#
+
+# This migration script will remove the deprecated 'listen-on' statement
+# from the dns forwarding service and will add the corresponding
+# listen-address nodes instead. This is required as PowerDNS can only listen
+# on interface addresses and not on interface names.
+
+import sys
+
+from ipaddress import ip_interface
+from vyos.configtree import ConfigTree
+from vyos.interfaces import get_type_of_interface
+
+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 = ['service', 'dns', 'forwarding']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ if config.exists(base + ['listen-on']):
+ listen_intf = config.return_values(base + ['listen-on'])
+ # Delete node with abandoned command
+ config.delete(base + ['listen-on'])
+
+ # retrieve interface addresses for every configured listen-on interface
+ listen_addr = []
+ for intf in listen_intf:
+ # we need to treat vif and vif-s interfaces differently,
+ # both "real interfaces" use dots for vlan identifiers - those
+ # need to be exchanged with vif and vif-s identifiers
+ if intf.count('.') == 1:
+ # this is a regular VLAN interface
+ intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1]
+ elif intf.count('.') == 2:
+ # this is a QinQ VLAN interface
+ intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2]
+
+ path = ['interfaces', get_type_of_interface(intf), intf, 'address']
+
+ # retrieve corresponding interface addresses in CIDR format
+ # those need to be converted in pure IP addresses without network information
+ for addr in config.return_values(path):
+ listen_addr.append( ip_interface(addr).ip )
+
+ for addr in listen_addr:
+ config.set(base + ['listen-address'], value=addr, replace=False)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1
index 38f2bd8f5..96e18b5d5 100755
--- a/src/migration-scripts/interfaces/0-to-1
+++ b/src/migration-scripts/interfaces/0-to-1
@@ -30,20 +30,22 @@ else:
#
for br in config.list_nodes(base):
# STP: check if enabled
- stp_val = config.return_value(base + [br, 'stp'])
- # STP: delete node with old syntax
- config.delete(base + [br, 'stp'])
- # STP: set new node - if enabled
- if stp_val == "true":
- config.set(base + [br, 'stp'], value=None)
+ if config.exists(base + [br, 'stp']):
+ stp_val = config.return_value(base + [br, 'stp'])
+ # STP: delete node with old syntax
+ config.delete(base + [br, 'stp'])
+ # STP: set new node - if enabled
+ if stp_val == "true":
+ config.set(base + [br, 'stp'], value=None)
# igmp-snooping: check if enabled
- igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier'])
- # igmp-snooping: delete node with old syntax
- config.delete(base + [br, 'igmp-snooping', 'querier'])
- # igmp-snooping: set new node - if enabled
- if igmp_val == "enable":
- config.set(base + [br, 'igmp', 'querier'], value=None)
+ if config.exists(base + [br, 'igmp-snooping', 'querier']):
+ igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier'])
+ # igmp-snooping: delete node with old syntax
+ config.delete(base + [br, 'igmp-snooping', 'querier'])
+ # igmp-snooping: set new node - if enabled
+ if igmp_val == "enable":
+ config.set(base + [br, 'igmp', 'querier'], value=None)
#
# move interface based bridge-group to actual bridge (de-nest)
diff --git a/src/migration-scripts/interfaces/1-to-2 b/src/migration-scripts/interfaces/1-to-2
new file mode 100755
index 000000000..050137318
--- /dev/null
+++ b/src/migration-scripts/interfaces/1-to-2
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+
+# Change syntax of bond interface
+# - move interface based bond-group to actual bond (de-nest)
+# https://phabricator.vyos.net/T1614
+
+import sys
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base = ['interfaces', 'bonding']
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ #
+ # move interface based bond-group to actual bond (de-nest)
+ #
+ for intf in config.list_nodes(['interfaces', 'ethernet']):
+ # check if bond-group exists
+ if config.exists(['interfaces', 'ethernet', intf, 'bond-group']):
+ # get configured bond interface
+ bond = config.return_value(['interfaces', 'ethernet', intf, 'bond-group'])
+ # delete old interface asigned (nested) bond group
+ config.delete(['interfaces', 'ethernet', intf, 'bond-group'])
+ # create new bond member interface
+ config.set(base + [bond, 'member', 'interface'], value=intf, replace=False)
+
+ #
+ # some combinations were allowed in the past from a CLI perspective
+ # but the kernel overwrote them - remove from CLI to not confuse the users.
+ # In addition new consitency checks are in place so users can't repeat the
+ # mistake. One of those nice issues is https://phabricator.vyos.net/T532
+ for bond in config.list_nodes(base):
+ if config.exists(base + [bond, 'arp-monitor', 'interval']) and config.exists(base + [bond, 'mode']):
+ mode = config.return_value(base + [bond, 'mode'])
+ if mode in ['802.3ad', 'transmit-load-balance', 'adaptive-load-balance']:
+ intvl = int(config.return_value(base + [bond, 'arp-monitor', 'interval']))
+ if intvl > 0:
+ # this is not allowed and the linux kernel replies with:
+ # option arp_interval: mode dependency failed, not supported in mode 802.3ad(4)
+ # option arp_interval: mode dependency failed, not supported in mode balance-alb(6)
+ # option arp_interval: mode dependency failed, not supported in mode balance-tlb(5)
+ #
+ # so we simply disable arp_interval by setting it to 0 and miimon will take care about the link
+ config.set(base + [bond, 'arp-monitor', 'interval'], value='0')
+
+ 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/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py
new file mode 100755
index 000000000..0e52b9086
--- /dev/null
+++ b/src/op_mode/clear_conntrack.py
@@ -0,0 +1,26 @@
+#!/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/>.
+
+import subprocess
+import sys
+
+from vyos.util import ask_yes_no
+
+if not ask_yes_no("This will clear all currently tracked and expected connections. Continue?"):
+ sys.exit(1)
+else:
+ subprocess.check_call(['/usr/sbin/conntrack -F'], shell=True, stderr=subprocess.DEVNULL)
+ subprocess.check_call(['/usr/sbin/conntrack -F expect'], shell=True, stderr=subprocess.DEVNULL)
diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py
new file mode 100755
index 000000000..5a3b250ee
--- /dev/null
+++ b/src/op_mode/format_disk.py
@@ -0,0 +1,148 @@
+#!/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/>.
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+from datetime import datetime
+from time import sleep
+
+from vyos.util import is_admin, ask_yes_no
+
+
+def list_disks():
+ 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])
+ return disks
+
+
+def is_busy(disk: str):
+ """Check if given disk device is busy by re-reading it's partition table"""
+
+ cmd = 'sudo blockdev --rereadpt /dev/{}'.format(disk)
+ status = subprocess.call([cmd], shell=True, stderr=subprocess.DEVNULL)
+ return status != 0
+
+
+def backup_partitions(disk: str):
+ """Save sfdisk partitions output to a backup file"""
+
+ device_path = '/dev/' + disk
+ backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M')
+ backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts)
+ cmd = 'sudo /sbin/sfdisk -d {} > {}'.format(device_path, backup_file)
+ subprocess.check_call([cmd], shell=True)
+
+
+def list_partitions(disk: str):
+ """List partition numbers of a given disk"""
+
+ parts = set()
+ part_num_expr = re.compile(disk + '([0-9]+)')
+ with open('/proc/partitions') as partitions_file:
+ for line in partitions_file:
+ fields = line.strip().split()
+ if len(fields) == 4 and fields[3] != 'name' and part_num_expr.match(fields[3]):
+ part_idx = part_num_expr.match(fields[3]).group(1)
+ parts.add(int(part_idx))
+ return parts
+
+
+def delete_partition(disk: str, partition_idx: int):
+ cmd = 'sudo /sbin/parted /dev/{} rm {}'.format(disk, partition_idx)
+ subprocess.check_call([cmd], shell=True)
+
+
+def format_disk_like(target: str, proto: str):
+ cmd = 'sudo /sbin/sfdisk -d /dev/{} | sudo /sbin/sfdisk --force /dev/{}'.format(proto, target)
+ subprocess.check_call([cmd], shell=True)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ group = parser.add_argument_group()
+ group.add_argument('-t', '--target', type=str, required=True, help='Target device to format')
+ group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference')
+ args = parser.parse_args()
+
+ if not is_admin():
+ print('Must be admin or root to format disk')
+ sys.exit(1)
+
+ target_disk = args.target
+ eligible_target_disks = list_disks()
+
+ proto_disk = args.proto
+ eligible_proto_disks = eligible_target_disks.copy()
+ eligible_proto_disks.remove(target_disk)
+
+ fmt = {
+ 'target_disk': target_disk,
+ 'proto_disk': proto_disk,
+ }
+
+ if proto_disk == target_disk:
+ print('The two disk drives must be different.')
+ sys.exit(1)
+
+ if not os.path.exists('/dev/' + proto_disk):
+ print('Device /dev/{proto_disk} does not exist'.format_map(fmt))
+ sys.exit(1)
+
+ if not os.path.exists('/dev/' + target_disk):
+ print('Device /dev/{target_disk} does not exist'.format_map(fmt))
+ sys.exit(1)
+
+ if target_disk not in eligible_target_disks:
+ print('Device {target_disk} can not be formatted'.format_map(fmt))
+ sys.exit(1)
+
+ if proto_disk not in eligible_proto_disks:
+ print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt))
+ sys.exit(1)
+
+ if is_busy(target_disk):
+ print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt))
+ sys.exit(1)
+
+ print('This will re-format disk {target_disk} so that it has the same disk\n'
+ 'partion sizes and offsets as {proto_disk}. This will not copy\n'
+ 'data from {proto_disk} to {target_disk}. But this will erase all\n'
+ 'data on {target_disk}.\n'.format_map(fmt))
+
+ if not ask_yes_no("Do you wish to proceed?"):
+ print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt))
+ sys.exit(0)
+
+ print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt))
+
+ print('Making backup copy of partitions...')
+ backup_partitions(target_disk)
+ sleep(1)
+
+ print('Deleting old partitions...')
+ for p in list_partitions(target_disk):
+ delete_partition(disk=target_disk, partition_idx=p)
+
+ print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt))
+ format_disk_like(target=target_disk, proto=proto_disk)
+ print('Done.')
diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py
new file mode 100755
index 000000000..f205919b8
--- /dev/null
+++ b/src/op_mode/generate_ssh_server_key.py
@@ -0,0 +1,27 @@
+#!/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/>.
+
+import subprocess
+import sys
+
+from vyos.util import ask_yes_no
+
+if not ask_yes_no('Do you really want to remove the existing SSH host keys?'):
+ sys.exit(0)
+else:
+ subprocess.check_call(['sudo rm -v /etc/ssh/ssh_host_*'], shell=True)
+ subprocess.check_call(['sudo dpkg-reconfigure openssh-server'], shell=True)
+ subprocess.check_call(['sudo systemctl restart ssh'], shell=True)
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index 2f6112fb7..e3644e063 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -22,20 +22,7 @@ import re
from datetime import datetime, timedelta, time as type_time, date as type_date
from subprocess import check_output, CalledProcessError, STDOUT
-
-def yn(msg, default=False):
- default_msg = "[Y/n]" if default else "[y/N]"
- while True:
- sys.stdout.write("%s %s " % (msg,default_msg))
- c = input().lower()
- if c == '':
- return default
- elif c in ("y", "ye","yes"):
- return True
- elif c in ("n", "no"):
- return False
- else:
- sys.stdout.write("Please respond with yes/y or no/n\n")
+from vyos.util import ask_yes_no
def valid_time(s):
@@ -80,7 +67,7 @@ def cancel_shutdown():
def execute_shutdown(time, reboot = True, ask=True):
if not ask:
action = "reboot" if reboot else "poweroff"
- if not yn("Are you sure you want to %s this system?" % action):
+ if not ask_yes_no("Are you sure you want to %s this system?" % action):
sys.exit(0)
action = "-r" if reboot else "-P"
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
new file mode 100755
index 000000000..23a8156ec
--- /dev/null
+++ b/src/op_mode/show_openvpn.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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 jinja2
+import argparse
+
+from vyos.config import Config
+
+outp_tmpl = """
+{% if clients %}
+OpenVPN status on {{ intf }}
+
+Client CN Remote Host Local Host TX bytes RX bytes Connected Since
+--------- ----------- ---------- -------- -------- ---------------
+{%- for c in clients %}
+{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-21s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.tx_bytes) }} {{ c.online_since }}
+{%- endfor %}
+{% endif %}
+"""
+
+def bytes2HR(size):
+ # we need to operate in integers
+ size = int(size)
+
+ suff = ['B', 'KB', 'MB', 'GB', 'TB']
+ suffIdx = 0
+
+ while size > 1024:
+ # incr. suffix index
+ suffIdx += 1
+ # divide
+ size = size/1024.0
+
+ output="{0:.1f} {1}".format(size, suff[suffIdx])
+ return output
+
+def get_status(mode, interface):
+ status_file = '/opt/vyatta/etc/openvpn/status/{}.status'.format(interface)
+ # this is an empirical value - I assume we have no more then 999999
+ # current OpenVPN connections
+ routing_table_line = 999999
+
+ data = {
+ 'mode': mode,
+ 'intf': interface,
+ 'local': 'N/A',
+ 'date': '',
+ 'clients': [],
+ }
+
+ with open(status_file, 'r') as f:
+ lines = f.readlines()
+ for line_no, line in enumerate(lines):
+ # remove trailing newline character first
+ line = line.rstrip('\n')
+
+ # check first line header
+ if line_no == 0:
+ if mode == 'server':
+ if not line == 'OpenVPN CLIENT LIST':
+ raise NameError('Expected "OpenVPN CLIENT LIST"')
+ else:
+ if not line == 'OpenVPN STATISTICS':
+ raise NameError('Expected "OpenVPN STATISTICS"')
+
+ continue
+
+ # second line informs us when the status file has been last updated
+ if line_no == 1:
+ data['date'] = line.lstrip('Updated,').rstrip('\n')
+ continue
+
+ if mode == 'server':
+ # followed by line3 giving output information and the actual output data
+ #
+ # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
+ # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019
+ # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019
+ # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019
+ if (line_no >= 3) and (line_no < routing_table_line):
+ # indicator that there are no more clients and we will continue with the
+ # routing table
+ if line == 'ROUTING TABLE':
+ routing_table_line = line_no
+ continue
+
+ client = {
+ 'name': line.split(',')[0],
+ 'remote': line.split(',')[1],
+ 'rx_bytes': bytes2HR(line.split(',')[2]),
+ 'tx_bytes': bytes2HR(line.split(',')[3]),
+ 'online_since': line.split(',')[4]
+ }
+
+ data['clients'].append(client)
+ continue
+ else:
+ if line_no == 2:
+ client = {
+ 'name': 'N/A',
+ 'remote': 'N/A',
+ 'rx_bytes': bytes2HR(line.split(',')[1]),
+ 'tx_bytes': '',
+ 'online_since': 'N/A'
+ }
+ continue
+
+ if line_no == 3:
+ client['tx_bytes'] = bytes2HR(line.split(',')[1])
+ data['clients'].append(client)
+ break
+
+ return data
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-m', '--mode', help='OpenVPN operation mode (server, client, site-2-site)', required=True)
+
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ config = Config()
+ if len(config.list_effective_nodes('interfaces openvpn')) == 0:
+ print("No OpenVPN interfaces configured")
+ sys.exit(0)
+
+ # search all OpenVPN interfaces and add those with a matching mode to our
+ # interfaces list
+ interfaces = []
+ for intf in config.list_effective_nodes('interfaces openvpn'):
+ # get interface type (server, client, site-to-site)
+ mode = config.return_effective_value('interfaces openvpn {} mode'.format(intf))
+ if args.mode == mode:
+ interfaces.append(intf)
+
+ for intf in interfaces:
+ data = get_status(args.mode, intf)
+ local_host = config.return_effective_value('interfaces openvpn {} local-host'.format(intf))
+ local_port = config.return_effective_value('interfaces openvpn {} local-port'.format(intf))
+ if local_host and local_port:
+ data['local'] = local_host + ':' + local_port
+
+ if args.mode in ['client', 'site-to-site']:
+ for client in data['clients']:
+ if config.exists_effective('interfaces openvpn {} shared-secret-key-file'.format(intf)):
+ client['name'] = "None (PSK)"
+
+ remote_host = config.return_effective_values('interfaces openvpn {} remote-host'.format(intf))
+ remote_port = config.return_effective_value('interfaces openvpn {} remote-port'.format(intf))
+ if len(remote_host) >= 1:
+ client['remote'] = str(remote_host[0]) + ':' + remote_port
+
+ tmpl = jinja2.Template(outp_tmpl)
+ print(tmpl.render(data))
+
diff --git a/src/op_mode/toggle_help_binding.sh b/src/op_mode/toggle_help_binding.sh
new file mode 100755
index 000000000..a8708f3da
--- /dev/null
+++ b/src/op_mode/toggle_help_binding.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+# 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/>.
+
+# Script for [un-]binding the question mark key for getting help
+if [ "$1" == 'disable' ]; then
+ sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc
+ echo "bind '\"?\": self-insert' # vyatta key binding" >> $HOME/.bashrc
+ bind '"?": self-insert'
+else
+ sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc
+ bind '"?": possible-completions'
+fi
diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py
index 66622c04c..4e93ec6aa 100755
--- a/src/op_mode/wireguard.py
+++ b/src/op_mode/wireguard.py
@@ -19,91 +19,139 @@
import argparse
import os
import sys
+import shutil
import subprocess
import syslog as sl
+
from vyos import ConfigError
dir = r'/config/auth/wireguard'
-pk = dir + '/private.key'
-pub = dir + '/public.key'
psk = dir + '/preshared.key'
+
def check_kmod():
- """ check if kmod is loaded, if not load it """
- if not os.path.exists('/sys/module/wireguard'):
- sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
- if os.system('sudo modprobe wireguard') != 0:
- sl.syslog(sl.LOG_ERR, "modprobe wireguard failed")
- raise ConfigError("modprobe wireguard failed")
-
-def generate_keypair():
- """ generates a keypair which is stored in /config/auth/wireguard """
- ret = subprocess.call(['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True)
- if ret != 0:
- raise ConfigError("wireguard key-pair generation failed")
- else:
- sl.syslog(sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir)
-
-def genkey():
- """ helper function to check, regenerate the keypair """
- old_umask = os.umask(0o077)
- if os.path.exists(pk) and os.path.exists(pub):
- try:
- choice = input("You already have a wireguard key-pair already, do you want to re-generate? [y/n] ")
- if choice == 'y' or choice == 'Y':
- generate_keypair()
- except KeyboardInterrupt:
- sys.exit(0)
- else:
- """ if keypair is bing executed from a running iso """
- if not os.path.exists(dir):
- os.umask(old_umask)
- subprocess.call(['sudo mkdir -p ' + dir], shell=True)
- subprocess.call(['sudo chgrp vyattacfg ' + dir], shell=True)
- subprocess.call(['sudo chmod 770 ' + dir], shell=True)
- generate_keypair()
- os.umask(old_umask)
+ """ check if kmod is loaded, if not load it """
+ if not os.path.exists('/sys/module/wireguard'):
+ sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
+ if os.system('sudo modprobe wireguard') != 0:
+ sl.syslog(sl.LOG_ERR, "modprobe wireguard failed")
+ raise ConfigError("modprobe wireguard failed")
-def showkey(key):
- """ helper function to show privkey or pubkey """
- if key == "pub":
- if os.path.exists(pub):
- print ( open(pub).read().strip() )
+
+def generate_keypair(pk, pub):
+ """ generates a keypair which is stored in /config/auth/wireguard """
+ old_umask = os.umask(0o027)
+ ret = subprocess.call(
+ ['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True)
+ if ret != 0:
+ raise ConfigError("wireguard key-pair generation failed")
else:
- print("no public key found")
+ sl.syslog(
+ sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir)
+ os.umask(old_umask)
- if key == "pk":
- if os.path.exists(pk):
- print ( open(pk).read().strip() )
+
+def genkey(location):
+ """ helper function to check, regenerate the keypair """
+ pk = "{}/private.key".format(location)
+ pub = "{}/public.key".format(location)
+ old_umask = os.umask(0o027)
+ if os.path.exists(pk) and os.path.exists(pub):
+ try:
+ choice = input(
+ "You already have a wireguard key-pair, do you want to re-generate? [y/n] ")
+ if choice == 'y' or choice == 'Y':
+ generate_keypair(pk, pub)
+ except KeyboardInterrupt:
+ sys.exit(0)
else:
- print("no private key found")
+ """ if keypair is bing executed from a running iso """
+ if not os.path.exists(location):
+ subprocess.call(['sudo mkdir -p ' + location], shell=True)
+ subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True)
+ subprocess.call(['sudo chmod 750 ' + location], shell=True)
+ generate_keypair(pk, pub)
+ os.umask(old_umask)
+
+
+def showkey(key):
+ """ helper function to show privkey or pubkey """
+ if os.path.exists(key):
+ print (open(key).read().strip())
+ else:
+ print ("{} not found".format(key))
+
def genpsk():
- """ generates a preshared key and shows it on stdout, it's stroed only in the config """
- subprocess.call(['wg genpsk'], shell=True)
+ """
+ generates a preshared key and shows it on stdout,
+ it's stored only in the cli config
+ """
+
+ subprocess.call(['wg genpsk'], shell=True)
+
+def list_key_dirs():
+ """ lists all dirs under /config/auth/wireguard """
+ if os.path.exists(dir):
+ nks = next(os.walk(dir))[1]
+ for nk in nks:
+ print (nk)
+
+def del_key_dir(kname):
+ """ deletes /config/auth/wireguard/<kname> """
+ kdir = "{0}/{1}".format(dir,kname)
+ if not os.path.isdir(kdir):
+ print ("named keypair {} not found".format(kname))
+ return 1
+ shutil.rmtree(kdir)
+
if __name__ == '__main__':
- check_kmod()
-
- parser = argparse.ArgumentParser(description='wireguard key management')
- parser.add_argument('--genkey', action="store_true", help='generate key-pair')
- parser.add_argument('--showpub', action="store_true", help='shows public key')
- parser.add_argument('--showpriv', action="store_true", help='shows private key')
- parser.add_argument('--genpsk', action="store_true", help='generates preshared-key')
- args = parser.parse_args()
-
- try:
- if args.genkey:
- genkey()
- if args.showpub:
- showkey("pub")
- if args.showpriv:
- showkey("pk")
- if args.genpsk:
- genpsk()
-
- except ConfigError as e:
- print(e)
- sys.exit(1)
+ check_kmod()
+ parser = argparse.ArgumentParser(description='wireguard key management')
+ parser.add_argument(
+ '--genkey', action="store_true", help='generate key-pair')
+ parser.add_argument(
+ '--showpub', action="store_true", help='shows public key')
+ parser.add_argument(
+ '--showpriv', action="store_true", help='shows private key')
+ parser.add_argument(
+ '--genpsk', action="store_true", help='generates preshared-key')
+ parser.add_argument(
+ '--location', action="store", help='key location within {}'.format(dir))
+ parser.add_argument(
+ '--listkdir', action="store_true", help='lists named keydirectories')
+ parser.add_argument(
+ '--delkdir', action="store_true", help='removes named keydirectories')
+ args = parser.parse_args()
+
+ try:
+ if args.genkey:
+ if args.location:
+ genkey("{0}/{1}".format(dir, args.location))
+ else:
+ genkey("{}/default".format(dir))
+ if args.showpub:
+ if args.location:
+ showkey("{0}/{1}/public.key".format(dir, args.location))
+ else:
+ showkey("{}/default/public.key".format(dir))
+ if args.showpriv:
+ if args.location:
+ showkey("{0}/{1}/private.key".format(dir, args.location))
+ else:
+ showkey("{}/default/private.key".format(dir))
+ if args.genpsk:
+ genpsk()
+ if args.listkdir:
+ list_key_dirs()
+ if args.delkdir:
+ if args.location:
+ del_key_dir(args.location)
+ else:
+ del_key_dir("default")
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
new file mode 100755
index 000000000..8f70eb4e9
--- /dev/null
+++ b/src/services/vyos-hostsd
@@ -0,0 +1,284 @@
+#!/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/>.
+#
+#
+
+import os
+import sys
+import time
+import json
+import signal
+import traceback
+
+import zmq
+
+import jinja2
+
+debug = True
+
+DATA_DIR = "/var/lib/vyos/"
+STATE_FILE = os.path.join(DATA_DIR, "hostsd.state")
+
+SOCKET_PATH = "ipc:///run/vyos-hostsd.sock"
+
+RESOLV_CONF_FILE = '/etc/resolv.conf'
+HOSTS_FILE = '/etc/hosts'
+
+hosts_tmpl_source = """
+### Autogenerated by VyOS ###
+### Do not edit, your changes will get overwritten ###
+
+# Local host
+127.0.0.1 localhost
+127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }}{% endif %}
+
+# The following lines are desirable for IPv6 capable hosts
+::1 localhost ip6-localhost ip6-loopback
+fe00::0 ip6-localnet
+ff00::0 ip6-mcastprefix
+ff02::1 ip6-allnodes
+ff02::2 ip6-allrouters
+
+# From DHCP and "system static host-mapping"
+{%- if hosts %}
+{% for h in hosts -%}
+{{hosts[h]['address']}}\t{{h}}\t{% for a in hosts[h]['aliases'] %} {{a}} {% endfor %}
+{% endfor %}
+{%- endif %}
+"""
+
+hosts_tmpl = jinja2.Template(hosts_tmpl_source)
+
+resolv_tmpl_source = """
+### Autogenerated by VyOS ###
+### Do not edit, your changes will get overwritten ###
+
+{% for ns in name_servers -%}
+nameserver {{ns}}
+{% endfor -%}
+
+{%- if domain_name %}
+domain {{ domain_name }}
+{%- endif %}
+
+{%- if search_domains %}
+search {{ search_domains | join(" ") }}
+{%- endif %}
+
+"""
+
+resolv_tmpl = jinja2.Template(resolv_tmpl_source)
+
+# The state data includes a list of name servers
+# and a list of hosts entries.
+#
+# Name servers have the following structure:
+# {"server": {"tag": <str>}}
+#
+# Hosts entries are similar:
+# {"host": {"tag": <str>, "address": <str>, "aliases": <str list>}}
+#
+# The tag is either "static" or "dhcp-<intf>"
+# It's used to distinguish entries created
+# by different scripts so that they can be removed
+# and re-created without having to track what needs
+# to be changed
+STATE = {
+ "name_servers": {},
+ "hosts": {},
+ "host_name": "vyos",
+ "domain_name": "",
+ "search_domains": []}
+
+
+def make_resolv_conf(data):
+ resolv_conf = resolv_tmpl.render(data)
+ print("Writing /etc/resolv.conf")
+ with open(RESOLV_CONF_FILE, 'w') as f:
+ f.write(resolv_conf)
+
+def make_hosts_file(state):
+ print("Writing /etc/hosts")
+ hosts = hosts_tmpl.render(state)
+ with open(HOSTS_FILE, 'w') as f:
+ f.write(hosts)
+
+def add_hosts(data, entries, tag):
+ hosts = data['hosts']
+
+ if not entries:
+ return
+
+ for e in entries:
+ host = e['host']
+ hosts[host] = {}
+ hosts[host]['tag'] = tag
+ hosts[host]['address'] = e['address']
+ hosts[host]['aliases'] = e['aliases']
+
+def delete_hosts(data, tag):
+ hosts = data['hosts']
+ keys_for_deletion = []
+
+ # You can't delete items from a dict while iterating over it,
+ # so we build a list of doomed items first
+ for h in hosts:
+ if hosts[h]['tag'] == tag:
+ keys_for_deletion.append(h)
+
+ for k in keys_for_deletion:
+ del hosts[k]
+
+def add_name_servers(data, entries, tag):
+ name_servers = data['name_servers']
+
+ if not entries:
+ return
+
+ for e in entries:
+ name_servers[e] = {}
+ name_servers[e]['tag'] = tag
+
+def delete_name_servers(data, tag):
+ name_servers = data['name_servers']
+ keys_for_deletion = []
+
+ for ns in name_servers:
+ if name_servers[ns]['tag'] == tag:
+ keys_for_deletion.append(ns)
+
+ for k in keys_for_deletion:
+ del name_servers[k]
+
+def set_host_name(state, data):
+ if data['host_name']:
+ state['host_name'] = data['host_name']
+ if data['domain_name']:
+ state['domain_name'] = data['domain_name']
+ if data['search_domains']:
+ state['search_domains'] = data['search_domains']
+
+def get_name_servers(state, tag):
+ ns = []
+ data = state['name_servers']
+ for n in data:
+ if data[n]['tag'] == tag:
+ ns.append(n)
+ return ns
+
+def get_option(msg, key):
+ if key in msg:
+ return msg[key]
+ else:
+ raise ValueError("Missing required option \"{0}\"".format(key))
+
+def handle_message(msg_json):
+ msg = json.loads(msg_json)
+
+ op = get_option(msg, 'op')
+ _type = get_option(msg, 'type')
+
+ if op == 'delete':
+ tag = get_option(msg, 'tag')
+
+ if _type == 'name_servers':
+ delete_name_servers(STATE, tag)
+ elif _type == 'hosts':
+ delete_hosts(STATE, tag)
+ else:
+ raise ValueError("Unknown message type {0}".format(_type))
+ elif op == 'add':
+ tag = get_option(msg, 'tag')
+ entries = get_option(msg, 'data')
+ if _type == 'name_servers':
+ add_name_servers(STATE, entries, tag)
+ elif _type == 'hosts':
+ add_hosts(STATE, entries, tag)
+ else:
+ raise ValueError("Unknown message type {0}".format(_type))
+ elif op == 'set':
+ # Host name/domain name/search domain are set without a tag,
+ # there can be only one anyway
+ data = get_option(msg, 'data')
+ if _type == 'host_name':
+ set_host_name(STATE, data)
+ else:
+ raise ValueError("Unknown message type {0}".format(_type))
+ elif op == 'get':
+ tag = get_option(msg, 'tag')
+ if _type == 'name_servers':
+ result = get_name_servers(STATE, tag)
+ else:
+ raise ValueError("Unimplemented")
+ return result
+ else:
+ raise ValueError("Unknown operation {0}".format(op))
+
+ make_resolv_conf(STATE)
+ make_hosts_file(STATE)
+
+ print("Saving state to {0}".format(STATE_FILE))
+ with open(STATE_FILE, 'w') as f:
+ json.dump(STATE, f)
+
+def exit_handler(sig, frame):
+ """ Clean up the state when shutdown correctly """
+ print("Cleaning up state")
+ os.unlink(STATE_FILE)
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ signal.signal(signal.SIGTERM, exit_handler)
+
+ # Create a directory for state checkpoints
+ os.makedirs(DATA_DIR, exist_ok=True)
+ if os.path.exists(STATE_FILE):
+ with open(STATE_FILE, 'r') as f:
+ try:
+ data = json.load(f)
+ STATE = data
+ except:
+ print(traceback.format_exc())
+ print("Failed to load the state file, using default")
+
+ context = zmq.Context()
+ socket = context.socket(zmq.REP)
+ socket.bind(SOCKET_PATH)
+
+ while True:
+ # Wait for next request from client
+ message = socket.recv().decode()
+ print("Received a configuration change request")
+ if debug:
+ print("Request data: {0}".format(message))
+
+ resp = {}
+
+ try:
+ result = handle_message(message)
+ resp['data'] = result
+ except ValueError as e:
+ resp['error'] = str(e)
+ except:
+ print(traceback.format_exc())
+ resp['error'] = "Internal error"
+
+ if debug:
+ print("Sent response: {0}".format(resp))
+
+ # Send reply back to client
+ socket.send(json.dumps(resp).encode())
diff --git a/src/system/unpriv-ip b/src/system/unpriv-ip
new file mode 100755
index 000000000..1ea0d626a
--- /dev/null
+++ b/src/system/unpriv-ip
@@ -0,0 +1,2 @@
+#!/bin/sh
+sudo /sbin/ip $*
diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service
new file mode 100644
index 000000000..731e570c9
--- /dev/null
+++ b/src/systemd/vyos-hostsd.service
@@ -0,0 +1,31 @@
+[Unit]
+Description=VyOS DNS configuration keeper
+
+# Without this option, lots of default dependencies are added,
+# among them network.target, which creates a dependency cycle
+DefaultDependencies=no
+
+# Seemingly sensible way to say "as early as the system is ready"
+# All vyos-hostsd needs is read/write mounted root
+After=systemd-remount-fs.service
+
+[Service]
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd
+Type=idle
+KillMode=process
+
+SyslogIdentifier=vyos-hostsd
+SyslogFacility=daemon
+
+Restart=on-failure
+
+# Does't work in Jessie but leave it here
+User=root
+Group=vyattacfg
+
+[Install]
+
+# Note: After= doesn't actually create a dependency,
+# it just sets order for the case when both services are to start,
+# and without RequiredBy it *does not* set vyos-hostsd to start.
+RequiredBy=cloud-init-local.service vyos-router.service
diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client
new file mode 100755
index 000000000..d3105c9cf
--- /dev/null
+++ b/src/utils/vyos-hostsd-client
@@ -0,0 +1,82 @@
+#!/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/>.
+#
+#
+
+import sys
+import argparse
+
+import vyos.hostsd_client
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument('--add-hosts', action="store_true")
+group.add_argument('--delete-hosts', action="store_true")
+group.add_argument('--add-name-servers', action="store_true")
+group.add_argument('--delete-name-servers', action="store_true")
+group.add_argument('--set-host-name', action="store_true")
+
+parser.add_argument('--host', type=str, action="append")
+parser.add_argument('--name-server', type=str, action="append")
+parser.add_argument('--host-name', type=str)
+parser.add_argument('--domain-name', type=str)
+parser.add_argument('--search-domain', type=str, action="append")
+
+parser.add_argument('--tag', type=str)
+
+args = parser.parse_args()
+
+try:
+ client = vyos.hostsd_client.Client()
+
+ if args.add_hosts:
+ if not args.tag:
+ raise ValueError("Tag is required for this operation")
+ data = []
+ for h in args.host:
+ entry = {}
+ params = h.split(",")
+ if len(params) < 2:
+ raise ValueError("Malformed host entry")
+ entry['host'] = params[0]
+ entry['address'] = params[1]
+ entry['aliases'] = params[2:]
+ data.append(entry)
+ client.add_hosts(args.tag, data)
+ elif args.delete_hosts:
+ if not args.tag:
+ raise ValueError("Tag is required for this operation")
+ client.delete_hosts(args.tag)
+ elif args.add_name_servers:
+ if not args.tag:
+ raise ValueError("Tag is required for this operation")
+ client.add_name_servers(args.tag, args.name_server)
+ elif args.delete_name_servers:
+ if not args.tag:
+ raise ValueError("Tag is required for this operation")
+ client.delete_name_servers(args.tag)
+ elif args.set_host_name:
+ client.set_host_name(args.host_name, args.domain_name, args.search_domain)
+ else:
+ raise ValueError("Operation required")
+
+except ValueError as e:
+ print("Incorrect options: {0}".format(e))
+ sys.exit(1)
+except vyos.hostsd_client.VyOSHostsdError as e:
+ print("Server returned an error: {0}".format(e))
+ sys.exit(1)
+