Skip to content

Instantly share code, notes, and snippets.

@jmendeth jmendeth/README.md
Last active Jul 25, 2019

Embed
What would you like to do?
Helper program to add HTTP/SOCKS proxy support to SSH

ssh-proxy-dialer

This program adds proxy support to ssh. Once installed, ssh will obey the ssh_proxy environment variable (or all_proxy as a fallback) and will try to connect to the server through that proxy. Example:

export ssh_proxy="socks5://10.139.2.1:8066"
ssh example.com  # will connect through SOCKS5 proxy

export ssh_proxy="http://myproxy.com:3128"
ssh example.com  # will connect through HTTP proxy

export ssh_proxy="socks4://10.139.2.1:8066"
ssh example.com  # will connect through SOCKS4A proxy

ssh will work just as before if no proxy is set. The nice thing about this is, after the proxied connection has been established, this program just passes the socket FD back to ssh and exits. No extra processes and zero overhead.

Installation

No dependencies, just compile and put somewhere in your PATH:

wget https://gist.github.com/jmendeth/9b3c50226aa82a292a452107b34aca79/raw/ssh-proxy-dialer.c
cc ssh-proxy-dialer.c && sudo install a.out /usr/local/bin/ssh-proxy-dialer

Then, append the following to your /etc/ssh/ssh_config:

Match exec "ssh-proxy-dialer test"
ProxyCommand ssh-proxy-dialer dial '%h' '%p'
ProxyUseFdpass yes

Note: You need at least OpenSSH 6.4. If you run OSX 10.11 or newer you're fine, otherwise you may need to upgrade.

#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
#include <ctype.h>
#include <errno.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
char xdigitvalue(char x) {
if (x > '9') x += 9;
return x & 0xF;
}
void percent_decode(char *p) {
while (*p) {
if (!(*(p++) == '%' && isxdigit(p[0]) && isxdigit(p[1]))) continue;
char c = (xdigitvalue(p[0]) << 4) | xdigitvalue(p[1]);
if (!c) continue;
p[-1] = c;
memmove(p, p+2, strlen(p+2));
}
}
struct uri_parts {
char *scheme;
char *auth;
char *host;
int port;
char *path;
char *fragment;
};
/* Scans the URI in name, sets each part if present.
Note: this will destroy `name` contents.
Note: host is not validated.
Note: fields are not percent-decoded.
*/
int scan_uri(char *name, struct uri_parts *parts) {
char *p;
memset(parts, 0, sizeof(struct uri_parts));
if ((p = strchr(name, '#')) != NULL) {
*p++ = '\0';
parts->fragment = p;
}
if ((p = strchr(name, ' ')) != NULL) *p++ = '\0';
for (p = name; *p; p++) {
if (isspace((int) *p)) {
char *orig = p, *dest = p+1;
while ((*orig++ = *dest++));
p = p-1;
}
if (*p == '/' || *p == '#' || *p == '?')
break;
if (*p == ':') {
*p = 0;
parts->scheme = name;
name = p+1;
break;
}
}
p = name;
if (p[0] != '/' || p[1] != '/') return -1;
parts->host = p+2;
*p = 0;
p = strchr(parts->host, '/');
if (p) {
*p = 0;
parts->path = p+1;
}
p = parts->host + strlen(parts->host);
while (p > parts->host && isdigit(*(--p)));
if (*p == ':') {
*p++ = 0;
if (*p) {
parts->port = atoi(p);
if (!(parts->port > 0 && parts->port < 65536)) return -1;
}
}
p = parts->host;
while (isalnum(*p) || strchr("-._~!$&'()*+,;=%:", *p) != NULL) p++;
if (*p == '@') {
*p++ = 0;
parts->auth = parts->host;
parts->host = p;
}
if (!(parts->scheme && *parts->scheme && *parts->host)) return -1;
return 0;
}
int connect_to(const char *hostname, int port) {
struct addrinfo hints;
struct addrinfo *result, *rp;
int r, sock, connect_errno = -1;
char service [8];
assert(port > 0 && port < 65536);
r = sprintf(service, "%d", port);
assert(r >= 0);
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
r = getaddrinfo(hostname, service, &hints, &result);
if (r != 0) {
fprintf(stderr, "ssh: couldn't resolve %s: %s\n", hostname, gai_strerror(r));
exit(EXIT_FAILURE);
}
for (rp = result; rp != NULL; rp = rp->ai_next) {
sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sock == -1)
continue;
if (connect(sock, rp->ai_addr, rp->ai_addrlen) != -1)
break;
connect_errno = errno;
close(sock);
}
freeaddrinfo(result);
if (rp == NULL) {
fprintf(stderr, "ssh: couldn't connect to %s port %d: %s\n", hostname, port, strerror(connect_errno));
return -1;
}
return sock;
}
/* Borrowed from ssh source code */
int mm_send_fd(int sock, int fd) {
struct msghdr msg;
union {
struct cmsghdr hdr;
char buf[CMSG_SPACE(sizeof(int))];
} cmsgbuf;
struct cmsghdr *cmsg;
struct iovec vec;
char ch = '\0';
ssize_t n;
struct pollfd pfd;
memset(&msg, 0, sizeof(msg));
memset(&cmsgbuf, 0, sizeof(cmsgbuf));
msg.msg_control = (caddr_t)&cmsgbuf.buf;
msg.msg_controllen = sizeof(cmsgbuf.buf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
*(int *)CMSG_DATA(cmsg) = fd;
vec.iov_base = &ch;
vec.iov_len = 1;
msg.msg_iov = &vec;
msg.msg_iovlen = 1;
pfd.fd = sock;
pfd.events = POLLOUT;
while ((n = sendmsg(sock, &msg, 0)) == -1 &&
(errno == EAGAIN || errno == EINTR)) {
(void)poll(&pfd, 1, -1);
}
if (n == -1) {
fprintf(stderr, "%s: sendmsg(%d): %s", __func__, fd, strerror(errno));
return -1;
}
if (n != 1) {
fprintf(stderr, "%s: sendmsg: expected sent 1 got %zd", __func__, n);
return -1;
}
return 0;
}
// TODO: implement authentication
int dial_http(int sock, const char *proxy_auth, const char *hostname, int port) {
int r, terminator_len;
char terminator_str [24];
struct iovec iov[3];
char code_str [4];
size_t code_len = 0;
char c;
assert(port > 0 && port < 65536);
terminator_len = sprintf(terminator_str, ":%d HTTP/1.1\r\n\r\n", port);
assert(terminator_len >= 0);
iov[0].iov_base = "CONNECT ";
iov[0].iov_len = sizeof("CONNECT ")-1;
iov[1].iov_base = (char *) hostname;
iov[1].iov_len = strlen(hostname);
iov[2].iov_base = terminator_str;
iov[2].iov_len = terminator_len;
if ((r = writev(sock, iov, 3)) < 0) {
fprintf(stderr, "ssh: couldn't send request to proxy: %s\n", strerror(errno));
return 1;
}
while (1) {
if ((r = read(sock, &c, 1)) != 1) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
if (!isdigit(c) || code_len >= 3)
break;
code_str[code_len++] = c;
}
code_str[code_len] = 0;
if (code_len != 3 || (c != ' ' && c != '\r')) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
int line_empty = 0;
while (1) {
while (c != '\r') {
line_empty = 0;
if (c == '\n' || (r = read(sock, &c, 1)) != 1) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
}
if ((r = read(sock, &c, 1)) != 1 || c != '\n') {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
if (line_empty) break;
if ((r = read(sock, &c, 1)) != 1) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
line_empty = 1;
}
if (strcmp(code_str, "200") != 0) {
fprintf(stderr, "ssh: proxy rejected connection (%s response)\n", code_str);
return 1;
}
return 0;
}
int dial_socks4(int sock, const char *proxy_auth, const char *hostname, int port) {
int r;
char command [9], reply [8];
struct iovec iov [3];
in_addr_t ip = inet_addr(hostname);
uint16_t nport = htons(port);
if (ip == INADDR_NONE) ip = inet_addr("0.0.0.1");
else hostname = NULL;
if (proxy_auth)
fprintf(stderr, "ssh: warning: authentication not supported with SOCKS4\n");
/* Send connect command */
command[0] = 4;
command[1] = 1;
memcpy(&command[2], &nport, 2);
memcpy(&command[4], &ip, 4);
command[8] = 0;
iov[0].iov_base = command;
iov[0].iov_len = sizeof(command);
if (hostname) {
iov[1].iov_base = (char *) hostname;
iov[1].iov_len = strlen(hostname) + 1;
r = writev(sock, iov, 2);
} else {
r = writev(sock, iov, 1);
}
if (r < 0) {
fprintf(stderr, "ssh: couldn't send request to proxy: %s\n", strerror(errno));
return 1;
}
/* Receive response */
if ((r = read(sock, reply, sizeof(reply))) != sizeof(reply)) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
if (reply[0] != 0 || reply[1] < 0x5A || reply[1] > 0x5D) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
if (reply[1] != 0x5A) {
fprintf(stderr, "ssh: connection rejected by proxy (0x%02X)\n", reply[1]);
return 1;
}
return 0;
}
int dial_socks5(int sock, const char *proxy_auth, const char *hostname, int port) {
static const char* SOCKS5_MESSAGES [] = {
"OK",
"general failure",
"connection not allowed by ruleset",
"network unreachable",
"host unreachable",
"connection refused by destination host",
"TTL expired",
"command not supported / protocol error",
"address type not supported",
};
int r, to_discard;
char greeting [3], server_greeting [2], command [5], reply [5];
struct iovec iov [5];
in_addr_t ip = inet_addr(hostname);
uint16_t nport = htons(port);
char auth_method = proxy_auth ? 0x02 : 0x00;
/* Send greeting */
greeting[0] = 5;
greeting[1] = 1;
greeting[2] = auth_method;
if ((r = write(sock, greeting, sizeof(greeting))) != sizeof(greeting)) {
fprintf(stderr, "ssh: couldn't send greeting to proxy\n");
return 1;
}
/* Receive response */
if ((r = read(sock, server_greeting, sizeof(server_greeting))) != sizeof(server_greeting)) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
if (server_greeting[0] != 5 || (server_greeting[1] != auth_method && server_greeting[1] != -1)) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
if (server_greeting[1] == -1) {
fprintf(stderr, "ssh: authentication method not supported by SOCKS5 proxy\n");
return 1;
}
/* Authenticate */
if (auth_method) {
char version = 1, username_length, password_length;
char auth_reply [2];
char *username, *password;
if ((username = strdup(proxy_auth)) == NULL) abort();
for (password = username; *password; password++)
if (*password == ':') {
*password++ = 0;
break;
}
username_length = strlen(username);
password_length = strlen(password);
iov[0].iov_base = &version;
iov[0].iov_len = sizeof(version);
iov[1].iov_base = &username_length;
iov[1].iov_len = sizeof(username_length);
iov[2].iov_base = username;
iov[2].iov_len = username_length;
iov[3].iov_base = &password_length;
iov[3].iov_len = sizeof(password_length);
iov[4].iov_base = password;
iov[4].iov_len = password_length;
if ((r = writev(sock, iov, 5)) < 0) {
fprintf(stderr, "ssh: couldn't send authentication data to proxy\n");
free(username);
return 1;
}
if ((r = read(sock, auth_reply, sizeof(auth_reply))) != sizeof(auth_reply) || auth_reply[0] != version) {
fprintf(stderr, "ssh: malformed response\n");
free(username);
return 1;
}
if (auth_reply[1] != 0) {
fprintf(stderr, "ssh: proxy authentication failed\n");
free(username);
return 1;
}
free(username);
}
/* Send connect request */
command[0] = 5;
command[1] = 1;
command[2] = 0;
iov[0].iov_base = command;
iov[0].iov_len = 4;
// FIXME: implement IPv6 case
if (ip != INADDR_NONE) {
command[3] = 1;
iov[1].iov_base = &ip;
iov[1].iov_len = sizeof(ip);
} else {
command[3] = 3;
command[4] = strlen(hostname);
iov[0].iov_len = 5;
iov[1].iov_base = (char *) hostname;
iov[1].iov_len = strlen(hostname);
}
iov[2].iov_base = &nport;
iov[2].iov_len = sizeof(nport);
if ((r = writev(sock, iov, 3)) < 0) {
fprintf(stderr, "ssh: couldn't send request to proxy\n");
return 1;
}
/* Receive response */
if ((r = read(sock, reply, sizeof(reply))) != sizeof(reply)) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
if (reply[0] != 5 || reply[1] < 0 || reply[1] > 8 || reply[2] != 0) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
to_discard = 2;
switch (reply[3]) {
case 1:
to_discard += 3;
break;
case 3:
to_discard += reply[4];
break;
case 4:
to_discard += 15;
break;
default:
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
while (to_discard > 0) {
char c;
if ((r = read(sock, &c, 1)) != 1) {
fprintf(stderr, "ssh: malformed response\n");
return 1;
}
--to_discard;
}
if (reply[1] != 0) {
fprintf(stderr, "ssh: tunnel failed (0x%02X): %s\n", reply[1], SOCKS5_MESSAGES[reply[1]]);
return 1;
}
return 0;
}
struct proxy_protocol {
const char *scheme;
int default_port;
int (*dialer)(int sock, const char *proxy_auth, const char *hostname, int port);
};
struct proxy_protocol proxy_protocols[] = {
{ "http", 80, dial_http },
{ "socks4", 1080, dial_socks4 },
{ "socks", 1080, dial_socks5 },
{ "socks5", 1080, dial_socks5 },
{ NULL }
};
const struct proxy_protocol* match_scheme(const char *scheme) {
int i;
for (i = 0; proxy_protocols[i].scheme; i++)
if (strcasecmp(proxy_protocols[i].scheme, scheme) == 0)
return &proxy_protocols[i];
return NULL;
}
int dial_proxy(const char *proxy_url, const char *hostname, const char *port_str) {
int r, sock, port;
char *end_ptr, *tmpbuffer;
struct uri_parts proxy;
const struct proxy_protocol *protocol;
/* Parse port, validate hostname */
port = strtol(port_str, &end_ptr, 10);
if (!(*port_str && !(*end_ptr) && port > 0 && port < 65536)) {
fprintf(stderr, "Invalid port given\n");
return 1;
}
/* Parse proxy URL */
if (!proxy_url) {
fprintf(stderr, "No proxy URL set, please check your ssh config\n");
return 1;
}
if ((tmpbuffer = strdup(proxy_url)) == NULL) abort();
if ((r = scan_uri(tmpbuffer, &proxy))) {
fprintf(stderr, "ssh: Invalid proxy URL: \"%s\"\n", proxy_url);
return 1;
}
/* Match scheme */
if ((protocol = match_scheme(proxy.scheme)) == NULL) {
fprintf(stderr, "ssh: Unknown scheme \"%s\" in proxy URL: \"%s\"\n", proxy.scheme, proxy_url);
return 1;
}
if (!proxy.port)
proxy.port = protocol->default_port;
/* Connect to proxy */
if ((sock = connect_to(proxy.host, proxy.port)) == -1)
return 1;
/* Make connection */
if ((r = protocol->dialer(sock, proxy.auth, hostname, port)))
return 1;
/* Pass connected FD back to ssh */
if ((r = mm_send_fd(1, sock)))
return 1;
free(tmpbuffer);
return 0;
}
int main(int argc, char **argv) {
char *proxy_url = getenv("ssh_proxy");
if (proxy_url == NULL)
proxy_url = getenv("all_proxy");
if (argc == 2 && strcmp(argv[1], "test") == 0)
return (proxy_url && *proxy_url) ? 0 : 1;
if (argc == 4 && strcmp(argv[1], "dial") == 0)
return dial_proxy(proxy_url, argv[2], argv[3]);
fprintf(stderr, "Usage: %1$s test\n %1$s dial <host> <port>\n\n"
"Helper program to be invoked by ssh(1) to open a proxied connection\n"
"if $ssh_proxy or $all_proxy are set. To use, put something like:\n"
"\n"
" Match exec \"%1$s test\"\n"
" ProxyCommand %1$s dial '%%h' '%%p'\n"
" ProxyUseFdpass yes\n"
"\n"
"In your ssh config file.\n\n",
argv[0]);
return 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.