#!/bin/sh # Enterprise Onion Toolkit # A NOTE TO CONTRIBUTORS: yes this is a big script, but it's also # written to be easy to understand (and reason about) what it is # doing; this is why there are `for` loops around `scp` rather than # tarballing up a bunch of file and shipping them to remote machines, # unpacking them there. This is (mostly) why there are some # long_variable_names. I plan to keep it that way. #rsync_flags="-n" # testing # expected by tools and libraries cd `dirname $0` || exit 1 export EOTK_HOME=`pwd` # for invoking myself prog=`basename $0` self=$EOTK_HOME/$prog # meta version=1.5.0 # set project directory; this path is hard-replicated elsewhere project_dir=$EOTK_HOME/projects.d # mirror directory mirrors_dir=$EOTK_HOME/mirrors.d # onionbalance directory ob_dir=$EOTK_HOME/onionbalance.d ob_conf=$ob_dir/config.yaml ob_status_sock=$ob_dir/ob-status.sock ob_tor_conf=$ob_dir/tor.conf ob_tor_control_sock=$ob_dir/tor-control.sock # what are the hostnames of the remote workers? host_list=$EOTK_HOME/eotk-workers.conf # <--------------------------- HOST FILE # where we put the persistent, shared config site_conf=$EOTK_HOME/eotk-site.conf # init script name init=eotk-init.sh housekeeping=eotk-housekeeping.sh # log retention log_retain=8 # hostname for log purposes hostname=`uname -n` # set path export PATH=$EOTK_HOME/opt.d:$EOTK_HOME/lib.d:$EOTK_HOME:$PATH # ------------------------------------------------------------------ # argument stripping flag_remote=false flag_local=false flag_boot=false propagate_flags="" while : ; do case "x$1" in x--local) ## [command] ... | runs commands locally, ignoring onionbalance if configured flag_local=true propagate_flags="$propagate_flags $1" shift ;; x--boot) ## [command] ... | used by startup scripts flag_boot=true propagate_flags="$propagate_flags $1" shift ;; x--remote) ## [command] ... | used during remote execution on workers flag_remote=true propagate_flags="$propagate_flags $1" shift ;; x--debug) ## [command] ... | enables execution tracing set -x shift ;; *) break ;; esac done CloudHosts() { # use --remote hack to forcibly stop risk of recursion... if $flag_remote ; then echo "localhost" # this will be treated as a magic sentinel return 0 fi # perversely, --local, exactly the same if $flag_local ; then echo "localhost" return 0 fi # if --boot, we want localhost IFF we would have it anyway... if $flag_boot ; then if [ -s $host_list ] ; then # does it contain localhost...? egrep '^(localhost)\b' $host_list else echo "localhost" fi return 0 fi # todo: if hosts-file exists, # cat it (strip comments?) # and return if [ -s $host_list ] ; then cat $host_list return 0 fi # else we are working just on this machine echo "localhost" # this will be treated as a magic sentinel } cloud_hosts=`CloudHosts` # saves multiple invocations need_to_run_locally=false for host in $cloud_hosts ; do test "x$host" = "xlocalhost" && need_to_run_locally=true done # delete dangerous stuff on boot OnBootCleanup() { pidfiles=`find $EOTK_HOME -type f -name "*.pid"` sockfiles=`find $EOTK_HOME -type s -name "*.sock"` for file in $pidfiles $sockfiles ; do if [ -e $file ] ; then # -e => "exists" rm $file || exit 1 fi Warn purged $file done } # ------------------------------------------------------------------ # print a formatted message to stdout Print() { echo "$prog:" "$@" } # print a formatted message to stderr Warn() { Print "$@" 1>&2 } # compress logfiles LogRotate() { SUFFIX=.bz2 do_proj=$1 # true|false do_ob=$2 # true|false WHERE="" $do_proj && WHERE="$WHERE $project_dir" $do_ob && WHERE="$WHERE $ob_dir" if [ "x$WHERE" = "x" ] ; then Print error: LogRotate has no targets exit 1 fi logfiles=`find $WHERE -type f -name "*.log" -print` for logfile in $logfiles ; do hi=`expr $log_retain - 1` dst=$logfile.$hi$SUFFIX if [ -f $dst ] ; then rm $dst || exit 1 Print purged $dst fi while [ $hi -gt 0 ] ; do lo=`expr $hi - 1` src=$logfile.$lo$SUFFIX dst=$logfile.$hi$SUFFIX if [ -f $src ] ; then mv $src $dst || exit 1 Print bumped $dst fi hi=$lo done dst=$logfile.$hi mv $logfile $dst || exit 1 Print created $dst to_compress="$to_compress $dst" done if $do_proj ; then $self --local nxreload -a $self --local torreload -a fi if $do_ob ; then $self ob-restart -a fi if [ "x$to_compress" != "x" ] ; then Print $hostname COMPRESSING LOGS: patience please, this may take several minutes... ls -l $to_compress bzip2 -fv $to_compress fi } # essentially the projects.d folder is a little database ListProjects() { if [ x$1 = x--softmap-only ] ; then softmap_only=true else softmap_only=false fi ( test -d $project_dir || exit 1 cd $project_dir || exit 1 for d in *.d/ ; do if $softmap_only ; then test -f $d/softmap.conf || continue fi echo `basename $d .d` done ) } # TODO(alecm) resolve potential clashes between project names and # various other directory names ending in ".d"; maybe use ".proj"? SpotPush() { # push any file named FOO from $EOTK_HOME and/or projects.d for filename in "$@" ; do # GENERATE/USE ABSOLUTE PATHS files=`find $project_dir -type f -name "$filename" | sort` # special: check top-level dir, too... tldfile=${EOTK_HOME}/$filename # USE ABSOLUTE PATHS test -f $tldfile && files="$files $tldfile" for host in $cloud_hosts ; do test "x$host" = "xlocalhost" && continue # skip self echo :::: push $host $filename :::: for filepath in $files ; do Print pushing $host:$filepath scp -p $filepath $host:$filepath || exit 1 done done done } # push eotk directory to remote DestructivePush() { for host in $cloud_hosts ; do test "x$host" = "xlocalhost" && continue # skip self echo :::: rnap $host :::: rsync $rsync_flags \ -av \ --delete \ --delete-excluded \ --exclude="*.log" \ --exclude="*.pid" \ --exclude="*.sock" \ --exclude="*.yaml" \ --exclude="*~" \ --exclude="docs.d/" \ --exclude="mirrors.d/" \ --exclude="onionbalance.d/" \ --exclude="secrets.d/" \ --exclude=".git/" \ --exclude=".gitignore" \ --exclude="cached-certs" \ --exclude="cached-microdesc*" \ --exclude="configure*.log" \ --exclude="eotk-workers.conf" \ --exclude="hostname" \ --exclude="lock" \ --exclude="onion_service_non_anonymous" \ --exclude="private_key" \ --exclude="state" \ ${EOTK_HOME}/ \ $host:${EOTK_HOME}/ done } # mirror remote back for log review, backup, etc Mirror() { for host in $cloud_hosts ; do test "x$host" = "xlocalhost" && continue # skip self echo :::: mirror $host :::: test -d $mirrors_dir || mkdir -p $mirrors_dir || exit 1 chmod 700 $mirrors_dir || exit 1 rsync $rsync_flags -av \ --delete \ --delete-excluded \ --exclude="cached-certs" \ --exclude="cached-microdesc*" \ $host:${EOTK_HOME}/ $mirrors_dir/$host/ done } # run a command in the context of the local projects directory RunLocally() { action=$1 shift project=$1 shift echo :::: $action $project $@ :::: sh "$project_dir/$project.d/$action.sh" "$@" } # $1=action, rest = project names RunLocallyOverProjects() { action=$1 shift # remaining arguments are projects if [ "x$1" = "x" ] ; then # test for no args Print error: missing project name, try: $prog projects for a list, or -a for all return 1 elif [ "x$1" = "x-a" ] ; then # test for / expand the "-a" flag projects=`ListProjects` else # do what we are told projects="$*" fi # loop for project in $projects ; do RunLocally $action $project done } # run a command on remote machines, or possibly locally InvokeRemotely() { for host in $cloud_hosts ; do test "x$host" = "xlocalhost" && continue # skip self echo :::: remote $host: $* :::: ssh "$host" "$self --remote $*" done } # get a config file and re/populate the projects directory with it Configure() { log=configure$$.log # make the siteconf if it does not already exist test -f $site_conf || touch $site_conf || exit 1 for file in "$@" ; do echo :::: configure $file :::: case "$file" in *.conf) : happy bunnies ;; *.tconf) file2=`basename $file .tconf`.conf if [ -s $file2 ] ; then Print info: $file: using existing $file2 else Print info: Processing $file Print info: Populating $file2 with onions Print info: Please be patient... expand-config.pl $file >$file2 fi file="$file2" ;; *) Print error: bad config file suffix, was expecting: .conf, .tconf exit 1 ;; esac if ! $EOTK_HOME/lib.d/do-configure.pl "$file" ; then Print error: failure processing $file: see $log exit 1 fi done 2>$log Print done. logfile is $log } # argument 'parser' - ha! cmd="$1" # we may need the remaining args test $# = 0 || shift # if no cmd-arg skip shifting, fall-thru case "$cmd" in version|test) ## print version information for all tools test -d .git && git show -s --oneline tor --version || Warn cannot find tor executable nginx -v || Warn cannot find nginx executable onionbalance --version || Warn cannot find onionbalance executable Print $version $EOTK_HOME `uname -a` InvokeRemotely version ;; projects|proj) ## lists all projects ListProjects ;; configure|config|conf) ## file ... | use config file to rebuild project directories Configure "$@" ;; genkey|gen) ## generate an onion certificate and print its address secrets_dir=secrets.d test -d $secrets_dir || mkdir -p $secrets_dir || exit 1 chmod 700 $secrets_dir || exit 1 ( cd $secrets_dir generate-onion-key.sh ) ;; # ACTIONS start) ## project* ... | start the cited projects $flag_boot && OnBootCleanup $need_to_run_locally && RunLocallyOverProjects start "$@" InvokeRemotely start "$@" ;; stop) ## project* ... | stop the cited projects $need_to_run_locally && RunLocallyOverProjects stop "$@" InvokeRemotely stop "$@" ;; restart|bounce|reload) ## project* ... | restart the cited projects $need_to_run_locally && RunLocallyOverProjects bounce "$@" InvokeRemotely bounce "$@" ;; nxreload|nx-reload|ob-nxreload) ## project* ... | live-reload nginx configs for projects $need_to_run_locally && RunLocallyOverProjects nxreload "$@" InvokeRemotely nxreload "$@" ;; torreload|tor-reload) ## project* ... | live-reload tor configs for projects $need_to_run_locally && RunLocallyOverProjects torreload "$@" InvokeRemotely torreload "$@" ;; debugon|debug-on) ## project* ... | enable service debugging for projects $need_to_run_locally && RunLocallyOverProjects debugon "$@" InvokeRemotely debugon "$@" ;; debugoff|debug-off) ## project* ... | disable service debugging for projects $need_to_run_locally && RunLocallyOverProjects debugoff "$@" InvokeRemotely debugoff "$@" ;; clean|cleanup) ## project* ... | stop and remove trash files for projects (eg: after crash, "nginx.pid exists!" and ".sock exists!" errors, etc) $need_to_run_locally && RunLocallyOverProjects cleanup "$@" InvokeRemotely cleanup "$@" ;; syntax) ## project* ... | perform nginx syntax check for projects $need_to_run_locally && RunLocallyOverProjects syntax "$@" InvokeRemotely syntax "$@" ;; harvest|onions) ## project* ... | list onions used for projects (including: used by onionbalance) $need_to_run_locally && RunLocallyOverProjects harvest "$@" InvokeRemotely harvest "$@" ;; status) ## project* ... | print eotk process status (known processes) for projects $self ob-ps # because common $need_to_run_locally && RunLocallyOverProjects status "$@" InvokeRemotely status "$@" ;; maps|map) ## project* ... | print onion mappings for projects (including: used by onionbalance) $need_to_run_locally && RunLocallyOverProjects maps "$@" InvokeRemotely maps "$@" ;; delete) Print $cmd not yet implemented, sorry. ;; # DIAGS ps) ## list all (probable) eotk processes on all workers $flag_remote || echo :::: eotk processes :::: ps auxww | egrep '\b(eotk|onionbalance|nginx)\b' # InvokeRemotely prints remote-ness banner diags InvokeRemotely ps ;; df) ## print filestore usage of eotk directory on all workers $flag_remote || echo :::: eotk filesystem space :::: df -akh $EOTK_HOME # InvokeRemotely prints remote-ness banner diags InvokeRemotely df ;; logsize|logsizes) ## list logfile sizes on all workers $flag_remote || echo :::: eotk log sizes :::: find $EOTK_HOME -name "*.log" -o -name "*.log.*" -ls # InvokeRemotely prints remote-ness banner diags InvokeRemotely logsize ;; # ---- ONIONBALANCE ---- push) Print push is destructive and has been renamed, see the documentation ;; shutdown) ## shutdown all projects on all workers (and onionbalance) $self $propagate_flags ob-stop $self $propagate_flags stop -a ;; # this used to be called 'push' but got renamed because oops. # ---- DO NOT USE THIS CASUALLY, LEARN FROM MY MISTAKES ---- ob-remote-nuke-and-push|rnap) ## DESTRUCTIVE PUSH of local project configuration to workers; THERE ARE NO SAFETY CHECKS, do not use on live system. $self stop -a DestructivePush ;; nxpush|ob-nxpush) ## push nginx config files to all workers # gently push all the nginx configs to the onionbalance workers; same as "spotpush nginx.conf" SpotPush nginx.conf ;; torpush|ob-torpush) ## push tor config files to all workers # gently push all the tor configs to the onionbalance workers; same as "spotpush tor.conf" SpotPush tor.conf ;; spotpush|ob-spotpush) ## file ... | push instances of files named to all workers # gently push all instances of given filename, to onionbalance workers SpotPush "$@" ;; ob-config|obconfig|ob-configure|obconfigure) Print ob-configure had been rolled into ob-start Print please skip forward to: $prog ob-start project ... ;; ob-gather|obgather) ## onionbalance: gather all worker mappings, and build onionbalance configs; is automatically called by ob-start if needed # ob storage test -d $ob_dir || mkdir -p $ob_dir || exit 1 chmod 700 $ob_dir # otherwise tor complains # sanity check that args are provided if [ "x$1" = "x" ] ; then # test for no args Print error: missing project name, try: $prog projects for a list, or -a for all "(if applicable)" exit 1 elif [ "x$1" = "x-a" ] ; then # test for / expand the "-a" flag projects=`ListProjects --softmap-only` else # do what we are told projects="$*" fi tor_address=127.0.0.1 tor_port=9055 # get the mappings Print gathering mappings for OnionBalance for projects: $projects mappings=__om$$.tmp $self maps $projects | awk '/^::::/{next;} $4=="softmap"{print;}' >$mappings # check the mappings for p in $projects ; do n=`awk '$3=="'"$p"'"'<$mappings | wc -l` if [ $n = 0 ] ; then Warn no mappings yet for project $p: does it exist / is it started / is it not a softmap project "?" rm $mappings exit 1 else m=`awk '$3=="'"$p"'"{print $1}'<$mappings | sort -u | wc -l` w=`awk '$3=="'"$p"'"{print $6}'<$mappings | sort -u | wc -l` Print project $p contains $n mappings Print project $p uses $m master onions Print project $p uses $w worker onions fi done Print building OnionBalance configurations... ( # notes/commentary, including onionbalance defaults, at: # https://media.readthedocs.org/pdf/onionbalance/latest/onionbalance.pdf echo LOG_LEVEL: info echo TOR_ADDRESS: $tor_address echo TOR_PORT: $tor_port echo STATUS_SOCKET_LOCATION: $ob_status_sock # REFRESH_INTERVAL How often to check for updated backend # hidden service descriptors. This value can be decreased # if your backend instance are under heavy loaded causing # them to rotate introduction points quickly. (default: # 600 seconds). refresh=600 # to be tuned echo REFRESH_INTERVAL: $refresh # INITIAL_DELAY How long to wait between starting # OnionBalance and publishing the master descrip- tor. If # you have more than 20 backend instances you may need to # wait longer for all instance descriptors to download # before starting (default: 45 seconds). echo INITIAL_DELAY: `expr $refresh / 6` # PUBLISH_CHECK_INTERVAL How often should to check if new # descriptors need to be published for the master hidden # service (default: 360 seconds). echo PUBLISH_CHECK_INTERVAL: `expr $refresh / 2` # DESCRIPTOR_OVERLAP_PERIOD How long to overlap hidden # service descriptors when changing descriptor IDs # (default: 3600 seconds) echo DESCRIPTOR_OVERLAP_PERIOD: `expr $refresh '*' 4` # DESCRIPTOR_UPLOAD_PERIOD How often to publish a # descriptor, even when the introduction points don't # change (default: 3600 seconds) echo DESCRIPTOR_UPLOAD_PERIOD: `expr $refresh '*' 4` # DESCRIPTOR_VALIDITY_PERIOD How long a hidden service # descriptor remains valid (default: 86400 seconds) echo DESCRIPTOR_VALIDITY_PERIOD: 86400 # 1 day do-obconfig.pl <$mappings ) > $ob_conf # clean up rm $mappings Print building OnionBalance-Tor configurations... ( echo DataDirectory $ob_dir echo ControlPort unix:$ob_tor_control_sock echo PidFile $ob_dir/tor.pid echo "#" Log info file $ob_dir/tor.log echo Log notice file $ob_dir/tor.log echo SafeLogging 1 echo HeartbeatPeriod 60 minutes echo RunAsDaemon 1 echo "#" onionbalance # echo SocksPort unix:$ob_dir/tor-socks.sock # curl 7.38 does not like this echo SocksPort $tor_address:$tor_port echo CookieAuthentication 1 # echo MaxClientCircuitsPending 1024 ) > $ob_tor_conf ;; ob-start|obstart) ## start onionbalance (will gather and build configs if needed) # do not test for ob_dir here, it is okay to not exist if [ -f $ob_dir/ob.pid ] ; then Warn file $ob_dir/ob.pid already exists, OnionBalance may already be running. Warn Aborting... exit 1 fi if [ -f $ob_dir/tor.pid ] ; then Warn file $ob_dir/tor.pid already exists, OnionBalance-Tor may already be running. Warn Aborting... exit 1 fi # ob-gather checks args $self ob-gather "$@" || exit 1 Print starting OnionBalance-Tor tor -f $ob_tor_conf >$ob_dir/tor-startup.log 2>&1 Print starting OnionBalance onionbalance \ -s $ob_tor_control_sock \ -c $ob_conf $ob_dir/onionbalance.log 2>&1 & # bg ob_pid=$! echo $ob_pid >$ob_dir/ob.pid ;; ob-stop|obstop) ## stop onionbalance for pidfile in $ob_dir/ob.pid $ob_dir/tor.pid ; do test -s $pidfile || continue pid=`cat $pidfile` Print sending SIGTERM to $pid in $pidfile kill $pid done rm -f $ob_dir/ob.pid ;; ob-restart) ## restart onionbalance # ob-stop takes no args, but ob-start requires them; try to be consistent, therefore: if [ "x$1" = "x" ] ; then # test for no args Print error: missing project name, try: $prog projects for a list, or -a for all "(if applicable)" exit 1 fi $self ob-stop $self ob-start "$@" ;; ob-ps) ## print list of onionbalance processes test -d $ob_dir || exit 0 echo :::: onionbalance processes :::: pidfiles=`find $ob_dir -name "*.pid"` if [ "x$pidfiles" != "x" ] ; then ps -p `cat $pidfiles` fi ;; ob-status|obstatus) ## query onionbalance for its status $self ob-ps if [ -S $ob_status_sock ] ; then echo "" socat - unix-connect:$ob_status_sock fi ;; ob-maps|obmaps) ## project* ... | print onionbalance mappings for projects if [ "x$1" = "x" ] ; then # test for no args Print error: missing project name, try: $prog projects for a list, or -a for all "(if applicable)" exit 1 fi $self maps "$@" | awk '/^::::/{next;} $4=="softmap"{print $1, $2}' | sort -k 2 -u ;; ob-watch|obwatch) ## use 'watch' to iterate 'ob-status' watch -n 5 $self ob-status ;; # FREEZE/BACKUP mirror|pull) ## pull a copy (rsync) of all workers to local 'mirrors.d' directory Mirror ;; backup|freeze) ## as 'mirror' but also create a datestamped compressed tarball once mirror is complete Mirror ( ds=`date "+%Y%m%d%H%M%S"` cd $mirrors_dir || exit 1 for directory in */ ; do test -d "$directory" || exit 1 # did */ expand? dir=`basename $directory` # strip trailing / echo :::: backup $dir :::: tar cf - $dir | bzip2 > $dir-$ds.tar.bz2 done ) ;; logrotate) ## compress logs and reload configurations on all workers InvokeRemotely logrotate # do we need to do onionbalance logs? if [ -d $ob_dir ] ; then need_to_do_ob_logs=true else need_to_do_ob_logs=false fi LogRotate $need_to_run_locally $need_to_do_ob_logs ;; script|scripts|make-script|make-scripts) ## create 'boot' and 'cron' housekeeping scripts for t in $init $housekeeping ; do lib.d/expand-template.pl templates.d/$t.txt $t 2>/dev/null chmod 755 $t Print created: $t done Print please read those files for installation instructions. ;; shell) ## run a shell in the eotk PATH environment env PS1='eotk-env$ ' ${SHELL:-sh} -i ;; help|*) ## prints this text Print "switches and commands:" echo " * project* => supports '-a' for all" echo " * synonyms are in " echo "" exec ./lib.d/explain.pl $0 exit 1 ;; esac exit 0