summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/completion/list_pppoe_peers.sh6
-rwxr-xr-xsrc/conf_mode/https.py5
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py21
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py291
-rwxr-xr-xsrc/conf_mode/service-pppoe.py9
-rwxr-xr-xsrc/conf_mode/snmp.py20
-rwxr-xr-xsrc/conf_mode/system-login-radius.py166
-rwxr-xr-xsrc/conf_mode/system-login.py (renamed from src/conf_mode/system-login-user.py)158
-rw-r--r--src/etc/systemd/system/ppp@.service11
-rwxr-xr-xsrc/migration-scripts/interfaces/4-to-5112
-rwxr-xr-xsrc/op_mode/connect_disconnect.py98
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()