/* * ===================================================================== * UPDATE: this program is now maintained by Marcus Glocker, and a newer * version is available on http://www.nazgul.ch/dev.html * ===================================================================== * * gotthard.c : tunnel ssh through web proxy * * see http://www.kb.cert.org/vuls/id/150227 * * $ gcc -Wall -o gotthard gotthard.c * * $ ./gotthard ~/.gotthard.conf * * gotthard daemonizes (detaches from controlling terminal), binds to the * specified address and port and waits for connections. It forks for each * connection (supporting multiple concurrent connections) and connects * to the external host through the proxy. It strips all data prior to the * ssh version string and then forwards all data between the two connections. * * The configuration file has the following format: * * pid /path/to/gotthard.pid * log /path/to/gotthard.log * listen 127.0.0.1:2222 * proxy 10.1.2.3:8080 * external 12.34.56.78:443 * * Where 12.34.56.78 is running an sshd on port 443 (or forwards port 443 to * an sshd port). Note that some web proxies only allow port 80 or 443. Some * proxies also require additional authentication headers, which currently * have to be added to the source manually (grep for CONNECT below). If * you're unsure what headers and format to use, tcpdump/snoop an ordinary * https connection and copy. * * Since the tunnel is transparent, any ssh client can be used, as in * * $ ssh -p 2222 localhost * $ scp -P 2222 /local/file 127.0.0.1:/remote/path/ * * Of course, ssh itself allows further tunneling, as in * * $ ssh -p 2222 -L 6667:irc.openprojects.net:6667 localhost * * Then connect your irc client to localhost:6667, etc. * * Note that usage of such tunnels is likely to violate security policies. * Keeping a long-lifed ssh connection with -R forwarding open to allow * external connections back into the local network might be particularly * offending. Exercise common sense. * * * Copyright (c) 2001 Daniel Hartmeier * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Solaris quirks typedef int socklen_t; #define INADDR_NONE ((in_addr_t)-1) */ struct config { char *pid; char *log; char *addr_local; char *addr_proxy; char *addr_ext; unsigned port_local; unsigned port_proxy; unsigned port_ext; } config; void handle_client(int); int open_proxy(); int sync_write(int, const char *, int); void log(const char *, ...); void read_config(FILE *f); void parse_host(char *, char **, unsigned *); FILE *logfile = NULL, *pidfile = NULL; pid_t parentpid; void handle_signal(int sigraised) { switch (sigraised) { case SIGTERM: case SIGINT: log("SIGTERM/SIGINT, terminating gracefully"); if (logfile) { fflush(logfile); fclose(logfile); } if (pidfile) { fclose(pidfile); unlink(config.pid); } exit(0); break; case SIGCHLD: { pid_t pid; int status; while ((pid = waitpid(-1, &status, WNOHANG)) > 0) if (WIFEXITED(status)) log("child %i exited with status %i", pid, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) log("child %i exited due to signal %i", pid, WTERMSIG(status)); else log("child %i stopped"); signal(SIGCHLD, handle_signal); break; } case SIGHUP: signal(SIGHUP, handle_signal); break; case SIGQUIT: log("SIGQUIT, ignored"); signal(SIGQUIT, handle_signal); break; case SIGPIPE: log("SIGPIPE, ignored"); signal(SIGPIPE, handle_signal); break; case SIGALRM: log("SIGALRM, ignored"); signal(SIGALRM, handle_signal); break; default: log("unexpected signal %i, ignored", sigraised); break; } } int main(int argc, char *argv[]) { FILE *configfile = NULL; pid_t pid; int listen_fd = -1; struct sockaddr_in sa; socklen_t len; int val; int r; memset(&config, 0, sizeof(config)); if (argc != 2) { fprintf(stderr, "%s \n", argv[0]); exit(1); } configfile = fopen(argv[1], "rb"); if (!configfile) { perror("error opening config file"); exit(1); } read_config(configfile); fclose(configfile); configfile = NULL; if (!config.addr_local || !config.addr_proxy || !config.addr_ext || !config.port_local || !config.port_proxy || !config.port_ext) { fprintf(stderr, "config file incomplete\n"); exit(1); } /* detach from controlling terminal */ if ((pid = fork()) < 0) { perror("fork() failed"); goto error; } else if (pid > 0) exit(0); if ((pid = setsid()) == -1) { perror("setsid() failed"); goto error; } if ((pid = fork()) < 0) { perror("fork() failed"); goto error; } else if (pid > 0) exit(0); parentpid = getpid(); if (chdir("/")) { perror("chdir() failed"); goto error; } umask(0); signal(SIGCHLD, handle_signal); signal(SIGHUP , handle_signal); signal(SIGINT , handle_signal); signal(SIGQUIT, handle_signal); signal(SIGPIPE, handle_signal); signal(SIGALRM, handle_signal); signal(SIGTERM, handle_signal); if (config.pid) { char s[128]; if ((pidfile = fopen(config.pid, "rb"))) { pid_t pid; fread(s, 1, 128, pidfile); fclose(pidfile); pidfile = NULL; pid = atol(s); if (kill(pid, SIGHUP) && errno == ESRCH) { fprintf(stderr, "overwriting stale pid file\n"); } else { fprintf(stdout, "%s is already running (pid %u)\n", argv[0], pid); goto error; } } pidfile = fopen(config.pid, "wb"); if (!pidfile) { perror("error creating pid file"); goto error; } else { snprintf(s, 128, "%i\n", parentpid); fwrite(s, 1, strlen(s), pidfile); fflush(pidfile); } } if (config.log) { if (!(logfile = fopen(config.log, "ab"))) { perror("error opening log file"); goto error; } } close(0); close(1); close(2); open("/dev/null", O_RDWR); dup(logfile ? fileno(logfile) : 0); dup(logfile ? fileno(logfile) : 0); log("STARTED as %s %s", argv[0], argv[1]); log(" pid %s [%i]", config.pid, parentpid); log(" log %s", config.log); log(" listen %s:%u", config.addr_local, config.port_local); log(" proxy %s:%u", config.addr_proxy, config.port_proxy); log(" external %s:%u", config.addr_ext, config.port_ext); if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket() failed"); goto error; } if (fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFL) | O_NONBLOCK)) { perror("fcntl(F_SETFL, O_NONBLOCK) failed"); goto error; } val = 1; if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&val, sizeof(val))) { perror("setsockopt(SO_REUSEADDR)"); goto error; } memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_addr.s_addr = inet_addr(config.addr_local); sa.sin_port = htons(config.port_local); if (bind(listen_fd, (const struct sockaddr *)&sa, sizeof(sa))) { perror("bind() failed"); goto error; } if (listen(listen_fd, 1)) { perror("listen() failed"); goto error; } /* handle incoming client connections */ while (1) { fd_set readfds; struct timeval tv; FD_ZERO(&readfds); FD_SET(listen_fd, &readfds); tv.tv_sec = 10; tv.tv_usec = 0; r = select(listen_fd+1, &readfds, NULL, NULL, &tv); if (r < 0) { if (errno != EINTR) { perror("select() failed"); break; } } else if (r > 0 && FD_ISSET(listen_fd, &readfds)) { int client_fd; memset(&sa, 0, sizeof(sa)); len = sizeof(sa); client_fd = accept(listen_fd, (struct sockaddr *)&sa, &len); if (client_fd < 0 || len != sizeof(sa)) { perror("accept() failed"); break; } pid = fork(); if (pid < 0) { perror("fork() failed"); break; } if (pid) close(client_fd); else { close(listen_fd); log("connection from %s:%i", inet_ntoa(sa.sin_addr), ntohs(sa.sin_port)); handle_client(client_fd); close(client_fd); return 0; } } } error: if (listen_fd) close(listen_fd); if (configfile) fclose(configfile); if (pidfile) { fclose(pidfile); unlink(config.pid); } if (logfile) { fflush(logfile); fclose(logfile); } return 0; } void handle_client(int client_fd) { int proxy_fd; fd_set readfds; struct timeval tv; int r, max, len; char buf[65535]; time_t t; unsigned long bytes_in, bytes_out; proxy_fd = open_proxy(); if (!proxy_fd) { log("handle_client() can't open proxy connection"); return; } if (fcntl(proxy_fd, F_SETFL, fcntl(proxy_fd, F_GETFL) | O_NONBLOCK)) perror("fcntl(proxy_fd, F_SETFL, O_NONBLOCK) failed"); max = client_fd > proxy_fd ? client_fd : proxy_fd; t = time(0); bytes_in = bytes_out = 0; while (1) { FD_ZERO(&readfds); FD_SET(proxy_fd, &readfds); FD_SET(client_fd, &readfds); tv.tv_sec = 10; tv.tv_usec = 0; r = select(max+1, &readfds, NULL, NULL, &tv); if (r < 0) { if (errno != EINTR) { perror("select() failed"); break; } } else if (r > 0) { if (FD_ISSET(proxy_fd, &readfds)) { len = read(proxy_fd, buf, 65535); if (len) { if (sync_write(client_fd, buf, len)) { log("incomplete write"); break; } bytes_in += len; } else { log("connection closed by proxy"); break; } } if (FD_ISSET(client_fd, &readfds)) { len = read(client_fd, buf, 65535); if (len) { if (sync_write(proxy_fd, buf, len)) { log("incomplete write"); break; } bytes_out += len; } else { log("connection closed by client"); break; } } } } log("%lu bytes in, %lu bytes out, %u seconds", bytes_in, bytes_out, time(0)-t); } int open_proxy() { int fd; struct sockaddr_in sa; char buf[1024]; int i, len; if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket() failed"); return 0; } memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_addr.s_addr = inet_addr(config.addr_proxy); if (sa.sin_addr.s_addr == INADDR_NONE) { struct hostent *h = gethostbyname(config.addr_proxy); if (h == NULL) { log("invalid host name %s", config.addr_proxy); return 0; } memcpy(&sa.sin_addr.s_addr, h->h_addr, sizeof(in_addr_t)); } sa.sin_port = htons(config.port_proxy); if (connect(fd, (struct sockaddr *)&sa, sizeof(sa))) { perror("connect() failed"); close(fd); return 0; } snprintf(buf, 1024, "CONNECT %s:%u HTTP/1.0\r\nHost: %s\r\n\r\n", config.addr_ext, config.port_ext, config.addr_ext); write(fd, buf, strlen(buf)); i = 0; while ((len = read(fd, buf+i, 1)) > 0) { if (buf[i] == '\n' || i == 1023) { if (i && buf[i-1] == '\r') buf[i-1] = 0; else buf[i] = 0; i = 0; if (!buf[0]) return fd; } else i++; } if (len < 0) perror("read() failed"); log("didn't detect proxy response"); return 0; } int sync_write(int fd, const char *buf, int len) { int off = 0, r; fd_set writefds; struct timeval tv; while (len > off) { FD_ZERO(&writefds); FD_SET(fd, &writefds); tv.tv_sec = 10; tv.tv_usec = 0; r = select(fd+1, NULL, &writefds, NULL, &tv); if (r < 0) { if (errno != EINTR) { perror("select() failed"); return 1; } } else if (r > 0 && FD_ISSET(fd, &writefds)) { r = write(fd, buf+off, len-off); if (r < 0) { perror("write() failed"); return 1; } off += r; } } return 0; } void log(const char *format, ...) { time_t t; struct tm *tm; pid_t pid; va_list ap; t = time(0); tm = localtime(&t); if (tm) fprintf(stderr, "%4.4i.%2.2i.%2.2i %2.2i:%2.2i:%2.2i ", tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); pid = getpid(); if (pid == parentpid) fprintf(stderr, "*** "); else fprintf(stderr, "%u ", pid); va_start(ap, format); vfprintf(stderr, format, ap); va_end(ap); fprintf(stderr, "\n"); fflush(stderr); } void read_config(FILE *f) { char b[1024]; int i = 0; while (fread(b+i, 1, 1, f)) { if (b[i] == '\n' || i == 1023) { b[i] = 0; if (i > 0 && b[0] != '#') { int j = 0, k; while (b[j] && isspace((int)b[j])) j++; k = j; while (b[k] && !isspace((int)b[k])) k++; if (b[k]) b[k++] = 0; while (b[k] && isspace((int)b[k])) k++; if (!strcmp(b+j, "pid")) config.pid = strdup(b+k); else if (!strcmp(b+j, "log")) config.log = strdup(b+k); else if (!strcmp(b+j, "listen")) parse_host(b+k, &config.addr_local, &config.port_local); else if (!strcmp(b+j, "proxy")) parse_host(b+k, &config.addr_proxy, &config.port_proxy); else if (!strcmp(b+j, "external")) parse_host(b+k, &config.addr_ext, &config.port_ext); else fprintf(stderr, "ignored unknown option %s\n", b+j); } i = 0; } else i++; } } void parse_host(char *s, char **addr, unsigned *port) { int i = 0; while (s[i] && s[i] != ':') i++; if (s[i]) s[i++] = 0; *addr = strdup(s); *port = atoi(s+i); }