diff --git a/README.md b/README.md index ac0c024e9..93b61d2cb 100644 --- a/README.md +++ b/README.md @@ -323,12 +323,18 @@ are routed back and forth between the client and server. "Hello, wolfSSL!" 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. To connect with -the ssh tool, using one of the following command lines. You can run either of -the ssh command lines from anywhere: +The echoserver will handle local and remote port forwarding when started +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. Started as root with +`-F`, the echoserver runs as `nobody` once it is listening; `-u` names +another user. Shells run as that user too, so a login whose home +directory has mode 0700 gets no shell. To connect with the ssh tool, use +one of the following command lines. You can run either of the ssh command +lines from anywhere: src/wolfssl$ ./examples/server/server - src/wolfssh$ ./examples/echoserver/echoserver + src/wolfssh$ ./examples/echoserver/echoserver -F anywhere 1$ ssh -p 22222 -L 12345:localhost:11111 jill@localhost anywhere 2$ ssh -p 22222 -R 12345:localhost:11111 jill@localhost src/wolfssl$ ./examples/client/client -p 12345 @@ -336,6 +342,10 @@ the ssh command lines from anywhere: This will allow port forwarding between the wolfSSL client and server like in the previous example. +A remote forward for `localhost` or `127.0.0.1` listens on the loopback +address only, so with `-R` the wolfSSL client runs on the echoserver's +machine. The echoserver refuses a remote forward of a port below 1024. + SFTP ==== diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index ca45977b0..e244b814d 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -103,6 +103,18 @@ #include #endif /* WOLFSSH_AGENT */ +#if defined(WOLFSSH_FWD) && !defined(USE_WINDOWS_API) && \ + !defined(WOLFSSL_NUCLEUS) && !defined(WOLFSSH_ZEPHYR) && \ + !defined(NO_MAIN_DRIVER) + /* Gives up root before serving; forwarding opens sockets for the + * peer with the server's credentials. Standalone only: the tests run + * echoserver_test() inside their own process. */ + #define ECHOSERVER_DROP_PRIVS + #include + #include + #include +#endif + #ifdef HAVE_SYS_SELECT_H #include #endif @@ -199,8 +211,12 @@ typedef struct WS_AppCtx { #ifdef WOLFSSH_AGENT +#define ECHOSERVER_AGENT_DIR "/tmp/wolfserver.XXXXXX" + typedef struct WS_AgentCbActionCtx { struct sockaddr_un name; + /* The private directory holding name, empty when there is none. */ + char dirName[sizeof(ECHOSERVER_AGENT_DIR)]; } WS_AgentCbActionCtx; #endif @@ -425,23 +441,34 @@ static int wolfSSH_AGENT_DefaultActions(WS_AgentCbAction action, void* vCtx) if (action == WOLFSSH_AGENT_LOCAL_SETUP) { struct sockaddr_un* name = &agentCtx->name; - size_t size; + size_t size = 0; int envSet = 0, nameBound = 0; WMEMSET(name, 0, sizeof(struct sockaddr_un)); name->sun_family = AF_LOCAL; - ret = snprintf(name->sun_path, sizeof(name->sun_path), - "/tmp/wolfserver.%d", (int)getpid()); + /* mkdtemp() makes a new 0700 directory under an unpredictable name, + * so no other user can claim or reach the socket inside it. */ + WMEMCPY(agentCtx->dirName, ECHOSERVER_AGENT_DIR, + sizeof(ECHOSERVER_AGENT_DIR)); + if (mkdtemp(agentCtx->dirName) == NULL) { + agentCtx->dirName[0] = '\0'; + ret = -1; + } - if (ret >= 0) { - name->sun_path[sizeof(name->sun_path) - 1] = '\0'; - size = WSTRLEN(name->sun_path); - ret = (size < WSTRLEN("/tmp/wolfserver.")); + if (ret == 0) { + ret = snprintf(name->sun_path, sizeof(name->sun_path), + "%s/agent", agentCtx->dirName); + if (ret > 0 && (size_t)ret < sizeof(name->sun_path)) { + size = (size_t)ret + offsetof(struct sockaddr_un, sun_path); + ret = 0; + } + else { + ret = -1; + } } if (ret == 0) { - size += offsetof(struct sockaddr_un, sun_path); ctx->listenFd = socket(AF_UNIX, SOCK_STREAM, 0); ret = (ctx->listenFd == -1) ? -1 : 0; } @@ -462,12 +489,15 @@ static int wolfSSH_AGENT_DefaultActions(WS_AgentCbAction action, void* vCtx) } if (ret == 0) { + printf("Agent socket: %s\n", name->sun_path); + fflush(stdout); ctx->state = APP_STATE_LISTEN; } else { if (nameBound) { unlink(agentCtx->name.sun_path); } + agentCtx->name.sun_path[0] = '\0'; if (envSet) { unsetenv(EnvNameAuthPort); } @@ -475,14 +505,33 @@ static int wolfSSH_AGENT_DefaultActions(WS_AgentCbAction action, void* vCtx) close(ctx->listenFd); ctx->listenFd = -1; } + if (agentCtx->dirName[0] != '\0') { + rmdir(agentCtx->dirName); + agentCtx->dirName[0] = '\0'; + } ret = WS_AGENT_SETUP_E; } } else if (action == WOLFSSH_AGENT_LOCAL_CLEANUP) { - WCLOSESOCKET(ctx->listenFd); - ctx->listenFd = -1; - unlink(agentCtx->name.sun_path); - unsetenv(EnvNameAuthPort); + /* The accepted agent client goes with the listener. */ + if (ctx->appFd >= 0) { + WCLOSESOCKET(ctx->appFd); + ctx->appFd = -1; + } + if (ctx->listenFd >= 0) { + WCLOSESOCKET(ctx->listenFd); + ctx->listenFd = -1; + } + if (agentCtx->name.sun_path[0] != '\0') { + unlink(agentCtx->name.sun_path); + agentCtx->name.sun_path[0] = '\0'; + unsetenv(EnvNameAuthPort); + } + if (agentCtx->dirName[0] != '\0') { + rmdir(agentCtx->dirName); + agentCtx->dirName[0] = '\0'; + } + ctx->state = APP_STATE_INIT; } else ret = WS_AGENT_INVALID_ACTION; @@ -536,6 +585,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) { @@ -544,10 +688,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 @@ -603,15 +764,16 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, if (ret == 0) { WMEMSET(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; + addr.sin_port = htons((word16)port); + addrSz = sizeof addr; if (WSTRCMP(name, "") == 0 || - WSTRCMP(name, "0.0.0.0") == 0 || - WSTRCMP(name, "localhost") == 0 || + WSTRCMP(name, "0.0.0.0") == 0) { + addr.sin_addr.s_addr = htonl(INADDR_ANY); + } + else if (WSTRCMP(name, "localhost") == 0 || WSTRCMP(name, "127.0.0.1") == 0) { - - addr.sin_addr.s_addr = INADDR_ANY; - addr.sin_family = AF_INET; - addr.sin_port = htons((word16)port); - addrSz = sizeof addr; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); } else { printf("Not using IPv6 yet.\n"); @@ -619,6 +781,13 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, } } + if (ret == 0 && (port > 65535 || + (port != 0 && port < WS_FWD_PORT_CHECK))) { + /* The peer does not get the server's privileged ports. */ + printf("Refusing to forward port %u.\n", port); + ret = -1; + } + if (ret == 0) { ret = bind(appCtx->listenFd, (const struct sockaddr*)&addr, addrSz); @@ -1460,8 +1629,12 @@ static int ssh_worker(thread_ctx_t* threadCtx) if (!agentOpened) { int agentRc = wolfSSH_AGENT_ChannelOpen(ssh); - if (agentRc == WS_SUCCESS) + if (agentRc == WS_SUCCESS) { + /* The relay below addresses the agent channel by its + * own id, which only internal.h exposes. */ + agentChannelId = ssh->agent->channel; agentOpened = 1; + } else if (agentRc == WS_WANT_WRITE) wantWrite = 1; } @@ -1620,23 +1793,41 @@ static int ssh_worker(thread_ctx_t* threadCtx) * pumps below, on every pass. */ #ifdef WOLFSSH_AGENT if (lastChannel == agentChannelId) { - cnt_r = wolfSSH_ChannelIdRead(ssh, agentChannelId, - threadCtx->agentCtx.fdBuffer, - sizeof threadCtx->agentCtx.fdBuffer); - if (cnt_r <= 0) { + int agentRxd = 0; + + /* One message can carry more than fdBuffer + * holds, and no later report asks for the + * rest, so drain it all now. */ + for (;;) { + cnt_r = wolfSSH_ChannelIdRead(ssh, + agentChannelId, + threadCtx->agentCtx.fdBuffer, + sizeof threadCtx->agentCtx.fdBuffer); + if (cnt_r <= 0) + break; + agentRxd = 1; + #ifdef SHELL_DEBUG + buf_dump(threadCtx->agentCtx.fdBuffer, + cnt_r); + #endif + /* A reply for an agent client that has + * hung up has nowhere to go; drop it. */ + if (agentFd >= 0 && app_write_all(agentFd, + threadCtx->agentCtx.fdBuffer, + (word32)cnt_r) < 0) { + agentRxd = -1; + break; + } + } + if (agentRxd < 0) + break; + if (agentRxd == 0) { /* Nothing was buffered. Only an actual data * report makes that a failure. */ if (rc == WS_REKEYING && cnt_r == 0) continue; break; } - #ifdef SHELL_DEBUG - buf_dump(threadCtx->agentCtx.fdBuffer, cnt_r); - #endif - if (app_write_all(agentFd, - threadCtx->agentCtx.fdBuffer, - (word32)cnt_r) < 0) - break; } #endif } @@ -2375,6 +2566,12 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) WCLOSESOCKET(threadCtx->fd); threadCtx->fd = -1; } +#ifdef WOLFSSH_AGENT + /* The library never calls the cleanup on a server, so retire the + * listener, its socket and directory, and any agent client here. */ + (void)wolfSSH_AGENT_DefaultActions(WOLFSSH_AGENT_LOCAL_CLEANUP, + &threadCtx->agentCtx); +#endif #ifdef WOLFSSH_FWD if (threadCtx->fwdCbCtx.hostName != NULL) { WFREE(threadCtx->fwdCbCtx.hostName, NULL, 0); @@ -3648,6 +3845,36 @@ static int SetDefaultSftpPath(WOLFSSH* ssh, const char* defaultSftpPath, #endif +#ifdef ECHOSERVER_DROP_PRIVS +/* Runs the process as userName for good. Without root this only checks + * that userName is the current user. Returns 0, or -1. */ +static int DropPrivileges(const char* userName) +{ + struct passwd* pw; + gid_t gid; + uid_t uid; + + pw = getpwnam(userName); + if (pw == NULL) + return -1; + gid = pw->pw_gid; + uid = pw->pw_uid; + + if (geteuid() != 0) + return (uid == geteuid()) ? 0 : -1; + + if (setgroups(1, &gid) != 0 || setgid(gid) != 0 || setuid(uid) != 0) + return -1; + + /* Make sure root cannot be taken back. */ + if (uid != 0 && (setuid(0) == 0 || seteuid(0) == 0)) + return -1; + + return 0; +} +#endif + + static void ShowUsage(void) { printf("echoserver %s linked with wolfSSL %s\n", LIBWOLFSSH_VERSION_STRING, @@ -3662,6 +3889,16 @@ static void ShowUsage(void) printf(" -A drive channels from the application callbacks\n"); printf(" -p port to connect on, default %d\n", wolfSshPort); printf(" -N use non-blocking sockets\n"); +#ifdef WOLFSSH_FWD + printf(" -F allow the peer's port forwarding requests\n"); + printf(" -L 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 ECHOSERVER_DROP_PRIVS + printf(" -u run as user once listening; started as root with\n" + " -F, the default is nobody\n"); +#endif #ifdef WOLFSSH_SFTP printf(" -d set the home directory for SFTP connections\n"); printf(" -D confine SFTP connections to the home directory," @@ -3713,7 +3950,7 @@ static void ShowUsage(void) } -#define ECHOSERVER_OPTLIST "?1a:Ad:DefEp: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:u:" #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Detects whether argv or the environment requests a host key from the @@ -3845,6 +4082,12 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) int peerEcc = 0; int echo = 0; int appChannels = 0; +#ifdef WOLFSSH_FWD + int allowFwd = 0; +#endif +#ifdef ECHOSERVER_DROP_PRIVS + const char* dropUser = NULL; +#endif int ch; word16 port = wolfSshPort; char* readyFile = NULL; @@ -3869,6 +4112,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) { @@ -3910,6 +4156,30 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) appChannels = 1; break; + case 'F': + #ifdef WOLFSSH_FWD + allowFwd = 1; + #endif + break; + + case 'u': + #ifdef ECHOSERVER_DROP_PRIVS + dropUser = myoptarg; + #else + fprintf(stderr, "Warning: -u is ignored in this " + "build\n"); + #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"); @@ -4151,7 +4421,11 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) wolfSSH_CTX_set_agent_cb(ctx, wolfSSH_AGENT_DefaultActions, NULL); #endif #ifdef WOLFSSH_FWD - wolfSSH_CTX_SetFwdCb(ctx, wolfSSH_FwdDefaultActions, NULL); + /* The callback opens sockets with the server's own credentials, so + * forwarding is refused unless asked for. Without a callback the + * library turns every forwarding request down. */ + if (allowFwd) + wolfSSH_CTX_SetFwdCb(ctx, wolfSSH_FwdDefaultActions, NULL); #endif /* With -A the echoserver drives its own channels: accept() stops at * userauth and these callbacks start the shell, subsystem or transfer. @@ -4570,6 +4844,23 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #endif } +#ifdef ECHOSERVER_DROP_PRIVS + /* After the listener and the keys, before any peer is served. */ + if (dropUser == NULL && allowFwd && geteuid() == 0) { + dropUser = "nobody"; + fprintf(stderr, "Warning: running as nobody for -F; -u picks the " + "user.\n"); + #ifdef WOLFSSH_SHELL + fprintf(stderr, "Warning: shells run as nobody too; a login whose " + "home has mode 0700 gets no shell.\n"); + #endif + } + if (dropUser != NULL && DropPrivileges(dropUser) != 0) { + WCLOSESOCKET(listenFd); + ES_ERROR("Couldn't run as user %s.\n", dropUser); + } +#endif + SignalTcpReady(serverArgs->signal, port); do { diff --git a/scripts/fwd-bulk.test b/scripts/fwd-bulk.test index 5e69b8ff3..b39af564c 100755 --- a/scripts/fwd-bulk.test +++ b/scripts/fwd-bulk.test @@ -252,7 +252,7 @@ do_tail_attempt() { kill -0 "$tail_nc_server_pid" 2>/dev/null \ || do_fail "couldn't listen on port $tail_target_port, is it in use?" - ./examples/echoserver/echoserver -1 -f -R "$tail_ready_file" \ + ./examples/echoserver/echoserver -1 -f -F -R "$tail_ready_file" \ > "$tail_server_log" 2>&1 & tail_server_pid=$! @@ -333,7 +333,7 @@ then exit 1 fi -./examples/echoserver/echoserver -1 -f -R "$ready_file" \ +./examples/echoserver/echoserver -1 -f -F -R "$ready_file" \ > "$server_log" 2>&1 & server_pid=$! @@ -463,7 +463,7 @@ done [ -s "$echo_target_ready" ] \ || do_fail "couldn't listen on port $echo_target_port, is it in use?" -./examples/echoserver/echoserver -1 -f -R "$echo_ready_file" \ +./examples/echoserver/echoserver -1 -f -F -R "$echo_ready_file" \ > "$echo_server_log" 2>&1 & echo_server_pid=$! diff --git a/scripts/fwd.test.expect b/scripts/fwd.test.expect index a4836fed3..1ef36384b 100755 --- a/scripts/fwd.test.expect +++ b/scripts/fwd.test.expect @@ -190,7 +190,7 @@ proc run_phase {label reverse entry_port target_port} { puts " PID $nc_server_pid - waiting for a connection..." puts "\n\[2\] Starting wolfssh server..." - spawn sh -c "exec ./examples/echoserver/echoserver -1 -f -R $srv_ready >$srv_log 2>&1" + spawn sh -c "exec ./examples/echoserver/echoserver -1 -f -F -R $srv_ready >$srv_log 2>&1" set wolfssh_srv_id $spawn_id set wolfssh_srv_pid [exp_pid] diff --git a/scripts/openssh-interop.test b/scripts/openssh-interop.test index 752a6e460..92dc16b02 100755 --- a/scripts/openssh-interop.test +++ b/scripts/openssh-interop.test @@ -10,6 +10,10 @@ # peer that disagreed about the counters would fail the first packet that # 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 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 # makes the signature. @@ -22,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" @@ -63,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" } @@ -242,15 +252,20 @@ echo "Test the OpenSSH client against the wolfSSH echoserver" # A build with neither has only Ed25519, and the checked-in Ed25519 key is # PKCS#8, which an OpenSSH on LibreSSL won't load, so make a key of # OpenSSH's own and hand the echoserver its public half. +# Which of the attempts below opened the session, so the agent forwarding +# check can repeat it. +echo_keys=ecc run_openssh_client "$keys_dir/hansel-key-ecc.pem" -e session_status=$? if [ $session_status -ne 0 ]; then echo "the ECC attempt failed, retrying with the RSA sample key" + echo_keys=rsa run_openssh_client "$keys_dir/hansel-key-rsa.pem" session_status=$? fi if [ $session_status -ne 0 ] && command -v ssh-keygen > /dev/null 2>&1; then echo "the RSA attempt failed, retrying with an Ed25519 key" + echo_keys=ed25519 ssh-keygen -q -t ed25519 -N '' -C hansel -f "$work_dir/ed25519" \ < /dev/null || fail "couldn't make an Ed25519 client key" run_openssh_client "$work_dir/ed25519" -j "$work_dir/ed25519.pub" @@ -284,6 +299,287 @@ fi echo "the OpenSSH client negotiated strict KEX with the echoserver" +# Part 1b forwards OpenSSH's ssh-agent through the echoserver. A local agent +# client on the echoserver's side lists the forwarded key over the socket +# the echoserver made for it, twice, so the second goes through a fresh +# accept. The socket's directory must be private and gone once the session +# is over. +if ! ./apps/wolfssh-options 2>/dev/null | grep -qx 'AGENT'; then + echo "the echoserver has no agent support, skipping agent forwarding" +elif ! command -v ssh-agent > /dev/null 2>&1 || + ! command -v ssh-add > /dev/null 2>&1 || + ! command -v ssh-keygen > /dev/null 2>&1 || + ! command -v mkfifo > /dev/null 2>&1; then + echo "no ssh-agent, ssh-add, ssh-keygen or mkfifo," \ + "skipping agent forwarding" +else + echo "Test agent forwarding through the wolfSSH echoserver" + + # The agent picks its own socket path; see Part 3. + SSH_AGENT_PID= + eval `ssh-agent -s` > /dev/null + [ -n "$SSH_AGENT_PID" ] || fail "couldn't start ssh-agent" + agent_pid=$SSH_AGENT_PID + + ssh-keygen -q -t ed25519 -N '' -C wolfssh-agent-fwd \ + -f "$work_dir/agent-fwd" < /dev/null \ + || fail "couldn't make a key for the agent" + ssh-add -q "$work_dir/agent-fwd" 2>/dev/null \ + || fail "couldn't add the key to ssh-agent" + # Long comments push the list reply past the echoserver's relay + # buffer, so it has to be drained in more than one read. + agent_pad=`printf '%1500s' '' | tr ' ' x` + for pad in 1 2 3 4; do + ssh-keygen -q -t ed25519 -N '' -C "wolfssh-agent-pad$pad-$agent_pad" \ + -f "$work_dir/agent-pad$pad" < /dev/null \ + || fail "couldn't make a padding key for the agent" + ssh-add -q "$work_dir/agent-pad$pad" 2>/dev/null \ + || fail "couldn't add a padding key to ssh-agent" + done + + # The client key was copied to $work_dir/id by the attempt that worked. + case $echo_keys in + ecc) start_echoserver -e ;; + rsa) start_echoserver ;; + *) start_echoserver -j "$work_dir/ed25519.pub" ;; + esac + + # The session stays open while the agent is used through it. + rm -f "$work_dir/fwd_in" "$client_out" "$client_log" + mkfifo "$work_dir/fwd_in" || fail "couldn't make a fifo" + ssh -A -T -p $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" & + fwd_ssh_pid=$! + exec 3> "$work_dir/fwd_in" + printf '%s\n' "$probe" >&3 + + fwd_sock= + counter=0 + while [ -z "$fwd_sock" ] && [ "$counter" -lt 100 ]; do + fwd_sock=`sed -n 's/^Agent socket: //p' "$work_dir/server.log"` + [ -z "$fwd_sock" ] && sleep 0.1 + counter=$((counter + 1)) + done + if [ -z "$fwd_sock" ]; then + exec 3>&- + cat "$work_dir/server.log" + fail "the echoserver never opened an agent socket" + fi + fwd_dir=`dirname "$fwd_sock"` + + case `ls -ld "$fwd_dir"` in + drwx------*) ;; + *) exec 3>&-; fail "the agent socket's directory is not private" ;; + esac + + for attempt in 1 2; do + SSH_AUTH_SOCK="$fwd_sock" ssh-add -l > "$work_dir/fwd_list" 2>&1 & + wait_with_limit $! + if ! grep -q 'wolfssh-agent-fwd' "$work_dir/fwd_list" || + [ `grep -c 'wolfssh-agent-pad' "$work_dir/fwd_list"` -ne 4 ]; then + exec 3>&- + cat "$work_dir/fwd_list" + fail "the forwarded agent didn't list its keys (try $attempt)" + fi + done + + printf '\003' >&3 + exec 3>&- + wait_with_limit $fwd_ssh_pid + wait_with_limit $server_pid + server_pid=$no_pid + + grep -q "$probe" "$client_out" \ + || fail "the session didn't survive the forwarded agent" + [ -e "$fwd_dir" ] && fail "the echoserver left $fwd_dir behind" + + kill $agent_pid 2>/dev/null + agent_pid=$no_pid + + echo "the forwarded agent answered through the echoserver" +fi + +# Part 1c: the echoserver forwards ports only when started with -F. +# ExitOnForwardFailure turns a refused remote forward into exit status 255. +# +# $1 - the remote port to ask for, 0 to let the server pick +# rest - extra echoserver arguments +run_openssh_forward() { + fwd_port=$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" + printf '%s\n\003' "$probe" | \ + ssh -T -p $port \ + -o ExitOnForwardFailure=yes \ + -R $fwd_port:127.0.0.1:9 \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o IdentitiesOnly=yes \ + -o PreferredAuthentications=publickey \ + -i "$work_dir/id" hansel@127.0.0.1 \ + > "$client_out" 2> "$client_log" & + wait_with_limit $! +} + +if ./apps/wolfssh-options 2>/dev/null | grep -qx 'FWD'; then + echo "Test the echoserver's forwarding opt-in" + + run_openssh_forward 0 + [ $? -eq 255 ] && grep -q 'remote port forwarding failed' "$client_log" \ + || fail "the echoserver forwarded a port without -F" + + run_openssh_forward 0 -F + if [ $? -ne 0 ] || ! grep -q "$probe" "$client_out"; then + cat "$client_log" + fail "the echoserver refused a forward with -F" + fi + + # A privileged port is refused by the echoserver's own check, which + # logs it, not by a bind() failing for a non-root user. The server + # exits after the session, flushing its log. + run_openssh_forward 80 -F + fwd_status=$? + wait_with_limit $server_pid + server_pid=$no_pid + if [ $fwd_status -ne 255 ] || + ! grep -q 'remote port forwarding failed' "$client_log" || + ! grep -q 'Refusing to forward port 80\.' "$work_dir/server.log"; then + cat "$client_log" "$work_dir/server.log" + fail "the echoserver didn't refuse a forward of port 80" + 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 # worth failing the test over. sshd_bin=