9003f92e8f
There's a race window between the accept loop's call to accept(2) and it checking `ctx.keep_going`. Forcefully close the server socket such that any raced `accept` ends up failing. Signed-off-by: Jon Simons <jon@jonsimons.org> Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
514 строки
12 KiB
C
514 строки
12 KiB
C
/*
|
|
* pkd_daemon.c -- a sample public-key testing daemon using libssh
|
|
*
|
|
* Uses public key authentication to establish an exec channel and
|
|
* echo back payloads to the user.
|
|
*
|
|
* (c) 2014 Jon Simons
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <netinet/in.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
|
|
#include <libssh/callbacks.h>
|
|
#include <libssh/libssh.h>
|
|
#include <libssh/server.h>
|
|
|
|
#include "pkd_daemon.h"
|
|
|
|
#include <setjmp.h> // for cmocka
|
|
#include <cmocka.h>
|
|
|
|
static int pkdout_enabled;
|
|
static int pkderr_enabled;
|
|
|
|
static void pkdout(const char *fmt, ...) PRINTF_ATTRIBUTE(1, 2);
|
|
static void pkderr(const char *fmt, ...) PRINTF_ATTRIBUTE(1, 2);
|
|
|
|
static void pkdout(const char *fmt, ...) {
|
|
va_list vargs;
|
|
if (pkdout_enabled) {
|
|
va_start(vargs, fmt);
|
|
vfprintf(stdout, fmt, vargs);
|
|
va_end(vargs);
|
|
}
|
|
}
|
|
|
|
static void pkderr(const char *fmt, ...) {
|
|
va_list vargs;
|
|
if (pkderr_enabled) {
|
|
va_start(vargs, fmt);
|
|
vfprintf(stderr, fmt, vargs);
|
|
va_end(vargs);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* pkd state: only one thread can run pkd at a time ---------------------
|
|
*/
|
|
|
|
static struct {
|
|
int rc;
|
|
pthread_t tid;
|
|
int keep_going;
|
|
volatile int pkd_ready;
|
|
} ctx;
|
|
|
|
static struct {
|
|
int server_fd;
|
|
int req_exec_received;
|
|
int close_received;
|
|
int eof_received;
|
|
} pkd_state;
|
|
|
|
static void pkd_sighandler(int signum) {
|
|
(void) signum;
|
|
}
|
|
|
|
static int pkd_init_libssh() {
|
|
int rc = ssh_threads_set_callbacks(ssh_threads_get_pthread());
|
|
return (rc == SSH_OK) ? 0 : 1;
|
|
}
|
|
|
|
static int pkd_init_server_fd(short port) {
|
|
int rc = 0;
|
|
int yes = 1;
|
|
struct sockaddr_in addr;
|
|
|
|
int server_fd = socket(PF_INET, SOCK_STREAM, 0);
|
|
if (server_fd < 0) {
|
|
rc = -1;
|
|
goto out;
|
|
}
|
|
|
|
rc = setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
|
|
if (rc != 0) {
|
|
goto outclose;
|
|
}
|
|
|
|
memset(&addr, 0x0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(port);
|
|
addr.sin_addr.s_addr = INADDR_ANY;
|
|
rc = bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
|
|
if (rc != 0) {
|
|
goto outclose;
|
|
}
|
|
|
|
rc = listen(server_fd, 128);
|
|
if (rc == 0) {
|
|
goto out;
|
|
}
|
|
|
|
outclose:
|
|
close(server_fd);
|
|
server_fd = -1;
|
|
out:
|
|
pkd_state.server_fd = server_fd;
|
|
return rc;
|
|
}
|
|
|
|
static int pkd_accept_fd() {
|
|
int fd = -1;
|
|
struct sockaddr_in addr;
|
|
socklen_t len = sizeof(addr);
|
|
|
|
do {
|
|
fd = accept(pkd_state.server_fd, (struct sockaddr *) &addr, &len);
|
|
} while ((ctx.keep_going != 0) && (fd < 0) && (errno == EINTR));
|
|
|
|
return fd;
|
|
}
|
|
|
|
static void pkd_eof(ssh_session session,
|
|
ssh_channel channel,
|
|
void *userdata) {
|
|
(void) session;
|
|
(void) channel;
|
|
(void) userdata;
|
|
pkdout("pkd_eof\n");
|
|
pkd_state.eof_received = 1;
|
|
}
|
|
|
|
static void pkd_chan_close(ssh_session session,
|
|
ssh_channel channel,
|
|
void *userdata) {
|
|
(void) session;
|
|
(void) channel;
|
|
(void) userdata;
|
|
pkdout("pkd_chan_close\n");
|
|
pkd_state.close_received = 1;
|
|
}
|
|
|
|
static int pkd_req_exec(ssh_session s,
|
|
ssh_channel c,
|
|
const char *cmd,
|
|
void *userdata) {
|
|
(void) s;
|
|
(void) c;
|
|
(void) cmd;
|
|
(void) userdata;
|
|
/* assumes pubkey authentication has already succeeded */
|
|
pkdout("pkd_req_exec\n");
|
|
pkd_state.req_exec_received = 1;
|
|
return 0;
|
|
}
|
|
|
|
/* assumes there is only ever a single channel */
|
|
static struct ssh_channel_callbacks_struct pkd_channel_cb = {
|
|
.channel_eof_function = pkd_eof,
|
|
.channel_close_function = pkd_chan_close,
|
|
.channel_exec_request_function = pkd_req_exec,
|
|
};
|
|
|
|
static int pkd_auth_pubkey_cb(ssh_session s,
|
|
const char *user,
|
|
ssh_key key,
|
|
char state,
|
|
void *userdata) {
|
|
(void) s;
|
|
(void) user;
|
|
(void) key;
|
|
(void) state;
|
|
(void) userdata;
|
|
pkdout("pkd_auth_pubkey_cb keytype %s, state: %d\n",
|
|
ssh_key_type_to_char(ssh_key_type(key)), state);
|
|
if ((state == SSH_PUBLICKEY_STATE_NONE) ||
|
|
(state == SSH_PUBLICKEY_STATE_VALID)) {
|
|
return SSH_AUTH_SUCCESS;
|
|
}
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
|
|
static int pkd_service_request_cb(ssh_session session,
|
|
const char *service,
|
|
void *userdata) {
|
|
(void) session;
|
|
(void) userdata;
|
|
pkdout("pkd_service_request_cb: %s\n", service);
|
|
return (0 == (strcmp(service, "ssh-userauth"))) ? 0 : -1;
|
|
}
|
|
|
|
static ssh_channel pkd_channel_openreq_cb(ssh_session s,
|
|
void *userdata) {
|
|
ssh_channel c = NULL;
|
|
ssh_channel *out = (ssh_channel *) userdata;
|
|
|
|
/* assumes pubkey authentication has already succeeded */
|
|
pkdout("pkd_channel_openreq_cb\n");
|
|
|
|
c = ssh_channel_new(s);
|
|
if (c == NULL) {
|
|
pkderr("ssh_channel_new: %s\n", ssh_get_error(s));
|
|
return NULL;
|
|
}
|
|
|
|
ssh_callbacks_init(&pkd_channel_cb);
|
|
pkd_channel_cb.userdata = userdata;
|
|
if (ssh_set_channel_callbacks(c, &pkd_channel_cb) != SSH_OK) {
|
|
pkderr("ssh_set_channel_callbacks: %s\n", ssh_get_error(s));
|
|
ssh_channel_free(c);
|
|
c = NULL;
|
|
}
|
|
|
|
*out = c;
|
|
|
|
return c;
|
|
}
|
|
|
|
static struct ssh_server_callbacks_struct pkd_server_cb = {
|
|
.auth_pubkey_function = pkd_auth_pubkey_cb,
|
|
.service_request_function = pkd_service_request_cb,
|
|
.channel_open_request_session_function = pkd_channel_openreq_cb,
|
|
};
|
|
|
|
static int pkd_exec_hello(int fd, struct pkd_daemon_args *args) {
|
|
int rc = -1;
|
|
ssh_bind b = NULL;
|
|
ssh_session s = NULL;
|
|
ssh_event e = NULL;
|
|
ssh_channel c = NULL;
|
|
enum ssh_bind_options_e opts = -1;
|
|
|
|
int level = args->opts.libssh_log_level;
|
|
enum pkd_hostkey_type_e type = args->type;
|
|
const char *hostkeypath = args->hostkeypath;
|
|
|
|
pkd_state.eof_received = 0;
|
|
pkd_state.close_received = 0;
|
|
pkd_state.req_exec_received = 0;
|
|
|
|
b = ssh_bind_new();
|
|
if (b == NULL) {
|
|
pkderr("ssh_bind_new\n");
|
|
goto outclose;
|
|
}
|
|
|
|
if (type == PKD_RSA) {
|
|
opts = SSH_BIND_OPTIONS_RSAKEY;
|
|
} else if (type == PKD_DSA) {
|
|
opts = SSH_BIND_OPTIONS_DSAKEY;
|
|
} else if (type == PKD_ECDSA) {
|
|
opts = SSH_BIND_OPTIONS_ECDSAKEY;
|
|
} else {
|
|
pkderr("unknown kex algorithm: %d\n", type);
|
|
rc = -1;
|
|
goto outclose;
|
|
}
|
|
|
|
rc = ssh_bind_options_set(b, opts, hostkeypath);
|
|
if (rc != 0) {
|
|
pkderr("ssh_bind_options_set: %s\n", ssh_get_error(b));
|
|
goto outclose;
|
|
}
|
|
|
|
rc = ssh_bind_options_set(b, SSH_BIND_OPTIONS_LOG_VERBOSITY, &level);
|
|
if (rc != 0) {
|
|
pkderr("ssh_bind_options_set log verbosity: %s\n", ssh_get_error(b));
|
|
goto outclose;
|
|
}
|
|
|
|
s = ssh_new();
|
|
if (s == NULL) {
|
|
pkderr("ssh_new\n");
|
|
goto outclose;
|
|
}
|
|
|
|
/*
|
|
* ssh_bind_accept loads host key as side-effect. If this
|
|
* succeeds, the given 'fd' will be closed upon 'ssh_free(s)'.
|
|
*/
|
|
rc = ssh_bind_accept_fd(b, s, fd);
|
|
if (rc != SSH_OK) {
|
|
pkderr("ssh_bind_accept_fd: %s\n", ssh_get_error(b));
|
|
goto outclose;
|
|
}
|
|
|
|
/* accept only publickey-based auth */
|
|
ssh_set_auth_methods(s, SSH_AUTH_METHOD_PUBLICKEY);
|
|
|
|
/* initialize callbacks */
|
|
ssh_callbacks_init(&pkd_server_cb);
|
|
pkd_server_cb.userdata = &c;
|
|
rc = ssh_set_server_callbacks(s, &pkd_server_cb);
|
|
if (rc != SSH_OK) {
|
|
pkderr("ssh_set_server_callbacks: %s\n", ssh_get_error(s));
|
|
goto out;
|
|
}
|
|
|
|
/* first do key exchange */
|
|
rc = ssh_handle_key_exchange(s);
|
|
if (rc != SSH_OK) {
|
|
pkderr("ssh_handle_key_exchange: %s\n", ssh_get_error(s));
|
|
goto out;
|
|
}
|
|
|
|
/* setup and pump event to carry out exec channel */
|
|
e = ssh_event_new();
|
|
if (e == NULL) {
|
|
pkderr("ssh_event_new\n");
|
|
goto out;
|
|
}
|
|
|
|
rc = ssh_event_add_session(e, s);
|
|
if (rc != SSH_OK) {
|
|
pkderr("ssh_event_add_session\n");
|
|
goto out;
|
|
}
|
|
|
|
/* poll until exec channel established */
|
|
while ((ctx.keep_going != 0) &&
|
|
(rc != SSH_ERROR) && (pkd_state.req_exec_received == 0)) {
|
|
rc = ssh_event_dopoll(e, -1 /* infinite timeout */);
|
|
}
|
|
|
|
if (rc == SSH_ERROR) {
|
|
pkderr("ssh_event_dopoll\n");
|
|
goto out;
|
|
} else if (c == NULL) {
|
|
pkderr("poll loop exited but exec channel not ready\n");
|
|
rc = -1;
|
|
goto out;
|
|
}
|
|
|
|
rc = ssh_channel_write(c, "hello\n", 6); /* XXX: customizable payloads */
|
|
if (rc != 6) {
|
|
pkderr("ssh_channel_write partial (%d)\n", rc);
|
|
}
|
|
|
|
rc = ssh_channel_request_send_exit_status(c, 0);
|
|
if (rc != SSH_OK) {
|
|
pkderr("ssh_channel_request_send_exit_status: %s\n",
|
|
ssh_get_error(s));
|
|
goto out;
|
|
}
|
|
|
|
rc = ssh_channel_send_eof(c);
|
|
if (rc != SSH_OK) {
|
|
pkderr("ssh_channel_send_eof: %s\n", ssh_get_error(s));
|
|
goto out;
|
|
}
|
|
|
|
rc = ssh_channel_close(c);
|
|
if (rc != SSH_OK) {
|
|
pkderr("ssh_channel_close: %s\n", ssh_get_error(s));
|
|
goto out;
|
|
}
|
|
|
|
while ((ctx.keep_going != 0) &&
|
|
(pkd_state.eof_received == 0) &&
|
|
(pkd_state.close_received == 0)) {
|
|
rc = ssh_event_dopoll(e, 1000 /* milliseconds */);
|
|
if (rc == SSH_ERROR) {
|
|
pkderr("ssh_event_dopoll for eof + close: %s\n", ssh_get_error(s));
|
|
break;
|
|
} else {
|
|
rc = 0;
|
|
}
|
|
}
|
|
|
|
while ((ctx.keep_going != 0) &&
|
|
(ssh_is_connected(s))) {
|
|
rc = ssh_event_dopoll(e, 1000 /* milliseconds */);
|
|
if (rc == SSH_ERROR) {
|
|
pkderr("ssh_event_dopoll for session connection: %s\n", ssh_get_error(s));
|
|
break;
|
|
} else {
|
|
rc = 0;
|
|
}
|
|
}
|
|
goto out;
|
|
|
|
outclose:
|
|
close(fd);
|
|
out:
|
|
if (c != NULL) {
|
|
ssh_channel_free(c);
|
|
}
|
|
if (e != NULL) {
|
|
ssh_event_remove_session(e, s);
|
|
ssh_event_free(e);
|
|
}
|
|
if (s != NULL) {
|
|
ssh_disconnect(s);
|
|
ssh_free(s);
|
|
}
|
|
if (b != NULL) {
|
|
ssh_bind_free(b);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/*
|
|
* main loop ------------------------------------------------------------
|
|
*/
|
|
|
|
static void *pkd_main(void *args) {
|
|
int rc = -1;
|
|
struct pkd_daemon_args *a = (struct pkd_daemon_args *) args;
|
|
|
|
struct sigaction act = { .sa_handler = pkd_sighandler, };
|
|
|
|
pkd_state.server_fd = -1;
|
|
pkd_state.req_exec_received = 0;
|
|
pkd_state.close_received = 0;
|
|
pkd_state.eof_received = 0;
|
|
|
|
/* SIGUSR1 is used to interrupt 'pkd_accept_fd'. */
|
|
rc = sigaction(SIGUSR1, &act, NULL);
|
|
if (rc != 0) {
|
|
pkderr("sigaction: %d\n", rc);
|
|
goto out;
|
|
}
|
|
|
|
rc = pkd_init_libssh();
|
|
if (rc != 0) {
|
|
pkderr("pkd_init_libssh: %d\n", rc);
|
|
goto out;
|
|
}
|
|
|
|
rc = pkd_init_server_fd(1234);
|
|
if (rc != 0) {
|
|
pkderr("pkd_init_server_fd: %d\n", rc);
|
|
goto out;
|
|
}
|
|
|
|
ctx.pkd_ready = 1;
|
|
|
|
while (ctx.keep_going != 0) {
|
|
int fd = pkd_accept_fd();
|
|
if (fd < 0) {
|
|
if (ctx.keep_going != 0) {
|
|
pkderr("pkd_accept_fd");
|
|
rc = -1;
|
|
} else {
|
|
rc = 0;
|
|
}
|
|
break;
|
|
}
|
|
|
|
rc = pkd_exec_hello(fd, a);
|
|
if (rc != 0) {
|
|
pkderr("pkd_exec_hello: %d\n", rc);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (pkd_state.server_fd != -1) {
|
|
close(pkd_state.server_fd);
|
|
}
|
|
pkd_state.server_fd = -1;
|
|
out:
|
|
ctx.rc = rc;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* pkd start and stop used by setup/teardown test scaffolding -----------
|
|
*/
|
|
|
|
int pkd_start(struct pkd_daemon_args *args) {
|
|
int rc = 0;
|
|
|
|
pkdout_enabled = args->opts.log_stdout;
|
|
pkderr_enabled = args->opts.log_stderr;
|
|
|
|
/* Initialize the pkd context. */
|
|
ctx.rc = -1;
|
|
ctx.keep_going = 1;
|
|
ctx.pkd_ready = 0;
|
|
rc = pthread_create(&ctx.tid, NULL, &pkd_main, args);
|
|
assert_int_equal(rc, 0);
|
|
|
|
/* Busy-spin until pkd thread is ready. */
|
|
while (ctx.pkd_ready == 0);
|
|
|
|
return rc;
|
|
}
|
|
|
|
void pkd_stop(struct pkd_result *out) {
|
|
int rc = 0;
|
|
|
|
ctx.keep_going = 0;
|
|
close(pkd_state.server_fd);
|
|
|
|
rc = pthread_kill(ctx.tid, SIGUSR1);
|
|
assert_int_equal(rc, 0);
|
|
|
|
rc = pthread_join(ctx.tid, NULL);
|
|
assert_int_equal(rc, 0);
|
|
|
|
assert_non_null(out);
|
|
out->ok = (ctx.rc == 0);
|
|
|
|
return;
|
|
}
|