#!/usr/bin/env bash
# [ʞ] vpn
# ~ lexi hale <lexi@hale.su>
# ® affero general public license
# $ vpn (join | part | info | clean | key | pid) <name>
# : vim:ft=bash
# vpn is a simple wrapper around openvpn to make it suitable
# for everyday use. it takes two arguments: the vpn, and
# what to do with it.
# - vpn join <vpn>: opens a connection to vpn <name>
# - vpn part <vpn>: closes an active connection to <name>
# - vpn info <vpn>: reports the status of of a vpn
# - vpn key <host>: automatically provisions the client
# with RSA keys from <host> and creates
# a configuration file if one does not
# already exist
# - vpn part <vpn>: closes an active connection to <vpn>
# - vpn pid <vpn>: print the pid of an active connection
# - vpn help: display this text
#
# a number of environment variables affect the behavior of
# vpn. these are listed in the source with their defaults
# and an explantion of their function. if you do not wish
# users to be able to change the behavior of vpn with
# setenv, you must change the param invokation to a simple
# assignment.
#
# note that you may want to add a visudo line allowing
# %wheel or perhaps even all users to execute openvpn
# without a password; otherwise, only sudoers will be able
# to use vpn and the root password will be required every
# time a user connects. root is not needed to tear down
# connections, as by default the openvpn process is set to
# pivot to the account that invoked the script after it is
# done with tasks that require privileged access.
#
# (it should go without saying, but ensure you understand
# the security implications before editing sudoers on a
# multiuser machine or one that is directly exposed to the
# internet.)
#
# this script is designed to automatically handle
# certificate-based authentication. if there is a file named
# "ca.crt" in the config folder for a script, the script
# will look for files named $vpn_cn.crt and $vpn_cn.key,
# which are automatically passed to openvpn (you don't need
# to name them in the config file; this feature is designed
# to enable the syncing of config files across multiple
# devices each with their own certificate for the host. it
# can also automatically download the necessary certificate
# files from the server using ssh (scp); to enable this
# feature, $vpn_srv_keydir must be set to the location of
# the "pki" directory on the server. users may then invoke
# `vpn key $server-url` to create a default configuration
# and provision the device. $vpn_cn should probably be
# set to your hostname (the default) or a device-unique
# identifier issued by your organization.
# TODO set up `key` mode so that it is able to accept
# paths from the command line as well as in the
# $vpn_srv_keydir environment variable.
_text_=$LINENO
param(){ eval $1=\${$1:-$2}; }
param vpn_basedir ~/opt/vpn
# the directory in which vpn's logfiles are stored,
# and possibly the script itself
param vpn_confdir $vpn_basedir
# the directory that contains the individual vpn
# directories
param vpn_global $vpn_basedir/conf
# a configuration file that is applied to all vpns
param vpn_srv_keydir /srv/vpn/ca
# the directory on the server where vpn client keys
# are stored
param vpn_cn $(uname -n)
# the name of the device / user / resource connecting.
# `vpn key` will use this value to determine which
# client keys to download. i recommend a one-key-per-
# device setup with the certificate CN used to assign a
# name in the tunnel's DNS server, but one-key-per-
# user or one-key-period setups are also possible.
param TMPDIR /tmp
# a directory for temporary files, preferably one that
# does not persist across boots (e.g. a tmpfs)
param USER $(whoami)
# the user who should own all files and processes
# created and destroyed by vpn
param vpn_pidbox $TMPDIR/pid.$USER
# a directory for storing pids in. this should be chmod
# 700 and owned by the user invoking vpn, ideally on a
# tmpfs
param vpn_bin openvpn
# the binary to use. if openvpn is not in your path,
# enter its absolute path here
param vpn_script $0
# a path to the executable
param vpn_scrname $(basename $vpn_script)
# the name of the executable
# thus ends the admin-configurable portion of this script.
# abandon all hope, ye who enter here
err(){ echo -e "\e[1;31merror:\e[m" $* >&2; exit 1; }
warn(){ echo -e "\e[1;33mwarn:\e[m " $* >&2; }
assert(){ msg=$1; shift; test ! $@ && err $msg; }
test "$1" == help || assert "incorrect number of arguments" $# -eq 2
act=$1; target=$2
vpnd=$vpn_confdir/$target
conf=$vpnd/conf
test "$act" == key && {
mkdir -p $vpnd
test -e $conf || {
echo dev tun > $conf
echo proto udp >> $conf
echo remote $target 1194 >> $conf
}
}
assert "target \e[1m$target\e[m does not exist!" -e $vpn_confdir/$target
assert "target \e[1m$target\e[m has no configuration file" -e $conf
pidfile="$vpn_pidbox/openvpn.$target"
stale() {
((clean == 1)) && {
echo " → deleting stale pidfile"
rm -f $pidfile
} || {
echo -e " - run \e[1m$vpn_scrname clean\e[m to clean up"
}
}
goodpid(){
test -e $pidfile || return 1
local proc=$(ps ho fname $(cat $pidfile)) || return 2
test "$proc" == openvpn || return 2
return 0
}
case $act in
# automatically provision machine
( key | setup | provision | download | dl | k )
scp $target:$vpn_srv_keydir/pki/{ca.crt,{issued,private}/$vpn_cn.\*} $vpnd/ || err "could not retrieve keys for \e[1m$target\e[m; ensure ssh is properly configured and machine keys are assigned to the correct user" ;;
# connect to the openvpn server
( join | up | connect | con | start | j | u )
# don't clobber an existing connection
goodpid && err "\e[1m$target\e[m is already up!"
# make sure a private pid directory exists
test ! -e $vpn_pidbox &&
mkdir -p $vpn_pidbox
chmod 700 $vpn_pidbox
# make sure a private base directory exists
test ! -e $vpn_basedir && {
mkdir -p $vpn_basedir && {
chmod 700 $USER $vpn_pidbox
} || {
err '$vpn_basedir is not set or points to a nonexistent directory you cannot create.'
}
}
test -e $vpn_global || err "global configuration file $vpn_global does not exist or is not in $vpn_basedir"
# check and see if we're using automatic
# host certificates; tell openvpn if so
test -e $vpnd/ca.crt && {
hostcert=$vpnd/$vpn_cn
cmd=(
--askpass
--ca $vpnd/ca.crt
--cert $hostcert.crt
--key $hostcert.key
)
}
touch $pidfile # make sure invoker owns it
chmod 600 $pidfile # bc fuck you, that's why
sudo openvpn --daemon --config $vpn_global \
--config $conf \
--user $USER \
${cmd[@]} \
--writepid $pidfile || rm $pidfile;;
# kill an existing connection
( part | down | disc* | stop | p | d )
test -e $pidfile && {
goodpid && {
kill $(cat $pidfile) && rm -f $pidfile
} || {
warn 'pidfile exists but does not name an openvpn process'
echo ' → removing pidfile for safety' >&2
rm -f $pidfile
}
} || {
echo -ne "\e[1mvpn $target\e[m is not up" >&2
} ;;
# clean up dirty pidfiles
( clean | wipe | clear | fix | tidy ) clean=1 ;&
# return profile status
( info | stat* | detail* | i )
echo -ne "\e[1mvpn $target:\e[m " >&2
test ! -e $pidfile && {
echo -ne "\e[31mno\e[m current connection" >&2
pids=($(pidof openvpn))
let pidc=${#pids}
((pidc > 0)) && {
echo ", but $pidc vpn instances are running" >&2
exit 2
} || { echo; exit 0; }
} || {
echo -n "pidfile exists" >&2
proc=$(ps ho fname $(cat $pidfile))
(($? != 0)) && {
echo ", but there are no processes with that pid" >&2
stale; exit 1
} || test "$proc" == "openvpn" && {
echo -e " and openvpn is \e[32mrunning\e[m" >&2
} || {
echo -e " but named process is \e[31mnot a vpn instance!\e[m" >&2
stale; exit 1
}
} ;;
( help ) head -n $(expr $_text_ - 2) $vpn_script |
tail -n $(expr $_text_ - 2); exit 255;;
( pid ) test -e $pidfile && cat $pidfile ||
err "no pidfile exists for \e[1m$target\e[m; are you sure you're connected" ;;
( * ) err "action must be one of: join | part | info | clean | key | pid | help" ;;
esac
exit 0