Skip to content

Instantly share code, notes, and snippets.

@mildsunrise
Last active January 10, 2022 00:20
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save mildsunrise/9b3c50226aa82a292a452107b34aca79 to your computer and use it in GitHub Desktop.
Save mildsunrise/9b3c50226aa82a292a452107b34aca79 to your computer and use it in GitHub Desktop.
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.

Note: This doesn't support HTTPS for now, only protocols where the socket can be passed directly to SSH.

Installation

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

wget https://gist.github.com/mildsunrise/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 (*p && (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;
}
@zjx20
Copy link

zjx20 commented Nov 2, 2020

@mildsunrise Thank you for sharing such a good approach, I have been using this for a few years. Recently I found the code isn't compatible with macOS 10.15, and here is the fix. The man page of strchr() says that The terminating null character is considered to be part of the string, so the original code causes a bad memory access. Hope it helps.

@@ -116,7 +116,7 @@ int scan_uri(char *name, struct uri_parts *parts) {
 	}
 
 	p = parts->host;
-	while (isalnum(*p) || strchr("-._~!$&'()*+,;=%:", *p) != NULL) p++;
+	while (*p && (isalnum(*p) || strchr("-._~!$&'()*+,;=%:", *p) != NULL)) p++;
 	if (*p == '@') {
 		*p++ = 0;
 		parts->auth = parts->host;

@mildsunrise
Copy link
Author

Fixed, thanks for spotting this 💙

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment