diff options
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | GNUmakefile | 26 | ||||
-rw-r--r-- | src/main.c | 103 | ||||
-rw-r--r-- | src/pty.c | 128 | ||||
-rw-r--r-- | src/pty.h | 15 | ||||
-rw-r--r-- | src/session.c | 271 | ||||
-rw-r--r-- | src/session.h | 10 | ||||
-rw-r--r-- | src/util.h | 32 |
8 files changed, 591 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa0ede0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bastion +libssh/ +ssh_host_*_key +test_client.c +*.[do] +*.sw[a-p] diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..f6e0f7c --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,26 @@ +CC=clang +DEBUG=-g +CFLAGS= ${DEBUG} -Wall -Werror -Wextra + +sources=$(wildcard src/*.c) +OBJ=$(sources:.c=.o) + +all: bastion + +bastion: $(OBJ) + $(CC) ${DEBUG} -o bastion $(OBJ) -lssh -lpthread -lssh_threads -lutil + +clean: + $(RM) bastion *.[do] src/*.[do] + +%.o: %.c + $(CC) -c $(CFLAGS) $*.c -o $*.o + $(CC) -MM $(CFLAGS) $*.c > $*.d + @mv -f $*.d $*.d.tmp + @sed -e 's|.*:|$*.o:|' < $*.d.tmp > $*.d + @sed -e 's/.*://' -e 's/\\$$//' < $*.d.tmp | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $*.d + @rm -f $*.d.tmp + +valgrind: + valgrind --leak-check=full --show-leak-kinds=all --suppressions=$HOME/.valgrind_suppressions ./bastion + #valgrind -v --leak-check=full --show-leak-kinds=all --suppressions=$HOME/.valgrind_suppressions --gen-suppressions=yes ./bastion diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..cd8e74f --- /dev/null +++ b/src/main.c @@ -0,0 +1,103 @@ +#include <libssh/callbacks.h> +#include <libssh/server.h> +//#include <fcntl.h> +//#include <libutil.h> +//#include <poll.h> +//#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +//#include <sys/ioctl.h> +#include <sys/wait.h> +//#include <util.h> + +#include "session.h" + +/* SIGCHLD handler for cleaning up dead children. */ +static void sigchld_handler(int signo) { + (void) signo; + while (waitpid(-1, NULL, WNOHANG) > 0); +} + +int main() +{ + // Set up SIGCHLD handler + struct sigaction sa; + sa.sa_handler = sigchld_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; + if (sigaction(SIGCHLD, &sa, NULL) != 0) { + fprintf(stderr, "Failed to register SIGCHLD handler\n"); + return 1; + } + + // Initializing ssh context + ssh_threads_set_callbacks(ssh_threads_get_pthread()); + ssh_init(); + + // Initializing ssh_bind + ssh_bind sshbind = ssh_bind_new(); + if (sshbind == NULL) { + fprintf(stderr, "Error initializing ssh_bind\n"); + exit(-1); + } + int port = 2222; + ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDPORT, &port); + ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_DSAKEY, "ssh_host_dsa_key"); + ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, "ssh_host_rsa_key"); + ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_ECDSAKEY, "ssh_host_ecdsa_key"); + + if (ssh_bind_listen(sshbind) < 0) { + printf("Error listening to socket: %s\n", ssh_get_error(sshbind)); + return 1; + } + + while (1) { + ssh_session session = ssh_new(); + if (session == NULL) { + fprintf(stderr, "Error initializing ssh_session\n"); + break; + } + int verbosity = SSH_LOG_PROTOCOL; + ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity); + + // Blocks until there is a new incoming connection + if (ssh_bind_accept(sshbind,session) == SSH_OK){ + switch(fork()) { + case 0: + /* Remove the SIGCHLD handler inherited from parent. */ + sa.sa_handler = SIG_DFL; + sigaction(SIGCHLD, &sa, NULL); + /* Remove socket binding, which allows us to restart the + * parent process, without terminating existing sessions. */ + ssh_bind_free(sshbind); + + ssh_event event = ssh_event_new(); + if (event != NULL) { + /* Blocks until the SSH session ends by either + * child process exiting, or client disconnecting. */ + handle_session(event, session); + ssh_event_free(event); + } else { + fprintf(stderr, "Could not create polling context\n"); + } + ssh_disconnect(session); + ssh_free(session); + + exit(0); + case -1: + fprintf(stderr, "Failed to fork\n"); + } + } else { + fprintf(stderr, "Error accepting a connection : %s\n", ssh_get_error(sshbind)); + exit(1); + } + /* Since the session has been passed to a child fork, do some cleaning + * up at the parent process. */ + ssh_disconnect(session); + ssh_free(session); + } + + ssh_bind_free(sshbind); + ssh_finalize(); + return 0; +} diff --git a/src/pty.c b/src/pty.c new file mode 100644 index 0000000..c888df3 --- /dev/null +++ b/src/pty.c @@ -0,0 +1,128 @@ +#include <pty.h> +#include <stdio.h> +#include <stdlib.h> +#include <utmp.h> + +#include "pty.h" + +// callback function for SSH channel PTY request from a client +int pty_request(ssh_session session, ssh_channel channel, + const char *term, int cols, int rows, int py, int px, + void *userdata) { + struct channel_data_struct *cdata = (struct channel_data_struct *)userdata; + + (void) session; + (void) channel; + (void) term; + + cdata->winsize->ws_row = rows; + cdata->winsize->ws_col = cols; + cdata->winsize->ws_xpixel = px; + cdata->winsize->ws_ypixel = py; + + if (openpty(&cdata->pty_master, &cdata->pty_slave, NULL, NULL, + cdata->winsize) != 0) { + fprintf(stderr, "Failed to open pty\n"); + return SSH_ERROR; + } + return SSH_OK; +} + +// callback function for SSH channel PTY resize from a client +int pty_resize(ssh_session session, ssh_channel channel, int cols, + int rows, int py, int px, void *userdata) { + struct channel_data_struct *cdata = (struct channel_data_struct *)userdata; + + (void) session; + (void) channel; + + cdata->winsize->ws_row = rows; + cdata->winsize->ws_col = cols; + cdata->winsize->ws_xpixel = px; + cdata->winsize->ws_ypixel = py; + + if (cdata->pty_master != -1) { + return ioctl(cdata->pty_master, TIOCSWINSZ, cdata->winsize); + } + + return SSH_ERROR; +} + +int exec_pty(const char *mode, const char *command, + struct channel_data_struct *cdata) { + switch(cdata->pid = fork()) { + case -1: + close(cdata->pty_master); + close(cdata->pty_slave); + fprintf(stderr, "Failed to fork\n"); + return SSH_ERROR; + case 0: + close(cdata->pty_master); + if (login_tty(cdata->pty_slave) != 0) { + exit(1); + } + execl("/bin/sh", "sh", mode, command, NULL); + exit(0); + default: + close(cdata->pty_slave); + /* pty fd is bi-directional */ + cdata->child_stdout = cdata->child_stdin = cdata->pty_master; + } + return SSH_OK; +} + +int exec_nopty(const char *command, struct channel_data_struct *cdata) { + int in[2], out[2], err[2]; + + /* Do the plumbing to be able to talk with the child process. */ + if (pipe(in) != 0) { + goto stdin_failed; + } + if (pipe(out) != 0) { + goto stdout_failed; + } + if (pipe(err) != 0) { + goto stderr_failed; + } + + switch(cdata->pid = fork()) { + case -1: + goto fork_failed; + case 0: + /* Finish the plumbing in the child process. */ + close(in[1]); + close(out[0]); + close(err[0]); + dup2(in[0], STDIN_FILENO); + dup2(out[1], STDOUT_FILENO); + dup2(err[1], STDERR_FILENO); + close(in[0]); + close(out[1]); + close(err[1]); + /* exec the requested command. */ + execl("/bin/sh", "sh", "-c", command, NULL); + exit(0); + } + + close(in[0]); + close(out[1]); + close(err[1]); + + cdata->child_stdin = in[1]; + cdata->child_stdout = out[0]; + cdata->child_stderr = err[0]; + + return SSH_OK; + +fork_failed: + close(err[0]); + close(err[1]); +stderr_failed: + close(out[0]); + close(out[1]); +stdout_failed: + close(in[0]); + close(in[1]); +stdin_failed: + return SSH_ERROR; +} diff --git a/src/pty.h b/src/pty.h new file mode 100644 index 0000000..c130f4a --- /dev/null +++ b/src/pty.h @@ -0,0 +1,15 @@ +#ifndef _PTY_H_ +#define _PTY_H_ + +#include "util.h" + +int pty_request(ssh_session session, ssh_channel channel, + const char *term, int cols, int rows, int py, int px, + void *userdata); +int pty_resize(ssh_session session, ssh_channel channel, int cols, + int rows, int py, int px, void *userdata); +int exec_pty(const char *mode, const char *command, + struct channel_data_struct *cdata); +int exec_nopty(const char *command, struct channel_data_struct *cdata); + +#endif diff --git a/src/session.c b/src/session.c new file mode 100644 index 0000000..6186e6c --- /dev/null +++ b/src/session.c @@ -0,0 +1,271 @@ +#include <libssh/callbacks.h> +#include <libssh/server.h> +#include <poll.h> +#include <pty.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> + +#include "pty.h" +#include "session.h" + +#define USER "julien" +#define PASS "graou" +#define BUF_SIZE 1048576 +#define SESSION_END (SSH_CLOSED | SSH_CLOSED_ERROR) +#define SFTP_SERVER_PATH "/usr/lib/sftp-server" + +// callback function for channel data and exceptions +static int data_function(ssh_session session, ssh_channel channel, void *data, + uint32_t len, int is_stderr, void *userdata) { + struct channel_data_struct *cdata = (struct channel_data_struct *) userdata; + + (void) session; + (void) channel; + (void) is_stderr; + + if (len == 0 || cdata->pid < 1 || kill(cdata->pid, 0) < 0) { + return 0; + } + + return write(cdata->child_stdin, (char *) data, len); +} + +static int exec_request(ssh_session session, ssh_channel channel, + const char *command, void *userdata) { + struct channel_data_struct *cdata = (struct channel_data_struct *) userdata; + + + (void) session; + (void) channel; + + if(cdata->pid > 0) { + return SSH_ERROR; + } + + if (cdata->pty_master != -1 && cdata->pty_slave != -1) { + return exec_pty("-c", command, cdata); + } + return exec_nopty(command, cdata); +} + +static int shell_request(ssh_session session, ssh_channel channel, + void *userdata) { + struct channel_data_struct *cdata = (struct channel_data_struct *) userdata; + + (void) session; + (void) channel; + + if(cdata->pid > 0) { + return SSH_ERROR; + } + + if (cdata->pty_master != -1 && cdata->pty_slave != -1) { + return exec_pty("-l", NULL, cdata); + } + /* Client requested a shell without a pty, let's pretend we allow that */ + return SSH_OK; +} + +static int subsystem_request(ssh_session session, ssh_channel channel, + const char *subsystem, void *userdata) { + /* subsystem requests behave simillarly to exec requests. */ + if (strcmp(subsystem, "sftp") == 0) { + return exec_request(session, channel, SFTP_SERVER_PATH, userdata); + } + return SSH_ERROR; +} + +static int auth_password(ssh_session session, const char *user, + const char *pass, void *userdata) { + struct session_data_struct *sdata = (struct session_data_struct *) userdata; + + (void) session; + + if (strcmp(user, USER) == 0 && strcmp(pass, PASS) == 0) { + sdata->authenticated = 1; + return SSH_AUTH_SUCCESS; + } + + sdata->auth_attempts++; + return SSH_AUTH_DENIED; +} + +static ssh_channel channel_open(ssh_session session, void *userdata) { + struct session_data_struct *sdata = (struct session_data_struct *) userdata; + + sdata->channel = ssh_channel_new(session); + return sdata->channel; +} + +static int process_stdout(socket_t fd, int revents, void *userdata) { + char buf[BUF_SIZE]; + int n = -1; + ssh_channel channel = (ssh_channel) userdata; + + if (channel != NULL && (revents & POLLIN) != 0) { + n = read(fd, buf, BUF_SIZE); + if (n > 0) { + ssh_channel_write(channel, buf, n); + // TODO implement logging here + } + } + + return n; +} + +static int process_stderr(socket_t fd, int revents, void *userdata) { + char buf[BUF_SIZE]; + int n = -1; + ssh_channel channel = (ssh_channel) userdata; + + if (channel != NULL && (revents & POLLIN) != 0) { + n = read(fd, buf, BUF_SIZE); + if (n > 0) { + ssh_channel_write_stderr(channel, buf, n); + } + } + + return n; +} + +void handle_session(ssh_event event, ssh_session session) { + int n, rc; + + /* Structure for storing the pty size. */ + struct winsize wsize = { + .ws_row = 0, + .ws_col = 0, + .ws_xpixel = 0, + .ws_ypixel = 0 + }; + + /* Our struct holding information about the channel. */ + struct channel_data_struct cdata = { + .pid = 0, + .pty_master = -1, + .pty_slave = -1, + .child_stdin = -1, + .child_stdout = -1, + .child_stderr = -1, + .event = NULL, + .winsize = &wsize + }; + + /* Our struct holding information about the session. */ + struct session_data_struct sdata = { + .channel = NULL, + .auth_attempts = 0, + .authenticated = 0 + }; + + struct ssh_channel_callbacks_struct channel_cb = { + .userdata = &cdata, + .channel_pty_request_function = pty_request, + .channel_pty_window_change_function = pty_resize, + .channel_shell_request_function = shell_request, + .channel_exec_request_function = exec_request, + .channel_data_function = data_function, + .channel_subsystem_request_function = subsystem_request + }; + + struct ssh_server_callbacks_struct server_cb = { + .userdata = &sdata, + .auth_password_function = auth_password, + .channel_open_request_session_function = channel_open, + }; + + ssh_callbacks_init(&server_cb); + ssh_callbacks_init(&channel_cb); + + ssh_set_server_callbacks(session, &server_cb); + + if (ssh_handle_key_exchange(session) != SSH_OK) { + fprintf(stderr, "%s\n", ssh_get_error(session)); + return; + } + + ssh_set_auth_methods(session, SSH_AUTH_METHOD_PASSWORD); + ssh_event_add_session(event, session); + + n = 0; + while (sdata.authenticated == 0 || sdata.channel == NULL) { + /* If the user has used up all attempts, or if he hasn't been able to + * authenticate in 10 seconds (n * 100ms), disconnect. */ + if (sdata.auth_attempts >= 3 || n >= 100) { + return; + } + + if (ssh_event_dopoll(event, 100) == SSH_ERROR) { + fprintf(stderr, "%s\n", ssh_get_error(session)); + return; + } + n++; + } + + ssh_set_channel_callbacks(sdata.channel, &channel_cb); + + do { + /* Poll the main event which takes care of the session, the channel and + * even our child process's stdout/stderr (once it's started). */ + if (ssh_event_dopoll(event, -1) == SSH_ERROR) { + ssh_channel_close(sdata.channel); + } + + /* If child process's stdout/stderr has been registered with the event, + * or the child process hasn't started yet, continue. */ + if (cdata.event != NULL || cdata.pid == 0) { + continue; + } + /* Executed only once, once the child process starts. */ + cdata.event = event; + /* If stdout valid, add stdout to be monitored by the poll event. */ + if (cdata.child_stdout != -1) { + if (ssh_event_add_fd(event, cdata.child_stdout, POLLIN, process_stdout, + sdata.channel) != SSH_OK) { + fprintf(stderr, "Failed to register stdout to poll context\n"); + ssh_channel_close(sdata.channel); + } + } + + /* If stderr valid, add stderr to be monitored by the poll event. */ + if (cdata.child_stderr != -1){ + if (ssh_event_add_fd(event, cdata.child_stderr, POLLIN, process_stderr, + sdata.channel) != SSH_OK) { + fprintf(stderr, "Failed to register stderr to poll context\n"); + ssh_channel_close(sdata.channel); + } + } + } while(ssh_channel_is_open(sdata.channel) && + (cdata.pid == 0 || waitpid(cdata.pid, &rc, WNOHANG) == 0)); + + close(cdata.pty_master); + close(cdata.child_stdin); + close(cdata.child_stdout); + close(cdata.child_stderr); + + /* Remove the descriptors from the polling context, since they are now + * closed, they will always trigger during the poll calls. */ + ssh_event_remove_fd(event, cdata.child_stdout); + ssh_event_remove_fd(event, cdata.child_stderr); + + /* If the child process exited. */ + if (kill(cdata.pid, 0) < 0 && WIFEXITED(rc)) { + rc = WEXITSTATUS(rc); + ssh_channel_request_send_exit_status(sdata.channel, rc); + /* If client terminated the channel or the process did not exit nicely, + * but only if something has been forked. */ + } else if (cdata.pid > 0) { + kill(cdata.pid, SIGKILL); + } + + ssh_channel_send_eof(sdata.channel); + ssh_channel_close(sdata.channel); + + /* Wait up to 5 seconds for the client to terminate the session. */ + for (n = 0; n < 50 && (ssh_get_status(session) & SESSION_END) == 0; n++) { + ssh_event_dopoll(event, 100); + } +} diff --git a/src/session.h b/src/session.h new file mode 100644 index 0000000..2a4ea8d --- /dev/null +++ b/src/session.h @@ -0,0 +1,10 @@ +#ifndef _SESSION_H_ +#define _SESSION_H_ + +#include <libssh/libssh.h> + +#include "util.h" + +void handle_session(ssh_event event, ssh_session session); + +#endif diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..48a83ff --- /dev/null +++ b/src/util.h @@ -0,0 +1,32 @@ +#ifndef _UTIL_H_ +#define _UTIL_H_ + +#include <libssh/libssh.h> + +/* A userdata struct for channel. */ +struct channel_data_struct { + /* pid of the child process the channel will spawn. */ + pid_t pid; + /* For PTY allocation */ + socket_t pty_master; + socket_t pty_slave; + /* For communication with the child process. */ + socket_t child_stdin; + socket_t child_stdout; + /* Only used for subsystem and exec requests. */ + socket_t child_stderr; + /* Event which is used to poll the above descriptors. */ + ssh_event event; + /* Terminal size struct. */ + struct winsize *winsize; +}; + +/* A userdata struct for session. */ +struct session_data_struct { + /* Pointer to the channel the session will allocate. */ + ssh_channel channel; + int auth_attempts; + int authenticated; +}; + +#endif |