mirror of https://github.com/wolfSSL/wolfssh.git
echoserver: limit direct-tcpip destinations
With -F a peer's direct-tcpip open is set up only for a loopback destination. The new -L option replaces that default with a list of allowed host or host:port entries. Any other open is refused, which the library reports as administratively prohibited. - Check the destination in the WOLFSSH_FWD_LOCAL_SETUP action - Document -L in the usage text and README.md - openssh-interop.test checks a non-loopback destination and one left out of -L are refused, and that host and host:port entries connect Issue: F-11935pull/1338/merge
parent
ecb81b5b7f
commit
e7b7e8ccfe
|
|
@ -324,7 +324,9 @@ The source for portfwd provides an example on how to set up and use the
|
|||
port forwarding support in wolfSSH.
|
||||
|
||||
The echoserver will handle local and remote port forwarding when started
|
||||
with `-F`; without it every forwarding request is refused. To connect with
|
||||
with `-F`; without it every forwarding request is refused. Local forwards
|
||||
may connect only to loopback destinations unless `-L` names the allowed
|
||||
`host` or `host:port` entries, comma separated. To connect with
|
||||
the ssh tool, using one of the following command lines. You can run either of
|
||||
the ssh command lines from anywhere:
|
||||
|
||||
|
|
|
|||
|
|
@ -573,6 +573,101 @@ static WS_SOCKET_T connect_addr(const char* name, word16 port)
|
|||
}
|
||||
|
||||
|
||||
/* The -L arguments, each a comma separated list of host or host:port. */
|
||||
#define FWD_ALLOW_ARGS_MAX 8
|
||||
static const char* fwdAllowArgs[FWD_ALLOW_ARGS_MAX];
|
||||
static int fwdAllowArgsSz = 0;
|
||||
|
||||
|
||||
static int FwdIsLoopback(const char* host)
|
||||
{
|
||||
static const byte loopback6[16] =
|
||||
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
|
||||
struct in_addr addr4;
|
||||
struct in6_addr addr6;
|
||||
|
||||
if (WSTRLEN(host) == sizeof("localhost") - 1
|
||||
&& WSTRNCASECMP(host, "localhost", sizeof("localhost") - 1) == 0)
|
||||
return 1;
|
||||
if (inet_pton(AF_INET, host, &addr4) == 1)
|
||||
return (ntohl(addr4.s_addr) >> 24) == 127;
|
||||
if (inet_pton(AF_INET6, host, &addr6) == 1)
|
||||
return WMEMCMP(&addr6, loopback6, sizeof(loopback6)) == 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Matches one allow-list entry, host or host:port, of entrySz bytes. An
|
||||
* entry with more than one colon is an IPv6 address without a port. */
|
||||
static int FwdEntryMatches(const char* entry, word32 entrySz,
|
||||
const char* host, word32 port)
|
||||
{
|
||||
const char* colon = NULL;
|
||||
word32 hostSz = entrySz;
|
||||
word32 i;
|
||||
|
||||
for (i = 0; i < entrySz; i++) {
|
||||
if (entry[i] == ':') {
|
||||
if (colon != NULL) {
|
||||
colon = NULL;
|
||||
break;
|
||||
}
|
||||
colon = entry + i;
|
||||
}
|
||||
}
|
||||
|
||||
if (colon != NULL) {
|
||||
word32 entryPort = 0;
|
||||
const char* c;
|
||||
|
||||
hostSz = (word32)(colon - entry);
|
||||
if (colon + 1 == entry + entrySz)
|
||||
return 0;
|
||||
for (c = colon + 1; c < entry + entrySz; c++) {
|
||||
if (*c < '0' || *c > '9' || entryPort > 65535)
|
||||
return 0;
|
||||
entryPort = entryPort * 10 + (word32)(*c - '0');
|
||||
}
|
||||
if (entryPort != port)
|
||||
return 0;
|
||||
}
|
||||
|
||||
return hostSz > 0 && WSTRLEN(host) == hostSz
|
||||
&& WSTRNCASECMP(host, entry, hostSz) == 0;
|
||||
}
|
||||
|
||||
|
||||
/* Whether the peer may open a direct-tcpip channel to host:port: a loopback
|
||||
* destination by default, or only the -L entries when there are any. */
|
||||
static int FwdDestAllowed(const char* host, word32 port)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (host == NULL)
|
||||
return 0;
|
||||
if (fwdAllowArgsSz == 0)
|
||||
return FwdIsLoopback(host);
|
||||
|
||||
for (i = 0; i < fwdAllowArgsSz; i++) {
|
||||
const char* entry = fwdAllowArgs[i];
|
||||
|
||||
while (*entry != '\0') {
|
||||
const char* comma = WSTRCHR(entry, ',');
|
||||
word32 entrySz = (comma != NULL) ? (word32)(comma - entry)
|
||||
: (word32)WSTRLEN(entry);
|
||||
|
||||
if (FwdEntryMatches(entry, entrySz, host, port))
|
||||
return 1;
|
||||
entry += entrySz;
|
||||
if (*entry == ',')
|
||||
entry++;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx,
|
||||
const char* name, word32 port)
|
||||
{
|
||||
|
|
@ -581,10 +676,27 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx,
|
|||
int ret = 0;
|
||||
|
||||
if (action == WOLFSSH_FWD_LOCAL_SETUP) {
|
||||
fwdCbCtx->hostName = WSTRDUP(name, NULL, 0);
|
||||
fwdCbCtx->hostPort = port;
|
||||
fwdCbCtx->isDirect = 1;
|
||||
appCtx->state = APP_STATE_CONNECT;
|
||||
if (!FwdDestAllowed(name, port)) {
|
||||
char shown[65];
|
||||
word32 i;
|
||||
|
||||
/* The name is the peer's; log it bounded and printable. */
|
||||
for (i = 0; name != NULL && name[i] != '\0'
|
||||
&& i < sizeof(shown) - 1; i++) {
|
||||
shown[i] = (name[i] >= ' ' && name[i] <= '~') ? name[i] : '?';
|
||||
}
|
||||
shown[i] = '\0';
|
||||
/* The library refuses the open as administratively
|
||||
* prohibited. */
|
||||
printf("Refusing to forward to %s:%u.\n", shown, port);
|
||||
ret = WS_FWD_SETUP_E;
|
||||
}
|
||||
else {
|
||||
fwdCbCtx->hostName = WSTRDUP(name, NULL, 0);
|
||||
fwdCbCtx->hostPort = port;
|
||||
fwdCbCtx->isDirect = 1;
|
||||
appCtx->state = APP_STATE_CONNECT;
|
||||
}
|
||||
}
|
||||
else if (action == WOLFSSH_FWD_LOCAL_CLEANUP) {
|
||||
/* The channel id rides in the port parameter. A channel can outlive
|
||||
|
|
@ -3738,6 +3850,9 @@ static void ShowUsage(void)
|
|||
printf(" -N use non-blocking sockets\n");
|
||||
#ifdef WOLFSSH_FWD
|
||||
printf(" -F allow the peer's port forwarding requests\n");
|
||||
printf(" -L <list> with -F, the comma separated host or host:port\n"
|
||||
" destinations the peer may connect to, in place\n"
|
||||
" of the default of loopback only; repeatable\n");
|
||||
#endif
|
||||
#ifdef WOLFSSH_SFTP
|
||||
printf(" -d <string> set the home directory for SFTP connections\n");
|
||||
|
|
@ -3790,7 +3905,7 @@ static void ShowUsage(void)
|
|||
}
|
||||
|
||||
|
||||
#define ECHOSERVER_OPTLIST "?1a:Ad:DefEFp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:"
|
||||
#define ECHOSERVER_OPTLIST "?1a:Ad:DefEFL:p:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:"
|
||||
|
||||
#ifdef WOLFSSH_WINDOWS_CERT_STORE
|
||||
/* Detects whether argv or the environment requests a host key from the
|
||||
|
|
@ -3949,6 +4064,9 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args)
|
|||
kbAuthData.promptCount = 0;
|
||||
#endif
|
||||
|
||||
#ifdef WOLFSSH_FWD
|
||||
fwdAllowArgsSz = 0;
|
||||
#endif
|
||||
if (argc > 0) {
|
||||
myoptind = 0;
|
||||
while ((ch = mygetopt(argc, argv, ECHOSERVER_OPTLIST)) != -1) {
|
||||
|
|
@ -3996,6 +4114,15 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args)
|
|||
#endif
|
||||
break;
|
||||
|
||||
case 'L':
|
||||
#ifdef WOLFSSH_FWD
|
||||
if (fwdAllowArgsSz >= FWD_ALLOW_ARGS_MAX) {
|
||||
ES_ERROR("Too many -L options.\n");
|
||||
}
|
||||
fwdAllowArgs[fwdAllowArgsSz++] = myoptarg;
|
||||
#endif
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
if (myoptarg == NULL) {
|
||||
ES_ERROR("NULL port value\n");
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
# had to authenticate.
|
||||
#
|
||||
# Parts 1b and 1c reuse the OpenSSH client on the echoserver: 1b forwards
|
||||
# ssh-agent through it, 1c checks that it forwards ports only with -F.
|
||||
# ssh-agent through it, 1c checks that it forwards ports only with -F and
|
||||
# connects only to allowed destinations.
|
||||
#
|
||||
# Part 3 has the wolfSSH client authenticate to sshd with an RSA key held
|
||||
# by OpenSSH's ssh-agent. The client has only the public half; the agent
|
||||
|
|
@ -25,6 +26,7 @@ no_pid=-1
|
|||
server_pid=$no_pid
|
||||
sshd_pid=$no_pid
|
||||
agent_pid=$no_pid
|
||||
dest_pid=$no_pid
|
||||
work_dir="`pwd`/openssh_interop_test$$"
|
||||
ready_file="$work_dir/ready"
|
||||
client_out="$work_dir/client.out"
|
||||
|
|
@ -66,6 +68,11 @@ do_cleanup() {
|
|||
kill $agent_pid 2>/dev/null
|
||||
agent_pid=$no_pid
|
||||
fi
|
||||
if [ $dest_pid != $no_pid ]
|
||||
then
|
||||
kill -9 $dest_pid 2>/dev/null
|
||||
dest_pid=$no_pid
|
||||
fi
|
||||
rm -rf -- "$work_dir"
|
||||
}
|
||||
|
||||
|
|
@ -453,6 +460,124 @@ if ./apps/wolfssh-options 2>/dev/null | grep -qx 'FWD'; then
|
|||
fi
|
||||
|
||||
echo "the echoserver forwards only when asked to"
|
||||
|
||||
# A direct-tcpip open goes only to loopback by default, and only to
|
||||
# the -L entries when there are any. Refused opens fail at once, so
|
||||
# 192.0.2.1 (TEST-NET-1) is never dialed.
|
||||
run_openssh_stdio() {
|
||||
stdio_dest=$1
|
||||
shift
|
||||
case $echo_keys in
|
||||
ecc) start_echoserver -e "$@" ;;
|
||||
rsa) start_echoserver "$@" ;;
|
||||
*) start_echoserver -j "$work_dir/ed25519.pub" "$@" ;;
|
||||
esac
|
||||
|
||||
rm -f "$client_out" "$client_log"
|
||||
ssh -T -W "$stdio_dest" -p $port \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o IdentitiesOnly=yes \
|
||||
-o PreferredAuthentications=publickey \
|
||||
-i "$work_dir/id" hansel@127.0.0.1 \
|
||||
< /dev/null > "$client_out" 2> "$client_log" &
|
||||
wait_with_limit $!
|
||||
}
|
||||
|
||||
run_openssh_stdio 192.0.2.1:9 -F
|
||||
[ $? -eq 255 ] && grep -q 'administratively prohibited' "$client_log" \
|
||||
|| fail "the echoserver forwarded to a non-loopback destination"
|
||||
|
||||
run_openssh_stdio 127.0.0.1:9 -F -L 192.0.2.1:9
|
||||
[ $? -eq 255 ] && grep -q 'administratively prohibited' "$client_log" \
|
||||
|| fail "the echoserver forwarded to a destination -L left out"
|
||||
|
||||
# An allowed open reaches a second echoserver: an echo session with it
|
||||
# runs through a local forward over the first. $1 is the -L list, with
|
||||
# @PORT@ standing for the second echoserver's port.
|
||||
run_openssh_forward_allowed() {
|
||||
case $echo_keys in
|
||||
ecc) dest_keys=-e ;;
|
||||
rsa) dest_keys= ;;
|
||||
*) dest_keys="-j $work_dir/ed25519.pub" ;;
|
||||
esac
|
||||
|
||||
# A port to forward from: one the OS just handed out and let go.
|
||||
start_echoserver
|
||||
local_port=$port
|
||||
kill -9 $server_pid 2>/dev/null
|
||||
wait $server_pid 2>/dev/null
|
||||
server_pid=$no_pid
|
||||
|
||||
rm -f "$work_dir/dest_ready"
|
||||
./examples/echoserver/echoserver -1 -f $dest_keys \
|
||||
-R "$work_dir/dest_ready" > "$work_dir/dest.log" 2>&1 &
|
||||
dest_pid=$!
|
||||
counter=0
|
||||
while [ ! -s "$work_dir/dest_ready" ] && [ "$counter" -lt 100 ]; do
|
||||
sleep 0.1
|
||||
counter=$((counter + 1))
|
||||
done
|
||||
[ ! -s "$work_dir/dest_ready" ] \
|
||||
&& fail "NO ready file, the destination echoserver never came up"
|
||||
dest_port=`cat "$work_dir/dest_ready"`
|
||||
allow_list=`echo "$1" | sed "s/@PORT@/$dest_port/g"`
|
||||
|
||||
start_echoserver $dest_keys -F -L "$allow_list"
|
||||
|
||||
# The outer session stays open while the inner one runs.
|
||||
rm -f "$work_dir/fwd_in" "$client_out" "$client_log"
|
||||
mkfifo "$work_dir/fwd_in" || fail "couldn't make a fifo"
|
||||
ssh -v -T -p $port \
|
||||
-o ExitOnForwardFailure=yes \
|
||||
-L $local_port:127.0.0.1:$dest_port \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o IdentitiesOnly=yes \
|
||||
-o PreferredAuthentications=publickey \
|
||||
-i "$work_dir/id" hansel@127.0.0.1 \
|
||||
< "$work_dir/fwd_in" > "$client_out" 2> "$client_log" &
|
||||
outer_pid=$!
|
||||
exec 4> "$work_dir/fwd_in"
|
||||
counter=0
|
||||
while ! grep -q 'Entering interactive session' "$client_log" &&
|
||||
[ "$counter" -lt 100 ]; do
|
||||
sleep 0.1
|
||||
counter=$((counter + 1))
|
||||
done
|
||||
|
||||
rm -f "$work_dir/inner_out"
|
||||
printf '%s\n\003' "$probe" | \
|
||||
ssh -T -p $local_port \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o IdentitiesOnly=yes \
|
||||
-o PreferredAuthentications=publickey \
|
||||
-i "$work_dir/id" hansel@127.0.0.1 \
|
||||
> "$work_dir/inner_out" 2> "$work_dir/inner_log" &
|
||||
wait_with_limit $!
|
||||
|
||||
printf '\003' >&4
|
||||
exec 4>&-
|
||||
wait_with_limit $outer_pid
|
||||
kill -9 $dest_pid 2>/dev/null
|
||||
wait $dest_pid 2>/dev/null
|
||||
dest_pid=$no_pid
|
||||
|
||||
grep -q "$probe" "$work_dir/inner_out"
|
||||
}
|
||||
|
||||
if ! run_openssh_forward_allowed 'example.invalid,127.0.0.1:@PORT@'; then
|
||||
cat "$client_log" "$work_dir/inner_log"
|
||||
fail "the echoserver refused a host:port destination -L named"
|
||||
fi
|
||||
|
||||
if ! run_openssh_forward_allowed '127.0.0.1'; then
|
||||
cat "$client_log" "$work_dir/inner_log"
|
||||
fail "the echoserver refused a bare host -L named"
|
||||
fi
|
||||
|
||||
echo "the echoserver forwards only to allowed destinations"
|
||||
fi
|
||||
|
||||
# Part 2 needs an sshd to talk to, and a host key to give it. Neither is
|
||||
|
|
|
|||
Loading…
Reference in New Issue