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-11935
pull/1338/merge
John Safranek 2026-10-21 10:15:06 -07:00 committed by philljj
parent ecb81b5b7f
commit e7b7e8ccfe
3 changed files with 261 additions and 7 deletions

View File

@ -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:

View File

@ -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");

View File

@ -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