diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/completion/list_pppoe_peers.sh | 6 | ||||
-rwxr-xr-x | src/conf_mode/https.py | 5 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-openvpn.py | 21 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-pppoe.py | 291 | ||||
-rwxr-xr-x | src/conf_mode/service-pppoe.py | 9 | ||||
-rwxr-xr-x | src/conf_mode/snmp.py | 20 | ||||
-rwxr-xr-x | src/conf_mode/system-login-radius.py | 166 | ||||
-rwxr-xr-x | src/conf_mode/system-login.py (renamed from src/conf_mode/system-login-user.py) | 158 | ||||
-rw-r--r-- | src/etc/systemd/system/ppp@.service | 11 | ||||
-rwxr-xr-x | src/migration-scripts/interfaces/4-to-5 | 112 | ||||
-rwxr-xr-x | src/op_mode/connect_disconnect.py | 98 |
11 files changed, 698 insertions, 199 deletions
diff --git a/src/completion/list_pppoe_peers.sh b/src/completion/list_pppoe_peers.sh new file mode 100755 index 000000000..382a29264 --- /dev/null +++ b/src/completion/list_pppoe_peers.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +if [ -d /etc/ppp/peers ]; then + cd /etc/ppp/peers + ls pppoe* +fi diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 5d90b2b53..84d1a7691 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -47,8 +47,8 @@ server { # SSL configuration # {% if server.address == '*' %} - listen 443 ssl; - listen [::]:443 ssl; + listen {{ server.port }} ssl; + listen [::]:{{ server.port }} ssl; {% else %} listen {{ server.address }}:{{ server.port }} ssl; {% endif %} @@ -96,6 +96,7 @@ server { default_server_block = { 'address' : '*', + 'port' : '443', 'name' : ['_'], 'api' : {}, 'vyos_cert' : {}, diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 3a7bc6611..622543b58 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -51,7 +51,6 @@ config_tmpl = """ verb 3 status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30 writepid /var/run/openvpn/{{ intf }}.pid -daemon openvpn-{{ intf }} dev-type {{ type }} dev {{ intf }} @@ -162,6 +161,10 @@ cert {{ tls_cert }} key {{ tls_key }} {% endif %} +{%- if tls_crypt %} +tls-crypt {{ tls_crypt }} +{% endif %} + {%- if tls_crl %} crl-verify {{ tls_crl }} {% endif %} @@ -224,7 +227,7 @@ cipher aes-256-cbc {%- if ncp_ciphers %} ncp-ciphers {{ncp_ciphers}} -{% endif %} +{% endif %} {%- if disable_ncp %} ncp-disable {% endif %} @@ -319,6 +322,7 @@ default_config_data = { 'tls_crl': '', 'tls_dh': '', 'tls_key': '', + 'tls_crypt': '', 'tls_role': '', 'tls_version_min': '', 'type': 'tun', @@ -634,6 +638,11 @@ def get_config(): openvpn['tls_key'] = conf.return_value('tls key-file') openvpn['tls'] = True + # File containing key to encrypt control channel packets + if conf.exists('tls crypt-file'): + openvpn['tls_crypt'] = conf.return_value('tls crypt-file') + openvpn['tls'] = True + # Role in TLS negotiation if conf.exists('tls role'): openvpn['tls_role'] = conf.return_value('tls role') @@ -801,6 +810,9 @@ def verify(openvpn): if not openvpn['tls_key']: raise ConfigError('Must specify "tls key-file"') + if openvpn['tls_auth'] and openvpn['tls_crypt']: + raise ConfigError('TLS auth and crypt are mutually exclusive') + if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']): raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert'])) @@ -816,6 +828,10 @@ def verify(openvpn): if not checkCertHeader('-----BEGIN (?:RSA )?PRIVATE KEY-----', openvpn['tls_key']): raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key'])) + if openvpn['tls_crypt']: + if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_crypt']): + raise ConfigError('Specified TLS crypt-file "{}" is invalid'.format(openvpn['tls_crypt'])) + if openvpn['tls_crl']: if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']): raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl'])) @@ -968,6 +984,7 @@ def apply(openvpn): cmd += ' --exec /usr/sbin/openvpn' # now pass arguments to openvpn binary cmd += ' --' + cmd += ' --daemon openvpn-' + openvpn['intf'] cmd += ' --config ' + get_config_name(openvpn['intf']) # execute assembled command diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py new file mode 100755 index 000000000..6acb45d5e --- /dev/null +++ b/src/conf_mode/interfaces-pppoe.py @@ -0,0 +1,291 @@ +#!/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 sys import exit +from copy import deepcopy +from jinja2 import Template +from subprocess import Popen, PIPE +from time import sleep +from pwd import getpwnam +from grp import getgrnam + +from vyos.config import Config +from vyos.ifconfig import Interface +from vyos import ConfigError +from netifaces import interfaces + +# Please be careful if you edit the template. +config_pppoe_tmpl = """ +### Autogenerated by interfaces-pppoe.py ### + +{% if description %} +# {{ description }} +{% endif %} + +# Require peer to provide the local IP address if it is not +# specified explicitly in the config file. +noipdefault + +# Don't show the password in logfiles: +hide-password + +# Standard Link Control Protocol (LCP) parameters: +lcp-echo-interval 20 +lcp-echo-failure 3 + +# RFC 2516, paragraph 7 mandates that the following options MUST NOT be +# requested and MUST be rejected if requested by the peer: +# Address-and-Control-Field-Compression (ACFC) +noaccomp + +# Asynchronous-Control-Character-Map (ACCM) +default-asyncmap + +# Override any connect script that may have been set in /etc/ppp/options. +connect /bin/true + +# Don't try to authenticate the remote node +noauth + +# Don't try to proxy ARP for the remote endpoint. User can set proxy +# arp entries up manually if they wish. More importantly, having +# the "proxyarp" parameter set disables the "defaultroute" option. +noproxyarp + +plugin rp-pppoe.so +{{ source_interface }} +persist +ifname {{ intf }} +ipparam {{ intf }} +debug +logfile {{ logfile }} +{% if 'auto' in default_route -%} +defaultroute +{% elif 'force' in default_route -%} +defaultroute +replacedefaultroute +{% endif %} +mtu {{ mtu }} +mru {{ mtu }} +user "{{ auth_username }}" +password "{{ auth_password }}" +{% if name_server -%} +usepeerdns +{% endif %} +{% if ipv6_enable -%} ++ipv6 +{% endif %} +{% if service_name -%} +rp_pppoe_service "{{ service_name }}" +{% endif %} + +""" + +PPP_LOGFILE = '/var/log/vyatta/ppp_{}.log' + +default_config_data = { + 'access_concentrator': '', + 'auth_username': '', + 'auth_password': '', + 'on_demand': False, + 'default_route': 'auto', + 'deleted': False, + 'description': '', + 'disable': False, + 'intf': '', + 'idle_timeout': '', + 'ipv6_autoconf': False, + 'ipv6_enable': False, + 'local_address': '', + 'logfile': '', + 'mtu': '1492', + 'name_server': True, + 'remote_address': '', + 'service_name': '', + 'source_interface': '' +} + +def subprocess_cmd(command): + p = Popen(command, stdout=PIPE, shell=True) + p.communicate() + +def get_config(): + pppoe = deepcopy(default_config_data) + conf = Config() + base_path = ['interfaces', 'pppoe'] + + # determine tagNode instance + try: + pppoe['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + pppoe['logfile'] = PPP_LOGFILE.format(pppoe['intf']) + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists(base_path + [pppoe['intf']]): + pppoe['deleted'] = True + return pppoe + + # set new configuration level + conf.set_level(base_path + [pppoe['intf']]) + + # Access concentrator name (only connect to this concentrator) + if conf.exists(['access-concentrator']): + pppoe['access_concentrator'] = conf.return_values(['access-concentrator']) + + # Authentication name supplied to PPPoE server + if conf.exists(['authentication', 'user']): + pppoe['auth_username'] = conf.return_value(['authentication', 'user']) + + # Password for authenticating local machine to PPPoE server + if conf.exists(['authentication', 'password']): + pppoe['auth_password'] = conf.return_value(['authentication', 'password']) + + # Access concentrator name (only connect to this concentrator) + if conf.exists(['connect-on-demand']): + pppoe['on_demand'] = True + + # Enable/Disable default route to peer when link comes up + if conf.exists(['default-route']): + pppoe['default_route'] = conf.return_value(['default-route']) + + # Retrieve interface description + if conf.exists(['description']): + pppoe['description'] = conf.return_value(['description']) + + # Disable this interface + if conf.exists(['disable']): + pppoe['disable'] = True + + # Delay before disconnecting idle session (in seconds) + if conf.exists(['idle-timeout']): + pppoe['idle_timeout'] = conf.return_value(['idle-timeout']) + + # Enable Stateless Address Autoconfiguration (SLAAC) + if conf.exists(['ipv6', 'address', 'autoconf']): + pppoe['ipv6_autoconf'] = True + + # Activate IPv6 support on this connection + if conf.exists(['ipv6', 'enable']): + pppoe['ipv6_enable'] = True + + # IPv4 address of local end of PPPoE link + if conf.exists(['local-address']): + pppoe['local_address'] = conf.return_value(['local-address']) + + # Physical Interface used for this PPPoE session + if conf.exists(['source-interface']): + pppoe['source_interface'] = conf.return_value('source-interface') + + # Maximum Transmission Unit (MTU) + if conf.exists(['mtu']): + pppoe['mtu'] = conf.return_value(['mtu']) + + # Do not use DNS servers provided by the peer + if conf.exists(['no-peer-dns']): + pppoe['name_server'] = False + + # IPv4 address for remote end of PPPoE session + if conf.exists(['remote-address']): + pppoe['remote_address'] = conf.return_value(['remote-address']) + + # Service name, only connect to access concentrators advertising this + if conf.exists(['service-name']): + pppoe['service_name'] = conf.return_value(['service-name']) + + return pppoe + +def verify(pppoe): + if pppoe['deleted']: + # bail out early + return None + + if not pppoe['source_interface']: + raise ConfigError('PPPoE source interface is missing') + + if pppoe['source_interface'] not in interfaces(): + raise ConfigError('PPPoE source interface does not exist') + + return None + +def generate(pppoe): + config_file_pppoe = '/etc/ppp/peers/{}'.format(pppoe['intf']) + + # Always hang-up PPPoE connection prior generating new configuration file + cmd = 'systemctl stop ppp@{}.service'.format(pppoe['intf']) + subprocess_cmd(cmd) + + if pppoe['deleted']: + # Delete PPP configuration files + if os.path.exists(config_file_pppoe): + os.unlink(config_file_pppoe) + + else: + # Create PPP configuration files + tmpl = Template(config_pppoe_tmpl) + config_text = tmpl.render(pppoe) + with open(config_file_pppoe, 'w') as f: + f.write(config_text) + + return None + +def apply(pppoe): + if pppoe['deleted']: + # bail out early + return None + + if not pppoe['disable']: + # dial PPPoE connection + cmd = 'systemctl start ppp@{}.service'.format(pppoe['intf']) + subprocess_cmd(cmd) + + # make logfile owned by root / vyattacfg + if os.path.isfile(pppoe['logfile']): + uid = getpwnam('root').pw_uid + gid = getgrnam('vyattacfg').gr_gid + os.chown(pppoe['logfile'], uid, gid) + + # better late then sorry ... but we can only set interface alias after + # pppd has been launched and created the interface + cnt = 0 + while pppoe['intf'] not in interfaces(): + cnt += 1 + if cnt == 50: + break + + # sleep 250ms + sleep(0.250) + + try: + # we need to catch the exception if the interface is not up due to + # reason stated above + Interface(pppoe['intf']).set_alias(pppoe['description']) + except: + pass + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service-pppoe.py b/src/conf_mode/service-pppoe.py index 4090cb953..22250d18b 100755 --- a/src/conf_mode/service-pppoe.py +++ b/src/conf_mode/service-pppoe.py @@ -234,7 +234,6 @@ ipv6-peer-intf-id={{ppp_options['ipv6-peer-intf-id']}} ipv6-accept-peer-intf-id={{ppp_options['ipv6-accept-peer-intf-id']}} {% endif %} {% endif %} - mtu={{mtu}} [pppoe] @@ -251,9 +250,11 @@ interface=re:{{int}}\.\d+ {% endif %} {% endfor -%} {% endif -%} + {% if svc_name %} -service-name={{svc_name}} +service-name={{svc_name|join(',')}} {% endif -%} + {% if pado_delay %} pado-delay={{pado_delay}} {% endif %} @@ -343,7 +344,7 @@ def get_config(): 'client_ipv6_pool': {}, 'interface': {}, 'ppp_gw': '', - 'svc_name': '', + 'svc_name': [], 'dns': [], 'dnsv6': [], 'wins': [], @@ -360,7 +361,7 @@ def get_config(): if c.exists(['access-concentrator']): config_data['concentrator'] = c.return_value(['access-concentrator']) if c.exists(['service-name']): - config_data['svc_name'] = c.return_value(['service-name']) + config_data['svc_name'] = c.return_values(['service-name']) if c.exists(['interface']): for intfc in c.list_nodes(['interface']): config_data['interface'][intfc] = {'vlans': []} diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 7cffa5e04..ac94afb1a 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -710,18 +710,20 @@ def apply(snmp): # Passwords are not available immediately in the configuration file, # after daemon startup - we wait until they have been processed by # snmpd, which we see when a magic line appears in this file. - ready = False - while not ready: + while True: while not os.path.exists(config_file_user): sleep(0.5) - ready = True - with open(config_file_user, 'r') as f: - for line in f: - # Search for our magic string inside the file - if 'usmUser' in line: - ready = True - break + try: + with open(config_file_user, 'r') as f: + for line in f: + # Search for our magic string inside the file + if 'usmUser' in line: + break + except IOError: + continue + else: + break # net-snmp is now regenerating the configuration file in the background # thus we need to re-open and re-read the file as the content changed. diff --git a/src/conf_mode/system-login-radius.py b/src/conf_mode/system-login-radius.py deleted file mode 100755 index caa7f6b80..000000000 --- a/src/conf_mode/system-login-radius.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 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 pwd import getpwall, getpwnam -from stat import S_IRUSR, S_IWUSR - -from vyos.config import Config -from vyos.configdict import list_diff -from vyos import ConfigError - -radius_config_file = "/etc/pam_radius_auth.conf" -radius_config_tmpl = """ -# Automatically generated by VyOS -# RADIUS configuration file -# server[:port] shared_secret timeout (s) source_ip -{% if server -%} -{% for s in server -%} -{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if source_address -%}{{ source_address }}{% endif %} -{% endfor -%} - -priv-lvl 15 -mapped_priv_user radius_priv_user -{% endif %} - -""" - -default_config_data = { - 'server': [], - 'source_address': '', -} - -def get_local_users(): - """Returns list of dynamically allocated users (see Debian Policy Manual)""" - local_users = [] - for p in getpwall(): - username = p[0] - uid = getpwnam(username).pw_uid - if uid in range(1000, 29999): - if username not in ['radius_user', 'radius_priv_user']: - local_users.append(username) - - return local_users - -def get_config(): - radius = default_config_data - conf = Config() - base_level = ['system', 'login', 'radius'] - - if not conf.exists(base_level): - return radius - - conf.set_level(base_level) - - if conf.exists(['source-address']): - radius['source_address'] = conf.return_value(['source-address']) - - # Read in all RADIUS servers and store to list - for server in conf.list_nodes(['server']): - server_cfg = { - 'address': server, - 'key': '', - 'port': '1812', - 'timeout': '2' - } - conf.set_level(base_level + ['server', server]) - - # RADIUS shared secret - if conf.exists(['key']): - server_cfg['key'] = conf.return_value(['key']) - - # RADIUS authentication port - if conf.exists(['port']): - server_cfg['port'] = conf.return_value(['port']) - - # RADIUS session timeout - if conf.exists(['timeout']): - server_cfg['timeout'] = conf.return_value(['timeout']) - - # Append individual RADIUS server configuration to global server list - radius['server'].append(server_cfg) - - return radius - -def verify(radius): - pass - -def generate(radius): - if len(radius['server']) > 0: - tmpl = jinja2.Template(radius_config_tmpl) - config_text = tmpl.render(radius) - with open(radius_config_file, 'w') as f: - f.write(config_text) - - uid = getpwnam('root').pw_uid - gid = getpwnam('root').pw_gid - os.chown(radius_config_file, uid, gid) - os.chmod(radius_config_file, S_IRUSR | S_IWUSR) - else: - os.unlink(radius_config_file) - - return None - -def apply(radius): - if len(radius['server']) > 0: - try: - # Enable RADIUS in PAM - os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius") - - # Make NSS system aware of RADIUS, too - cmd = "sed -i -e \'/\smapname/b\' \ - -e \'/^passwd:/s/\s\s*/&mapuid /\' \ - -e \'/^passwd:.*#/s/#.*/mapname &/\' \ - -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \ - -e \'/^group:.*#/s/#.*/ mapname &/\' \ - -e \'/^group:[^#]*$/s/: */&mapname /\' \ - /etc/nsswitch.conf" - - os.system(cmd) - - except Exception as e: - raise ConfigError('RADIUS configuration failed: {}'.format(e)) - - else: - try: - # Disable RADIUS in PAM - os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius") - - cmd = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \ - -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \ - -e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \ - -e \'s/[ \t]*$//\' \ - /etc/nsswitch.conf" - - os.system(cmd) - - except Exception as e: - raise ConfigError('Removing RADIUS configuration failed'.format(e)) - - 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/system-login-user.py b/src/conf_mode/system-login.py index 087279dc7..a7fb8ee8f 100755 --- a/src/conf_mode/system-login-user.py +++ b/src/conf_mode/system-login.py @@ -16,6 +16,7 @@ import sys import os +import jinja2 from pwd import getpwall, getpwnam from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP @@ -26,10 +27,30 @@ from vyos.config import Config from vyos.configdict import list_diff from vyos import ConfigError +radius_config_file = "/etc/pam_radius_auth.conf" +radius_config_tmpl = """ +# Automatically generated by VyOS +# RADIUS configuration file +{%- if radius_server %} +# server[:port] shared_secret timeout (s) source_ip +{% for s in radius_server %} +{%- if not s.disabled -%} +{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if radius_source_address -%}{{ radius_source_address }}{% endif %} +{% endif %} +{%- endfor %} + +priv-lvl 15 +mapped_priv_user radius_priv_user +{% endif %} + +""" + default_config_data = { 'deleted': False, 'add_users': [], - 'del_users': [] + 'del_users': [], + 'radius_server': [], + 'radius_source_address': '', } def get_local_users(): @@ -55,7 +76,7 @@ def get_crypt_pw(password): def get_config(): login = default_config_data conf = Config() - base_level = ['system', 'login', 'user'] + base_level = ['system', 'login'] # We do not need to check if the nodes exist or not and bail out early # ... this would interrupt the following logic on determine which users @@ -64,7 +85,7 @@ def get_config(): # All fine so far! # Read in all local users and store to list - for username in conf.list_nodes(base_level): + for username in conf.list_nodes(base_level + ['user']): user = { 'name': username, 'password_plaintext': '', @@ -73,7 +94,7 @@ def get_config(): 'full_name': '', 'home_dir': '/home/' + username, } - conf.set_level(base_level + [username]) + conf.set_level(base_level + ['user', username]) # Plaintext password if conf.exists(['authentication', 'plaintext-password']): @@ -99,7 +120,7 @@ def get_config(): 'options': '', 'type': '' } - conf.set_level(base_level + [username, 'authentication', 'public-keys', id]) + conf.set_level(base_level + ['user', username, 'authentication', 'public-keys', id]) # Public Key portion if conf.exists(['key']): @@ -118,6 +139,44 @@ def get_config(): login['add_users'].append(user) + # + # RADIUS configuration + # + conf.set_level(base_level + ['radius']) + + if conf.exists(['source-address']): + login['radius_source_address'] = conf.return_value(['source-address']) + + # Read in all RADIUS servers and store to list + for server in conf.list_nodes(['server']): + server_cfg = { + 'address': server, + 'disabled': False, + 'key': '', + 'port': '1812', + 'timeout': '2' + } + conf.set_level(base_level + ['radius', 'server', server]) + + # Check if RADIUS server was temporary disabled + if conf.exists(['disable']): + server_cfg['disabled'] = True + + # RADIUS shared secret + if conf.exists(['key']): + server_cfg['key'] = conf.return_value(['key']) + + # RADIUS authentication port + if conf.exists(['port']): + server_cfg['port'] = conf.return_value(['port']) + + # RADIUS session timeout + if conf.exists(['timeout']): + server_cfg['timeout'] = conf.return_value(['timeout']) + + # Append individual RADIUS server configuration to global server list + login['radius_server'].append(server_cfg) + # users no longer existing in the running configuration need to be deleted local_users = get_local_users() cli_users = [tmp['name'] for tmp in login['add_users']] @@ -129,6 +188,7 @@ def get_config(): # system is rebooted. login['del_users'] = [tmp for tmp in all_users if tmp not in cli_users] + return login def verify(login): @@ -136,7 +196,17 @@ def verify(login): if cur_user in login['del_users']: raise ConfigError('Attempting to delete current user: {}'.format(cur_user)) - pass + # At lease one RADIUS server must not be disabled + if len(login['radius_server']) > 0: + fail = True + for server in login['radius_server']: + if not server['disabled']: + fail = False + if fail: + raise ConfigError('At least one RADIUS server must be active.') + + + return None def generate(login): # calculate users encrypted password @@ -150,6 +220,20 @@ def generate(login): os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication plaintext-password '' >/dev/null".format(user['name'])) os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}' >/dev/null".format(user['name'], user['password_encrypted'])) + if len(login['radius_server']) > 0: + tmpl = jinja2.Template(radius_config_tmpl) + config_text = tmpl.render(login) + with open(radius_config_file, 'w') as f: + f.write(config_text) + + uid = getpwnam('root').pw_uid + gid = getpwnam('root').pw_gid + os.chown(radius_config_file, uid, gid) + os.chmod(radius_config_file, S_IRUSR | S_IWUSR) + else: + if os.path.isfile(radius_config_file): + os.unlink(radius_config_file) + return None def apply(login): @@ -181,15 +265,19 @@ def apply(login): uid = getpwnam(user['name']).pw_uid gid = getpwnam(user['name']).pw_gid + # we should not rely on the home dir value stored in user['home_dir'] + # as if a crazy user will choose username root or any other system + # user this will fail. should be deny using root at all? + home_dir = getpwnam(user['name']).pw_dir # install ssh keys - key_dir = '{}/.ssh'.format(user['home_dir']) - if not os.path.isdir(key_dir): - os.mkdir(key_dir) - os.chown(key_dir, uid, gid) - os.chmod(key_dir, S_IRWXU | S_IRGRP | S_IXGRP) - - key_file = key_dir + '/authorized_keys'; - with open(key_file, 'w') as f: + ssh_key_dir = home_dir + '/.ssh' + if not os.path.isdir(ssh_key_dir): + os.mkdir(ssh_key_dir) + os.chown(ssh_key_dir, uid, gid) + os.chmod(ssh_key_dir, S_IRWXU | S_IRGRP | S_IXGRP) + + ssh_key_file = ssh_key_dir + '/authorized_keys'; + with open(ssh_key_file, 'w') as f: f.write("# Automatically generated by VyOS\n") f.write("# Do not edit, all changes will be lost\n") @@ -201,8 +289,8 @@ def apply(login): line += '{} {} {}\n'.format(id['type'], id['key'], id['name']) f.write(line) - os.chown(key_file, uid, gid) - os.chmod(key_file, S_IRUSR | S_IWUSR) + os.chown(ssh_key_file, uid, gid) + os.chmod(ssh_key_file, S_IRUSR | S_IWUSR) except Exception as e: raise ConfigError('Adding user "{}" raised an exception: {}'.format(user['name'], e)) @@ -220,6 +308,44 @@ def apply(login): except Exception as e: raise ConfigError('Deleting user "{}" raised an exception: {}'.format(user, e)) + # + # RADIUS configuration + # + if len(login['radius_server']) > 0: + try: + # Enable RADIUS in PAM + os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius") + + # Make NSS system aware of RADIUS, too + cmd = "sed -i -e \'/\smapname/b\' \ + -e \'/^passwd:/s/\s\s*/&mapuid /\' \ + -e \'/^passwd:.*#/s/#.*/mapname &/\' \ + -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \ + -e \'/^group:.*#/s/#.*/ mapname &/\' \ + -e \'/^group:[^#]*$/s/: */&mapname /\' \ + /etc/nsswitch.conf" + + os.system(cmd) + + except Exception as e: + raise ConfigError('RADIUS configuration failed: {}'.format(e)) + + else: + try: + # Disable RADIUS in PAM + os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius") + + cmd = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \ + -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \ + -e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \ + -e \'s/[ \t]*$//\' \ + /etc/nsswitch.conf" + + os.system(cmd) + + except Exception as e: + raise ConfigError('Removing RADIUS configuration failed'.format(e)) + return None if __name__ == '__main__': diff --git a/src/etc/systemd/system/ppp@.service b/src/etc/systemd/system/ppp@.service new file mode 100644 index 000000000..d271efb41 --- /dev/null +++ b/src/etc/systemd/system/ppp@.service @@ -0,0 +1,11 @@ +[Unit] +Description=Dialing PPP connection %I +After=network.target + +[Service] +ExecStart=/usr/sbin/pppd call %I nodetach nolog +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target diff --git a/src/migration-scripts/interfaces/4-to-5 b/src/migration-scripts/interfaces/4-to-5 new file mode 100755 index 000000000..2a42c60ff --- /dev/null +++ b/src/migration-scripts/interfaces/4-to-5 @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +# De-nest PPPoE interfaces +# Migrate boolean nodes to valueless + +import sys +from vyos.configtree import ConfigTree + +def migrate_dialer(config, tree, intf): + for pppoe in config.list_nodes(tree): + # assemble string, 0 -> pppoe0 + new_base = ['interfaces', 'pppoe'] + pppoe_base = new_base + ['pppoe' + pppoe] + config.set(new_base) + # format as tag node to avoid loading problems + config.set_tag(new_base) + + # Copy the entire old node to the new one before migrating individual + # parts + config.copy(tree + [pppoe], pppoe_base) + + # Instead of letting the user choose between auto and none + # where auto is default, it makes more sesne to just offer + # an option to disable the default behavior (declutter CLI) + if config.exists(pppoe_base + ['name-server']): + tmp = config.return_value(pppoe_base + ['name-server']) + if tmp == "none": + config.set(pppoe_base + ['no-peer-dns']) + config.delete(pppoe_base + ['name-server']) + + # Migrate user-id and password nodes under an 'authentication' + # node + if config.exists(pppoe_base + ['user-id']): + user = config.return_value(pppoe_base + ['user-id']) + config.set(pppoe_base + ['authentication', 'user'], value=user) + config.delete(pppoe_base + ['user-id']) + + if config.exists(pppoe_base + ['password']): + pwd = config.return_value(pppoe_base + ['password']) + config.set(pppoe_base + ['authentication', 'password'], value=pwd) + config.delete(pppoe_base + ['password']) + + # remove enable-ipv6 node and rather place it under ipv6 node + if config.exists(pppoe_base + ['enable-ipv6']): + config.set(pppoe_base + ['ipv6', 'enable']) + config.delete(pppoe_base + ['enable-ipv6']) + + # Source interface migration + config.set(pppoe_base + ['source-interface'], value=intf) + + # Remove IPv6 router-advert nodes as this makes no sense on a + # client diale rinterface to send RAs back into the network + # https://phabricator.vyos.net/T2055 + ipv6_ra = pppoe_base + ['ipv6', 'router-advert'] + if config.exists(ipv6_ra): + config.delete(ipv6_ra) + + +if __name__ == '__main__': + if (len(sys.argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = sys.argv[1] + + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + pppoe_links = ['bonding', 'ethernet'] + + for link_type in pppoe_links: + if not config.exists(['interfaces', link_type]): + continue + + for interface in config.list_nodes(['interfaces', link_type]): + # check if PPPoE exists + base_if = ['interfaces', link_type, interface] + pppoe_if = base_if + ['pppoe'] + if config.exists(pppoe_if): + for dialer in config.list_nodes(pppoe_if): + migrate_dialer(config, pppoe_if, interface) + + # Delete old PPPoE interface + config.delete(pppoe_if) + + # bail out early if there are no VLAN interfaces to migrate + if not config.exists(base_if + ['vif']): + continue + + # Migrate PPPoE interfaces attached to a VLAN + for vlan in config.list_nodes(base_if + ['vif']): + vlan_if = base_if + ['vif', vlan] + pppoe_if = vlan_if + ['pppoe'] + if config.exists(pppoe_if): + for dialer in config.list_nodes(pppoe_if): + intf = "{}.{}".format(interface, vlan) + migrate_dialer(config, pppoe_if, intf) + + # Delete old PPPoE interface + config.delete(pppoe_if) + + # Add interface description that this is required for PPPoE + if not config.exists(vlan_if + ['description']): + config.set(vlan_if + ['description'], value='PPPoE link interface') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py new file mode 100755 index 000000000..a22615096 --- /dev/null +++ b/src/op_mode/connect_disconnect.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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 argparse + +from sys import exit +from psutil import process_iter +from time import strftime, localtime, time + +PPP_LOGFILE = '/var/log/vyatta/ppp_{}.log' + +def check_interface(interface): + if not os.path.isfile('/etc/ppp/peers/{}'.format(interface)): + print('Interface {}: invalid!'.format(interface)) + exit(1) + +def check_ppp_running(interface): + """ + Check if ppp process is running in the interface in question + """ + for p in process_iter(): + if "pppd" in p.name(): + if interface in p.cmdline(): + return True + + return False + +def connect(interface): + """ + Connect PPP interface + """ + check_interface(interface) + + # Check if interface is already dialed + if os.path.isdir('/sys/class/net/{}'.format(interface)): + print('Interface {}: already connected!'.format(interface)) + elif check_ppp_running(interface): + print('Interface {}: connection is beeing established!'.format(interface)) + else: + print('Interface {}: connecting...'.format(interface)) + user = os.environ['SUDO_USER'] + tm = strftime("%a %d %b %Y %I:%M:%S %p %Z", localtime(time())) + with open(PPP_LOGFILE.format(interface), 'a') as f: + f.write('{}: user {} started PPP daemon for {} by connect command\n'.format(tm, user, interface)) + cmd = 'umask 0; setsid sh -c "nohup /usr/sbin/pppd call {0} > /tmp/{0}.log 2>&1 &"'.format(interface) + os.system(cmd) + + +def disconnect(interface): + """ + Disconnect PPP interface + """ + check_interface(interface) + + # Check if interface is already down + if not check_ppp_running(interface): + print('Interface {}: connection is already down'.format(interface)) + else: + print('Interface {}: disconnecting...'.format(interface)) + user = os.environ['SUDO_USER'] + tm = strftime("%a %d %b %Y %I:%M:%S %p %Z", localtime(time())) + with open(PPP_LOGFILE.format(interface), 'a') as f: + f.write('{}: user {} stopped PPP daemon for {} by disconnect command\n'.format(tm, user, interface)) + cmd = '/usr/bin/poff "{}"'.format(interface) + os.system(cmd) + +def main(): + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + group.add_argument("--connect", help="Bring up a connection-oriented network interface", action="store") + group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store") + args = parser.parse_args() + + if args.connect: + connect(args.connect) + elif args.disconnect: + disconnect(args.disconnect) + else: + parser.print_help() + + exit(0) + +if __name__ == '__main__': + main() |