segfault/master/cgi-bin/rpc
SkyperTHC bfee848525
rc1
2023-02-21 06:49:57 +00:00

597 lines
17 KiB
Bash
Executable File

#! /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-MSG> <STDERR-MSG> <INFO MSG>
# 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)) </dev/urandom | base64 -w0)
str=${str//[^[:alnum:]]}
str=${str:0:$len}
echo $str
}
net_print_example()
{
echo -en "\
To connect with ${M}Wiretap${N} use this command on the Exit Node:
${C}wiretap serve --private ${WT_PRIVATE:-<PrivateKey>} \\
--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:-<PrivateKey>}
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=<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=<NAME>${N} to delete <NAME>.
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=<SECRET>${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=<NAME|all>${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 </dev/null)
fi
[[ -n $str ]] && {
echo "$str"
[[ "$str" != *"latest handshake"* ]] && {
echo -e "\n${Y}WARNING${N}: EXIT NODE NOT CONNECTED.\nDid you forget to start Wiretap/WireGuard on the Exit Node?"
}
exit
}
echo -e "\
No interface connected.
Use ${C}curl rpc/net/list${N} to list all Exit Nodes.
Use ${C}curl rpc/net/up -d name=<NAME>${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=<NAME>] [-d private=<PrivateKey>]
[-d exit_public=<PublicKey>]
[-d exit_private=<PrivateKey>]${N}
Delete Exit Node : ${C}curl rpc/net/del -d name=<NAME|all>${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=<portsecret>${N}
Connect Exit Node:
${C}curl rpc/net/up -d name=<NAME>${N}
(and any parameters from rpc/net/init)
Disconnect Exit Node:
${C}curl rpc/net/down [-d name=<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 : <port|portsecret> <WT PRIVATE> <Name>
# OUTPUT: WG-Config & WT-Line
#
# /net/up <Name>
# - 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
}