Hi Steffen, On 11/27/20 05:59 AM, Steffen Nurpmeso wrote: > Nick Gasson wrote in > <87im9srza8....@bertha.nickg.me.uk>: > |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). > > By the way, the $SOCKS5_PROXY environment variable becomes used > for automatic selection of SOCKS5. (Some things on FreeBSD, > lynx(1), and, hm, the MUA i maintain, s-nail; maybe more.) >
(Sorry for the late reply.) Yes I see FreeBSD fetch added SOCKS5_PROXY recently. I've updated the diff below to support that too. Anyone interested? -- Thanks, Nick Index: usr.bin/ftp/fetch.c =================================================================== RCS file: /cvs/src/usr.bin/ftp/fetch.c,v retrieving revision 1.199 diff -u -p -u -r1.199 fetch.c --- usr.bin/ftp/fetch.c 1 Jan 2021 17:39:54 -0000 1.199 +++ usr.bin/ftp/fetch.c 2 Jan 2021 12:02:39 -0000 @@ -88,13 +88,18 @@ 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 */ +#define SOCKS5_PROXY "SOCKS5_PROXY" /* env var with socks5 proxy location */ #define EMPTYSTRING(x) ((x) == NULL || (*(x) == '\0')) @@ -345,6 +350,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 +461,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 +478,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 +637,25 @@ 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) { + close(fd); + errno = EINVAL; + fd = -1; + cause = "socks5"; + } + } + break; } freeaddrinfo(res0); @@ -641,9 +671,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 +738,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); @@ -1112,6 +1143,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)) @@ -1225,7 +1257,8 @@ auto_fetch(int argc, char *argv[], char char *xargv[5]; char *cp, *url, *host, *dir, *file, *portnum; char *username, *pass, *pathstart; - char *ftpproxy, *httpproxy; + char *ftpproxy, *httpproxy, *socks5env; + char *socks5proxy = NULL; int rval, xargc, lastfile; volatile int argpos; int dirhasglob, filehasglob, oautologin; @@ -1245,6 +1278,20 @@ auto_fetch(int argc, char *argv[], char ftpproxy = NULL; if ((httpproxy = getenv(HTTP_PROXY)) != NULL && *httpproxy == '\0') httpproxy = NULL; + if ((socks5env = getenv(SOCKS5_PROXY)) != NULL && *socks5env == '\0') + socks5env = NULL; + + /* + * If the SOCKS5_PROXY environment variable is set then this is + * used as the proxy for all http:// and ftp:// requests. + */ + if (socks5env != NULL) { + if (asprintf(&socks5proxy, "socks5://%s%s", socks5env, + strchr(socks5env, ':') ? "" : ":1080") == -1) + errx(1, "Cannot allocate memory for proxy URL"); + httpproxy = socks5proxy; + ftpproxy = socks5proxy; + } /* * Loop through as long as there's files to fetch. @@ -1523,6 +1570,7 @@ bad_ftp_url: } if (connected && rval != -1) disconnect(0, NULL); + free(socks5proxy); return (rval); } @@ -1682,6 +1730,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 2 Jan 2021 12:02:40 -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,17 @@ 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 SOCKS5_PROXY +Hostname or IP address of a SOCKS5 proxy to use when making HTTP or HTTPS +URL requests. An optional port can be specified separated with a colon. +If no port number is provided the default 1080 is used. This overrides +the value of +.Ev http_proxy +and +.Ev ftp_proxy +if set. .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 2 Jan 2021 12:02:40 -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