#! /bin/bash # Executed on MASTER WG_PORT_MIN=32768 WG_PORT_MAX=65535 COLOR="always" source /sf/bin/funcs.sh source /sf/bin/funcs_redis.sh [[ ! -d "/config/db" ]] && ERREXIT 255 "Not found: /config/db" [[ ! -d "/config/db/wg" ]] && mkdir -p "/config/db/wg" [[ -z $SF_FQDN ]] && SF_FQDN="SF_FQDN-NOT-SET.hack.segfault.net" echo -en "Content-Type: text/plain\r\n\r\n" # BAIL # STDOUT goes to user. # STDERR is logged. BAIL() { echo -e "$1" [[ -n $2 ]] && echo -e >&2 "[${CB}${LID:-$REMOTE_ADDR}${CN}] ${CR}$2${CN}$3" exit 255 } Sanitize() { REQUEST_BODY="${REQUEST_BODY//[^[:alnum:]_+=/&]}" REQUEST_URI="${REQUEST_URI//[^[:alnum:]_+=\/]}" [[ "${#REQUEST_BODY}" -gt 512 ]] && BAIL "To long" "To Long" ": REQUEST_BODY(${#REQUEST_BODY})=${REQUEST_BODY:0:32}..." [[ "${#REQUEST_URI}" -gt 512 ]] && BAIL "To long!" "ATTACK" ": REQUEST_URI(${#REQUEST_URI})=${REQUEST_URI:0:32}..." } GetFormVars() { local ifs ifs=$IFS IFS=\& read -r -a arr <<< "${REQUEST_BODY}" local i local str while [[ $i -lt ${#arr[@]} ]]; do str="${arr[$i]}" ((i++)) key=${str%%=*} [[ ${#key} -le 0 ]] && BAIL "Bad Request" "ERROR: " "Body contains bad variable: '$str'" val=${str#*=} [[ ${key} == "nocolor" ]] && unset COLOR # [[ ${key} == "verbose" ]] && IS_VERBOSE=1 [[ ${key} == "port" ]] && { val=$((${val//[^0-9]})); [[ $val -ge 32768 && $val -le 65535 ]] && R_PORT="$val"; } [[ ${key} == "portsecret" ]] && R_PORTSECRET="${val//[^[:alnum:]]}" [[ ${key} == "exit_private" ]] && R_WT_PRIVATE="${val//[^[:alnum:]+\/]}=" [[ ${key} == "exit_public" ]] && R_WT_PUBLIC="${val//[^[:alnum:]+\/]}=" [[ ${key} == "private" ]] && R_WG_PRIVATE="${val//[^[:alnum:]+\/]}=" [[ ${key} == "name" ]] && { val="${val//[^[:alnum:]]}"; R_WT_NAME="${val:0:13}"; } done IFS=$ifs } # Load PID of WireGuard container load_config() { source /dev/shm/config.txt && return BAIL "${R}ERROR${N}: Not ready. SF is still booting up..." "Failed to load: " "/dev/shm/config.txt" } GenSecret() { local len len=16 str=$(head -c $((len*2)) } \\ --public ${WG_PUBLIC} \\ --endpoint ${SF_FQDN}:${WG_PORT}${N} To connect with ${M}WireGuard${N} use this configuration on the Exit Node:${F} [Interface] PrivateKey = ${WT_PRIVATE:-} Address = 192.168.0.2/32 Address = fd::2/128 [Peer] PublicKey = ${WG_PUBLIC} AllowedIPs = 0.0.0.0/0, ::/0 EndPoint = ${SF_FQDN}:${WG_PORT} PersistentKeepalive = 25${N} " } cmd_net_list() { local fn local str local name local upstr local ex_wg_public local privstr load_port echo -en "\ Port : ${Y}$WG_PORT${N} Portsecret: ${Y}$PORTSECRET${N} " ifaces="$(nsenter.u1000 --setuid 0 --setgid 0 -t "$PID" -n wg show interfaces) " # List all configured names echo -e "\ Name (${G}active${N}) | | Private | Public ---------------+-----------+----------------------------------------------+---------------------------------------------" for fn in "${LID_WGDIR}/wg-"*; do [[ ! -f "$fn" ]] && break str=$(basename "$fn") name="${str#*-}" [[ $fn != "${LID_WGDIR}/wg-${name}" ]] && continue # BAD # load_wt "$name" load_wg "$name" str="${name} " unset upstr [[ $ifaces == *"wg${name} "* ]] && { upstr="${G}" ex_wg_public="$WG_PUBLIC" } privstr="${WT_PRIVATE:- }" echo -e "${upstr}${str:0:14}${N} | Server | ${F}${WG_PRIVATE:0:10}##################################${N} | ${WG_PUBLIC}\n | Exit Node | ${privstr:0:44} | ${F}$WT_PUBLIC${N}" done echo "" [[ -z $WT_PUBLIC ]] && { echo -e "${Y}WARNING${N}: No Exit Nodes configured. Use ${C}curl rpc/net/up${N} first." exit } [[ -z $ex_wg_public ]] && { echo -e "${Y}WARNING${N}: No interface is up. Use ${C}curl rpc/net/up -d name=${N} first." exit } net_print_example exit } net_init_print_info() { echo -e "\ Port : ${Y}$WG_PORT${N} Portsecret: ${Y}$PORTSECRET${N} Private : ${Y}$WG_PRIVATE${N} Name : ${Y}$WT_NAME${N} " } write_portfile() { echo -n "\ WG_PORT=\"${WG_PORT}\" ASSIGNED_LID=\"${LID}\" PORTSECRET=\"${PORTSECRET}\" " >"${LID_WGDIR}/port" || BAIL "Failed to store WireGuard Port." echo "WG_PORT=${WG_PORT}" >"/config/db/wg/sec2port-${PORTSECRET}" # Link to LID: ln -sf "../user/lg-${LID}/wg/port" "/config/db/wg/port-${WG_PORT}" } # [WT_NAME] write_wgfile() { local name name="$1" echo -n "\ WG_PRIVATE=\"${WG_PRIVATE}\" WG_PUBLIC=\"${WG_PUBLIC}\" WT_PRIVATE=\"${WT_PRIVATE}\" WT_PUBLIC=\"${WT_PUBLIC}\" WG_PORT=\"${WG_PORT}\" " >"${LID_WGDIR}/wg-${name}" || BAIL "Failed to store WG information." ln -sf "../user/lg-${LID}/wg/wg-${name}" "/config/db/wg/wg-${name}" } # Assign port to _this_ LID # [portsecret] cmd_net_init_move() { local psec psec="$1" source "/config/db/wg/sec2port-${psec}" 2>/dev/null || BAIL "Portsecret ${psec} is not known." [[ "${ASSIGNED_LID}" == "$LID" ]] && return # Already assigned to this LID ln -sf "../user/lg-${LID}/wg/port" "/config/db/wg/port-${WG_PORT}" PORTSECRET="$1" write_portfile } # rpc/net/init # Assign/Retrieve WireGuard port for this LID net_init() { local n local err local arr arr=($(echo "${LID_WGDIR}/wg-"*)) [[ ${#arr[@]} -gt 16 ]] && BAIL "${R}ERROR${N}: To many Peers. You must delete some first. Use ${C}curl rpc/net/list${N} to see them all. Use ${C}curl rpc/net/del -d name=${N} to delete . Use ${C}curl rpc/net/del -d name=all${N} to delete them all." "${R}PEERS-MAX${N} " "Limit: ${#arr[@]}" [[ -n ${R_PORTSECRET} ]] && cmd_net_init_move "${R_PORTSECRET}" if source "${LID_WGDIR}/port" 2>/dev/null; then # HERE: Port already assigned to this LID, [[ -n ${R_PORT} ]] && [[ $R_PORT -ne $WG_PORT ]] && echo -e "${Y}WARNING:${N} Ignoring request for Port ${R_PORT}. Port already set to ${WG_PORT}." else # HERE: No Port yet assigned to this LID. # Allow user to pick a port. if [[ -n ${R_PORT} ]]; then [[ -e "/config/db/wg/port-${R_PORT}" ]] && BAIL "\ Port ${R_PORT} is already in use. You can assign it to this server like so:\ ${C}curl rpc/net/init -d portsecret=${N}" WG_PORT=${R_PORT} else # Assign random port 32768...65535 n=0 while :; do WG_PORT="$((WG_PORT_MIN + RANDOM % (WG_PORT_MAX - WG_PORT_MIN + 1)))" [[ ! -e "/config/db/wg/port-${WG_PORT}" ]] && break ((n++)) [[ $n -gt 5 ]] && BAIL "Failed to find free WireGuard Port." done fi fi [[ -z $PORTSECRET ]] && { PORTSECRET="${WG_PORT}_$(GenSecret)" write_portfile } [[ -e "/config/db/wg/wg-${R_WT_NAME}" ]] && { echo -e "${R}ERROR${N}: '$R_WT_NAME' already exists. Delete it first with ${C}curl rpc/net/del -d name=${R_WT_NAME}${N}" # load_wt "${R_WT_NAME}" # load_wg "${R_WT_NAME}" # WT_NAME=${R_WT_NAME} # net_init_print_info # net_print_example exit } # We do not need the peer's private key but it is more convenient # to the user to show him one complete ./wiretap command line. if [[ -z $R_WT_PRIVATE ]]; then if [[ -z $R_WT_PUBLIC ]]; then # No PRIVATE and No PUBLIC. User wants us # to generate PUBLIC key (and for this we generate private) WT_PRIVATE=$(wg genkey) fi # WT_PRIVATE can be empty (The Exit Node's private does not need to be known) else WT_PRIVATE="$R_WT_PRIVATE" fi [[ -n $R_WT_PUBLIC ]] && WT_PUBLIC="$R_WT_PUBLIC" || WT_PUBLIC=$(echo "$WT_PRIVATE" | wg pubkey) [[ -n $R_WT_NAME ]] && WT_NAME="${R_WT_NAME}" || { val="${WT_PUBLIC//[^[:alnum:]]}"; WT_NAME="${val:0:4}"; } # write_wtfile "${WT_NAME}" # Generate server's private unless it's provided by user [[ -n $R_WG_PRIVATE ]] && WG_PRIVATE="$R_WG_PRIVATE" || WG_PRIVATE=$(wg genkey) WG_PUBLIC=$(echo "$WG_PRIVATE" | wg pubkey) write_wgfile "${WT_NAME}" } cmd_net_init() { net_init net_init_print_info # net_print_example echo -e "Use ${C}curl rpc/net/up -d name=${WT_NAME}${N} to connect an Exit Node." exit } load_port() { source "${LID_WGDIR}/port" 2>/dev/null || BAIL "${R}ERROR${N}: No port found. Use ${C}curl rpc/net/init${N} first." } # load_wt() # { # source "${LID_WGDIR}/wt-${1}" || BAIL "Not found: ${1}. Try ${C}curl rpc/net/init -d name=${1}${N}" "ERROR: " "Not found: wg/wt-${1}" # } load_wg() { source "${LID_WGDIR}/wg-${1}" || BAIL "Not found." "ERROR: " "Not found: wg/wg-${1}" } xrm() { local fn local err err=0 for fn in "$@"; do [[ ! -f "$fn" ]] && { err=255; continue; } rm -f "$fn" done return $err } net_down() { local name name="$1" # Shut down WG interface if [[ -z $name || $name == "all" ]]; then nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip link delete group 31337 2>/dev/null || return else # Return early if device did not exist. nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip link delete "wg${name}" 2>/dev/null || return fi # Restore default routing nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip route add default via "${SF_NET_LG_ROUTER_IP}" || { echo "Oops. Could not set default route."; return; } echo -e "${Y}WARNING${N}: All traffic exits via the DEFAULT ROUTE now." } cmd_net_del() { load_port local fn [[ -z $R_WT_NAME ]] && BAIL "Use ${C}-d name=${N}" net_down "$R_WT_NAME" if [[ "$R_WT_NAME" == "all" ]]; then # Delete all private keys for fn in "${LID_WGDIR}/wg-"*; do [[ ! -f "$fn" ]] && break str=$(basename "$fn") name="${str#*-}" [[ $fn != "${LID_WGDIR}/wg-${name}" ]] && continue # BAD # Delete all links # xrm "/config/db/wg/wg-${name}" "/config/db/wg/wt-${name}" # xrm "${LID_WGDIR}/wg-${name}" "${LID_WGDIR}/wt-${name}" xrm "/config/db/wg/wg-${name}" "${LID_WGDIR}/wg-${name}" done echo -en "All private keys deleted." exit fi [[ ! -f "${LID_WGDIR}/wg-${R_WT_NAME}" ]] && BAIL "${R}Not found${N}: ${R_WT_NAME}" xrm "/config/db/wg/wg-${R_WT_NAME}" "${LID_WGDIR}/wg-${R_WT_NAME}" echo -en "\ ${G}Private key deleted${N} ($R_WT_NAME). " exit } cmd_net() { load_port cmd_net_print_info exit } cmd_net_show() { local str local dev dev="all" [[ -n $R_WT_NAME ]] && dev="wg${R_WT_NAME}" unset IFS if [[ -z $COLOR ]]; then str=$(nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n wg show "${dev}") else # Use 'script' to force color output str=$(script -q -c "nsenter.u1000 --setuid 0 --setgid 0 -t \"${PID}\" -n wg show \"${dev}\"" /dev/null ${N} to connect to an Exit Node." exit } cmd_net_down() { local dev net_down "${R_WT_NAME}" exit } cmd_net_help() { echo -en "\ Create Exit Node : ${C}curl rpc/net/init [-d name=] [-d private=] [-d exit_public=] [-d exit_private=]${N} Delete Exit Node : ${C}curl rpc/net/del -d name=${N} List Exit Nodes : ${C}curl rpc/net/list${N} Show connections : ${C}curl rpc/net/show${N} Move port to a different Root Server: ${C}curl rpc/net/init -d portsecret=${N} Connect Exit Node: ${C}curl rpc/net/up -d name=${N} (and any parameters from rpc/net/init) Disconnect Exit Node: ${C}curl rpc/net/down [-d name=]${N} " exit } CheckGoodKey() { local key local opt key=$1 opt=$2 [[ -z $key ]] && return [[ ${#key} -eq 44 ]] && return BAIL "${R}ERROR${N}: Bad Key for ${opt}=" } # shellcheck disable=2188 # unrecognized redirection (haha. shellsheck you suck) 0<&- # Close STDIN Sanitize GetFormVars [[ -n $COLOR ]] && { # COLOR is set (to 'always') Y=$CDY C=$CDC R=$CR G=$CDG M=$CDM N=$CN F=$CF } CheckGoodKey "$R_WG_PRIVATE" "--private" CheckGoodKey "$R_WT_PRIVATE" "--exit_private" CheckGoodKey "$R_WT_PUBLIC" "--exit_public" [[ -n $R_WT_PRIVATE ]] && [[ -n $R_WT_PUBLIC ]] && BAIL "${R}ERROR${N}: Set either PRIVATE or PUBLIC but not both." # Split into arguments _IFS=$IFS IFS=/ read -r -a ARGS <<< "${REQUEST_URI:1}" # Ignore first '/'. Split into arguements. IFS=$_IFS [[ "${FCGI_CMD}" == "dmesg" ]] && { # dmesg --color=always -f kern --level err,warn -e | tail -n100 dmesg --color="${COLOR:-never}" -f kern --level err -e | tail -n20 exit } [[ -n $SF_DEBUG ]] && [[ "${FCGI_CMD}" == "env" ]] && { env; exit; } # /net/init # INPUT : # OUTPUT: WG-Config & WT-Line # # /net/up # - Do everything fromo INIT. # - # /net -> Show port assignment # /net/init -> Assigned port to this LID or create new port. # /net/up -> Create WireGuard interface # /net/show -> Show WireGuard peers # /net/down # /net/del # /net/list [[ "${FCGI_CMD}" == "net" ]] && { # Retrieve (LID CID PID) arr=($(redr GET "ip:${REMOTE_ADDR}")) || BAIL "Bad Value" "Bad Value: " "ret=$?, ${#arr[@]}" [[ ${#arr[@]} -ne 3 ]] && BAIL "Value != 3" "Value != 3: " "${#arr[@]}" LID="${arr[0]}" LID_WGDIR="/config/db/user/lg-${LID}/wg" [[ ! -d "${LID_WGDIR}" ]] && mkdir "${LID_WGDIR}" # CID="${arr[1]}" PID="${arr[2]}" DEBUGF "LID=$LID PID=$PID" # Show current port configuration [[ ${ARGS[0]} == 'net' && ${ARGS[1]} == 'show' ]] && cmd_net_show # Initialize or set port [[ ${ARGS[1]} == 'init' ]] && cmd_net_init [[ ${ARGS[1]} == 'del' ]] && cmd_net_del [[ ${ARGS[1]} == 'down' ]] && cmd_net_down [[ ${ARGS[1]} == 'help' ]] && cmd_net_help [[ ${ARGS[1]} == 'list' ]] && cmd_net_list # NOT 'up' -> EXIT [[ ${ARGS[1]} != 'up' ]] && { echo -e "${R}ERROR${N}: Unknown command."; cmd_net_help; } WT_NAME="$R_WT_NAME" [[ -z $WT_NAME ]] || [[ ! -f "${LID_WGDIR}/wg-${WT_NAME}" ]] && { # R_WT_NAME not supplied _or_ R_WT_NAME does not exist => Create new one. net_init net_init_print_info } load_port # load_wt "${WT_NAME}" load_wg "${WT_NAME}" dev="wg${WT_NAME}" load_config # Delete interface in WG namespace (should never happen): nsenter -t "${WG_PID}" -n ip link del "${dev}" 2>/dev/null # Delete all interface. Only allow ONE WG interface at a time. # WireGuard only supports 1 private key per port number. Sharing the same # private key among Exit Nodes wont work either as WG enforces a strict # Routing Policy where no two WG interfaces can route 0.0.0.0/0. # The only way around is to use different ports (and if we go down this route # then it would be easier to implement WireGuard Port Multiplexer simiar to # https://github.com/apernet/mwgp but use IPTABLES (NFQUEUE) instaed to make it # _far_ more efficient.) nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip link delete group 31337 2>/dev/null err=$(nsenter -t "${WG_PID}" -n ip link add "${dev}" type wireguard 2>&1) || BAIL "${R}ERROR${N}: Failed: ip link add $dev (${err:0:32})." "Failed $dev" ": $err" nsenter -t "${WG_PID}" -n ip link set "${dev}" group 31337 || BAIL "${R}ERROR${N}: ip link set FAILED." echo "$WG_PRIVATE" >/dev/shm/private.$$ err=$(nsenter -t "${WG_PID}" -n wg set "${dev}" listen-port "${WG_PORT}" private-key "/dev/shm/private.$$" peer "${WT_PUBLIC}" allowed-ips 0.0.0.0/0 2>&1) || BAIL "${R}ERROR${N}: Failed: wg set (${err:0:128})" rm -f /dev/shm/private.$$ # Move Interface to user's container: err=$(nsenter -t "${WG_PID}" -n ip link set "${dev}" netns "${PID}" 2>&1) || BAIL "${R}ERROR${N}: Failed to move $dev." "Failed $dev netns $PID" ": $err" # Configure interface after moving nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip -4 address add 192.168.0.2/32 dev "${dev}" err=$(nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip -6 address add fd::2/128 dev "${dev}" 2>&1) || echo >&2 "${CR}ERROR${CN}: ip -6: $err" nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip link set mtu 1420 up dev "${dev}" # Add static routes for RPC # nsenter -t "${PID}" -n ip route add "${SF_PC_IP}/32" dev eth0 # NOT NEEDED: RPC is on same network nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip route add "${SF_TOR_IP}" via "${SF_NET_LG_ROUTER_IP}" 2>/dev/null nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip route add "${SF_NET_ONION}" via "${SF_NET_LG_ROUTER_IP}" 2>/dev/null nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip route add "${SF_DNS}" via "${SF_NET_LG_ROUTER_IP}" 2>/dev/null nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip route del default 2>/dev/null nsenter.u1000 --setuid 0 --setgid 0 -t "${PID}" -n ip route add default dev "${dev}" echo -e "${G}SUCCESS${N}" net_print_example echo "---" echo -e "Use ${C}curl rpc/net/down -d name=${WT_NAME}${N} to disconnect." echo -e "Use ${C}curl rpc/net/del -d name=${WT_NAME}${N} to delete the keys." echo -e "Use ${Y}curl rpc/net/show${N} to check when the Exit Node '${WT_NAME}' has connected." exit }