diff options
author | Daniil Baturin <daniil@baturin.org> | 2019-08-19 12:04:56 -0400 |
---|---|---|
committer | Daniil Baturin <daniil@baturin.org> | 2019-08-19 12:04:56 -0400 |
commit | 212348145838e8791474b987efc624cba3fb8b00 (patch) | |
tree | c4914439531f177fd24fb083b785446f0c35c910 | |
parent | 589952faadcf7700702b24390c1d654706f3a857 (diff) | |
parent | dc8cfa6dfd1d95890b3e14c928e3d2064451a851 (diff) | |
download | vyos-1x-212348145838e8791474b987efc624cba3fb8b00.tar.gz vyos-1x-212348145838e8791474b987efc624cba3fb8b00.zip |
Merge branch 'current' into equuleus
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | debian/control | 3 | ||||
-rw-r--r-- | interface-definitions/https.xml | 24 | ||||
-rw-r--r-- | interface-definitions/interfaces-bridge.xml | 2 | ||||
-rw-r--r-- | interface-definitions/interfaces-openvpn.xml | 625 | ||||
-rw-r--r-- | interface-definitions/protocols-bfd.xml | 8 | ||||
-rw-r--r-- | op-mode-definitions/openvpn.xml | 112 | ||||
-rw-r--r-- | op-mode-definitions/show-protocols-bfd.xml | 6 | ||||
-rw-r--r-- | op-mode-definitions/show-system-info.xml | 167 | ||||
-rw-r--r-- | python/vyos/defaults.py | 19 | ||||
-rwxr-xr-x | src/completion/list_openvpn_clients.py | 57 | ||||
-rwxr-xr-x | src/conf_mode/host_name.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/http-api.py | 11 | ||||
-rwxr-xr-x | src/conf_mode/https.py | 22 | ||||
-rwxr-xr-x | src/conf_mode/interface-openvpn.py | 911 | ||||
-rwxr-xr-x | src/conf_mode/protocols_bfd.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/vyos_cert.py | 143 | ||||
-rwxr-xr-x | src/helpers/vyos-sudo.py | 40 | ||||
-rwxr-xr-x | src/op_mode/show_ram.sh | 33 | ||||
-rwxr-xr-x | src/op_mode/show_users.py | 111 |
20 files changed, 2277 insertions, 24 deletions
@@ -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><filename></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() |