summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile1
-rw-r--r--debian/control3
-rw-r--r--interface-definitions/https.xml24
-rw-r--r--interface-definitions/interfaces-bridge.xml2
-rw-r--r--interface-definitions/interfaces-openvpn.xml625
-rw-r--r--interface-definitions/protocols-bfd.xml8
-rw-r--r--op-mode-definitions/openvpn.xml112
-rw-r--r--op-mode-definitions/show-protocols-bfd.xml6
-rw-r--r--op-mode-definitions/show-system-info.xml167
-rw-r--r--python/vyos/defaults.py19
-rwxr-xr-xsrc/completion/list_openvpn_clients.py57
-rwxr-xr-xsrc/conf_mode/host_name.py2
-rwxr-xr-xsrc/conf_mode/http-api.py11
-rwxr-xr-xsrc/conf_mode/https.py22
-rwxr-xr-xsrc/conf_mode/interface-openvpn.py911
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py4
-rwxr-xr-xsrc/conf_mode/vyos_cert.py143
-rwxr-xr-xsrc/helpers/vyos-sudo.py40
-rwxr-xr-xsrc/op_mode/show_ram.sh33
-rwxr-xr-xsrc/op_mode/show_users.py111
20 files changed, 2277 insertions, 24 deletions
diff --git a/Makefile b/Makefile
index 89b83d4f4..ee01e5ad3 100644
--- a/Makefile
+++ b/Makefile
@@ -37,6 +37,7 @@ op_mode_definitions:
rm -f $(OP_TMPL_DIR)/monitor/node.def
rm -f $(OP_TMPL_DIR)/generate/node.def
rm -f $(OP_TMPL_DIR)/show/vpn/node.def
+ rm -f $(OP_TMPL_DIR)/show/system/node.def
.PHONY: all
all: clean interface_definitions op_mode_definitions
diff --git a/debian/control b/debian/control
index c8946e991..a65d0158e 100644
--- a/debian/control
+++ b/debian/control
@@ -58,6 +58,9 @@ Depends: python3,
pdns-recursor,
lcdproc,
lcdproc-extra-drivers,
+ openvpn,
+ openvpn-auth-ldap,
+ openvpn-auth-radius,
${shlibs:Depends},
${misc:Depends}
Description: VyOS configuration scripts and data
diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml
index 828de449c..13d5c43ea 100644
--- a/interface-definitions/https.xml
+++ b/interface-definitions/https.xml
@@ -27,6 +27,30 @@
</constraint>
</properties>
</leafNode>
+ <node name="certificates">
+ <properties>
+ <help>TLS certificates</help>
+ </properties>
+ <children>
+ <node name="system-generated-certificate" owner="${vyos_conf_scripts_dir}/vyos_cert.py">
+ <properties>
+ <help>Use an automatically generated self-signed certificate</help>
+ <valueless/>
+ </properties>
+ <children>
+ <leafNode name="lifetime">
+ <properties>
+ <help>Lifetime in days; default is 365</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Number of days</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
<node name="api" owner="${vyos_conf_scripts_dir}/http-api.py">
<properties>
<help>VyOS HTTP API configuration</help>
diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml
index d20582849..adb525a46 100644
--- a/interface-definitions/interfaces-bridge.xml
+++ b/interface-definitions/interfaces-bridge.xml
@@ -5,7 +5,7 @@
<tagNode name="bridge" owner="${vyos_conf_scripts_dir}/interface-bridge.py">
<properties>
<help>Bridge interface name</help>
- <priority>310</priority>
+ <priority>470</priority>
<constraint>
<regex>^br[0-9]+$</regex>
</constraint>
diff --git a/interface-definitions/interfaces-openvpn.xml b/interface-definitions/interfaces-openvpn.xml
new file mode 100644
index 000000000..d4e903c48
--- /dev/null
+++ b/interface-definitions/interfaces-openvpn.xml
@@ -0,0 +1,625 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="openvpn" owner="${vyos_conf_scripts_dir}/interface-openvpn.py">
+ <properties>
+ <help>OpenVPN tunnel interface name</help>
+ <priority>460</priority>
+ <constraint>
+ <regex>^vtun[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>OpenVPN tunnel interface must be named vtunN</constraintErrorMessage>
+ <valueHelp>
+ <format>vtunN</format>
+ <description>OpenVPN interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <node name="authentication">
+ <properties>
+ <help>Authentication options</help>
+ </properties>
+ <children>
+ <leafNode name="password">
+ <properties>
+ <help>OpenVPN password used for authentication</help>
+ </properties>
+ </leafNode>
+ <leafNode name="username">
+ <properties>
+ <help>OpenVPN username used for authentication</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="bridge-group">
+ <properties>
+ <help>Interface to be added to a bridge group</help>
+ </properties>
+ <children>
+ <leafNode name="bridge">
+ <properties>
+ <help>Interface to a bridge-group</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py -t bridge</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="cost">
+ <properties>
+ <help>Path cost for this port</help>
+ <valueHelp>
+ <format>0-2147483647</format>
+ <description>Path cost for this port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-2147483647"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="cost">
+ <properties>
+ <help>Path priority for this port</help>
+ <valueHelp>
+ <format>0-255</format>
+ <description>Path priority for this port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="description">
+ <properties>
+ <help>Description</help>
+ </properties>
+ </leafNode>
+ <leafNode name="device-type">
+ <properties>
+ <help>OpenVPN interface device-type</help>
+ <completionHelp>
+ <list>tun tap</list>
+ </completionHelp>
+ <valueHelp>
+ <format>tun</format>
+ <description>TUN device, required for OSI layer 3</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tap</format>
+ <description>TAP device, required for OSI layer 2</description>
+ </valueHelp>
+ <constraint>
+ <regex>(tun|tap)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="encryption">
+ <properties>
+ <help>Data Encryption Algorithm</help>
+ <completionHelp>
+ <list>des 3des bf128 bf256 aes128 aes192 aes256</list>
+ </completionHelp>
+ <valueHelp>
+ <format>des</format>
+ <description>DES algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>3des</format>
+ <description>DES algorithm with triple encryption</description>
+ </valueHelp>
+ <valueHelp>
+ <format>bf128</format>
+ <description>Blowfish algorithm with 128-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>bf256</format>
+ <description>Blowfish algorithm with 256-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes128</format>
+ <description>AES algorithm with 128-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes192</format>
+ <description>AES algorithm with 192-bit key</description>
+ </valueHelp>
+ <valueHelp>
+ <format>aes256</format>
+ <description>AES algorithm with 256-bit key</description>
+ </valueHelp>
+ <constraint>
+ <regex>(des|3des|bf128|bf256|aes128|aes192|aes256)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="hash">
+ <properties>
+ <help>Hashing Algorithm</help>
+ <completionHelp>
+ <list>md5 sha1 sha256 sha384 sha512</list>
+ </completionHelp>
+ <valueHelp>
+ <format>md5</format>
+ <description>MD5 algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha1</format>
+ <description>SHA-1 algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha256</format>
+ <description>SHA-256 algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha384</format>
+ <description>SHA-384 algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha512</format>
+ <description>SHA-512 algorithm</description>
+ </valueHelp>
+ <constraint>
+ <regex>(md5|sha1|sha256|sha384|sha512)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="keep-alive">
+ <properties>
+ <help>Keepalive helper options</help>
+ </properties>
+ <children>
+ <leafNode name="failure-count">
+ <properties>
+ <help>Maximum number of keepalive packet failures [default 6]</help>
+ <valueHelp>
+ <format>0-1000</format>
+ <description>Maximum number of keepalive packet failures</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-1000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="interval">
+ <properties>
+ <help>Keepalive packet interval (seconds) [default 10]</help>
+ <valueHelp>
+ <format>0-600</format>
+ <description>Keepalive packet interval (seconds)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-600"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="local-address">
+ <properties>
+ <help>Local IP address of tunnel</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="subnet-mask">
+ <properties>
+ <help>Subnet-mask for local IP address of tunnel</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="local-host">
+ <properties>
+ <help>Local IP address to accept connections (all if not set)</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Local IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="local-port">
+ <properties>
+ <help>Local port number to accept connections</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mode">
+ <properties>
+ <help>OpenVPN mode of operation</help>
+ <completionHelp>
+ <list>site-to-site client server</list>
+ </completionHelp>
+ <valueHelp>
+ <format>site-to-site</format>
+ <description>Site-to-site mode</description>
+ </valueHelp>
+ <valueHelp>
+ <format>client</format>
+ <description>Client in client-server mode</description>
+ </valueHelp>
+ <valueHelp>
+ <format>server</format>
+ <description>Server in client-server mode</description>
+ </valueHelp>
+ <constraint>
+ <regex>(site-to-site|client|server)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="openvpn-option">
+ <properties>
+ <help>Additional OpenVPN options. You must
+ use the syntax of openvpn.conf in this text-field. Using this
+ without proper knowledge may result in a crashed OpenVPN server.
+ Check system log to look for errors.</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="persistent-tunnel">
+ <properties>
+ <help>Do not close and reopen interface (TUN/TAP device) on client restarts</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="protocol">
+ <properties>
+ <help>OpenVPN communication protocol</help>
+ <completionHelp>
+ <list>udp tcp-passive tcp-active</list>
+ </completionHelp>
+ <valueHelp>
+ <format>udp</format>
+ <description>Site-to-site mode</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp-passive</format>
+ <description>TCP and accepts connections passively</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp-active</format>
+ <description>TCP and initiates connections actively</description>
+ </valueHelp>
+ <constraint>
+ <regex>(udp|tcp-passive|tcp-active)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-address">
+ <properties>
+ <help>IP address of remote end of tunnel</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Remote end IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-host">
+ <properties>
+ <help>Remote host to connect to (dynamic if not set)</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IP address of remote host</description>
+ </valueHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Hostname of remote host</description>
+ </valueHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="remote-port">
+ <properties>
+ <help>Remote port number to connect to</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="replace-default-route">
+ <properties>
+ <help>OpenVPN tunnel to be used as the default route</help>
+ </properties>
+ <children>
+ <leafNode name="local">
+ <properties>
+ <help>Tunnel endpoints are on the same subnet</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="server">
+ <properties>
+ <help>Server-mode options</help>
+ </properties>
+ <children>
+ <node name="2-factor-authentication">
+ <properties>
+ <help>Two Factor Authentication providers</help>
+ </properties>
+ <children>
+ <node name="authy">
+ <properties>
+ <help>Authy Two Factor Authentication providers</help>
+ </properties>
+ <children>
+ <leafNode name="api-key">
+ <properties>
+ <help>Authy api key</help>
+ </properties>
+ </leafNode>
+ <tagNode name="user">
+ <properties>
+ <help>Authy users (must be email address)</help>
+ <constraint>
+ <regex>[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid email address</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="country-calling-code">
+ <properties>
+ <help>Country calling codes</help>
+ <constraint>
+ <regex>[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid Country Calling Code</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="phone-number">
+ <properties>
+ <help>Mobile phone number</help>
+ <constraint>
+ <regex>[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid Phone Number</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <tagNode name="client">
+ <properties>
+ <help>Client-specific settings</help>
+ <valueHelp>
+ <format>name</format>
+ <description>Client common-name in the certificate</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="disable">
+ <properties>
+ <help>Option to disable client connection</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ip">
+ <properties>
+ <help>IP address of the client</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Client IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="push-route">
+ <properties>
+ <help>Route to be pushed to the client</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 network and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="subnet">
+ <properties>
+ <help>Subnet belonging to the client</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 network and prefix length belonging to the client</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="domain-name">
+ <properties>
+ <help>DNS suffix to be pushed to all clients</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Domain Name Server suffix</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="max-connections">
+ <properties>
+ <help>Number of maximum client connections</help>
+ <valueHelp>
+ <format>1-4096</format>
+ <description>Number of concurrent clients</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4096"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="name-server">
+ <properties>
+ <help>Domain Name Server (DNS)</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>DNS server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="push-route">
+ <properties>
+ <help>Route to be pushed to all clients</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 network and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="reject-unconfigured-clients">
+ <properties>
+ <help>Reject connections from clients that are not explicitly configured</help>
+ </properties>
+ </leafNode>
+ <leafNode name="subnet">
+ <properties>
+ <help>Server-mode subnet (from which client IPs are allocated)</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="topology">
+ <properties>
+ <help>Topology for clients</help>
+ <completionHelp>
+ <list>point-to-point subnet</list>
+ </completionHelp>
+ <valueHelp>
+ <format>point-to-point</format>
+ <description>Point-to-point topology</description>
+ </valueHelp>
+ <valueHelp>
+ <format>subnet</format>
+ <description>Subnet topology</description>
+ </valueHelp>
+ <constraint>
+ <regex>(subnet|point-to-point)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="shared-secret-key-file">
+ <properties>
+ <help>File containing the secret key shared with remote end of tunnel</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>
+ <node name="tls">
+ <properties>
+ <help>Transport Layer Security (TLS) options</help>
+ </properties>
+ <children>
+ <leafNode name="ca-cert-file">
+ <properties>
+ <help>File containing certificate for Certificate Authority (CA)</help>
+ </properties>
+ </leafNode>
+ <leafNode name="cert-file">
+ <properties>
+ <help>File containing certificate for this host</help>
+ </properties>
+ </leafNode>
+ <leafNode name="crl-file">
+ <properties>
+ <help>File containing certificate revocation list (CRL) for this host</help>
+ </properties>
+ </leafNode>
+ <leafNode name="dh-file">
+ <properties>
+ <help>File containing Diffie Hellman parameters (server only)</help>
+ </properties>
+ </leafNode>
+ <leafNode name="key-file">
+ <properties>
+ <help>File containing this host's private key</help>
+ </properties>
+ </leafNode>
+ <leafNode name="role">
+ <properties>
+ <help>File containing this host's private key</help>
+ <completionHelp>
+ <list>active passive</list>
+ </completionHelp>
+ <valueHelp>
+ <format>active</format>
+ <description>Initiate TLS negotiation actively</description>
+ </valueHelp>
+ <valueHelp>
+ <format>passive</format>
+ <description>Waiting for TLS connections passively</description>
+ </valueHelp>
+ <constraint>
+ <regex>(active|passive)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="use-lzo-compression">
+ <properties>
+ <help>Use fast LZO compression on this TUN/TAP interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/protocols-bfd.xml b/interface-definitions/protocols-bfd.xml
index f2d7d7d2f..62e2c87b9 100644
--- a/interface-definitions/protocols-bfd.xml
+++ b/interface-definitions/protocols-bfd.xml
@@ -20,6 +20,10 @@
<format>ipv6</format>
<description>BFD peer IPv6 address</description>
</valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
</properties>
<children>
<node name="source">
@@ -46,6 +50,10 @@
<format>ipv6</format>
<description>Local IPv6 address used to connect to the peer</description>
</valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
</properties>
</leafNode>
</children>
diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml
new file mode 100644
index 000000000..4c958257a
--- /dev/null
+++ b/op-mode-definitions/openvpn.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="openvpn">
+ <properties>
+ <help>OpenVPN key generation tool</help>
+ </properties>
+ <children>
+ <tagNode name="key">
+ <properties>
+ <help>Generate shared-secret key with specified file name</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>
+ result=1;
+ key_path=$4
+ full_path=
+
+ # Prepend /config/auth if the path is not absolute
+ if echo $key_path | egrep -ve '^/.*' > /dev/null; then
+ full_path=/config/auth/$key_path
+ else
+ full_path=$key_path
+ fi
+
+ key_dir=`dirname $full_path`
+ if [ ! -d $key_dir ]; then
+ echo "Directory $key_dir does not exist!"
+ exit 1
+ fi
+
+ echo "Generating OpenVPN key to $full_path"
+ sudo /usr/sbin/openvpn --genkey --secret "$full_path"
+ result=$?
+ if [ $result = 0 ]; then
+ echo "Your new local OpenVPN key has been generated"
+ fi
+ /usr/libexec/vyos/validators/file-exists --directory /config/auth "$full_path"
+ </command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="reset">
+ <children>
+ <node name="openvpn">
+ <children>
+ <tagNode name="client">
+ <properties>
+ <help>Reset specified OpenVPN client</help>
+ <completionHelp>
+ <script>sudo ${vyos_completion_dir}/list_openvpn_clients.py --all</script>
+ </completionHelp>
+ </properties>
+ <command>echo kill $4 | socat - UNIX-CONNECT:/tmp/openvpn-mgmt-intf > /dev/null</command>
+ </tagNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Reset OpenVPN process on interface</help>
+ <completionHelp>
+ <script>sudo ${vyos_completion_dir}/list_interfaces.py --type openvpn</script>
+ </completionHelp>
+ </properties>
+ <command>sudo kill -SIGUSR1 $(cat /var/run/openvpn/$4.pid)</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <node name="openvpn">
+ <properties>
+ <help>Show OpenVPN interface information</help>
+ </properties>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed OpenVPN interface information</help>
+ </properties>
+ <command>${vyatta_bindir}/vyatta-show-interfaces.pl --intf-type=openvpn --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ <tagNode name="openvpn">
+ <properties>
+ <help>Show OpenVPN interface information</help>
+ <completionHelp>
+ <script>sudo ${vyos_completion_dir}/list_interfaces.py --type openvpn</script>
+ </completionHelp>
+ </properties>
+ <command>${vyatta_bindir}/vyatta-show-interfaces.pl --intf=$4</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show summary of specified OpenVPN interface information</help>
+ </properties>
+ <command>${vyatta_bindir}/vyatta-show-interfaces.pl --intf="$4" --action=show-brief</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-protocols-bfd.xml b/op-mode-definitions/show-protocols-bfd.xml
index 2a94d0497..398a81d1b 100644
--- a/op-mode-definitions/show-protocols-bfd.xml
+++ b/op-mode-definitions/show-protocols-bfd.xml
@@ -24,16 +24,16 @@
<properties>
<help>Show Bidirectional Forwarding Detection (BFD) peer status</help>
<completionHelp>
- <script>/usr/bin/vtysh -c "show bfd peer" | grep peer | awk '{print $2}'</script>
+ <script>/usr/bin/vtysh -c "show bfd peers" | awk '/[:blank:]*peer/ { printf "%s\n", $2 }'</script>
</completionHelp>
</properties>
- <command>/usr/bin/vtysh -c "show bfd peer $5"</command>
+ <command>/usr/bin/vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 '($0 ~ peer BFD_PEER) { system("/usr/bin/vtysh -c \"show bfd " $0 "\"") }'</command>
<children>
<leafNode name="counters">
<properties>
<help>Show Bidirectional Forwarding Detection (BFD) peer counters</help>
</properties>
- <command>/usr/bin/vtysh -c "show bfd peer $5 counters"</command>
+ <command>/usr/bin/vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 '($0 ~ peer BFD_PEER) { system("/usr/bin/vtysh -c \"show bfd " $0 " counters\"") }'</command>
</leafNode>
</children>
</tagNode>
diff --git a/op-mode-definitions/show-system-info.xml b/op-mode-definitions/show-system-info.xml
new file mode 100644
index 000000000..ade3829f2
--- /dev/null
+++ b/op-mode-definitions/show-system-info.xml
@@ -0,0 +1,167 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="system">
+ <properties>
+ <help>Show system information</help>
+ </properties>
+ <children>
+
+ <node name="connections">
+ <properties>
+ <help>Show active network connections on the system</help>
+ </properties>
+ <command>netstat -an</command>
+ <children>
+ <node name="tcp">
+ <properties>
+ <help>Show TCP connection information</help>
+ </properties>
+ <command>ss -t -r</command>
+ <children>
+ <leafNode name="all">
+ <properties>
+ <help>Show all TCP connections</help>
+ </properties>
+ <command>ss -t -a</command>
+ </leafNode>
+ <leafNode name="numeric">
+ <properties>
+ <help>Show TCP connection without resolving names</help>
+ </properties>
+ <command>ss -t -n</command>
+ </leafNode>
+ </children>
+ </node>
+ <node name="udp">
+ <properties>
+ <help>Show UDP socket information</help>
+ </properties>
+ <command>ss -u -a -r</command>
+ <children>
+ <leafNode name="numeric">
+ <properties>
+ <help>Show UDP socket information without resolving names</help>
+ </properties>
+ <command>ss -u -a -n</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+
+ <leafNode name="kernel-messages">
+ <properties>
+ <help>Show messages in kernel ring buffer</help>
+ </properties>
+ <command>sudo dmesg</command>
+ </leafNode>
+
+ <node name="login">
+ <properties>
+ <help>Show user accounts</help>
+ </properties>
+ <children>
+ <node name="users">
+ <properties>
+ <help>Show user account information</help>
+ </properties>
+ <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py</command>
+ <children>
+ <leafNode name="all">
+ <properties>
+ <help>Show information about all accounts</help>
+ </properties>
+ <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py all</command>
+ </leafNode>
+ <leafNode name="locked">
+ <properties>
+ <help>Show information about locked accounts</help>
+ </properties>
+ <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py locked</command>
+ </leafNode>
+ <leafNode name="other">
+ <properties>
+ <help>Show information about non VyOS user accounts</help>
+ </properties>
+ <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py other</command>
+ </leafNode>
+ <leafNode name="vyos">
+ <properties>
+ <help>Show information about VyOS user accounts</help>
+ </properties>
+ <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py vyos</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+
+ <node name="memory">
+ <properties>
+ <help>Show system memory usage</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_ram.sh</command>
+ <children>
+ <leafNode name="cache">
+ <properties>
+ <help>Show kernel cache information</help>
+ </properties>
+ <command>sudo slabtop -o</command>
+ </leafNode>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed system memory usage</help>
+ </properties>
+ <command>cat /proc/meminfo</command>
+ </leafNode>
+ </children>
+ </node>
+
+ <node name="processes">
+ <properties>
+ <help>Show system processes</help>
+ </properties>
+ <command>ps ax</command>
+ <children>
+ <leafNode name="extensive">
+ <properties>
+ <help>Show extensive process info</help>
+ </properties>
+ <command>top -b -n1</command>
+ </leafNode>
+ <leafNode name="summary">
+ <properties>
+ <help>Show summary of system processes</help>
+ </properties>
+ <command>uptime</command>
+ </leafNode>
+ <leafNode name="tree">
+ <properties>
+ <help>Show process tree</help>
+ </properties>
+ <command>ps -ejH</command>
+ </leafNode>
+ </children>
+ </node>
+
+ <leafNode name="storage">
+ <properties>
+ <help>Show filesystem usage</help>
+ </properties>
+ <command>df -h -x squashfs</command>
+ </leafNode>
+
+ <leafNode name="uptime">
+ <properties>
+ <help>Show how long the system has been up</help>
+ </properties>
+ <command>uptime</command>
+ </leafNode>
+
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 524b80424..3e4c02562 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -27,3 +27,22 @@ cfg_group = 'vyattacfg'
cfg_vintage = 'vyatta'
commit_lock = '/opt/vyatta/config/.lock'
+
+https_data = {
+ 'listen_address' : [ '127.0.0.1' ]
+}
+
+api_data = {
+ 'listen_address' : '127.0.0.1',
+ 'port' : '8080',
+ 'strict' : 'false',
+ 'debug' : 'false',
+ 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ]
+}
+
+vyos_cert_data = {
+ "conf": "/etc/nginx/snippets/vyos-cert.conf",
+ "crt": "/etc/ssl/certs/vyos-selfsigned.crt",
+ "key": "/etc/ssl/private/vyos-selfsign",
+ "lifetime": "365",
+}
diff --git a/src/completion/list_openvpn_clients.py b/src/completion/list_openvpn_clients.py
new file mode 100755
index 000000000..828ce6b5e
--- /dev/null
+++ b/src/completion/list_openvpn_clients.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import argparse
+
+from vyos.interfaces import list_interfaces_of_type
+
+def get_client_from_interface(interface):
+ clients = []
+ with open('/opt/vyatta/etc/openvpn/status/' + interface + '.status', 'r') as f:
+ dump = False
+ for line in f:
+ if line.startswith("Common Name,"):
+ dump = True
+ continue
+ if line.startswith("ROUTING TABLE"):
+ dump = False
+ continue
+ if dump:
+ # client entry in this file looks like
+ # client1,172.18.202.10:47495,2957,2851,Sat Aug 17 00:07:11 2019
+ # we are only interested in the client name 'client1'
+ clients.append(line.split(',')[0])
+
+ return clients
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-i", "--interface", type=str, help="List connected clients per interface")
+ parser.add_argument("-a", "--all", action='store_true', help="List all connected OpenVPN clients")
+ args = parser.parse_args()
+
+ clients = []
+
+ if args.interface:
+ clients = get_client_from_interface(args.interface)
+ elif args.all:
+ for interface in list_interfaces_of_type("openvpn"):
+ clients += get_client_from_interface(interface)
+
+ print(" ".join(clients))
+
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index 16467c8df..2fad57db6 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -45,7 +45,7 @@ config_file_resolv = '/etc/resolv.conf'
config_tmpl_hosts = """
### Autogenerated by host_name.py ###
127.0.0.1 localhost
-127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }}{% endif %}
+127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }} {{ hostname }}{% endif %}
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index c1d596ea3..1f91ac582 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -27,14 +27,6 @@ from vyos import ConfigError
config_file = '/etc/vyos/http-api.conf'
-default_config_data = {
- 'listen_address' : '127.0.0.1',
- 'port' : '8080',
- 'strict' : 'false',
- 'debug' : 'false',
- 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ]
-}
-
vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode']
# XXX: this model will need to be extended for tag nodes
@@ -43,7 +35,8 @@ dependencies = [
]
def get_config():
- http_api = default_config_data
+ http_api = vyos.defaults.api_data
+
conf = Config()
if not conf.exists('service https api'):
return None
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index e1e81eef1..289eacf69 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -21,6 +21,7 @@ import os
import jinja2
+import vyos.defaults
from vyos.config import Config
from vyos import ConfigError
@@ -45,11 +46,16 @@ server {
#
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
+
+{% if vyos_cert %}
+ include {{ vyos_cert.conf }};
+{% else %}
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
include snippets/snakeoil.conf;
+{% endif %}
{% for l_addr in listen_address %}
server_name {{ l_addr }};
@@ -75,16 +81,8 @@ server {
}
"""
-default_config_data = {
- 'listen_address' : [ '127.0.0.1' ]
-}
-
-default_api_config_data = {
- 'port' : '8080',
-}
-
def get_config():
- https = default_config_data
+ https = vyos.defaults.https_data
conf = Config()
if not conf.exists('service https'):
return None
@@ -95,8 +93,12 @@ def get_config():
addrs = conf.return_values('listen-address')
https['listen_address'] = addrs[:]
+ if conf.exists('certificates'):
+ if conf.exists('certificates system-generated-certificate'):
+ https['vyos_cert'] = vyos.defaults.vyos_cert_data
+
if conf.exists('api'):
- https['api'] = default_api_config_data
+ https['api'] = vyos.defaults.api_data
if conf.exists('api port'):
port = conf.return_value('api port')
diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py
new file mode 100755
index 000000000..e4bde7bb0
--- /dev/null
+++ b/src/conf_mode/interface-openvpn.py
@@ -0,0 +1,911 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import os
+import re
+import pwd
+import grp
+import sys
+import stat
+import copy
+import jinja2
+import psutil
+from ipaddress import ip_address,ip_network,IPv4Interface
+
+from signal import SIGUSR1
+from subprocess import Popen, PIPE
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.validate import is_addr_assigned
+
+user = 'nobody'
+group = 'nogroup'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+### Autogenerated by interfaces-openvpn.py ###
+#
+# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
+# for individual keyword definition
+
+{% if description %}
+# {{ description }}
+{% endif %}
+
+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 }}
+user {{ uid }}
+group {{ gid }}
+persist-key
+
+proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %}
+
+{%- if local_host %}
+local {{ local_host }}
+{% endif %}
+
+{%- if local_port %}
+lport {{ local_port }}
+{% endif %}
+
+{%- if remote_port %}
+rport {{ remote_port }}
+{% endif %}
+
+{%- if remote_host %}
+{% for remote in remote_host -%}
+remote {{ remote }}
+{% endfor -%}
+{% endif %}
+
+{%- if shared_secret_file %}
+secret {{ shared_secret_file }}
+{% endif %}
+
+{%- if persistent_tunnel %}
+persist-tun
+{% endif %}
+
+{%- if mode %}
+{%- if 'client' in mode %}
+#
+# OpenVPN Client mode
+#
+client
+nobind
+{%- elif 'server' in mode %}
+#
+# OpenVPN Server mode
+#
+mode server
+tls-server
+keepalive {{ ping_interval }} {{ ping_restart }}
+management /tmp/openvpn-mgmt-intf unix
+
+{%- if server_topology %}
+topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %}
+{% endif %}
+
+{% for ns in server_dns_nameserver -%}
+push "dhcp-option DNS {{ ns }}"
+{% endfor -%}
+
+{% for route in server_push_route -%}
+push "route {{ route }}"
+{% endfor -%}
+
+{%- if server_domain %}
+push "dhcp-option DOMAIN {{ server_domain }}"
+{% endif %}
+
+{%- if server_max_conn %}
+max-clients {{ server_max_conn }}
+{% endif %}
+
+{%- if bridge_member %}
+server-bridge nogw
+{%- else %}
+server {{ server_subnet }}
+{% endif %}
+
+{%- if server_reject_unconfigured %}
+ccd-exclusive
+{% endif %}
+
+{%- else %}
+#
+# OpenVPN site-2-site mode
+#
+ping {{ ping_interval }}
+ping-restart {{ ping_restart }}
+
+{%- if local_address_subnet %}
+ifconfig {{ local_address }} {{ local_address_subnet }}
+{% elif remote_address %}
+ifconfig {{ local_address }} {{ remote_address }}
+{% endif %}
+
+{% endif %}
+{% endif %}
+
+{%- if tls_ca_cert %}
+ca {{ tls_ca_cert }}
+{% endif %}
+
+{%- if tls_cert %}
+cert {{ tls_cert }}
+{% endif %}
+
+{%- if tls_key %}
+key {{ tls_key }}
+{% endif %}
+
+{%- if tls_crl %}
+crl-verify {{ tls_crl }}
+{% endif %}
+
+{%- if tls_dh %}
+dh {{ tls_dh }}
+{% endif %}
+
+{%- if 'active' in tls_role %}
+tls-client
+{%- elif 'passive' in tls_role %}
+tls-server
+{% endif %}
+
+{%- if redirect_gateway %}
+push "redirect-gateway {{ redirect_gateway }}"
+{% endif %}
+
+{%- if compress_lzo %}
+compress lzo
+{% endif %}
+
+{%- if hash %}
+auth {{ hash }}
+{% endif %}
+
+{%- if encryption %}
+{%- if 'des' in encryption %}
+cipher des-cbc
+{%- elif '3des' in encryption %}
+cipher des-ede3-cbc
+{%- elif 'bf128' in encryption %}
+cipher bf-cbc
+keysize 128
+{%- elif 'bf256' in encryption %}
+cipher bf-cbc
+keysize 25
+{%- elif 'aes128' in encryption %}
+cipher aes-128-cbc
+{%- elif 'aes192' in encryption %}
+cipher aes-192-cbc
+{%- elif 'aes256' in encryption %}
+cipher aes-256-cbc
+{% endif %}
+{% endif %}
+
+{%- if auth %}
+auth-user-pass /tmp/openvpn-{{ intf }}-pw
+auth-retry nointeract
+{% endif %}
+
+{%- if client %}
+client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }}
+{% endif %}
+
+{% for option in options -%}
+{{ option }}
+{% endfor -%}
+
+{%- if server_2fa_authy_key %}
+plugin /usr/lib/authy/authy-openvpn.so https://api.authy.com/protected/json {{ server_2fa_authy_key }} nopam
+{% endif %}
+"""
+
+client_tmpl = """
+### Autogenerated by interfaces-openvpn.py ###
+
+ifconfig-push {{ ip }} {{ remote_netmask }}
+{% for route in push_route -%}
+push "route {{ route }}"
+{% endfor -%}
+
+{% for net in subnet -%}
+iroute {{ net }}
+{% endfor -%}
+
+{%- if disable %}
+disable
+{% endif %}
+"""
+
+default_config_data = {
+ 'address': [],
+ 'auth_user': '',
+ 'auth_pass': '',
+ 'auth': False,
+ 'bridge_member': [],
+ 'compress_lzo': False,
+ 'deleted': False,
+ 'description': '',
+ 'disable': False,
+ 'encryption': '',
+ 'hash': '',
+ 'intf': '',
+ 'ping_restart': '60',
+ 'ping_interval': '10',
+ 'local_address': '',
+ 'local_address_subnet': '',
+ 'local_host': '',
+ 'local_port': '',
+ 'mode': '',
+ 'options': [],
+ 'persistent_tunnel': False,
+ 'protocol': '',
+ 'redirect_gateway': '',
+ 'remote_address': '',
+ 'remote_host': [],
+ 'remote_port': '',
+ 'server_2fa_authy_key': '',
+ 'server_2fa_authy': [],
+ 'client': [],
+ 'server_domain': '',
+ 'server_max_conn': '',
+ 'server_dns_nameserver': [],
+ 'server_push_route': [],
+ 'server_reject_unconfigured': False,
+ 'server_subnet': '',
+ 'server_topology': '',
+ 'shared_secret_file': '',
+ 'tls': False,
+ 'tls_ca_cert': '',
+ 'tls_cert': '',
+ 'tls_crl': '',
+ 'tls_dh': '',
+ 'tls_key': '',
+ 'tls_role': '',
+ 'type': 'tun',
+ 'uid': user,
+ 'gid': group,
+}
+
+def subprocess_cmd(command):
+ p = Popen(command, stdout=PIPE, shell=True)
+ p.communicate()
+
+def get_config_name(intf):
+ cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf)
+ return cfg_file
+
+def openvpn_mkdir(directory):
+ # create directory on demand
+ if not os.path.exists(directory):
+ os.mkdir(directory)
+
+ # fix permissions - corresponds to mode 755
+ os.chmod(directory, stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
+ uid = pwd.getpwnam(user).pw_uid
+ gid = grp.getgrnam(group).gr_gid
+ os.chown(directory, uid, gid)
+
+def fixup_permission(filename, permission=stat.S_IRUSR):
+ """
+ Check if the given file exists and change ownershit to root/vyattacfg
+ and appripriate file access permissions - default is user and group readable
+ """
+ if os.path.isfile(filename):
+ os.chmod(filename, permission)
+
+ # make file owned by root / vyattacfg
+ uid = pwd.getpwnam('root').pw_uid
+ gid = grp.getgrnam('vyattacfg').gr_gid
+ os.chown(filename, uid, gid)
+
+def checkCertHeader(header, filename):
+ """
+ Verify if filename contains specified header.
+ Returns True on success or on file not found to not trigger the exceptions
+ """
+ if not os.path.isfile(filename):
+ return True
+
+ with open(filename, 'r') as f:
+ for line in f:
+ if re.match(header, line):
+ return True
+
+ return False
+
+def get_config():
+ openvpn = copy.deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ try:
+ openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ except KeyError as E:
+ print("Interface not specified")
+
+ # Check if interface instance has been removed
+ if not conf.exists('interfaces openvpn ' + openvpn['intf']):
+ openvpn['deleted'] = True
+ return openvpn
+
+ # Check if we belong to any bridge interface
+ for bridge in conf.list_nodes('interfaces bridge'):
+ for intf in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)):
+ if intf == openvpn['intf']:
+ openvpn['bridge_member'].append(intf)
+
+ # set configuration level
+ conf.set_level('interfaces openvpn ' + openvpn['intf'])
+
+ # retrieve authentication options - username
+ if conf.exists('authentication username'):
+ openvpn['auth_user'] = conf.return_value('authentication username')
+ openvpn['auth'] = True
+
+ # retrieve authentication options - username
+ if conf.exists('authentication password'):
+ openvpn['auth_pass'] = conf.return_value('authentication password')
+ openvpn['auth'] = True
+
+ # retrieve interface description
+ if conf.exists('description'):
+ openvpn['description'] = conf.return_value('description')
+
+ # interface device-type
+ if conf.exists('device-type'):
+ openvpn['type'] = conf.return_value('device-type')
+
+ # disable interface
+ if conf.exists('disable'):
+ openvpn['disable'] = True
+
+ # data encryption algorithm
+ if conf.exists('encryption'):
+ openvpn['encryption'] = conf.return_value('encryption')
+
+ # hash algorithm
+ if conf.exists('hash'):
+ openvpn['hash'] = conf.return_value('hash')
+
+ # Maximum number of keepalive packet failures
+ if conf.exists('keep-alive failure-count') and conf.exists('keep-alive interval'):
+ fail_count = conf.return_value('keep-alive failure-count')
+ interval = conf.return_value('keep-alive interval')
+ openvpn['ping_interval' ] = interval
+ openvpn['ping_restart' ] = int(interval) * int(fail_count)
+
+ # Local IP address of tunnel - even as it is a tag node - we can only work
+ # on the first address
+ if conf.exists('local-address'):
+ openvpn['local_address'] = conf.list_nodes('local-address')[0]
+ if conf.exists('local-address {} subnet-mask'.format(openvpn['local_address'])):
+ openvpn['local_address_subnet'] = conf.return_value('local-address {} subnet-mask'.format(openvpn['local_address']))
+
+ # Local IP address to accept connections
+ if conf.exists('local-host'):
+ openvpn['local_host'] = conf.return_value('local-host')
+
+ # Local port number to accept connections
+ if conf.exists('local-port'):
+ openvpn['local_port'] = conf.return_value('local-port')
+
+ # OpenVPN operation mode
+ if conf.exists('mode'):
+ mode = conf.return_value('mode')
+ openvpn['mode'] = mode
+
+ # Additional OpenVPN options
+ if conf.exists('openvpn-option'):
+ openvpn['options'] = conf.return_values('openvpn-option')
+
+ # Do not close and reopen interface
+ if conf.exists('persistent-tunnel'):
+ openvpn['persistent_tunnel'] = True
+
+ # Communication protocol
+ if conf.exists('protocol'):
+ openvpn['protocol'] = conf.return_value('protocol')
+
+ # IP address of remote end of tunnel
+ if conf.exists('remote-address'):
+ openvpn['remote_address'] = conf.return_value('remote-address')
+
+ # Remote host to connect to (dynamic if not set)
+ if conf.exists('remote-host'):
+ openvpn['remote_host'] = conf.return_values('remote-host')
+
+ # Remote port number to connect to
+ if conf.exists('remote-port'):
+ openvpn['remote_port'] = conf.return_value('remote-port')
+
+ # OpenVPN tunnel to be used as the default route
+ # see https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/
+ # redirect-gateway flags
+ if conf.exists('replace-default-route'):
+ openvpn['redirect_gateway'] = 'def1'
+
+ if conf.exists('replace-default-route local'):
+ openvpn['redirect_gateway'] = 'local def1'
+
+ # Two Factor Authentication providers
+ # currently limited to authy
+ if conf.exists('2-factor-authentication authy api-key'):
+ openvpn['server_2fa_authy_key'] = conf.return_value('2-factor-authentication authy api-key')
+
+ # Authy users (must be email address)
+ for user in conf.list_nodes('server 2-factor-authentication authy user'):
+ # set configuration level
+ conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' 2-factor-authentication authy user ' + user)
+ data = {
+ 'user': user,
+ 'country_code': '',
+ 'mobile_number': ''
+ }
+
+ # Country calling codes
+ if conf.exists('country-calling-code'):
+ data['country_code'] = conf.return_value('country-calling-code')
+
+ # Mobile phone number
+ if conf.exists('phone-number'):
+ data['mobile_number'] = conf.return_value('phone-number')
+
+ openvpn['server_2fa_authy'].append(data)
+
+ # Topology for clients
+ if conf.exists('server topology'):
+ openvpn['server_topology'] = conf.return_value('server topology')
+
+ # Server-mode subnet (from which client IPs are allocated)
+ if conf.exists('server subnet'):
+ network = conf.return_value('server subnet')
+ tmp = IPv4Interface(network).with_netmask
+ # convert the network in format: "192.0.2.0 255.255.255.0" for later use in template
+ openvpn['server_subnet'] = tmp.replace(r'/', ' ')
+
+ # Client-specific settings
+ for client in conf.list_nodes('server client'):
+ # set configuration level
+ conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client ' + client)
+ data = {
+ 'name': client,
+ 'disable': False,
+ 'ip': '',
+ 'push_route': [],
+ 'subnet': [],
+ 'remote_netmask': ''
+ }
+
+ # note: with "topology subnet", this is "<ip> <netmask>".
+ # with "topology p2p", this is "<ip> <our_ip>".
+ if openvpn['server_topology'] == 'subnet':
+ # we are only interested in the netmask portion of server_subnet
+ data['remote_netmask'] = openvpn['server_subnet'].split(' ')[1]
+ else:
+ # we need the server subnet in format 192.0.2.0/255.255.255.0
+ subnet = openvpn['server_subnet'].replace(' ', r'/')
+ # get iterator over the usable hosts in the network
+ tmp = ip_network(subnet).hosts()
+ # OpenVPN always uses the subnets first available IP address
+ data['remote_netmask'] = list(tmp)[0]
+
+ # Option to disable client connection
+ if conf.exists('disable'):
+ data['disable'] = True
+
+ # IP address of the client
+ if conf.exists('ip'):
+ data['ip'] = conf.return_value('ip')
+
+ # Route to be pushed to the client
+ for network in conf.return_values('push-route'):
+ tmp = IPv4Interface(network).with_netmask
+ data['push_route'].append(tmp.replace(r'/', ' '))
+
+ # Subnet belonging to the client
+ for network in conf.return_values('subnet'):
+ tmp = IPv4Interface(network).with_netmask
+ data['subnet'].append(tmp.replace(r'/', ' '))
+
+ # Append to global client list
+ openvpn['client'].append(data)
+
+ # re-set configuration level
+ conf.set_level('interfaces openvpn ' + openvpn['intf'])
+
+ # DNS suffix to be pushed to all clients
+ if conf.exists('server domain-name'):
+ openvpn['server_domain'] = conf.return_value('server domain-name')
+
+ # Number of maximum client connections
+ if conf.exists('server max-connections'):
+ openvpn['server_max_conn'] = conf.return_value('server max-connections')
+
+ # Domain Name Server (DNS)
+ if conf.exists('server name-server'):
+ openvpn['server_dns_nameserver'] = conf.return_values('server name-server')
+
+ # Route to be pushed to all clients
+ if conf.exists('server push-route'):
+ network = conf.return_value('server push-route')
+ tmp = IPv4Interface(network).with_netmask
+ openvpn['server_push_route'] = tmp.replace(r'/', ' ')
+
+ # Reject connections from clients that are not explicitly configured
+ if conf.exists('server reject-unconfigured-clients'):
+ openvpn['server_reject_unconfigured'] = True
+
+ # File containing certificate for Certificate Authority (CA)
+ if conf.exists('tls ca-cert-file'):
+ openvpn['tls_ca_cert'] = conf.return_value('tls ca-cert-file')
+ openvpn['tls'] = True
+
+ # File containing certificate for this host
+ if conf.exists('tls cert-file'):
+ openvpn['tls_cert'] = conf.return_value('tls cert-file')
+ openvpn['tls'] = True
+
+ # File containing certificate revocation list (CRL) for this host
+ if conf.exists('tls crl-file'):
+ openvpn['tls_crl'] = conf.return_value('tls crl-file')
+ openvpn['tls'] = True
+
+ # File containing Diffie Hellman parameters (server only)
+ if conf.exists('tls dh-file'):
+ openvpn['tls_dh'] = conf.return_value('tls dh-file')
+ openvpn['tls'] = True
+
+ # File containing this host's private key
+ if conf.exists('tls key-file'):
+ openvpn['tls_key'] = conf.return_value('tls key-file')
+ openvpn['tls'] = True
+
+ # Role in TLS negotiation
+ if conf.exists('tls role'):
+ openvpn['tls_role'] = conf.return_value('tls role')
+ openvpn['tls'] = True
+
+ if conf.exists('shared-secret-key-file'):
+ openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file')
+
+ if conf.exists('use-lzo-compression'):
+ openvpn['compress_lzo'] = True
+
+ return openvpn
+
+def verify(openvpn):
+ if openvpn['deleted']:
+ return None
+
+ if not openvpn['mode']:
+ raise ConfigError('Must specify OpenVPN operation mode')
+
+ # Checks which need to be performed on interface rmeoval
+ if openvpn['deleted']:
+ # OpenVPN interface can not be deleted if it's still member of a bridge
+ if openvpn['bridge_member']:
+ raise ConfigError('Can not delete {} as it is a member interface of bridge {}!'.format(openvpn['intf'], bridge))
+
+ #
+ # OpenVPN client mode - VERIFY
+ #
+ if openvpn['mode'] == 'client':
+ if openvpn['local_port']:
+ raise ConfigError('Cannot specify "local-port" in client mode')
+
+ if openvpn['local_host']:
+ raise ConfigError('Cannot specify "local-host" in client mode')
+
+ if openvpn['protocol'] == 'tcp-passive':
+ raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
+
+ if not openvpn['remote_host']:
+ raise ConfigError('Must specify "remote-host" in client mode')
+
+ if openvpn['tls_dh']:
+ raise ConfigError('Cannot specify "tls dh-file" in client mode')
+
+ #
+ # OpenVPN site-to-site - VERIFY
+ #
+ if openvpn['mode'] == 'site-to-site':
+ if not (openvpn['local_address'] or openvpn['bridge_member']):
+ raise ConfigError('Must specify "local-address" or "bridge member interface"')
+
+ for host in openvpn['remote_host']:
+ if host == openvpn['remote_address']:
+ raise ConfigError('"remote-address" cannot be the same as "remote-host"')
+
+ if openvpn['type'] == 'tun':
+ if not openvpn['remote_address']:
+ raise ConfigError('Must specify "remote-address"')
+
+ if openvpn['local_address'] == openvpn['remote_address']:
+ raise ConfigError('"local-address" and "remote-address" cannot be the same')
+
+ if openvpn['local_address'] == openvpn['local_host']:
+ raise ConfigError('"local-address" cannot be the same as "local-host"')
+
+ else:
+ if openvpn['local_address'] or openvpn['remote_address']:
+ raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server mode')
+
+ elif openvpn['bridge_member']:
+ raise ConfigError('Cannot specify "local-address" or "remote-address" in bridge mode')
+
+ #
+ # OpenVPN server mode - VERIFY
+ #
+ if openvpn['mode'] == 'server':
+ if openvpn['protocol'] == 'tcp-active':
+ raise ConfigError('Protocol "tcp-active" is not valid in server mode')
+
+ if openvpn['remote_port']:
+ raise ConfigError('Cannot specify "remote-port" in server mode')
+
+ if openvpn['remote_host']:
+ raise ConfigError('Cannot specify "remote-host" in server mode')
+
+ if openvpn['protocol'] == 'tcp-passive' and len(openvpn['remote_host']) > 1:
+ raise ConfigError('Cannot specify more than 1 "remote-host" with "tcp-passive"')
+
+ if not openvpn['tls_dh']:
+ raise ConfigError('Must specify "tls dh-file" in server mode')
+
+ if not openvpn['server_subnet']:
+ if not openvpn['bridge_member']:
+ raise ConfigError('Must specify "server subnet" option in server mode')
+
+ else:
+ # checks for both client and site-to-site go here
+ if openvpn['server_reject_unconfigured']:
+ raise ConfigError('reject-unconfigured-clients is only supported in OpenVPN server mode')
+
+ if openvpn['server_topology']:
+ raise ConfigError('The "topology" option is only valid in server mode')
+
+ if (not openvpn['remote_host']) and openvpn['redirect_gateway']:
+ raise ConfigError('Cannot set "replace-default-route" without "remote-host"')
+
+ #
+ # OpenVPN common verification section
+ # not depending on any operation mode
+ #
+
+ # verify specified IP address is present on any interface on this system
+ if openvpn['local_host']:
+ if not is_addr_assigned(openvpn['local_host']):
+ raise ConfigError('No interface on system with specified local-host IP address: {}'.format(openvpn['local_host']))
+
+ # TCP active
+ if openvpn['protocol'] == 'tcp-active':
+ if openvpn['local_port']:
+ raise ConfigError('Cannot specify "local-port" with "tcp-active"')
+
+ if not openvpn['remote_host']:
+ raise ConfigError('Must specify "remote-host" with "tcp-active"')
+
+ # shared secret and TLS
+ if not (openvpn['shared_secret_file'] or openvpn['tls']):
+ raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"')
+
+ if openvpn['shared_secret_file'] and openvpn['tls']:
+ raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"')
+
+ if openvpn['mode'] in ['client', 'server']:
+ if not openvpn['tls']:
+ raise ConfigError('Must specify "tls" in client-server mode')
+
+ #
+ # TLS/encryption
+ #
+ if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['shared_secret_file']):
+ raise ConfigError('Specified shared-secret-key-file "{}" is not valid'.format(openvpn['shared_secret_file']))
+
+ if openvpn['tls']:
+ if not openvpn['tls_ca_cert']:
+ raise ConfigError('Must specify "tls ca-cert-file"')
+
+ if not (openvpn['mode'] == 'client' and openvpn['auth']):
+ if not openvpn['tls_cert']:
+ raise ConfigError('Must specify "tls cert-file"')
+
+ if not openvpn['tls_key']:
+ raise ConfigError('Must specify "tls key-file"')
+
+ if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']):
+ raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert']))
+
+ if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_cert']):
+ raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert']))
+
+ if not checkCertHeader('-----BEGIN (?:RSA )?PRIVATE KEY-----', openvpn['tls_key']):
+ raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key']))
+
+ if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']):
+ raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl']))
+
+ if not checkCertHeader('-----BEGIN DH PARAMETERS-----', openvpn['tls_dh']):
+ raise ConfigError('Specified dh-file "{}" is not valid'.format(openvpn['tls_dh']))
+
+ if openvpn['tls_role']:
+ if openvpn['mode'] in ['client', 'server']:
+ raise ConfigError('Cannot specify "tls role" in client-server mode')
+
+ if openvpn['tls_role'] == 'active':
+ if openvpn['protocol'] == 'tcp-passive':
+ raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
+
+ if openvpn['tls_dh']:
+ raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"')
+
+ elif openvpn['tls_role'] == 'passive':
+ if openvpn['protocol'] == 'tcp-active':
+ raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
+
+ if not openvpn['tls_dh']:
+ raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"')
+
+ #
+ # Auth user/pass
+ #
+ if openvpn['auth']:
+ if not openvpn['auth_user']:
+ raise ConfigError('Username for authentication is missing')
+
+ if not openvpn['auth_pass']:
+ raise ConfigError('Password for authentication is missing')
+
+ #
+ # Client
+ #
+ subnet = openvpn['server_subnet'].replace(' ', '/')
+ for client in openvpn['client']:
+ if not ip_address(client['ip']) in ip_network(subnet):
+ raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet))
+
+
+
+ return None
+
+def generate(openvpn):
+ if openvpn['deleted'] or openvpn['disable']:
+ return None
+
+ interface = openvpn['intf']
+ directory = os.path.dirname(get_config_name(interface))
+
+ # create config directory on demand
+ openvpn_mkdir(directory)
+ # create status directory on demand
+ openvpn_mkdir(directory + '/status')
+ # create client config dir on demand
+ openvpn_mkdir(directory + '/ccd')
+ # crete client config dir per interface on demand
+ openvpn_mkdir(directory + '/ccd/' + interface)
+
+ # Fix file permissons for keys
+ fixup_permission(openvpn['shared_secret_file'])
+ fixup_permission(openvpn['tls_key'])
+
+ # Generate User/Password authentication file
+ if openvpn['auth']:
+ auth_file = '/tmp/openvpn-{}-pw'.format(interface)
+ with open(auth_file, 'w') as f:
+ f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass']))
+
+ fixup_permission(auth_file)
+
+ # get numeric uid/gid
+ uid = pwd.getpwnam(user).pw_uid
+ gid = grp.getgrnam(group).gr_gid
+
+ # Generate client specific configuration
+ for client in openvpn['client']:
+ client_file = directory + '/ccd/' + interface + '/' + client['name']
+ tmpl = jinja2.Template(client_tmpl)
+ client_text = tmpl.render(client)
+ with open(client_file, 'w') as f:
+ f.write(client_text)
+ os.chown(client_file, uid, gid)
+
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(openvpn)
+ with open(get_config_name(interface), 'w') as f:
+ f.write(config_text)
+ os.chown(get_config_name(interface), uid, gid)
+
+ return None
+
+def apply(openvpn):
+ interface = openvpn['intf']
+
+ pid = 0
+ pidfile = '/var/run/openvpn/{}.pid'.format(interface)
+ if os.path.isfile(pidfile):
+ pid = 0
+ with open(pidfile, 'r') as f:
+ pid = int(f.read())
+
+ # If tunnel interface has been deleted - stop service
+ if openvpn['deleted'] or openvpn['disable']:
+ directory = os.path.dirname(get_config_name(interface))
+
+ # we only need to stop the demon if it's running
+ # daemon could have died or killed by someone
+ if psutil.pid_exists(pid):
+ cmd = 'start-stop-daemon --stop --quiet'
+ cmd += ' --pidfile ' + pidfile
+ subprocess_cmd(cmd)
+
+ # cleanup old PID file
+ if os.path.isfile(pidfile):
+ os.remove(pidfile)
+
+ # cleanup old configuration file
+ if os.path.isfile(get_config_name(interface)):
+ os.remove(get_config_name(interface))
+
+ # cleanup client config dir
+ if os.path.isdir(directory + '/ccd/' + interface):
+ try:
+ os.remove(directory + '/ccd/' + interface + '/*')
+ except:
+ pass
+
+ return None
+
+ # Send SIGUSR1 to the process instead of creating a new process
+ if psutil.pid_exists(pid):
+ os.kill(pid, SIGUSR1)
+ return None
+
+ # No matching OpenVPN process running - maybe it got killed or none
+ # existed - nevertheless, spawn new OpenVPN process
+ cmd = 'start-stop-daemon --start --quiet'
+ cmd += ' --pidfile ' + pidfile
+ cmd += ' --exec /usr/sbin/openvpn'
+ # now pass arguments to openvpn binary
+ cmd += ' --'
+ cmd += ' --config ' + get_config_name(interface)
+
+ # execute assembled command
+ subprocess_cmd(cmd)
+
+ 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/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index 98f38035a..9ca194edd 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -176,6 +176,10 @@ def verify(bfd):
if peer['multihop'] and peer['echo_mode']:
raise ConfigError('Multihop and echo-mode cannot be used together')
+ # multihop doesn't accept interface names
+ if peer['multihop'] and peer['src_if']:
+ raise ConfigError('Multihop and source interface cannot be used together')
+
# echo interval can be configured only with enabled echo-mode
if peer['echo_interval'] != '' and not peer['echo_mode']:
raise ConfigError('echo-interval can be configured only with enabled echo-mode')
diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py
new file mode 100755
index 000000000..4a44573ca
--- /dev/null
+++ b/src/conf_mode/vyos_cert.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+import os
+import subprocess
+import tempfile
+import pathlib
+import ssl
+
+import vyos.defaults
+from vyos.config import Config
+from vyos import ConfigError
+
+vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
+
+# XXX: this model will need to be extended for tag nodes
+dependencies = [
+ 'https.py',
+]
+
+def status_self_signed(cert_data):
+# check existence and expiration date
+ path = pathlib.Path(cert_data['conf'])
+ if not path.is_file():
+ return False
+ path = pathlib.Path(cert_data['crt'])
+ if not path.is_file():
+ return False
+ path = pathlib.Path(cert_data['key'])
+ if not path.is_file():
+ return False
+
+ # check if certificate is 1/2 past lifetime, with openssl -checkend
+ end_days = int(cert_data['lifetime'])
+ end_seconds = int(0.5*60*60*24*end_days)
+ checkend_cmd = ('openssl x509 -checkend {end} -noout -in {crt}'
+ ''.format(end=end_seconds, **cert_data))
+ try:
+ subprocess.check_call(checkend_cmd, shell=True)
+ return True
+ except subprocess.CalledProcessError as err:
+ if err.returncode == 1:
+ return False
+ else:
+ print("Called process error: {}.".format(err))
+
+def generate_self_signed(cert_data):
+ san_config = None
+
+ if ssl.OPENSSL_VERSION_INFO < (1, 1, 1, 0, 0):
+ san_config = tempfile.NamedTemporaryFile()
+ with open(san_config.name, 'w') as fd:
+ fd.write('[req]\n')
+ fd.write('distinguished_name=req\n')
+ fd.write('[san]\n')
+ fd.write('subjectAltName=DNS:vyos\n')
+
+ openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
+ '-newkey rsa:4096 -keyout {key} -out {crt} '
+ '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
+ '-extensions san -config {san_conf}'
+ ''.format(san_conf=san_config.name,
+ **cert_data))
+
+ else:
+ openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
+ '-newkey rsa:4096 -keyout {key} -out {crt} '
+ '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
+ '-addext "subjectAltName=DNS:vyos"'
+ ''.format(**cert_data))
+
+ try:
+ subprocess.check_call(openssl_req_cmd, shell=True)
+ except subprocess.CalledProcessError as err:
+ print("Called process error: {}.".format(err))
+
+ os.chmod('{key}'.format(**cert_data), 0o400)
+
+ with open('{conf}'.format(**cert_data), 'w') as f:
+ f.write('ssl_certificate {crt};\n'.format(**cert_data))
+ f.write('ssl_certificate_key {key};\n'.format(**cert_data))
+
+ if san_config:
+ san_config.close()
+
+def get_config():
+ vyos_cert = vyos.defaults.vyos_cert_data
+
+ conf = Config()
+ if not conf.exists('service https certificates system-generated-certificate'):
+ return None
+ else:
+ conf.set_level('service https certificates system-generated-certificate')
+
+ if conf.exists('lifetime'):
+ lifetime = conf.return_value('lifetime')
+ vyos_cert['lifetime'] = lifetime
+
+ return vyos_cert
+
+def verify(vyos_cert):
+ return None
+
+def generate(vyos_cert):
+ if vyos_cert is None:
+ return None
+
+ if not status_self_signed(vyos_cert):
+ generate_self_signed(vyos_cert)
+
+def apply(vyos_cert):
+ for dep in dependencies:
+ cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
+ try:
+ subprocess.check_call(cmd, shell=True)
+ except subprocess.CalledProcessError as err:
+ raise ConfigError("{}.".format(err))
+
+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/helpers/vyos-sudo.py b/src/helpers/vyos-sudo.py
new file mode 100755
index 000000000..0101a0c95
--- /dev/null
+++ b/src/helpers/vyos-sudo.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import getpass
+import grp
+import os
+import sys
+
+
+def is_admin() -> bool:
+ """Look if current user is in sudo group"""
+ current_user = getpass.getuser()
+ (_, _, _, admin_group_members) = grp.getgrnam('sudo')
+ return current_user in admin_group_members
+
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print('Missing command argument')
+ sys.exit(1)
+
+ if not is_admin():
+ print('This account is not authorized to run this command')
+ sys.exit(1)
+
+ os.execvp('sudo', ['sudo'] + sys.argv[1:])
diff --git a/src/op_mode/show_ram.sh b/src/op_mode/show_ram.sh
new file mode 100755
index 000000000..b013e16f8
--- /dev/null
+++ b/src/op_mode/show_ram.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+#
+# Module: vyos-show-ram.sh
+# Displays memory usage information in minimalistic format
+#
+# 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 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/>.
+
+MB_DIVISOR=1024
+
+TOTAL=$(cat /proc/meminfo | grep -E "^MemTotal:" | awk -F ' ' '{print $2}')
+FREE=$(cat /proc/meminfo | grep -E "^MemFree:" | awk -F ' ' '{print $2}')
+BUFFERS=$(cat /proc/meminfo | grep -E "^Buffers:" | awk -F ' ' '{print $2}')
+CACHED=$(cat /proc/meminfo | grep -E "^Cached:" | awk -F ' ' '{print $2}')
+
+DISPLAY_FREE=$(( ($FREE + $BUFFERS + $CACHED) / $MB_DIVISOR ))
+DISPLAY_TOTAL=$(( $TOTAL / $MB_DIVISOR ))
+DISPLAY_USED=$(( $DISPLAY_TOTAL - $DISPLAY_FREE ))
+
+echo "Total: $DISPLAY_TOTAL"
+echo "Free: $DISPLAY_FREE"
+echo "Used: $DISPLAY_USED"
diff --git a/src/op_mode/show_users.py b/src/op_mode/show_users.py
new file mode 100755
index 000000000..8e4f12851
--- /dev/null
+++ b/src/op_mode/show_users.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import argparse
+import pwd
+import spwd
+import struct
+import sys
+from time import ctime
+
+from tabulate import tabulate
+from vyos.config import Config
+
+
+class UserInfo:
+ def __init__(self, uid, name, user_type, is_locked, login_time, tty, host):
+ self.uid = uid
+ self.name = name
+ self.user_type = user_type
+ self.is_locked = is_locked
+ self.login_time = login_time
+ self.tty = tty
+ self.host = host
+
+
+filters = {
+ 'default': lambda user: not user.is_locked, # Default is everything but locked accounts
+ 'vyos': lambda user: user.user_type == 'vyos',
+ 'other': lambda user: user.user_type != 'vyos',
+ 'locked': lambda user: user.is_locked,
+ 'all': lambda user: True
+}
+
+
+def is_locked(user_name: str) -> bool:
+ """Check if a given user has password in shadow db"""
+
+ try:
+ encrypted_password = spwd.getspnam(user_name)[1]
+ return encrypted_password == '*' or encrypted_password.startswith('!')
+ except (KeyError, PermissionError):
+ print('Cannot access shadow database, ensure this script is run with sufficient permissions')
+ sys.exit(1)
+
+
+def decode_lastlog(lastlog_file, uid: int):
+ """Decode last login info of a given user uid from the lastlog file"""
+
+ struct_fmt = '=L32s256s'
+ recordsize = struct.calcsize(struct_fmt)
+ lastlog_file.seek(recordsize * uid)
+ buf = lastlog_file.read(recordsize)
+ if len(buf) < recordsize:
+ return None
+ (time, tty, host) = struct.unpack(struct_fmt, buf)
+ time = 'never logged in' if time == 0 else ctime(time)
+ tty = tty.strip(b'\x00')
+ host = host.strip(b'\x00')
+ return time, tty, host
+
+
+def list_users():
+ cfg = Config()
+ vyos_users = cfg.list_effective_nodes('system login user')
+ users = []
+ with open('/var/log/lastlog', 'rb') as lastlog_file:
+ for (name, _, uid, _, _, _, _) in pwd.getpwall():
+ lastlog_info = decode_lastlog(lastlog_file, uid)
+ if lastlog_info is None:
+ continue
+ user_info = UserInfo(
+ uid, name,
+ user_type='vyos' if name in vyos_users else 'other',
+ is_locked=is_locked(name),
+ login_time=lastlog_info[0],
+ tty=lastlog_info[1],
+ host=lastlog_info[2])
+ users.append(user_info)
+ return users
+
+
+def main():
+ parser = argparse.ArgumentParser(prog=sys.argv[0], add_help=False)
+ parser.add_argument('type', nargs='?', choices=['all', 'vyos', 'other', 'locked'])
+ args = parser.parse_args()
+
+ filter_type = args.type if args.type is not None else 'default'
+ filter_expr = filters[filter_type]
+
+ headers = ['Username', 'Type', 'Locked', 'Tty', 'From', 'Last login']
+ table_data = []
+ for user in list_users():
+ if filter_expr(user):
+ table_data.append([user.name, user.user_type, user.is_locked, user.tty, user.host, user.login_time])
+ print(tabulate(table_data, headers, tablefmt='simple'))
+
+
+if __name__ == '__main__':
+ main()