This diff adds a -s flag to ftp(1) to let the user specify the source IP address of the connection. This is useful when using ftp(1) over VPN tunnels or when an alternate source IP is required to fetch a file from a FTP/HTTP/HTTPS server due to access control policies.
The -s flag is present in ftp(1) on FreeBSD, NetBSD, DragonFly BSD, and even OS X so it is very portable in shell scripts. :) I have tested -s with both IPv4 and IPv6 with a variety of different flags. The following is the test script that I used during development to confirm that this diff does not break existing behavior and to ensure that -s works with related flags: http://lteo.net/stuff/ftp-test These are the test results: http://lteo.net/stuff/ftp-test-results.txt (with my IPv6 address masked out) Note: If you would like to replicate the test with my test script, please assign two IPv4 addresses and two IPv6 addresses to your egress interface. If you don't have IPv6 connectivity, you can run the script in IPv4-only mode with "ftp-test -4" Comments and feedback would be appreciated. Thanks, Lawrence Index: fetch.c =================================================================== RCS file: /cvs/src/usr.bin/ftp/fetch.c,v retrieving revision 1.103 diff -u -p -r1.103 fetch.c --- fetch.c 25 Aug 2010 20:32:37 -0000 1.103 +++ fetch.c 15 Apr 2012 02:08:43 -0000 @@ -179,7 +179,7 @@ url_get(const char *origline, const char char *hosttail, *cause = "unknown", *newline, *host, *port, *buf = NULL; char *epath, *redirurl, *loctail; int error, i, isftpurl = 0, isfileurl = 0, isredirect = 0, rval = -1; - struct addrinfo hints, *res0, *res; + struct addrinfo hints, *res0, *res, *ares = NULL; const char * volatile savefile; char * volatile proxyurl = NULL; char *cookie = NULL; @@ -198,6 +198,7 @@ url_get(const char *origline, const char #endif /* !SMALL */ SSL *ssl = NULL; int status; + int save_errno; direction = "received"; @@ -490,6 +491,16 @@ noslash: goto cleanup_url_get; } +#ifndef SMALL + if (srcaddr) { + error = getaddrinfo(srcaddr, NULL, &hints, &ares); + if (error) { + warnx("%s: %s", gai_strerror(error), srcaddr); + goto cleanup_url_get; + } + } +#endif /* !SMALL */ + s = -1; for (res = res0; res; res = res->ai_next) { if (getnameinfo(res->ai_addr, res->ai_addrlen, hbuf, @@ -504,6 +515,26 @@ noslash: continue; } +#ifndef SMALL + if (srcaddr) { + if (ares->ai_family != res->ai_family) { + close(s); + s = -1; + errno = EINVAL; + cause = "bind"; + continue; + } + if (bind(s, ares->ai_addr, ares->ai_addrlen) < 0) { + save_errno = errno; + close(s); + errno = save_errno; + s = -1; + cause = "bind"; + continue; + } + } +#endif /* !SMALL */ + again: if (connect(s, res->ai_addr, res->ai_addrlen) < 0) { int save_errno; @@ -532,6 +563,10 @@ again: break; } freeaddrinfo(res0); +#ifndef SMALL + if (srcaddr) + freeaddrinfo(ares); +#endif /* !SMALL */ if (s < 0) { warn("%s", cause); goto cleanup_url_get; Index: ftp.1 =================================================================== RCS file: /cvs/src/usr.bin/ftp/ftp.1,v retrieving revision 1.81 diff -u -p -r1.81 ftp.1 --- ftp.1 26 Jul 2010 21:31:34 -0000 1.81 +++ ftp.1 15 Apr 2012 02:08:43 -0000 @@ -42,10 +42,12 @@ .Op Fl k Ar seconds .Op Fl P Ar port .Op Fl r Ar seconds +.Op Fl s Ar srcaddr .Op Ar host Op Ar port .Nm ftp .Op Fl C .Op Fl o Ar output +.Op Fl s Ar srcaddr .Sm off .No ftp:// Oo Ar user : password No @ .Oc Ar host Oo : Ar port @@ -57,6 +59,7 @@ .Op Fl C .Op Fl c Ar cookie .Op Fl o Ar output +.Op Fl s Ar srcaddr .Sm off .No http:// Ar host Oo : Ar port .Oc No / Ar file @@ -66,6 +69,7 @@ .Op Fl C .Op Fl c Ar cookie .Op Fl o Ar output +.Op Fl s Ar srcaddr .Sm off .No https:// Ar host Oo : Ar port .Oc No / Ar file @@ -74,6 +78,7 @@ .Nm ftp .Op Fl C .Op Fl o Ar output +.Op Fl s Ar srcaddr .Sm off .No file: Ar file .Sm on @@ -81,6 +86,7 @@ .Nm ftp .Op Fl C .Op Fl o Ar output +.Op Fl s Ar srcaddr .Sm off .Ar host : No / Ar file Oo / .Oc @@ -220,6 +226,12 @@ if the server does not support passive c .It Fl r Ar seconds Retry to connect if failed, pausing for number of .Ar seconds . +.It Fl s Ar srcaddr +Use +.Ar srcaddr +on the local machine as the source address +of the connection. +Only useful on systems with more than one address. .It Fl t Enables packet tracing. .It Fl V Index: ftp.c =================================================================== RCS file: /cvs/src/usr.bin/ftp/ftp.c,v retrieving revision 1.81 diff -u -p -r1.81 ftp.c --- ftp.c 3 Sep 2010 03:49:37 -0000 1.81 +++ ftp.c 15 Apr 2012 02:08:44 -0000 @@ -114,7 +114,7 @@ hookup(char *host, char *port) { int s, tos, error; static char hostnamebuf[MAXHOSTNAMELEN]; - struct addrinfo hints, *res, *res0; + struct addrinfo hints, *res, *res0, *ares; char hbuf[NI_MAXHOST]; char *cause = "unknown"; socklen_t namelen; @@ -163,6 +163,24 @@ hookup(char *host, char *port) else strlcpy(hostnamebuf, host, sizeof(hostnamebuf)); hostname = hostnamebuf; + +#ifndef SMALL + if (srcaddr) { + struct addrinfo ahints; + + memset(&ahints, 0, sizeof(ahints)); + ahints.ai_family = family; + ahints.ai_socktype = SOCK_STREAM; + ahints.ai_protocol = 0; + + error = getaddrinfo(srcaddr, NULL, &ahints, &ares); + if (error) { + warnx("%s: %s", gai_strerror(error), srcaddr); + code = -1; + return (0); + } + } +#endif /* !SMALL */ s = -1; for (res = res0; res; res = res->ai_next) { @@ -183,6 +201,25 @@ hookup(char *host, char *port) cause = "socket"; continue; } +#ifndef SMALL + if (srcaddr) { + if (ares->ai_family != res->ai_family) { + close(s); + s = -1; + errno = EINVAL; + cause = "bind"; + continue; + } + if (bind(s, ares->ai_addr, ares->ai_addrlen) < 0) { + cause = "bind"; + error = errno; + close(s); + errno = error; + s = -1; + continue; + } + } +#endif /* !SMALL */ while ((error = connect(s, res->ai_addr, res->ai_addrlen)) < 0 && errno == EINTR) { ; @@ -218,6 +255,12 @@ hookup(char *host, char *port) namelen = res->ai_addrlen; freeaddrinfo(res0); res0 = res = NULL; +#ifndef SMALL + if (srcaddr) { + freeaddrinfo(ares); + ares = NULL; + } +#endif /* !SMALL */ if (getsockname(s, (struct sockaddr *)&myctladdr, &namelen) < 0) { warn("getsockname"); code = -1; @@ -1240,12 +1283,30 @@ initconn(void) u_int af, hal, pal; char *pasvcmd = NULL; socklen_t namelen; + struct addrinfo *ares; if (myctladdr.su_family == AF_INET6 && (IN6_IS_ADDR_LINKLOCAL(&myctladdr.su_sin6.sin6_addr) || IN6_IS_ADDR_SITELOCAL(&myctladdr.su_sin6.sin6_addr))) { warnx("use of scoped address can be troublesome"); } +#ifndef SMALL + if (srcaddr) { + struct addrinfo ahints; + + memset(&ahints, 0, sizeof(ahints)); + ahints.ai_family = family; + ahints.ai_socktype = SOCK_STREAM; + ahints.ai_protocol = 0; + + error = getaddrinfo(srcaddr, NULL, &ahints, &ares); + if (error) { + warnx("%s: %s", gai_strerror(error), srcaddr); + code = -1; + return (0); + } + } +#endif /* !SMALL */ reinit: if (passivemode) { data_addr = myctladdr; @@ -1255,6 +1316,13 @@ reinit: return (1); } #ifndef SMALL + if (srcaddr) { + if (bind(data, ares->ai_addr, ares->ai_addrlen) < 0) { + warn("bind"); + close(data); + return (1); + } + } if ((options & SO_DEBUG) && setsockopt(data, SOL_SOCKET, SO_DEBUG, (char *)&on, sizeof(on)) < 0) Index: ftp_var.h =================================================================== RCS file: /cvs/src/usr.bin/ftp/ftp_var.h,v retrieving revision 1.31 diff -u -p -r1.31 ftp_var.h --- ftp_var.h 2 Jul 2010 22:01:10 -0000 1.31 +++ ftp_var.h 15 Apr 2012 02:08:44 -0000 @@ -165,6 +165,7 @@ size_t cursor_argc; /* location of cu size_t cursor_argo; /* offset of cursor in margv[cursor_argc] */ char *cookiefile; /* cookie jar to use */ int resume; /* continue transfer */ +char *srcaddr; /* source address to bind to */ #endif /* !SMALL */ off_t bytes; /* current # of bytes read */ Index: main.c =================================================================== RCS file: /cvs/src/usr.bin/ftp/main.c,v retrieving revision 1.81 diff -u -p -r1.81 main.c --- main.c 29 Jun 2010 23:12:33 -0000 1.81 +++ main.c 15 Apr 2012 02:08:44 -0000 @@ -114,6 +114,7 @@ main(volatile int argc, char *argv[]) hist = NULL; cookiefile = NULL; resume = 0; + srcaddr = NULL; marg_sl = sl_init(); #endif /* !SMALL */ mark = HASHBYTES; @@ -174,7 +175,7 @@ main(volatile int argc, char *argv[]) cookiefile = getenv("http_cookies"); #endif /* !SMALL */ - while ((ch = getopt(argc, argv, "46AaCc:dEegik:mno:pP:r:tvV")) != -1) { + while ((ch = getopt(argc, argv, "46AaCc:dEegik:mno:pP:r:s:tvV")) != -1) { switch (ch) { case '4': family = PF_INET; @@ -275,6 +276,12 @@ main(volatile int argc, char *argv[]) } break; + case 's': +#ifndef SMALL + srcaddr = optarg; +#endif /* !SMALL */ + break; + case 't': trace = 1; break; @@ -756,15 +763,21 @@ usage(void) " %s [-C] " #endif /* !SMALL */ "[-o output] " +#ifndef SMALL + "[-s srcaddr] " +#endif /* !SMALL */ "ftp://[user:password@]host[:port]/file[/] ...\n" " %s " #ifndef SMALL "[-C] [-c cookie] " #endif /* !SMALL */ "[-o output] " +#ifndef SMALL + "[-s srcaddr] " +#endif /* !SMALL */ "http://host[:port]/file ...\n" #ifndef SMALL - " %s [-C] [-c cookie] [-o output] " + " %s [-C] [-c cookie] [-o output] [-s srcaddr] " "https://host[:port]/file ...\n" #endif /* !SMALL */ " %s " @@ -772,12 +785,19 @@ usage(void) "[-C] " #endif /* !SMALL */ "[-o output] " +#ifndef SMALL + "[-s srcaddr] " +#endif /* !SMALL */ "file:file ...\n" " %s " #ifndef SMALL "[-C] " #endif /* !SMALL */ - "[-o output] host:/file[/] ...\n", + "[-o output] " +#ifndef SMALL + "[-s srcaddr] " +#endif /* !SMALL */ + "host:/file[/] ...\n", #ifndef SMALL __progname, __progname, __progname, __progname, __progname, __progname);