Hi, I often need to go through a SOCKS proxy to access certain sites. The diff below adds SOCKS5 support to ftp(1) for HTTP transfers, similar to curl(1). Enabled when http_proxy is set to a socks5:// URL.
Also fixes two existing memory leaks: proxyurl (set to NULL on line 646 before freeing) and sslpath (never freed). Tested with ssh -D and a few other SOCKS5 proxies. Also verified the existing HTTP proxy feature still works with squid(8). Index: usr.bin/ftp/fetch.c =================================================================== RCS file: /cvs/src/usr.bin/ftp/fetch.c,v retrieving revision 1.198 diff -u -p -u -r1.198 fetch.c --- usr.bin/ftp/fetch.c 18 Oct 2020 20:35:18 -0000 1.198 +++ usr.bin/ftp/fetch.c 26 Nov 2020 13:51:10 -0000 @@ -88,10 +88,14 @@ static int proxy_connect(int, char *, ch static int stdio_tls_write_wrapper(void *, const char *, int); static int stdio_tls_read_wrapper(void *, char *, int); #endif /* !NOSSL */ +static int read_fully(int, void *, size_t); +static int write_fully(int, const void *, size_t); +static int socks5_connect(int, const char *, const char *); #define FTP_URL "ftp://" /* ftp URL prefix */ #define HTTP_URL "http://" /* http URL prefix */ #define HTTPS_URL "https://" /* https URL prefix */ +#define SOCKS5_URL "socks5://" /* socks5 URL prefix */ #define FILE_URL "file:" /* file URL prefix */ #define FTP_PROXY "ftp_proxy" /* env var with ftp proxy location */ #define HTTP_PROXY "http_proxy" /* env var with http proxy location */ @@ -345,6 +349,7 @@ url_get(const char *origline, const char int save_errno; const size_t buflen = 128 * 1024; int chunked = 0; + enum proxy_scheme proxy = PROXY_NONE; direction = "received"; @@ -455,11 +460,16 @@ noslash: proxyurl = strdup(proxyenv); if (proxyurl == NULL) errx(1, "Can't allocate memory for proxy URL."); - if (strncasecmp(proxyurl, HTTP_URL, sizeof(HTTP_URL) - 1) == 0) + if (strncasecmp(proxyurl, HTTP_URL, sizeof(HTTP_URL) - 1) == 0) { host = proxyurl + sizeof(HTTP_URL) - 1; - else if (strncasecmp(proxyurl, FTP_URL, sizeof(FTP_URL) - 1) == 0) + proxy = PROXY_HTTP; + } else if (strncasecmp(proxyurl, FTP_URL, sizeof(FTP_URL) - 1) == 0) { host = proxyurl + sizeof(FTP_URL) - 1; - else { + proxy = PROXY_HTTP; /* Treat ftp:// as a HTTP proxy */ + } else if (strncasecmp(proxyurl, SOCKS5_URL, sizeof(SOCKS5_URL) - 1) == 0) { + host = proxyurl + sizeof(SOCKS5_URL) - 1; + proxy = PROXY_SOCKS5; + } else { warnx("Malformed proxy URL: %s", proxyenv); goto cleanup_url_get; } @@ -467,11 +477,14 @@ noslash: warnx("Malformed proxy URL: %s", proxyenv); goto cleanup_url_get; } + } + + if (proxy == PROXY_HTTP) { if (*--path == '\0') *path = '/'; /* add / back to real path */ path = strchr(host, '/'); /* remove trailing / on host */ if (!EMPTYSTRING(path)) - *path++ = '\0'; /* i guess this ++ is useless */ + *path = '\0'; path = strchr(host, '@'); /* look for credentials in proxy */ if (!EMPTYSTRING(path)) { @@ -623,9 +636,21 @@ noslash: port = NULL; #ifndef NOSSL - if (proxyenv && sslhost) + if (proxy == PROXY_HTTP && sslhost) proxy_connect(fd, sslhost, proxy_credentials); #endif /* !NOSSL */ + + if (proxy == PROXY_SOCKS5) { + portnum = strrchr(proxyhost, ':'); + if (portnum != NULL) + *portnum++ = '\0'; + else + portnum = ishttpsurl ? httpsport : httpport; + + if (socks5_connect(fd, proxyhost, portnum) != 0) + goto cleanup_url_get; + } + break; } freeaddrinfo(res0); @@ -641,9 +666,10 @@ noslash: #ifndef NOSSL if (ishttpsurl) { ssize_t ret; - if (proxyenv && sslpath) { + if (proxy == PROXY_HTTP && sslpath) { + /* HTTP proxy CONNECT handled above. */ ishttpsurl = 0; - proxyurl = NULL; + proxy = PROXY_NONE; path = sslpath; } if (sslhost == NULL) { @@ -707,7 +733,7 @@ noslash: #endif /* !NOSSL */ epath = url_encode(path); - if (proxyurl) { + if (proxy == PROXY_HTTP) { if (verbose) { fprintf(ttyout, "Requesting %s (via %s)\n", origline, proxyurl); @@ -1111,6 +1137,7 @@ cleanup_url_get: #endif /* !SMALL */ #ifndef NOSSL free(sslhost); + free(sslpath); #endif /* !NOSSL */ ftp_close(&fin, &tls, &fd); if (out >= 0 && out != fileno(stdout)) @@ -1681,6 +1708,161 @@ sockerror(struct tls *tls) } #endif return strerror(save_errno); +} + +static int +read_fully(int fd, void *buf, size_t len) +{ + ssize_t count; + char *p = buf; + + while ((count = read(fd, p, len)) > 0) { + len -= count; + p += count; + } + + return len == 0; +} + +static int +write_fully(int fd, const void *buf, size_t len) +{ + ssize_t count; + const char *p = buf; + + while ((count = write(fd, p, len)) > 0) { + len -= count; + p += count; + } + + return len == 0; +} + +/* + * Perform SOCKS5 handshake on a connected socket. Returns zero on + * success after which all subsequent data on the socket will be + * tunnelled to the target host. Does not support authentication. See + * RFC1928 for details. + */ +static int +socks5_connect(int socket, const char *host, const char *port) +{ + uint8_t packetbuf[NI_MAXHOST + 7]; + size_t bindlen = 0; + size_t domainlen = 0; + struct servent *servent = NULL; + uint16_t portnum = 0; + const char *errstr = NULL; + + domainlen = strlen(host); + if (domainlen >= NI_MAXHOST) { + warnx("Host name too long"); + return -1; + } + + servent = getservbyname(port, "tcp"); + if (servent != NULL) + portnum = servent->s_port; + else { + portnum = htons(strtonum(port, 1, UINT16_MAX, &errstr)); + if (errstr != NULL) { + warnx("Parsing port number: %s: %s", port, errstr); + return -1; + } + } + + /* + * 1. Send client hello with no authentication methods. + */ + + packetbuf[0] = 5; /* version */ + packetbuf[1] = 1; /* number of auth methods */ + packetbuf[2] = SOCKS_AUTH_NONE; /* auth method */ + + if (!write_fully(socket, packetbuf, 3)) { + warn("Error starting SOCKS5 handshake"); + return -1; + } + + /* + * 2. Wait for server hello. Check expected version and + * authentication method. + */ + + if (!read_fully(socket, packetbuf, 2)) { + warn("Error reading SOCKS5 handshake reply"); + return -1; + } + + if (packetbuf[0] /* version */ != 5) { + warnx("Bad version %d in reply from SOCKS5 server", + packetbuf[0]); + return -1; + } else if (packetbuf[1] /* auth method */ != SOCKS_AUTH_NONE) { + warnx("SOCKS5 server chose unsupported authentication method"); + return -1; + } + + /* + * 3. Send the connect request. The target host name is passed + * as a string and the DNS lookup will happen on the proxy + * server side. + */ + + packetbuf[0] = 5; /* version */ + packetbuf[1] = SOCKS_CMD_CONNECT; /* command */ + packetbuf[2] = 0; /* reserved */ + packetbuf[3] = SOCKS_ATYPE_DOMAIN; /* address type */ + packetbuf[4] = domainlen; /* name length */ + memcpy(packetbuf + 5, host, domainlen); + packetbuf[domainlen + 5] = portnum & 0xff; + packetbuf[domainlen + 6] = portnum >> 8; + + if (!write_fully(socket, packetbuf, domainlen + 7)) { + warn("Error sending SOCKS5 connect request"); + return -1; + } + + /* + * 4. Wait for the server reply and check the response code. + * Read and discard the bind address and port number that + * follows the reply header: this is usually a dummy value like + * 0.0.0.0 and not reliable. + */ + + if (!read_fully(socket, packetbuf, 4)) { + warn("Error reading SOCKS5 connect reply"); + return -1; + } + + if (packetbuf[0] /* version */ != 5) { + warnx("Bad version %d in reply from SOCKS5 server", + packetbuf[0]); + return -1; + } + + if (packetbuf[1] /* response code */ != SOCKS_REP_SUCCEEDED) { + warnx("SOCKS5 request failed with status %d", packetbuf[1]); + return -1; + } + + switch (packetbuf[3] /* address type */) { + case SOCKS_ATYPE_IPV4: bindlen = 4; break; + case SOCKS_ATYPE_IPV6: bindlen = 16; break; + default: + warnx("Unexpected SOCKS5 address type %d", packetbuf[3]); + return -1; + } + + if (!read_fully(socket, packetbuf + 4, bindlen + 2)) { + warn("Error reading SOCKS5 bind address"); + return -1; + } + + if (verbose) + fprintf(ttyout, "Connected to %s via SOCKS5 proxy\n", host); + + return 0; } #ifndef NOSSL Index: usr.bin/ftp/ftp.1 =================================================================== RCS file: /cvs/src/usr.bin/ftp/ftp.1,v retrieving revision 1.121 diff -u -p -u -r1.121 ftp.1 --- usr.bin/ftp/ftp.1 6 Sep 2020 09:15:04 -0000 1.121 +++ usr.bin/ftp/ftp.1 26 Nov 2020 13:51:10 -0000 @@ -1371,7 +1371,7 @@ with a password of An HTTP URL, retrieved using the HTTP protocol. If .Ev http_proxy -is defined, it is used as a URL to an HTTP proxy server. +is defined, it is used as a URL to an HTTP or SOCKS5 proxy server. If a .Ar user and @@ -1394,8 +1394,8 @@ using Basic authentication. An HTTPS URL, retrieved using the HTTPS protocol. If .Ev http_proxy -is defined, this HTTPS proxy server will be used to fetch the -file using the CONNECT method. +is defined, it is used as a URL to an HTTP or SOCKS5 proxy server. +If using an HTTP proxy server, it must support the CONNECT method. If a .Ar user and @@ -1753,7 +1753,8 @@ For default shell. URL of FTP proxy to use when making FTP URL requests (if not defined, use the standard FTP protocol). .It Ev http_proxy -URL of HTTP proxy to use when making HTTP or HTTPS URL requests. +URL of HTTP (http://) or SOCKS5 (socks5://) proxy to use when making +HTTP or HTTPS URL requests. .It Ev http_cookies Path of a Netscape-like cookiejar file to use when making HTTP or HTTPS URL requests. Index: usr.bin/ftp/ftp_var.h =================================================================== RCS file: /cvs/src/usr.bin/ftp/ftp_var.h,v retrieving revision 1.45 diff -u -p -u -r1.45 ftp_var.h --- usr.bin/ftp/ftp_var.h 1 Sep 2020 12:33:48 -0000 1.45 +++ usr.bin/ftp/ftp_var.h 26 Nov 2020 13:51:10 -0000 @@ -229,3 +229,33 @@ extern struct cmd cmdtab[]; extern struct tls_config *tls_config; extern int tls_session_fd; #endif /* !NOSSL */ + +enum proxy_scheme { + PROXY_NONE, PROXY_HTTP, PROXY_SOCKS5 +}; + +/* + * SOCKS protocol constants. + */ + +#define SOCKS_CMD_CONNECT 1 +#define SOCKS_CMD_BIND 2 +#define SOCKS_CMD_UDP_ASSOC 3 + +#define SOCKS_AUTH_NONE 0 +#define SOCKS_AUTH_GSSAPI 1 +#define SOCKS_AUTH_PASSWORD 2 + +#define SOCKS_ATYPE_IPV4 1 +#define SOCKS_ATYPE_DOMAIN 3 +#define SOCKS_ATYPE_IPV6 4 + +#define SOCKS_REP_SUCCEEDED 0 +#define SOCKS_REP_GENERAL_FAILURE 1 +#define SOCKS_REP_CONNECTION_NOT_ALLOWED 2 +#define SOCKS_REP_NETWORK_UNREACHABLE 3 +#define SOCKS_REP_HOST_UNREACHABLE 4 +#define SOCKS_REP_CONNECTION_REFUSED 5 +#define SOCKS_REP_TTL_EXPIRED 6 +#define SOCKS_REP_COMMAND_NOT_SUPPORTED 7 +#define SOCKS_REP_ADDRESS_NOT_SUPPORTED 8
