diff options
24 files changed, 1043 insertions, 277 deletions
diff --git a/.gitignore b/.gitignore index d89bf1cdc..e7c769aaa 100644 --- a/.gitignore +++ b/.gitignore @@ -117,9 +117,9 @@ debian/debhelper-build-stamp  debian/.debhelper/  debian/vyos-1x  debian/vyos-1x-vmware -debian/vyos-1x.postinst.debhelper -debian/vyos-1x.prerm.debhelper -debian/vyos-1x.substvars +debian/*.postinst.debhelper +debian/*.prerm.debhelper +debian/*.substvars  # Sonar Cloud  .scannerwork @@ -43,6 +43,8 @@ interface_definitions: $(BUILD_DIR) $(obj)  	rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif/node.tag/ip/node.def  	rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/ip/node.def  	rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ip/node.def +	rm -f $(TMPL_DIR)/interfaces/pppoe/node.tag/ip/node.def +	rm -f $(TMPL_DIR)/interfaces/pppoe/node.tag/ipv6/node.def  	rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/ip/node.def  	rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif/node.tag/ip/node.def  	rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/ip/node.def diff --git a/debian/control b/debian/control index dc6c9a8f4..fbcc3fa2e 100644 --- a/debian/control +++ b/debian/control @@ -84,6 +84,7 @@ Depends: python3,    wireless-regdb,    pmacct (>= 1.6.0),    python3-certbot-nginx, +  pppoe,    ${shlibs:Depends},    ${misc:Depends}  Description: VyOS configuration scripts and data diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index e1236b75b..eb19dafeb 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,7 +1,7 @@ +etc/dhcp  etc/init.d -etc/vyos  etc/systemd -etc/dhcp +etc/vyos  lib/  opt/  usr/ diff --git a/interface-definitions/include/interface-disable.xml.i b/interface-definitions/include/interface-disable.xml.i index c6c24f867..a4778859c 100644 --- a/interface-definitions/include/interface-disable.xml.i +++ b/interface-definitions/include/interface-disable.xml.i @@ -1,6 +1,6 @@  <leafNode name="disable">
    <properties>
 -    <help>Set interface to Administratively down</help>
 +    <help>Administratively disable interface</help>
      <valueless/>
    </properties>
  </leafNode>
 diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index bc1a159a9..cac0ee417 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -611,6 +611,18 @@                    </constraint>                  </properties>                </leafNode> +              <leafNode name="crypt-file"> +                <properties> +                  <help>File containing encryption key to authenticate control channel</help> +                  <valueHelp> +                    <format>file</format> +                    <description>File in /config/auth directory</description> +                  </valueHelp> +                  <constraint> +                    <validator name="file-exists" argument="--directory /config/auth"/> +                  </constraint> +                </properties> +              </leafNode>                <leafNode name="tls-version-min">                  <properties>                    <help>Specify the minimum required TLS version</help> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in new file mode 100644 index 000000000..b6b54c915 --- /dev/null +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -0,0 +1,174 @@ +<?xml version="1.0"?> +<interfaceDefinition> +  <node name="interfaces"> +    <children> +      <tagNode name="pppoe" owner="${vyos_conf_scripts_dir}/interfaces-pppoe.py"> +        <properties> +          <help>Point-to-Point Protocol over Ethernet (PPPoE)</help> +          <priority>321</priority> +          <constraint> +            <regex>pppoe[0-9]+$</regex> +            <validator name="numeric" argument="--range 1-99"/> +          </constraint> +          <constraintErrorMessage>PPPoE interface must be named pppoeN</constraintErrorMessage> +          <valueHelp> +            <format>pppoeN</format> +            <description>PPPoE interface name (1-15)</description> +          </valueHelp> +        </properties> +        <children> +          <leafNode name="access-concentrator"> +            <properties> +              <help>Access concentrator name (only connect to this concentrator)</help> +              <constraint> +                <regex>[a-zA-Z0-9]+$</regex> +              </constraint> +              <constraintErrorMessage>Access concentrator name must be composed of uppper and lower case letters or numbers only</constraintErrorMessage> +            </properties> +          </leafNode> +          <node name="authentication"> +            <properties> +              <help>Authentication settings</help> +            </properties> +            <children> +              <leafNode name="user"> +                <properties> +                  <help>User name</help> +                </properties> +              </leafNode> +              <leafNode name="password"> +                <properties> +                  <help>Password</help> +                </properties> +              </leafNode> +            </children> +          </node> +          <leafNode name="connect-on-demand"> +            <properties> +              <help>Automatic establishment of PPPOE connection when traffic is sent</help> +              <valueless/> +            </properties> +          </leafNode> +          <leafNode name="default-route"> +            <properties> +              <help>Default route insertion behaviour (default: auto)</help> +              <completionHelp> +                <list>auto none force</list> +              </completionHelp> +              <constraint> +                <regex>(auto|none|force)</regex> +              </constraint> +              <constraintErrorMessage>PPPoE default-route option must be 'auto', 'none', or 'force'</constraintErrorMessage> +              <valueHelp> +                <format>auto</format> +                <description>Automatically install a default route</description> +              </valueHelp> +              <valueHelp> +                <format>none</format> +                <description>Do not install a default route</description> +              </valueHelp> +              <valueHelp> +                <format>force</format> +                <description>Replace existing default route</description> +              </valueHelp> +            </properties> +          </leafNode> +          #include <include/interface-description.xml.i> +          #include <include/interface-disable.xml.i> +          <leafNode name="idle-timeout"> +            <properties> +              <help>Delay before disconnecting idle session (in seconds)</help> +              <valueHelp> +                <format>n</format> +                <description>Idle timeout in seconds</description> +              </valueHelp> +            </properties> +          </leafNode> +          <node name="ipv6"> +            <children> +              <node name="address"> +                <properties> +                  <help>IPv6 address configuration modes</help> +                </properties> +                <children> +                  <leafNode name="autoconf"> +                    <properties> +                      <help>Enable Stateless Address Autoconfiguration (SLAAC)</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                </children> +              </node> +              <leafNode name="enable"> +                <properties> +                  <help>Activate IPv6 support on this connection</help> +                  <valueless/> +                </properties> +              </leafNode> +            </children> +          </node> +          <leafNode name="source-interface"> +            <properties> +              <help>Physical Interface used for this PPPoE session</help> +              <completionHelp> +                <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> +              </completionHelp> +            </properties> +          </leafNode> +          <leafNode name="local-address"> +            <properties> +              <help>IPv4 address of local end of the PPPoE link</help> +              <valueHelp> +                <format>ipv4</format> +                <description>Address of local end of the PPPoE link</description> +              </valueHelp> +              <constraint> +                <validator name="ipv4-address"/> +              </constraint> +            </properties> +          </leafNode> +          <leafNode name="mtu"> +            <properties> +              <help>Maximum Transmission Unit (MTU)</help> +              <valueHelp> +                <format>68-1500</format> +                <description>Maximum Transmission Unit (default 1492)</description> +              </valueHelp> +              <constraint> +                <validator name="numeric" argument="--range 68-1500"/> +              </constraint> +              <constraintErrorMessage>MTU must be between 68 and 1500</constraintErrorMessage> +            </properties> +          </leafNode> +          <leafNode name="no-peer-dns"> +            <properties> +              <help>Do not use DNS servers provided by the peer</help> +              <valueless/> +            </properties> +          </leafNode> +          <leafNode name="remote-address"> +            <properties> +              <help>IPv4 address of remote end of the PPPoE link</help> +              <valueHelp> +                <format>ipv4</format> +                <description>Address of remote end of the PPPoE link</description> +              </valueHelp> +              <constraint> +                <validator name="ipv4-address"/> +              </constraint> +            </properties> +          </leafNode> +          <leafNode name="service-name"> +            <properties> +              <help>Service name, only connect to access concentrators advertising this</help> +              <constraint> +                <regex>[a-zA-Z0-9]+$</regex> +              </constraint> +              <constraintErrorMessage>Service name must be composed of uppper and lower case letters or numbers only</constraintErrorMessage> +            </properties> +          </leafNode> +        </children> +      </tagNode> +    </children> +  </node> +</interfaceDefinition> diff --git a/interface-definitions/service-pppoe.xml.in b/interface-definitions/service-pppoe.xml.in index 7f93a6e2f..b4950ede1 100644 --- a/interface-definitions/service-pppoe.xml.in +++ b/interface-definitions/service-pppoe.xml.in @@ -436,6 +436,7 @@                  <regex>[a-zA-Z0-9\-]{1,100}</regex>                </constraint>                <constraintErrorMessage>servicename can contain aplhanumerical characters and dashes only (max. 100)</constraintErrorMessage> +              <multi/>              </properties>            </leafNode>            <node name="wins-servers"> diff --git a/interface-definitions/system-login-banner.xml.in b/interface-definitions/system-login-banner.xml.in index b63a65d4d..c4bb14bd6 100644 --- a/interface-definitions/system-login-banner.xml.in +++ b/interface-definitions/system-login-banner.xml.in @@ -2,7 +2,11 @@  <interfaceDefinition>    <node name="system">      <children> -      <node name="login"> +      <node name="login" owner="${vyos_conf_scripts_dir}/system-login.py"> +        <properties> +          <help>System User Login Configuration</help> +          <priority>400</priority> +        </properties>          <children>            <node name="banner" owner="${vyos_conf_scripts_dir}/system-login-banner.py">              <properties> diff --git a/interface-definitions/system-login-radius.xml.in b/interface-definitions/system-login-radius.xml.in deleted file mode 100644 index 00e85db3e..000000000 --- a/interface-definitions/system-login-radius.xml.in +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> -  <node name="system"> -    <children> -      <node name="login"> -        <children> -          <node name="radius" owner="${vyos_conf_scripts_dir}/system-login-radius.py"> -            <properties> -              <help>RADIUS based user authentication</help> -            </properties> -            <children> -              <leafNode name="source-address"> -                <properties> -                  <help>RADIUS client source address</help> -                  <valueHelp> -                    <format>ipv4</format> -                    <description>TFTP IPv4 listen address</description> -                  </valueHelp> -                  <constraint> -                    <validator name="ipv4-address"/> -                  </constraint> -                </properties> -              </leafNode> -              <tagNode name="server"> -                <properties> -                  <help>RADIUS server configuration</help> -                </properties> -                <children> -                  <leafNode name="key"> -                    <properties> -                      <help>RADIUS shared secret key</help> -                    </properties> -                  </leafNode> -                  <leafNode name="port"> -                    <properties> -                      <help>RADIUS authentication port</help> -                      <valueHelp> -                        <format>1-65535</format> -                        <description>Numeric IP port (default: 1812)</description> -                      </valueHelp> -                      <constraint> -                        <validator name="numeric" argument="--range 1-65535"/> -                      </constraint> -                    </properties> -                  </leafNode> -                  <leafNode name="timeout"> -                    <properties> -                      <help>Timeout for RADIUS session</help> -                      <valueHelp> -                        <format>1-30</format> -                        <description>Session timeout in seconds (default: 2)</description> -                      </valueHelp> -                      <constraint> -                        <validator name="numeric" argument="--range 1-30"/> -                      </constraint> -                      <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage> -                    </properties> -                  </leafNode> -                </children> -              </tagNode> -            </children> -          </node> -        </children> -      </node> -    </children> -  </node> -</interfaceDefinition> diff --git a/interface-definitions/system-login-user.xml.in b/interface-definitions/system-login.xml.in index c183e93e3..3ed85b8d3 100644 --- a/interface-definitions/system-login-user.xml.in +++ b/interface-definitions/system-login.xml.in @@ -2,15 +2,15 @@  <interfaceDefinition>    <node name="system">      <children> -      <node name="login"> +      <node name="login" owner="${vyos_conf_scripts_dir}/system-login.py">          <properties> -          <help>User Login</help> +          <help>System User Login Configuration</help>            <priority>400</priority>          </properties>          <children> -          <tagNode name="user" owner="${vyos_conf_scripts_dir}/system-login-user.py"> +          <tagNode name="user">              <properties> -              <help>User account information</help> +              <help>Local user account information</help>                <constraint>                  <regex>[a-zA-Z0-9\-_\.]{1,100}</regex>                </constraint> @@ -110,6 +110,75 @@                </leafNode>              </children>            </tagNode> +          <node name="radius"> +            <properties> +              <help>RADIUS based user authentication</help> +            </properties> +            <children> +              <leafNode name="source-address"> +                <properties> +                  <help>RADIUS client source address</help> +                  <valueHelp> +                    <format>ipv4</format> +                    <description>TFTP IPv4 listen address</description> +                  </valueHelp> +                  <constraint> +                    <validator name="ipv4-address"/> +                  </constraint> +                </properties> +              </leafNode> +              <tagNode name="server"> +                <properties> +                  <help>RADIUS server configuration</help> +                  <valueHelp> +                    <format>ipv4</format> +                    <description>RADIUS server IPv4 address</description> +                  </valueHelp> +                  <constraint> +                    <validator name="ipv4-address"/> +                  </constraint> +                </properties> +                <children> +                  <leafNode name="disable"> +                    <properties> +                      <help>Temporary disable this server</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                  <leafNode name="key"> +                    <properties> +                      <help>Shared secret key</help> +                    </properties> +                  </leafNode> +                  <leafNode name="port"> +                    <properties> +                      <help>Authentication port</help> +                      <valueHelp> +                        <format>1-65535</format> +                        <description>Numeric IP port (default: 1812)</description> +                      </valueHelp> +                      <constraint> +                        <validator name="numeric" argument="--range 1-65535"/> +                      </constraint> +                    </properties> +                  </leafNode> +                  <leafNode name="timeout"> +                    <properties> +                      <help>Session timeout</help> +                      <valueHelp> +                        <format>1-30</format> +                        <description>Session timeout in seconds (default: 2)</description> +                      </valueHelp> +                      <constraint> +                        <validator name="numeric" argument="--range 1-30"/> +                      </constraint> +                      <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage> +                    </properties> +                  </leafNode> +                </children> +              </tagNode> +            </children> +          </node>          </children>        </node>      </children> diff --git a/op-mode-definitions/connect-disconnect.xml b/op-mode-definitions/connect-disconnect.xml new file mode 100644 index 000000000..77c334180 --- /dev/null +++ b/op-mode-definitions/connect-disconnect.xml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<interfaceDefinition> +  <node name="connect"> +    <properties> +      <help>Establish a connection</help> +    </properties> +    <children> +      <tagNode name="interface"> +        <properties> +          <help>Bring up a connection-oriented network interface</help> +          <completionHelp> +            <script>${vyos_completion_dir}/list_pppoe_peers.sh</script> +          </completionHelp> +        </properties> +        <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --connect "$3"</command> +      </tagNode> +    </children> +  </node> +  <node name="disconnect"> +    <properties> +      <help>Take down a connection</help> +    </properties> +    <children> +      <tagNode name="interface"> +        <properties> +          <help>Take down a connection-oriented network interface</help> +          <completionHelp> +            <script>${vyos_completion_dir}/list_pppoe_peers.sh</script> +          </completionHelp> +        </properties> +        <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --disconnect "$3"</command> +      </tagNode> +    </children> +  </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-pppoe.xml b/op-mode-definitions/show-interfaces-pppoe.xml new file mode 100644 index 000000000..3acb14486 --- /dev/null +++ b/op-mode-definitions/show-interfaces-pppoe.xml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<interfaceDefinition> +  <node name="show"> +    <children> +      <node name="interfaces"> +        <children> +          <tagNode name="pppoe"> +            <properties> +              <help>Show PPPoE interface information</help> +              <completionHelp> +                <script>${vyos_completion_dir}/list_pppoe_peers.sh</script> +              </completionHelp> +            </properties> +            <command>${vyatta_bindir}/vyatta-show-interfaces.pl pppoe --intf="$4"</command> +            <children> +              <node name="log"> +                <properties> +                  <help>Show PPPoE logs</help> +                </properties> +                <command>cat /var/log/vyatta/ppp_$4.log</command> +                <children> +                  <leafNode name="tail"> +                    <properties> +                      <help>Watch PPPoE logs</help> +                    </properties> +                    <command>tail --follow=name /var/log/vyatta/ppp_$4.log</command> +                  </leafNode> +                </children> +              </node> +            </children> +          </tagNode> +        </children> +      </node> +    </children> +  </node> +</interfaceDefinition> 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()  | 
