#!/bin/sh

# Copyright (C) 2015 by Yuri Victorovich. All rights reserved.

# PROVIDE: vm_to_tor
# REQUIRE: LOGIN ipfw sysctl
# KEYWORD: shutdown
# BEFORE: tor

# Add the following lines to /etc/rc.conf to enable vm_to_tor:
# vm_to_tor_enable (bool):            * Set to "YES" to enable vm-to-tor.
#                                       Default: "NO"
# vm_to_tor_ifaces (list):            * Set it to the list of interfaces.
#                                       At least one interface is required.
#                                       Default: "tap0"
# vm_to_tor_net_pattern (str):        * Set it to the network pattern.
#                                       Default: "172.16"
# vm_to_tor_rule_base (uint):         * Set it to the the rule number block
#                                       location. Default: "3000"
# vm_to_tor_extra_ports (list):       * Set it to the the list of ports to open
#                                       from host to VMs. Default: ""
# vm_to_tor_control_socket (bool):    * Set it to "YES" to allow host
#                                       administration using ControlSocket.
#                                       Default: "NO"
# vm_to_tor_allow_cookie_auth (bool): * Set it to "YES" to allow cookie
#                                       administration on ControlSocket.
#                                       Default: "NO"
# vm_to_tor_use_base (bool):          * Set it to "YES" to use central tor
#                                       daemon on host.
#                                       By default individual tor instance is
#                                       launched per interface.
#                                       Choosing "YES" will share the host tor
#                                       instance will require slightly less
#                                       resources, and will share the same
#                                       tor entry guard, but all data will pass
#                                       through the same process. This may be
#                                       considered more secure by some, and less
#                                       secure by others.
#                                       It requires tor service to run, but tor
#                                       should start after vm-to-tor.
#                                       CAVEAT Choosing "YES" runs into tor
#                                       inability to change config in a graceful
#                                       way, so vm-to-tor will be writing
#                                       torrc file, which can conflict with tor.
#                                       Default: "NO" (WARNING BROKEN, UNTESTED)
# vm_to_tor_hs (str):                 * Set it to the list of hidden services to
#                                       route to VMs.
#                                       List is 'newline' or '|' separarated.
#                                       Hidden services are in the format:
#                                       tapN /dir/to/hidden/svc oport iport
#                                       Default: ""
# vm_to_tor_user (str):               * Set it to the user name that vm-to-tor
#                                       uses. Should be the same that tor uses.
#                                       Default: "_tor"
# vm_to_tor_group (str):              * Set it to the group name that vm-to-tor
#                                       uses. Should be the same that tor uses.
#                                       Default: "_tor"
# vm_to_tor_vm_type (str):            * This option tells vm-to-tor what kind of
#                                       virtual machine software you are running.
#                                       Possible options are: vbox,bhyve,none.
#                                       Please note that per-vm option 'vm_type'
#                                       can specify different vm types for
#                                       different machines.
#                                       Default: "none"
# vm_to_tor_show_colors (bool):       * Choosing "NO" prevents colors in service
#                                       messages.
#                                       Default: "YES"
# vm_to_tor_fw_rule_gap (uint):       * Setting the positive value makes
#                                       vm-to-tor leave that many available ipfw
#                                       rules for every VM.
#                                       This is to allow an expecrt user to add
#                                       any extra-rules per VM.
#                                       Default: "0"
# ### per-VM options
# vm_to_tor_tapN_tor_options (str):   * Set it to the newline-separated list of
#                                       additional options to add to every VM.
#                                       Default: ""
# vm_to_tor_tapN_fw_allow_nfs (list): * Set it to space-separated list of NFS
#                                       servers VM is allowed to mount from.
#                                       Currently vm-to-tor only supports
#                                       NFS servers running on host. Please
#                                       note that adding remote NFS servers
#                                       would have required adding routing
#                                       entries or NAT rules.
#                                       Default: ""
# vm_to_tor_tapN_vm_type (str):       * Specifies vm type on per-vm basis. See
#                                       'vm_to_tor_vm_type'.
#                                       Default: ""
#

#
# This module depends on these external executables in LOCALBASE:
# * bin/tor
# * bin/tiny-dhcp-server
# * bin/tiny-udp-anti-nat
#

. /etc/rc.subr

name=vm_to_tor
nameb=vm-to-tor
rcvar=vm_to_tor_enable
start_cmd="vm_to_tor_start"
stop_cmd="vm_to_tor_stop"
status_cmd="vm_to_tor_status"
required_modules="ipfw ipdivert if_tap"

: ${LOCALBASE="/usr/local"}

# signature that will be placed into torrc to later be able to find our lines
torrc_signature="VBoxToTOR"
# where all tors data dirs are
tor_dir=/var/tmp/vm-to-tor
tor_conf=${LOCALBASE}/etc/tor/torrc
# consts
NL=$'\n'
TAB=$'\t'
nfs_ports="111 1110 2049 4045" # 111=portmap 1110=nfsd-status/nfsd-keepalive 2049=nfsd 4045=lockd + mountd port will be added based on the actual value

load_rc_config ${name}

: ${vm_to_tor_enable="NO"}
: ${vm_to_tor_ifaces="tap0"}
: ${vm_to_tor_net_pattern="172.16"}
: ${vm_to_tor_rule_base="3000"}
: ${vm_to_tor_extra_ports=""}
: ${vm_to_tor_control_socket="NO"}
: ${vm_to_tor_allow_cookie_auth="NO"}
: ${vm_to_tor_use_base="NO"}
: ${vm_to_tor_hs=""}
: ${vm_to_tor_user="_tor"}
: ${vm_to_tor_group="_tor"}
: ${vm_to_tor_vm_type="none"}
: ${vm_to_tor_show_colors="YES"}
: ${vm_to_tor_fw_rule_gap="0"}

## internals

echo_out() {
  echo "${nameb}: $1"
}

echo_err() {
  echo "${nameb}: $1" >&2
}

tm() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')]"
}

is_user_authorized() {
  [ $(id -u) = 0 -o $(id -un) = ${vm_to_tor_user} ] || return 1
}

is_user_authorized_with_msg() {
  is_user_authorized || ! echo_err "Only root and ${vm_to_tor_user} can $1" || return 1
}

ck_iface_exists() {
  ifconfig $1 >/dev/null 2>&1
}

are_any_processes_running() {
  local do_prn=$1
  [ -d ${tor_dir} ] || return 0
  local msg=""
  # dhcp daemon
  ! check_process_by_pid_file "${tor_dir}/dhcp-daemon.pid" || msg="${msg}* dhcp daemon pid=$(cat ${tor_dir}/dhcp-daemon.pid)${NL}"
  # interfaces, tor processes, anti-nat daemons
  for iface in ${vm_to_tor_ifaces}; do
    ! ck_iface_exists $iface || msg="${msg}* ${iface} network interface${NL}"
    ! check_process_by_pid_file "${tor_dir}/${iface}/tor.pid" || msg="${msg}* tor pid=$(cat ${tor_dir}/${iface}/tor.pid)${NL}"
    ! check_process_by_pid_file "${tor_dir}/${iface}/udp-anti-nat.pid" || msg="${msg}* udp anti-nat pid=$(cat ${tor_dir}/${iface}/udp-anti-nat.pid)${NL}"
  done
  # output
  [ -n "$msg" ] &&
    ([ $do_prn -eq 0 ] ||
      echo_err "can't start because essential elements are alive:${NL}${msg}Did you forget to stop ${nameb}? Is there the configuration problem?") &&
    return 1
  return 0
}

is_tor_installed() {
  [ -x ${LOCALBASE}/bin/tor ] || return 1
}

get_option_by_name() {
  eval echo "\"\$vm_to_tor_${1}\""
}

get_per_vm_option() {
  eval echo "\"\$vm_to_tor_${1}_${2}\""
}

get_per_vm_option_or_default() {
  local val=$(get_per_vm_option $1 $2)
  [ -n "$val" ] || val=$(get_option_by_name $2)
  echo "$val"
}

set_per_vm_option() {
  eval "vm_to_tor_${1}_${2}=\"${3}\""
}

unique_port_set() {
  echo $vm_to_tor_extra_ports $vm_to_tor_hs_all_iports | tr " ${TAB}" '\n' | sort | uniq
}

unique_port_set_for_iface() {
  # eliminate port 22 here, I would rather keep 22 as a separate line in ipfw section to keep it explicit
  eval "echo \$vm_to_tor_extra_ports \$vm_to_tor_hs_${1}_iports | tr \" \${TAB}\" '\\n' | sort | uniq | grep -v ^22\$"
}

lst_size() {
  local elt cnt=0
  for elt in $1; do
    cnt=$((cnt+1))
  done
  echo $cnt
}

lst_uniquify() {
  echo "$1" | tr " ${TAB}" '\n' | sort | uniq
}

calc_max_nfs_hosts_needed() {
  local max=0
  for iface in ${vm_to_tor_ifaces}; do
    local cnt=$(lst_size "$(get_per_vm_option ${iface} fw_allow_nfs)")
    if [ $cnt -gt $max ]; then
      max=$cnt
    fi
  done
  echo $max
}

nfs_ports_count() {
  # +1 due to the mountd variable port
  echo $(($(lst_size "$nfs_ports")+1))
}

nfs_mountd_actual_port() {
  local port
  port=$(rpcinfo -p 2>/dev/null | grep " mountd$" | awk '{print $4}' | uniq)
  if [ $? -ne 0 -o -z "$port" ]; then
    echo_err "can't determine mountd port: NFS requested but mountd isn't running?"
    return 1
  fi
  echo $port
}

calc_rule_step() {
  local cnt=0
  # add NFS ports if requested: tcp and udp per port
  cnt=$((cnt+2*$(nfs_ports_count)*$(calc_max_nfs_hosts_needed)))
  # 9 base rules required per interface
  cnt=$((cnt+9))
  # add extra ports and HS ports requested by the user (they are added together in add_ipfw_rules too)
  cnt=$((cnt+$(lst_size "$(unique_port_set)")))
  # add the gap if requested by user
  cnt=$((cnt+${vm_to_tor_fw_rule_gap}))
  # add deny-all rule
  cnt=$((cnt+1))
  # pick the round number closest to needed
  for step in 10 20 50 100 200 500 1000; do
    if [ $cnt -le $step ]; then
      echo ${step}
      return
    fi
  done
  echo_err "too many ports requested"
  return 1
}

run_su() {
  su -m ${vm_to_tor_user} -c "$1"
}

set_file_perms_r() {
  chown ${vm_to_tor_user}:${vm_to_tor_group} "$1"
  chmod 0400 "$1"
}

set_file_perms_rw() {
  chown ${vm_to_tor_user}:${vm_to_tor_group} "$1"
  chmod 0600 "$1"
}

set_file_perms_rwx() {
  chown ${vm_to_tor_user}:${vm_to_tor_group} "$1"
  chmod 0700 "$1"
}

save_daemon_cmd_file() {
  local cmd="$1"
  local dname="$2"
  echo "${cmd}" > ${tor_dir}/${dname}.cmd
  set_file_perms_r ${tor_dir}/${dname}.cmd
}

mkdir_perms() {
  (mkdir "${1}" && set_file_perms_rwx "${1}") > /dev/null 2>&1
}

vm_type_to_group() {
  case $1 in
    none)
      echo "nobody"
      ;;
    vbox)
      echo "vboxusers"
      ;;
    bhyve)
      echo "wheel"
      ;;
  esac
}

## major steps in init/fini

create_dirs() {
  rm -rf "${tor_dir}"
  mkdir_perms "${tor_dir}" || ! echo_err "failed to create work dir ${tor_dir}" || return 1
  for iface in ${vm_to_tor_ifaces}; do
    mkdir_perms "${tor_dir}/${iface}" || ! echo_err "failed to create work dir ${tor_dir}/${iface}" || return 1
  done
}

delete_dir() {
  rm -rf "${tor_dir}" > /dev/null 2>&1
}

check_tor_config() {
  # check we can really write /usr/local/etc/tor/torrc
  local rnd=$(jot -r 1 1000000 10000000)
  (test -w ${LOCALBASE}/etc/tor/torrc &&
   test -w ${LOCALBASE}/etc/tor &&
   touch ${LOCALBASE}/etc/tor/torrc.tmp.${rnd} &&
   rm -f ${LOCALBASE}/etc/tor/torrc.tmp.${rnd}) > /dev/null 2>&1 ||
     ! echo_err "failed to check_tor_config" || return 1
}

parse_hs() {
  # parse HS definitions, if any
  local IFS="${NL}|"
  for hs in ${vm_to_tor_hs}; do
    if [ -z "${hs}" ]; then
      continue
    fi
    local fidx=0
    local fld iface dir oport iport
    IFS=" ${TAB}"
    for fld in ${hs}; do
      case $fidx in
        0)
          iface="${fld}"
          ;;
        1)
          dir="${fld}"
          ;;
        2)
          oport="${fld}"
          ;;
        3)
          iport="${fld}"
          ;;
        *)
          echo_err "too many fields in the hidden service definition: ${hs}"
          return 1
          ;;
      esac
      fidx=$((fidx+1))
    done
    if [ -z "${iface}" -o -z "${dir}" -o -z "${oport}" -o -z "${iport}" ]; then
      echo_err "too few fields in the hidden service definition: ${hs}"
      return 1
    fi
    # fill hs info into global vars
    eval "vm_to_tor_hs_${iface}=\"\$vm_to_tor_hs_${iface} ${dir} ${oport} ${iport}\""
    eval "vm_to_tor_hs_${iface}_iports=\"\$vm_to_tor_hs_${iface}_iports ${iport}\""
    eval "vm_to_tor_hs_all_iports=\"\$vm_to_tor_hs_all_iports ${iport}\""
  done
  vm_to_tor_hs_all_iports=$(lst_uniquify "${vm_to_tor_hs_all_iports}")
}

check_process_by_pid_file() {
  local pid_file="$1"
  [ -r ${pid_file} -a -s ${pid_file} -a \
    "$(procstat $(cat ${pid_file} 2>/dev/null) 2>/dev/null | tail -1 | sed -E 's/^[[:space:]]*([0-9]+).*/\1/g' 2>/dev/null)" = "$(cat ${pid_file} 2>/dev/null)" ] >/dev/null 2>&1 || return 1
}

create_tap() {
  # link0 below is to keep the network interface open when vm is closed. The side-effect is that the IP address also stays.
  for iface in ${vm_to_tor_ifaces}; do
    local vm_id=$(echo $iface | sed -e 's/[a-z]*//')
    if !(ifconfig $iface create && \
         ifconfig $iface up && \
         ifconfig $iface inet "${vm_to_tor_net_pattern}.${vm_id}.1/24" link0 && \
         chown root:$(vm_type_to_group $(get_per_vm_option_or_default $iface "vm_type")) /dev/$iface && \
         chmod 0660 /dev/$iface) >/dev/null 2>/dev/null; then
      echo_err "failed to create interface $iface"
      destroy_tap_silently
      return 1
    fi
  done
}

destroy_tap() {
  for iface in ${vm_to_tor_ifaces}; do
    if ! ifconfig $iface destroy >/dev/null 2>/dev/null; then
      echo_err "failed to destroy interface $iface"
      destroy_tap_silently
      return 1
    fi
  done
}

destroy_tap_silently() {
  for iface in ${vm_to_tor_ifaces}; do
    ifconfig $iface destroy >/dev/null 2>&1
  done
  return 0
}

check_config_vm_type() {
  case "$1" in
    vbox|bhyve|none)
      ;;
    *)
      echo_err "unknown vm_type=$1, allowed values are vbox,bhyve,none"
      return 1
      ;;
  esac
}

check_config() {
  [ $(count_tunnels) -gt 0 ] ||
    ! echo_err "no tunnels: 'vm_to_tor_ifaces' should have at least one tapN tunnel set" || return 1
  check_config_vm_type "${vm_to_tor_vm_type}" || return 1
  for iface in ${vm_to_tor_ifaces}; do
    local vm_type=$(get_per_vm_option $iface "vm_type")
    [ -z "${vm_type}" ] || check_config_vm_type "${vm_type}" || return 1
  done
}

check_sysctl() {
  # VM process needs to be able to open tapN, so need net.link.tap.user_open=1.
  # Even when it is set in /etc/sysctl.conf, if_tap might have been not loaded
  # when sysctl was initialized. We depend on if_tap, so it must be loaded by now,
  # therefore rerun sysctl before the check to make sure it is set.
  /etc/rc.d/sysctl start >/dev/null 2>&1
  [ $(sysctl -n net.link.tap.user_open) -eq 1 ] ||
    ! echo_err "net.link.tap.user_open is not set" ||
    ! echo_err "please add the line 'net.link.tap.user_open=1' to /etc/sysctl.conf" ||
    return 1
}

add_ipfw_rules() {
  local cmd=$vm_to_tor_rule_base
  local step=$(calc_rule_step)
  local port
  local fwcmd="/sbin/ipfw -q"
  local nfs_mountd_port

  for iface in ${vm_to_tor_ifaces}; do
    local vm_id=$(echo $iface | sed -e 's/[a-z]*//')
    local vm_net="${vm_to_tor_net_pattern}.${vm_id}.0/24"
    local vm_gw="${vm_to_tor_net_pattern}.${vm_id}.1"

    # rule sub-index
    local pidx=0

    # NFS if requested (NFS should be befre the main section, because we override TCP here that normally goes to tor)
    local nfs_host nfs_port
    for nfs_host in $(get_per_vm_option $iface "fw_allow_nfs"); do
      [ -n "$nfs_mountd_port" ] || nfs_mountd_port=$(nfs_mountd_actual_port) || return 1
      for nfs_port in $nfs_ports $nfs_mountd_port; do
        ${fwcmd} add $((cmd+pidx+0)) allow tcp from ${vm_net} to ${nfs_host} ${nfs_port} in via $iface keep-state
        ${fwcmd} add $((cmd+pidx+1)) allow udp from ${vm_net} to ${nfs_host} ${nfs_port} in via $iface keep-state
        pidx=$((pidx+2))
      done
    done

    # TCP: sink all TCP from this net to Tor's TransPort
    ${fwcmd} add $((cmd+pidx+0)) fwd ${vm_gw},9030 tcp from $vm_net to any in via $iface keep-state

    ## DNS (direct): allow DNS to go to tor DNS port (no port conversion, needs net.inet.ip.portrange.reservedhigh=52)
    #${fwcmd} add $((cmd+pidx+1)) allow udp from ${vm_net} to ${vm_gw} 53 via $iface keep-state
    # DNS (with port conversion): convert UDP port 53->10053 using anti-nat, no need for sysctl change
    local divert_port=$cmd
    ${fwcmd} add $((cmd+pidx+1)) divert $divert_port log udp from ${vm_net} to any dst-port 53 in via $iface
    ${fwcmd} add $((cmd+pidx+2)) divert $divert_port log udp from ${vm_gw} to ${vm_net} src-port 10053 out via $iface
    ${fwcmd} add $((cmd+pidx+3)) allow log udp from ${vm_net} to ${vm_gw} dst-port 10053 in via $iface
    ${fwcmd} add $((cmd+pidx+4)) allow log udp from ${vm_gw} to ${vm_net} src-port 53 out via $iface
    set_per_vm_option ${iface} "dns_anti_nat_daemon_cmd" "${LOCALBASE}/bin/tiny-udp-anti-nat -d -U ${vm_to_tor_user}:${vm_to_tor_group} -p ${tor_dir}/${iface}/udp-anti-nat.pid -l ${tor_dir}/${iface}/udp-anti-nat.log -D ${vm_gw}:$divert_port -P 53:10053"

    # DHCP: sink DHCP to server on gateway (DHCP requests can have both zero and non-zero source addresses)
    ${fwcmd} add $((cmd+pidx+5)) fwd ${vm_gw} udp from 0.0.0.0 to 255.255.255.255 67 in via $iface
    ${fwcmd} add $((cmd+pidx+6)) fwd ${vm_gw} udp from ${vm_net} to 255.255.255.255 67 in via $iface
    ${fwcmd} add $((cmd+pidx+7)) allow udp from ${vm_gw} to any 68 out via $iface

    # SSH: allow host-to-VM SSH for local administration
    ${fwcmd} add $((cmd+pidx+8)) allow tcp from ${vm_gw} to ${vm_net} 22 out via $iface keep-state

    pidx=$((pidx+9)) # 9 main rules

    # HS: you can add any additional host-to-VM TCP ports for tor hidden services, or other purposes
    for port in $(unique_port_set_for_iface $iface); do
      ${fwcmd} add $((cmd+pidx)) allow tcp from ${vm_gw} to ${vm_net} ${port} out via $iface keep-state
      pidx=$((pidx+1))
    done

    # DENY: no other communication is allowed on this interface
    ${fwcmd} add $((cmd+step-1)) deny all from any to any via $iface

    # move on
    cmd=$((cmd+step))
  done
}

start_daemon_and_check() {
  local cmd="$1"
  local pid_file="$2"
  ${cmd} >/dev/null 2>&1
  check_process_by_pid_file "${pid_file}" || return 1
}

stop_daemon() {
  local pid_file="$1"
  check_process_by_pid_file "${pid_file}" && kill $(cat ${pid_file}) >/dev/null 2>&1 || return 1
  # TODO need to wait here for this pid, because when we delete the enclosing folder
  #      this daemon might still not have deleted its pid file, and will fail for that reason which isn't nice
  #      However, it looks like there is currently no way for the shell program to wait for the file deletion.
}

stop_daemon_silently() {
  [ -f "$1" ] && kill $(cat $1) >/dev/null 2>&1
}

start_daemons() {
  local dir="${tor_dir}"
  # dhcp daemon
  local cmd="${LOCALBASE}/bin/tiny-dhcp-server -d -U ${vm_to_tor_user}:${vm_to_tor_group} -p ${dir}/dhcp-daemon.pid -l ${dir}/dhcp-daemon.log ${vm_to_tor_ifaces}"
  start_daemon_and_check "${cmd}" "${dir}/dhcp-daemon.pid" ||
    ! echo_err "failed to start dhcp daemon" || return 1
  save_daemon_cmd_file "${cmd}" dhcp-daemon

  # anti-nat daemons
  for iface in ${vm_to_tor_ifaces}; do
    local cmd="$(get_per_vm_option ${iface} dns_anti_nat_daemon_cmd)"
    start_daemon_and_check "${cmd}" ${tor_dir}/${iface}/udp-anti-nat.pid ||
      ! echo_err "failed to start DNS anti-nat daemon for VM ${iface}" || return 1
    save_daemon_cmd_file "${cmd}" "${iface}/udp-anti-nat"
  done
}

stop_daemons() {
  stop_daemon "${tor_dir}/dhcp-daemon.pid" ||
    ! echo_err "failed to stop dhcp daemon" || return 1
  for iface in ${vm_to_tor_ifaces}; do
    stop_daemon "${tor_dir}/${iface}/udp-anti-nat.pid" ||
      ! echo_err "failed to stop DNS anti-nat daemon for VM ${iface}" || return 1
  done
}

stop_daemons_silently() {
  stop_daemon_silently "${tor_dir}/dhcp-daemon.pid"
  for iface in ${vm_to_tor_ifaces}; do
    stop_daemon_silently "${tor_dir}/${iface}/udp-anti-nat.pid"
  done
  return 0
}

start_tor_instance() {
  local iface="$1"
  local dir="$2"
  local gw="$3"
  local cl="$4"
  # create tor data dir
  mkdir_perms "${dir}/data" || ! echo_err "failed to create tor data dir ${dir}/data" || return 1
  # create blank torrc, because it isn't clear how to avoid using the default
  touch ${dir}/torrc && set_file_perms_r ${dir}/torrc
  # prepare command line arguments
  local cmd="${LOCALBASE}/bin/tor"
  cmd="${cmd} -f ${dir}/torrc"
  cmd="${cmd} --PidFile ${dir}/tor.pid"
  cmd="${cmd} --RunAsDaemon 1"
  cmd="${cmd} --DataDirectory ${dir}/data"
  cmd="${cmd} --+Log \"notice file ${dir}/tor.log\""
  cmd="${cmd} --DNSPort ${gw}:10053"
  cmd="${cmd} --TransProxyType ipfw"
  cmd="${cmd} --TransPort ${gw}:9030"
  cmd="${cmd} --SocksPort 0"
  if [ "${vm_to_tor_control_socket}" = "YES" ]; then
    cmd="${cmd} --ControlSocket ${dir}/ctrl"
    cmd="${cmd} --CookieAuthentication 1"
  fi
  # add per-VM tor options
  IFS_OLD="$IFS"
  local IFS="${NL}"
  local opts=$(get_per_vm_option ${iface} "tor_options") opt
  for opt in $opts; do
    opt=$(echo $opt | sed -e 's/^ *//g;s/ *$//g')
    if [ -n "${opt}" ]; then
      cmd="${cmd} --${opt}"
    fi
  done
  IFS="${IFS_OLD}"
  # enable hidden services resolution
  cmd="${cmd} --VirtualAddrNetworkIPv4 10.192.0.0/10"
  cmd="${cmd} --AutomapHostsOnResolve 1"
  # add hidden services that were requested
  local hidx=0
  local hs_fld hs_dir hs_oport hs_iport
  for hs_fld in $(eval "echo \$vm_to_tor_hs_${iface}"); do
    case $hidx in
      0)
        hs_dir=${hs_fld}
        ;;
      1)
        hs_oport=${hs_fld}
        ;;
      2)
        hs_iport=${hs_fld}
        cmd="${cmd} --HiddenServiceDir ${hs_dir}"
        cmd="${cmd} --HiddenServicePort \"${hs_oport} ${cl}:${hs_iport}\""
        ;;
    esac
    [ $hidx -lt 2 ] && hidx=$((hidx+1)) || hidx=0
  done
  # log
  echo "${cmd}" >> ${dir}/tor.cmd
  set_file_perms_r ${dir}/tor.cmd
  # run it
  echo "$(tm) ----- BEGIN OUTPUT (tunnel $iface) -----" >> ${dir}/cmd.log
  run_su "${cmd}" >> ${dir}/cmd.log 2>&1
  echo "$(tm) ----- END OUTPUT (tunnel $iface) -----" >> ${dir}/cmd.log
  set_file_perms_r ${dir}/cmd.log
  # check if it is running
  check_process_by_pid_file "${dir}/tor.pid" || return 1
}

start_tor_instances() {
  for iface in ${vm_to_tor_ifaces}; do
    local vm_id=$(echo $iface | sed -e 's/[a-z]*//')
    local vm_gw="${vm_to_tor_net_pattern}.${vm_id}.1"
    local vm_cl="${vm_to_tor_net_pattern}.${vm_id}.2"

    start_tor_instance "${iface}" "${tor_dir}/$iface" "${vm_gw}" "${vm_cl}" ||
      ! echo_err "failed to start tor instance for VM $iface" || return 1
  done
}

stop_tor_instances() {
  for iface in ${vm_to_tor_ifaces}; do
    ([ -s ${tor_dir}/${iface}/tor.pid ] && kill $(cat ${tor_dir}/${iface}/tor.pid)) ||
       ! echo_err "failed to stop tor instance for VM $iface" || ! stop_tor_instances_silently || return 1
  done
}

stop_tor_instances_silently() {
  for iface in ${vm_to_tor_ifaces}; do
    [ -s ${tor_dir}/${iface}/tor.pid ] && kill $(cat ${tor_dir}/${iface}/tor.pid) >/dev/null 2>&1
  done
  # emergency stop, something went wrong, so keep logs
  return 0
}

stop_all_silently() {
  stop_tor_instances_silently
  destroy_tap_silently
  stop_daemons_silently
}

start_tor() {
  (
    ([ ${vm_to_tor_use_base} = "YES" ] && write_tor_config_section) ||
    ([ ${vm_to_tor_use_base} = "NO" ] && start_tor_instances)
  ) || return 1
}

stop_tor() {
  (
    ([ ${vm_to_tor_use_base} = "YES" ] && delete_tor_config_section) ||
    ([ ${vm_to_tor_use_base} = "NO" ] && stop_tor_instances)
  ) || return 1
}

warn_to_restart_tor_if_running() {
  if [ -r /var/run/tor/tor.pid ] && procstat $(cat /var/run/tor/tor.pid) > /dev/null 2>/dev/null; then
    echo "WARNING vm-to-tor changed tor configuration, you need to restart tor: 'service tor restart'"
  fi
}

write_tor_config_section() {
  # The "right" way of doing this is through Tor control port.
  # However, this requires authentication, and we don't have
  # the password. vm-to-tor is an automated process, and has
  # to always succeed. So we just overwrite that file in the
  # old fashion way. We add our signature to every line, so
  # that we can always delete these lines when we are stopped.
  # See torproject ticket#15649 with the request for this
  # functionality

  local conf_tmp_old="${tor_conf}.vm-to-tor.old.tmp"
  local conf_tmp_new="${tor_conf}.vm-to-tor.new.tmp"

  rm -f $conf_tmp_new $conf_tmp_old &&
  (cat $tor_conf &&
   echo "# [$torrc_signature] BEGIN vm-to-tor section" &&
   (for iface in ${vm_to_tor_ifaces}; do
      local vm_id=$(echo $iface | sed -e 's/[a-z]*//')
      echo "DNSListenAddress   ${vm_to_tor_net_pattern}.${vm_id}.1    # $torrc_signature"
      echo "TransListenAddress ${vm_to_tor_net_pattern}.${vm_id}.1    # $torrc_signature"
    done) &&
   echo "# [$torrc_signature] END vm-to-tor section") > $conf_tmp_new
  if [ $? = 0 -a -f $conf_tmp_new ]; then
    mv $tor_conf $conf_tmp_old &&
    mv $conf_tmp_new $tor_conf &&
    rm -f $conf_tmp_old
    warn_to_restart_tor_if_running
  else
    return 1
  fi
}

delete_tor_config_section() {
  local conf_tmp_old="${tor_conf}.vm-to-tor.old.tmp"
  local conf_tmp_new="${tor_conf}.vm-to-tor.new.tmp"

  rm -f $conf_tmp_new $conf_tmp_old &&
  (cat $tor_conf | grep -v $torrc_signature) > $conf_tmp_new
  if [ $? = 0 -a -f $conf_tmp_new ]; then
    mv $tor_conf $conf_tmp_old &&
    mv $conf_tmp_new $tor_conf &&
    rm -f $conf_tmp_old
    warn_to_restart_tor_if_running
  else
    return 1
  fi
}

delete_ipfw_rules() {
  local cmd=$vm_to_tor_rule_base
  local step=$(calc_rule_step)
  local fwcmd="/sbin/ipfw -q"
  for iface in ${vm_to_tor_ifaces}; do
    ${fwcmd} delete $(jot - $cmd $((cmd+step-1))) 2>/dev/null
    cmd=$((cmd+step))
  done
}

guess_vm_type() {
  local proc_line=$(procstat -c $1 | tail -1)
  local proc_vbox=$(echo "${proc_line}" | grep " VirtualBox ")
  if [ -n "${proc_vbox}" ]; then
    echo "vbox"
    return
  fi
  local proc_bhyve=$(echo "${proc_line}" | grep " bhyve ")
  if [ -n "${proc_bhyve}" ]; then
    echo "bhyve"
    return
  fi
}

guess_process_name() {
  local proc_line=$(procstat -c $1 | tail -1)
  local proc_vbox=$(echo "${proc_line}" | grep " VirtualBox ")
  if [ -n "${proc_vbox}" ]; then
    echo $(echo ${proc_vbox} | sed -E "s/.*--comment (([^ ]| [^-]| -[^-])*)( --.*|[[:space:]]*)$/\1/g")
    return
  fi
  local proc_bhyve=$(echo "${proc_line}" | grep " bhyve ")
  if [ -n "${proc_bhyve}" ]; then
    echo $(echo ${proc_bhyve} | sed -E "s/.* bhyve: (.*)$/\1/g")
    return
  fi
}

count_tunnels() {
  local cnt=0
  local iface
  for iface in ${vm_to_tor_ifaces}; do
    cnt=$((cnt+1))
  done
  echo $cnt
}

print_hs_for_iface() {
  local iface="$1"
  local do_colors="$2"
  local hidx=0
  local hs_fld hs_dir hs_oport hs_iport
  local cnt=0
  for hs_fld in $(eval "echo \$vm_to_tor_hs_${iface}"); do
    case $hidx in
      0)
        hs_dir=${hs_fld}
        ;;
      1)
        hs_oport=${hs_fld}
        ;;
      2)
        hs_iport=${hs_fld}
        # output: no newline in the end
        if [ $cnt -gt 0 ]; then
          echo ""
        fi
        local hs_name
        if [ -r "${hs_dir}/hostname" ]; then
          echo -n "      * hs: $(color_hs_name $(cat ${hs_dir}/hostname) ${do_colors}) (${hs_dir}) ${hs_oport} -> ${hs_iport}"
        else
          echo -n "      * hs: $(color_hs_name ${hs_dir} ${do_colors}) ${hs_oport} -> ${hs_iport}"
        fi
        cnt=$((cnt+1))
        ;;
    esac
    [ $hidx -lt 2 ] && hidx=$((hidx+1)) || hidx=0
  done
}

## internals: colors

RCol='\e[0m'    # Text Reset

# Regular           Bold                Underline           High Intensity      BoldHigh Intens     Background          High Intensity Backgrounds
Bla='\e[0;30m';     BBla='\e[1;30m';    UBla='\e[4;30m';    IBla='\e[0;90m';    BIBla='\e[1;90m';   On_Bla='\e[40m';    On_IBla='\e[0;100m';
Red='\e[0;31m';     BRed='\e[1;31m';    URed='\e[4;31m';    IRed='\e[0;91m';    BIRed='\e[1;91m';   On_Red='\e[41m';    On_IRed='\e[0;101m';
Gre='\e[0;32m';     BGre='\e[1;32m';    UGre='\e[4;32m';    IGre='\e[0;92m';    BIGre='\e[1;92m';   On_Gre='\e[42m';    On_IGre='\e[0;102m';
Yel='\e[0;33m';     BYel='\e[1;33m';    UYel='\e[4;33m';    IYel='\e[0;93m';    BIYel='\e[1;93m';   On_Yel='\e[43m';    On_IYel='\e[0;103m';
Blu='\e[0;34m';     BBlu='\e[1;34m';    UBlu='\e[4;34m';    IBlu='\e[0;94m';    BIBlu='\e[1;94m';   On_Blu='\e[44m';    On_IBlu='\e[0;104m';
Pur='\e[0;35m';     BPur='\e[1;35m';    UPur='\e[4;35m';    IPur='\e[0;95m';    BIPur='\e[1;95m';   On_Pur='\e[45m';    On_IPur='\e[0;105m';
Cya='\e[0;36m';     BCya='\e[1;36m';    UCya='\e[4;36m';    ICya='\e[0;96m';    BICya='\e[1;96m';   On_Cya='\e[46m';    On_ICya='\e[0;106m';
Whi='\e[0;37m';     BWhi='\e[1;37m';    UWhi='\e[4;37m';    IWhi='\e[0;97m';    BIWhi='\e[1;97m';   On_Whi='\e[47m';    On_IWhi='\e[0;107m';

color_os_name() {
  local os_name="$1"
  local do_colors="$2"
  if [ ${vm_to_tor_show_colors} = "YES" -a ${do_colors} = 1 ]; then
    if expr "${os_name}" : "FreeBSD" > /dev/null; then
      echo -e "${Gre}${os_name}${RCol}"
    elif expr "${os_name}" : "Arch" > /dev/null; then
      echo -e "${Cya}${os_name}${RCol}"
    elif expr "${os_name}" : "Windows" > /dev/null; then
      echo -e "${Pur}${os_name}${RCol}"
    elif expr "${os_name}" : "Mint" > /dev/null; then
      echo -e "${Gre}${os_name}${RCol}"
    elif expr "${os_name}" : "Ubuntu" > /dev/null; then
      echo -e "${IRed}${os_name}${RCol}"
    elif expr "${os_name}" : "Fedora" > /dev/null; then
      echo -e "${IBlu}${os_name}${RCol}"
    else
      echo -e "${UBla}${os_name}${RCol}"
    fi
  else
    echo "${os_name}"
  fi
}

color_hs_name() {
  local hs_name="$1"
  local do_colors="$2"
  if [ ${vm_to_tor_show_colors} = "YES" -a ${do_colors} = 1 ]; then
    echo -e "${URed}${hs_name}${RCol}"
  else
    echo "${hs_name}"
  fi
}

## interface: for service manager

vm_to_tor_start() {
  # checks
  is_user_authorized_with_msg "start ${nameb}" || return 1
  are_any_processes_running 1 || return 1
  [ ${vm_to_tor_use_base} = "YES" ] || is_tor_installed || ! echo_err "tor is not installed, ${nameb} can't be started" || return 1
  # start (every procedure below is responsible for printing its error messages if any)
  (
    check_config &&
    check_sysctl &&
    create_dirs &&
    parse_hs &&
    ([ ${vm_to_tor_use_base} = "NO" ] || check_tor_config) &&
    create_tap &&
    add_ipfw_rules &&
    start_daemons &&
    start_tor &&
    echo_out "started with interfaces ${vm_to_tor_ifaces}"
  ) || ! echo_err "failed to start $nameb" || ! stop_all_silently || return 1
}

vm_to_tor_status() {
  parse_hs || return 1
  local cntOk=0
  local cntNo=0
  local msgOk msgNo strOpened
  local ntunnels=$(count_tunnels)
  local do_colors=0
  if [ -t 1 ]; then
    do_colors=1
  fi
  if [ $ntunnels -gt 1 ]; then
    local xnl="${NL}"
    local xbu=" * "
    local xlt=": "
    local xrt=""
  else
    local xnl=" "
    local xbu=""
    local xlt="("
    local xrt=")"
  fi
  for iface in ${vm_to_tor_ifaces}; do
    if ! (ls /dev/ | grep $iface >/dev/null 2>/dev/null); then
      cntNo=$((cntNo+1))
      if [ $cntNo -gt 1 ]; then
        msgNo="${msgNo}"
      fi
      msgNo="${msgNo}${iface}"
    else
      cntOk=$((cntOk+1))
      local descr=""
      # vm pid
      strOpenedPid=$(ifconfig ${iface} | grep -i open | sed -e 's/[a-zA-Z[:space:]]*//g')
      if [ -n "${strOpenedPid}" ]; then
        descr="vm-pid=${strOpenedPid}"
        local vm_type="$(guess_vm_type ${strOpenedPid})"
        if [ -n "${vm_type}" ]; then
          descr="${descr} vm-type=${vm_type}"
        fi
        local processName=$(color_os_name "$(guess_process_name ${strOpenedPid})" ${do_colors})
        if [ -n "${processName}" ]; then
          descr="${descr} vm-name=${processName}"
        fi
      fi
      # tor pid
      if [ ${vm_to_tor_use_base} = "NO" ]; then
        if [ -n "${descr}" ]; then
          descr="${descr} "
        fi
        descr="${descr}tor-pid=$(cat ${tor_dir}/${iface}/tor.pid 2>/dev/null || echo 'n/a')"
      fi
      # print iface status
      if [ $cntOk -gt 1 ]; then
        msgOk="${msgOk}${xnl}"
      fi
      if [ -n "${descr}" ]; then
        msgOk="${msgOk}${xbu}${iface}${xlt}${descr}${xrt}"
      else
        msgOk="${msgOk}${xbu}${iface}"
      fi
      local hs=$(print_hs_for_iface ${iface} ${do_colors})
      if [ -n "${hs}" ]; then
        msgOk="${msgOk}${NL}${hs}"
      fi
    fi
  done
  if [ $cntNo = 0 ]; then
    echo "$nameb is running with tunnels:${xnl}${msgOk}"
  elif [ $cntOk = 0 ]; then
    echo "$nameb is not running (${vm_to_tor_ifaces})"
    return 1
  else
    echo "$nameb isn't properly setup: $cntNo of $((cntOk+cntNo)) tunnels doesn't exist (${vm_to_tor_ifaces})"
    return 1
  fi
}

vm_to_tor_stop() {
  # checks
  is_user_authorized_with_msg "stop $nameb" || return 1
  ! are_any_processes_running 0 || ! echo_err "not running" || return 1
  # stop
  local fail=0
  parse_hs || fail=$((fail+1))
  delete_ipfw_rules || fail=$((fail+1))
  stop_daemons || fail=$((fail+1))
  destroy_tap || fail=$((fail+1))
  stop_tor || fail=$((fail+1))
  [ $fail -ne 0 ] || delete_dir || fail=$((fail+1))
  [ $fail -eq 0 ] || ! echo_err "failed to stop $nameb: $fail problems" || return 1
  [ $fail -ne 0 ] || echo_out "stopped"
}

command="/usr/bin/true"

run_rc_command "$1"
