diff options
Diffstat (limited to 'src/conf-mode')
-rwxr-xr-x | src/conf-mode/vyos-config-bcast-relay.py | 8 | ||||
-rwxr-xr-x | src/conf-mode/vyos-config-dns-forwarding.py | 213 | ||||
-rwxr-xr-x | src/conf-mode/vyos-config-mdns-repeater.py | 5 | ||||
-rwxr-xr-x | src/conf-mode/vyos-config-ntp.py | 161 | ||||
-rwxr-xr-x | src/conf-mode/vyos-config-ssh.py | 249 | ||||
-rwxr-xr-x | src/conf-mode/vyos-update-crontab.py | 2 |
6 files changed, 630 insertions, 8 deletions
diff --git a/src/conf-mode/vyos-config-bcast-relay.py b/src/conf-mode/vyos-config-bcast-relay.py index 3107dce43..785690d9c 100755 --- a/src/conf-mode/vyos-config-bcast-relay.py +++ b/src/conf-mode/vyos-config-bcast-relay.py @@ -23,7 +23,7 @@ import time import subprocess from vyos.config import Config -from vyos.util import ConfigError +from vyos import ConfigError config_file = r'/etc/default/udp-broadcast-relay' @@ -41,9 +41,9 @@ def get_config(): # split the interface name listing and form a list if conf.exists("{0} interface".format(id)): + intfs_names = [] intfs_names = conf.return_values("{0} interface".format(id)) - intfs_names=intfs_names.replace("'", "") - intfs_names=intfs_names.split() + for name in intfs_names: interface_list.append(name) @@ -90,7 +90,7 @@ def generate(relays): for relay in relays: file = config_file + str(relay["id"]) interfaces = ' '.join(str(intf) for intf in relay["interfaces"]) - config_args = 'DAEMON_ARGS="{0} {1} {2}"\n'.format(relay["id"], relay["port"], interfaces) + config_args = 'DAEMON_ARGS="{0} {1}"\n'.format(relay["port"], interfaces) f = open(file, 'w') f.write(config_header) diff --git a/src/conf-mode/vyos-config-dns-forwarding.py b/src/conf-mode/vyos-config-dns-forwarding.py new file mode 100755 index 000000000..be48cde60 --- /dev/null +++ b/src/conf-mode/vyos-config-dns-forwarding.py @@ -0,0 +1,213 @@ +#!/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 sys +import os + +import netifaces +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/powerdns/recursor.conf' + +# XXX: pdns recursor doesn't like whitespace near entry separators, +# especially in the semicolon-separated lists of name servers. +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by vyos-config-dns-forwarding.py ### + +# Non-configurable defaults +daemon=yes +threads=1 +allow-from=0.0.0.0/0 +log-common-errors=yes + +# cache-size +max-cache-entries={{ cache_size }} + +# ignore-hosts-file +export-etc-hosts={{ export_hosts_file }} + +# listen-on +local-address={{ listen_on | join(',') }} + +# domain ... server ... +{% if domains -%} + +forward-zones={% for d in domains %} +{{ d.name }}={{ d.servers | join(";") }} +{%- if loop.first %}, {% endif %} +{% endfor %} + +{% endif %} + +# name-server +forward-zones-recurse=.={{ name_servers | join(';') }} + +""" + +default_config_data = { + 'cache_size' : 10000, + 'export_hosts_file': 'yes', + 'listen_on': [], + 'interfaces': [], + 'name_servers': [], + 'domains': [] +} + + +# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! +def get_resolvers(file): + resolvers = [] + try: + with open(file, 'r') as resolvconf: + for line in resolvconf.readlines(): + line = line.split('#',1)[0]; + line = line.rstrip(); + if 'nameserver' in line: + resolvers.append(line.split()[1]) + return resolvers + except IOError: + return [] + +def get_config(): + dns = default_config_data + conf = Config() + if not conf.exists('service dns forwarding'): + return None + else: + conf.set_level('service dns forwarding') + + if conf.exists('cache-size'): + cache_size = conf.return_value('cache-size') + dns['cache_size'] = cache_size + + if conf.exists('domain'): + for node in conf.list_nodes('domain'): + server = conf.return_values("domain {0} server".format(node)) + domain = { + "name": node, + "servers": server + } + dns['domains'].append(domain) + + if conf.exists('ignore-hosts-file'): + dns.setdefault('export_hosts_file', "no") + + if conf.exists('name-server'): + name_servers = conf.return_values('name-server') + dns['name_servers'] = dns['name_servers'] + name_servers + + if conf.exists('system'): + conf.set_level('system') + system_name_servers = [] + system_name_servers = conf.return_values('name-server') + if not system_name_servers: + print("DNS forwarding warning: No name-servers set under 'system name-server'\n") + else: + dns['name_servers'] = dns['name_servers'] + system_name_servers + conf.set_level('service dns forwarding') + + ## 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'): + interfaces = conf.return_values('listen-on') + + listen4 = [] + listen6 = [] + for interface in interfaces: + addrs = netifaces.ifaddresses(interface) + for ip4 in addrs[netifaces.AF_INET]: + listen4.append(ip4['addr']) + + for ip6 in addrs[netifaces.AF_INET6]: + listen6.append(ip6['addr']) + + 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') + for interface in interfaces: + dhcp_resolvers = get_resolvers("/etc/resolv.conf.dhclient-new-{0}".format(interface)) + if dhcp_resolvers: + dns['name_servers'] = dns['name_servers'] + dhcp_resolvers + + return dns + +def verify(dns): + # bail out early - looks like removal from running config + if dns is None: + return None + + if not dns['interfaces']: + raise ConfigError('Error: DNS forwarding requires a configured listen interface!') + + for interface in dns['interfaces']: + try: + netifaces.ifaddresses(interface)[netifaces.AF_INET] + except KeyError as e: + raise ConfigError('Error: Interface {0} has no IP address assigned'.format(interface)) + + if dns['domains']: + for domain in dns['domains']: + if not domain['servers']: + raise ConfigError('Error: No server configured for domain {0}'.format(domain['name'])) + + return None + +def generate(dns): + # bail out early - looks like removal from running config + if dns is None: + return None + + tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + + config_text = tmpl.render(dns) + with open(config_file, 'w') as f: + f.write(config_text) + return None + +def apply(dns): + if dns is not None: + os.system("systemctl restart pdns-recursor") + else: + # DNS forwarding is removed in the commit + os.system("systemctl stop pdns-recursor") + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf-mode/vyos-config-mdns-repeater.py b/src/conf-mode/vyos-config-mdns-repeater.py index 04a17c126..e648fd64f 100755 --- a/src/conf-mode/vyos-config-mdns-repeater.py +++ b/src/conf-mode/vyos-config-mdns-repeater.py @@ -22,7 +22,7 @@ import netifaces import time from vyos.config import Config -from vyos.util import ConfigError +from vyos import ConfigError config_file = r'/etc/default/mdns-repeater' @@ -35,9 +35,8 @@ def get_config(): return interface_list if conf.exists('interface'): + intfs_names = [] intfs_names = conf.return_values('interface') - intfs_names=intfs_names.replace("'", "") - intfs_names=intfs_names.split() for name in intfs_names: interface_list.append(name) diff --git a/src/conf-mode/vyos-config-ntp.py b/src/conf-mode/vyos-config-ntp.py new file mode 100755 index 000000000..8be12e44e --- /dev/null +++ b/src/conf-mode/vyos-config-ntp.py @@ -0,0 +1,161 @@ +#!/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 sys +import os + +import jinja2 +import ipaddress + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/ntp.conf' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by vyos-config-ntp.py ### + +# +# Non-configurable defaults +# +driftfile /var/lib/ntp/ntp.drift +# By default, only allow ntpd to query time sources, ignore any incoming requests +restrict default ignore +# Local users have unrestricted access, allowing reconfiguration via ntpdc +restrict 127.0.0.1 +restrict -6 ::1 + + +# +# Configurable section +# + +{% if servers -%} +{% for s in servers -%} +# Server configuration for: {{ s.name }} +server {{ s.name }} iburst {{ s.options | join(" ") }} + +{% endfor -%} +{% endif %} + +{% if allowed_networks -%} +{% for n in allowed_networks -%} +# Client configuration for network: {{ n.network }} +restrict {{ n.address }} mask {{ n.netmask }} nomodify notrap nopeer + +{% endfor -%} +{% endif %} + +""" + +default_config_data = { + 'servers': [], + 'allowed_networks': [] +} + +def get_config(): + ntp = default_config_data + conf = Config() + if not conf.exists('system ntp'): + return None + else: + conf.set_level('system ntp') + + if conf.exists('allow-clients address'): + networks = conf.return_values('allow-clients address') + for n in networks: + addr = ipaddress.ip_network(n) + net = { + "network" : n, + "address" : addr.network_address, + "netmask" : addr.netmask + } + + ntp['allowed_networks'].append(net) + + if conf.exists('server'): + for node in conf.list_nodes('server'): + options = [] + server = { + "name": node, + "options": [] + } + if conf.exists('server {0} dynamic'.format(node)): + options.append('dynamic') + if conf.exists('server {0} noselect'.format(node)): + options.append('noselect') + if conf.exists('server {0} preempt'.format(node)): + options.append('preempt') + if conf.exists('server {0} prefer'.format(node)): + options.append('prefer') + + server['options'] = options + ntp['servers'].append(server) + + return ntp + +def verify(ntp): + # bail out early - looks like removal from running config + if ntp is None: + return None + + # Configuring allowed clients without a server makes no sense + if len(ntp['allowed_networks']) and not len(ntp['servers']): + raise ConfigError('NTP server not configured') + + for n in ntp['allowed_networks']: + try: + addr = ipaddress.ip_network( n['network'] ) + break + except ValueError: + raise ConfigError("{0} does not appear to be a valid IPv4 or IPv6 network, check host bits!".format(n['network'])) + + return None + +def generate(ntp): + # bail out early - looks like removal from running config + if ntp is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(ntp) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(ntp): + if ntp is not None: + os.system('sudo /usr/sbin/invoke-rc.d ntp force-reload') + else: + # NTP suuport is removed in the commit + os.system('sudo /usr/sbin/invoke-rc.d ntp stop') + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf-mode/vyos-config-ssh.py b/src/conf-mode/vyos-config-ssh.py new file mode 100755 index 000000000..a4857bba9 --- /dev/null +++ b/src/conf-mode/vyos-config-ssh.py @@ -0,0 +1,249 @@ +#!/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 sys +import os + +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/ssh/sshd_config' + +# Please be careful if you edit the template. +config_tmpl = """ + +### Autogenerated by vyos-config-ssh.py ### + +# Non-configurable defaults +Protocol 2 +HostKey /etc/ssh/ssh_host_rsa_key +HostKey /etc/ssh/ssh_host_dsa_key +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_ed25519_key +UsePrivilegeSeparation yes +KeyRegenerationInterval 3600 +ServerKeyBits 1024 +SyslogFacility AUTH +LoginGraceTime 120 +StrictModes yes +RSAAuthentication yes +PubkeyAuthentication yes +IgnoreRhosts yes +RhostsRSAAuthentication no +HostbasedAuthentication no +PermitEmptyPasswords no +ChallengeResponseAuthentication no +X11Forwarding yes +X11DisplayOffset 10 +PrintMotd no +PrintLastLog yes +TCPKeepAlive yes +Banner /etc/issue.net +Subsystem sftp /usr/lib/openssh/sftp-server +UsePAM yes +HostKey /etc/ssh/ssh_host_key +PermitRootLogin no + +# Specifies whether sshd should look up the remote host name, +# and to check that the resolved host name for the remote IP +# address maps back to the very same IP address. +UseDNS {{ host_validation }} + +# Specifies the port number that sshd listens on. The default is 22. +# Multiple options of this type are permitted. +Port {{ port }} + +# Gives the verbosity level that is used when logging messages from sshd +LogLevel {{ log_level }} + +# Specifies whether password authentication is allowed +PasswordAuthentication {{ password_authentication }} + +{% if listen_on -%} +# Specifies the local addresses sshd should listen on +{% for a in listen_on -%} +ListenAddress {{ a }} +{% endfor -%} +{% endif %} + +{% if ciphers -%} +# Specifies the ciphers allowed. Multiple ciphers must be comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/ +Ciphers {{ ciphers | join(",") }} +{% endif %} + +{% if mac -%} +# Specifies the available MAC (message authentication code) algorithms. The MAC +# algorithm is used for data integrity protection. Multiple algorithms must be +# comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/ +MACs {{ mac | join(",") }} +{% endif %} + +{% if key_exchange -%} +# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must +# be comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/ +KexAlgorithms {{ key_exchange | join(",") }} +{% endif %} + +{% if allow_users -%} +# This keyword can be followed by a list of user name patterns, separated by spaces. +# If specified, login is allowed only for user names that match one of the patterns. +# Only user names are valid, a numerical user ID is not recognized. +AllowUsers {{ allow_users | join(" ") }} +{% endif %} + +{% if allow_groups -%} +# This keyword can be followed by a list of group name patterns, separated by spaces. +# If specified, login is allowed only for users whose primary group or supplementary +# group list matches one of the patterns. Only group names are valid, a numerical group +# ID is not recognized. +AllowGroups {{ allow_groups | join(" ") }} +{% endif %} + +{% if deny_users -%} +# This keyword can be followed by a list of user name patterns, separated by spaces. +# Login is disallowed for user names that match one of the patterns. Only user names +# are valid, a numerical user ID is not recognized. +DenyUsers {{ deny_users | join(" ") }} +{% endif %} + +{% if deny_groups -%} +# This keyword can be followed by a list of group name patterns, separated by spaces. +# Login is disallowed for users whose primary group or supplementary group list matches +# one of the patterns. Only group names are valid, a numerical group ID is not recognized. +DenyGroups {{ deny_groups | join(" ") }} +{% endif %} +""" + +default_config_data = { + 'port' : '22', + 'log_level': 'INFO', + 'password_authentication': 'yes', + 'host_validation': 'yes' +} + +def get_config(): + ssh = default_config_data + conf = Config() + if not conf.exists('service ssh'): + return None + else: + conf.set_level('service ssh') + + if conf.exists('access-control allow user'): + allow_users = conf.return_values('access-control allow user') + ssh.setdefault('allow_users', allow_users) + + if conf.exists('access-control allow group'): + allow_groups = conf.return_values('access-control allow group') + ssh.setdefault('allow_groups', allow_groups) + + if conf.exists('access-control deny user'): + deny_users = conf.return_values('access-control deny user') + ssh.setdefault('deny_users', deny_users) + + if conf.exists('access-control deny group'): + deny_groups = conf.return_values('access-control deny group') + ssh.setdefault('deny_groups', deny_groups) + + if conf.exists('ciphers'): + ciphers = conf.return_values('ciphers') + ssh.setdefault('ciphers', ciphers) + + if conf.exists('disable-host-validation'): + ssh['host_validation'] = 'no' + + if conf.exists('disable-password-authentication'): + ssh['password_authentication'] = 'no' + + if conf.exists('key-exchange'): + kex = conf.return_values('key-exchange') + ssh.setdefault('key_exchange', kex) + + if conf.exists('listen-address'): + # We can listen on both IPv4 and IPv6 addresses + # Maybe there could be a check in the future if the configured IP address + # is configured on this system at all? + addresses = conf.return_values('listen-address') + listen = [] + + for addr in addresses: + listen.append(addr) + + ssh.setdefault('listen_on', listen) + + if conf.exists('loglevel'): + ssh['log_level'] = conf.return_value('loglevel') + + if conf.exists('mac'): + mac = conf.return_values('mac') + ssh.setdefault('mac', mac) + + if conf.exists('port'): + port = conf.return_value('port') + ssh.setdefault('port', port) + + return ssh + +def verify(ssh): + if ssh is None: + return None + + if 'loglevel' in ssh.keys(): + allowed_loglevel = 'QUIET, FATAL, ERROR, INFO, VERBOSE' + if not ssh['loglevel'] in allowed_loglevel: + raise ConfigError('loglevel must be one of "{0}"\n'.format(allowed_loglevel)) + + return None + +def generate(ssh): + if ssh is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(ssh) + with open(config_file, 'w') as f: + f.write(config_text) + return None + +def apply(ssh): + if ssh is not None and 'port' in ssh.keys(): + os.system("sudo systemctl restart ssh") + else: + # SSH access is removed in the commit + os.system("sudo systemctl stop ssh") + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf-mode/vyos-update-crontab.py b/src/conf-mode/vyos-update-crontab.py index 2d15de8ea..c19b88007 100755 --- a/src/conf-mode/vyos-update-crontab.py +++ b/src/conf-mode/vyos-update-crontab.py @@ -21,7 +21,7 @@ import re import sys from vyos.config import Config -from vyos.util import ConfigError +from vyos import ConfigError crontab_file = "/etc/cron.d/vyos-crontab" |