Client component: callhome.sh
#!/bin/sh -e
export PATH="/usr/local/bin:/usr/bin:/bin"
config="${HOME}/.ssh/callhome.config"
key="${HOME}/.ssh/callhome.key"
lock="${HOME}/.ssh/callhome.pid"
local_port="22222"
timeout="60"
ssh_config() {
cat <<EOCF
# No Agent or X11 forwards needed
ForwardAgent no
ForwardX11 no
ForwardX11Trusted no
# Timeout after a sensible amount of time
# Hosts with volatile uplinks need this to know when to relink
ConnectTimeout 30
ServerAliveInterval 20
ServerAliveCountMax 3
# This profile is used to transmit the newly generated key and port config
Host callhome-setup
HostName alba.cyberleo.net
Port 22
User cyberleo
# This profile is used to connect
Host callhome
HostName alba.cyberleo.net
Port 22
User callhome
IdentityFile ${key}
# Do not use SSH-Agent identities
IdentitiesOnly yes
# Do not prompt for passwords
PreferredAuthentications publickey
EOCF
}
setup_config() {
mkdir -p "$(dirname "${config}")"
ssh_config > "${config}"
}
callhome_ssh() {
ssh -F "${config}" "${@}"
}
setup_key() {
echo "Generating remote-access SSH key..." >&2
mkdir -p "$(dirname "${key}")"
ssh-keygen -q -b 3072 -P "" -f "${key}"
echo "" >&2
echo "Transmitting public key; please log in." >&2
echo "" >&2
callhome_ssh callhome-setup "/bin/mkdir -p ~/.ssh && /bin/cat >> ~/.ssh/pending_authorized_keys" < "${key}.pub" || {
echo "Key transmission failed. Check for any errors and correct them, then try again." >&2
echo "----8<----" >&2
cat "${key}.pub"
echo "----8<----" >&2
return 1
}
echo "Key transmitted. You can configure this key on the server now." >&2
return 0
}
regexpize_allowed_symbols() {
echo "${*}" | sed -Ee '
s/^[[:space:]]*/^\\(/;
s/[[:space:]]*$/\\):/;
s/[[:space:]]+/\\|/g;
s/$/ /
'
}
remote_config() {
allowed_symbols="ident port"
allowed_symbols_regexp="$(regexpize_allowed_symbols "${allowed_symbols}")"
while ! callhome_ssh callhome config
do
sleep 1
done | while read line
do
echo "${line}" | grep "${allowed_symbols_regexp}" || echo "Ignoring invalid config line: ${line}" >&2
done | sed -e "s/: /='/; s/$/'/"
}
remote_kill() {
callhome_ssh callhome kill
}
remote_loop() {
callhome_ssh -fn -o ExitOnForwardFailure=yes -L${local_port}:127.0.0.1:${port} -R${port}:127.0.0.1:22 callhome loop
}
case "$(uname -s)" in
FreeBSD)
# FreeBSD version
ssh_pid() {
sockstat -P tcp -lp "${local_port}" | tail -n +2 | while read user command pid fd proto local foreign
do
[ "${user}" = "${USER}" -a "${command}" = "ssh" ] || continue
echo "${pid}"
done | sort -u
}
;;
Linux)
# Linux version
ssh_pid() {
netstat --numeric-hosts --numeric-ports -eplt 2>&- | awk 'BEGIN{out=""}{if(out){print}}/^Proto/{out="yes"}' | while read proto recvq sendq local foreign state user inode pidcmd
do
pid="${pidcmd%%/*}"
command="${pidcmd##*/}"
[ "${user}" = "${USER}" -a "${command}" = "ssh" ] || continue
echo "${pid}"
done | sort -u
}
;;
Darwin)
# Darwin version
ssh_pid() {
me=$(id -u)
/usr/sbin/lsof -i -l -n -P | grep '(LISTEN)' | grep " ${me} " | grep 'ssh' | grep ":${local_port} " | while read command pid user fd type device size node name state
do
[ "${user}" = "${me}" -a "${command}" = "ssh" ] || continue
echo "${pid}"
done | sort -u
}
;;
*) echo "Unsupported OStype $(uname -s)" >&2; exit 1 ;;
esac
ssh_up() {
[ "$(ssh_pid)" ]
}
local_kill() {
pids="$(ssh_pid)"
[ -z "${pids}" ] || kill -TERM ${pids}
}
checkup() {
nc -w 20 127.0.0.1 "${local_port}" < /dev/null | grep -q ^SSH
}
terminate_cleanup() {
trap - EXIT HUP INT TERM KILL
local_kill
}
trap "terminate_cleanup" EXIT HUP INT TERM KILL
[ -f "${config}" -a -s "${config}" ] || setup_config
[ -f "${key}" ] || { setup_key; exit; }
eval $(remote_config)
[ "${ident}" -a "${port}" ] || {
echo "Configuration download failed" >&2
exit 1
}
printf "Calling home: I am %s (port %s)\n" "${ident}" "${port}"
while true
do
# Avoid looping more than once per second during quick failures
[ "$(date +%s)" -ne "${lastloop:-0}" ] || sleep 1
lastloop="$(date +%s)"
# Busy loop as long as everything is working
# Check that ssh is running every 20 seconds
# Poke the port every 60 seconds
while checkup
do
for iter in 1 2 3
do
ssh_up || break
sleep 20
done
done
# If it's not working, try and make it work
# Kill any local processes
local_kill || true
# Attempt to establish connection
if ! remote_loop
then
# If it doesn't work, try to kill remote and fall through for another loop
remote_kill || true
fi
done
This bit is FreeBSD-specific; but you can port it in much the same way as callhome.sh (see ssh_pid() ). Server component: force_cmd
#!/bin/sh -e
# Read in config
ident="${1}"
port="${2}"
kill_current_mapping_for_port() {
local port="${1}"
sockstat -P tcp -lp "${port}" | tail -n +2 | while read user command pid fd proto local foreign
do
[ "${user}" = "${USER}" -a "${command}" = "sshd" ] || continue
kill -TERM "${pid}"
echo "Killed ${pid}: ${command}" >&2
done
}
logit() {
echo "$(date): ${*}" >> "${HOME}/callhome.log"
}
logit "${ident}:${port} requested '${SSH_ORIGINAL_COMMAND}' from ${SSH_CLIENT}"
case "$SSH_ORIGINAL_COMMAND" in
config)
echo "ident: ${ident}"
echo "port: ${port}"
;;
loop)
echo "Keepalive loop started; break to terminate."
exec sh -ec 'while sleep 300; do printf "."; done'" # ${ident} ${port}"
;;
kill)
kill_current_mapping_for_port "${port}"
;;
ping)
echo "pong"
;;
*)
echo "Rejected"
exit 1
;;
esac
Place name and port number into force command to configure the client. Give each client its own distinct name and port number. Sample authorized_keys entry:
command="/usr/home/callhome/.ssh/force_cmd phoenix 22004" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDJDg49aa0ObN2Xmi2CxkQO9bKNu6SR+n0zzbnl+6CayqIdsBhm56RVnOPr3bAAMj+36ygdjqp2CEeGS4g0S5B9FcGZkrWxyy9t/ixhOWo2qB+Yx55M0C8uB0ie1aN7UV+k8nBy8ahxQ7NVQ6uPdJ8F/XlhuYPMkJtuLGDv/GjDWhAvv11J2Ff9p3Wl1AIVkyvvIil9BaBBg+XmnwQAAdKf5rb7+ouG4tnQ3Mo2OEkj61B10vvDQNAD1VqGAm+xskjNyoM4ETS3ehA/j4yPaF+vr6fcLL2UhA2MJm3Y8QP3xq1JzTEnmBrJM0pxBsy0NvsKchSyyH3GmgzaB/rxn6uAnQn87nZXoL1V7wghr0HylA/Dj5xq+34zcnv8osmz7B63xFujRxZuxSFjcHr4k+PUx4vjhvIDT9oXeckif71QfTulsBxnMyL2W7alY2eku00ATLOhr+QBIeURzrCSYSXgEm5CRFJ/9T7EDYJPwx2MeD8NRKgdjTiWbLaPYylLLD0= cyberleo@phoenix-lan
