ADDED vpn/conf Index: vpn/conf ================================================================== --- vpn/conf +++ vpn/conf @@ -0,0 +1,25 @@ +# [ʞ] vpn/conf +# ~ lexi hale +# © public domain / CC0 + +# this is the master configuration file for +# openvpn used by the `vpn` utility. these should +# be sane-enough defaults for almost all cases. +# server-specific config should be placed in +# $server/conf in this directory + +client +pull + +group nobody + +resolv-retry infinite +nobind + +persist-key +persist-tun + +log log +mute-replay-warnings +verb 3 +mute 10 ADDED vpn/vpn Index: vpn/vpn ================================================================== --- vpn/vpn +++ vpn/vpn @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# [ʞ] vpn +# ~ lexi hale +# ® affero general public license +# $ vpn (join | part | info | clean | key) +# : 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 : opens a connection to vpn +# - vpn part : closes an active connection to +# - vpn info : reports the status of of a vpn +# - vpn key : automatically provisions the client +# with RSA keys from and creates +# a configuration file if one does not +# already exist +# - vpn part : closes an active connection to +# - 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 for +# every state change. +__=$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 + +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" $* ; exit 1; } +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 + + # 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 + } || { + echo -e "\e[1;33mwarn:\e[m pidfile exists but does not name an openvpn process" + echo " → removing pidfile for safety" + rm -f $pidfile + } + + } || { + echo -ne "\e[1mvpn $target\e[m is not up" + } ;; + + # 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 " + test ! -e $pidfile && { + echo -ne "\e[31mno\e[m current connection" + pids=($(pidof openvpn)) + let pidc=${#pids} + ((pidc > 0)) && { + echo ", but $pidc vpn instances are running" + exit 2 + } || { echo; exit 0; } + } || { + echo -n "pidfile exists" + proc=$(ps ho fname $(cat $pidfile)) + (($? != 0)) && { + echo ", but there are no processes with that pid" + stale; exit 1 + } || test "$proc" == "openvpn" && { + echo -e " and openvpn is \e[32mrunning\e[m" + exit 0 + } || { + echo -e " but named process is \e[31mnot a vpn instance!\e[m" + stale; exit 1 + } + } ;; + ( help ) head -n $(expr $__ - 1) $vpn_script | + tail -n $(expr $__ - 2); exit 255;; + + ( * ) err "action must be one of: join | part | info | clean" ;; +esac +