From 2135f6a83c8e1b39db8af0b470ab6d0349144be9 Mon Sep 17 00:00:00 2001 From: mlaier Date: Sat, 28 Feb 2004 16:52:45 +0000 Subject: Vendor import of OpenBSD's pf userland as of OpenBSD 3.4 Approved by: bms(mentor), core(in general) --- contrib/pf/ftp-proxy/ftp-proxy.8 | 253 ++++++++ contrib/pf/ftp-proxy/ftp-proxy.c | 1320 ++++++++++++++++++++++++++++++++++++++ contrib/pf/ftp-proxy/getline.c | 259 ++++++++ contrib/pf/ftp-proxy/util.c | 296 +++++++++ contrib/pf/ftp-proxy/util.h | 68 ++ 5 files changed, 2196 insertions(+) create mode 100644 contrib/pf/ftp-proxy/ftp-proxy.8 create mode 100644 contrib/pf/ftp-proxy/ftp-proxy.c create mode 100644 contrib/pf/ftp-proxy/getline.c create mode 100644 contrib/pf/ftp-proxy/util.c create mode 100644 contrib/pf/ftp-proxy/util.h (limited to 'contrib/pf/ftp-proxy') diff --git a/contrib/pf/ftp-proxy/ftp-proxy.8 b/contrib/pf/ftp-proxy/ftp-proxy.8 new file mode 100644 index 0000000..2832ddb --- /dev/null +++ b/contrib/pf/ftp-proxy/ftp-proxy.8 @@ -0,0 +1,253 @@ +.\" $OpenBSD: ftp-proxy.8,v 1.37 2003/09/05 12:27:47 jmc Exp $ +.\" +.\" Copyright (c) 1996-2001 +.\" Obtuse Systems Corporation, All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" 3. Neither the name of the University nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY OBTUSE SYSTEMS AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL OBTUSE OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd August 17, 2001 +.Dt FTP-PROXY 8 +.Os +.Sh NAME +.Nm ftp-proxy +.Nd Internet File Transfer Protocol proxy server +.Sh SYNOPSIS +.Nm ftp-proxy +.Op Fl AnrVw +.Op Fl D Ar debuglevel +.Op Fl g Ar group +.Op Fl m Ar minport +.Op Fl M Ar maxport +.Op Fl t Ar timeout +.Op Fl u Ar user +.Sh DESCRIPTION +.Nm +is a proxy for the Internet File Transfer Protocol. +The proxy uses +.Xr pf 4 +and expects to have the FTP control connection as described in +.Xr services 5 +redirected to it via a +.Xr pf 4 +.Em rdr +command. +An example of how to do that is further down in this document. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl A +Permit only anonymous FTP connections. +The proxy will allow connections to log in to other sites as the user +.Qq ftp +or +.Qq anonymous +only. +Any attempt to log in as another user will be blocked by the proxy. +.It Fl D Ar debuglevel +Specify a debug level, where the proxy emits verbose debug output +into +.Xr syslogd 8 +at level +.Dv LOG_DEBUG . +Meaningful values of debuglevel are 0-3, where 0 is no debug output and +3 is lots of debug output, the default being 0. +.It Fl g Ar group +Specify the named group to drop group privileges to, after doing +.Xr pf 4 +lookups which require root. +By default, +.Nm +uses the default group of the user it drops privilege to. +.It Fl m Ar minport +Specify the lower end of the port range the proxy will use for all +data connections it establishes. +The default is +.Dv IPPORT_HIFIRSTAUTO +defined in +.Aq Pa netinet/in.h +as 49152. +.It Fl M Ar maxport +Specify the upper end of the port range the proxy will use for the +data connections it establishes. +The default is +.Dv IPPORT_HILASTAUTO +defined in +.Aq Pa netinet/in.h +as 65535. +.It Fl n +Activate network address translation +.Pq NAT +mode. +In this mode, the proxy will not attempt to proxy passive mode +.Pq PASV or EPSV +data connections. +In order for this to work, the machine running the proxy will need to +be forwarding packets and doing network address translation to allow +the outbound passive connections from the client to reach the server. +See +.Xr pf.conf 5 +for more details on NAT. +The proxy only ignores passive mode data connections when using this flag; +it will still proxy PORT and EPRT mode data connections. +Without this flag, +.Nm +does not require any IP forwarding or NAT beyond the +.Em rdr +necessary to capture the FTP control connection. +.It Fl r +Use reverse host +.Pq reverse DNS +lookups for logging and libwrap use. +By default, +the proxy does not look up hostnames for libwrap or logging purposes. +.It Fl t Ar timeout +Specifies a timeout, in seconds. +The proxy will exit and close open connections if it sees no data +for the duration of the timeout. +The default is 0, which means the proxy will not time out. +.It Fl u Ar user +Specify the named user to drop privilege to, after doing +.Xr pf 4 +lookups which require root privilege. +By default, +.Nm +drops privilege to the user +.Em proxy . +.Pp +Running as root means that the source of data connections the proxy makes +for PORT and EPRT will be the RFC mandated port 20. +When running as a non-root user, the source of the data connections from +.Nm +will be chosen randomly from the range +.Ar minport +to +.Ar maxport +as described above. +.It Fl V +Be verbose. +With this option the proxy logs the control commands +sent by clients and the replies sent by the servers to +.Xr syslogd 8 . +.It Fl w +Use the tcp wrapper access control library +.Xr hosts_access 3 , +allowing connections to be allowed or denied based on the tcp wrapper's +.Xr hosts.allow 5 +and +.Xr hosts.deny 5 +files. +The proxy does libwrap operations after determining the destination +of the captured control connection, so that tcp wrapper rules may +be written based on the destination as well as the source of FTP connections. +.El +.Pp +.Nm ftp-proxy +is run from +.Xr inetd 8 +and requires that FTP connections are redirected to it using a +.Em rdr +rule. +A typical way to do this would be to use a +.Xr pf.conf 5 +rule such as +.Bd -literal -offset 2n +int_if = xl0 +rdr on $int_if proto tcp from any to any port 21 -> 127.0.0.1 port 8021 +.Ed +.Pp +.Xr inetd 8 +must then be configured to run +.Nm +on the port from above using +.Bd -literal -offset 2n +127.0.0.1:8021 stream tcp nowait root /usr/libexec/ftp-proxy ftp-proxy +.Ed +.Pp +in +.Xr inetd.conf 5 . +.Pp +.Nm +accepts the redirected control connections and forwards them +to the server. +The proxy replaces the address and port number that the client +sends through the control connection to the server with its own +address and proxy port, where it listens for the data connection. +When the server opens the data connection back to this port, the +proxy forwards it to the client. +The +.Xr pf.conf 5 +rules need to let pass connections to these proxy ports +(see options +.Fl u , m , +and +.Fl M +above) in on the external interface. +The following example allows only ports 49152 to 65535 to pass in +statefully: +.Bd -literal -offset indent +block in on $ext_if proto tcp all +pass in on $ext_if inet proto tcp from any to $ext_if \e + port > 49151 keep state +.Ed +.Pp +Alternatively, rules can make use of the fact that by default, +.Nm +runs as user +.Qq proxy +to allow the backchannel connections, as in the following example: +.Bd -literal -offset indent +block in on $ext_if proto tcp all +pass in on $ext_if inet proto tcp from any to $ext_if \e + user proxy keep state +.Ed +.Pp +These examples do not cover the connections from the proxy to the +foreign FTP server. +If one does not pass outgoing connections by default additional rules +are needed. +.Sh SEE ALSO +.Xr ftp 1 , +.Xr pf 4 , +.Xr hosts.allow 5 , +.Xr hosts.deny 5 , +.Xr inetd.conf 5 , +.Xr pf.conf 5 , +.Xr inetd 8 , +.Xr pfctl 8 , +.Xr syslogd 8 +.Sh BUGS +Extended Passive mode +.Pq EPSV +is not supported by the proxy and will not work unless the proxy is run +in network address translation mode. +When not in network address translation mode, the proxy returns an error +to the client, hopefully forcing the client to revert to passive mode +.Pq PASV +which is supported. +EPSV will work in network address translation mode, assuming a +.Xr pf.conf 5 +setup which allows the EPSV connections through to their destinations. +.Pp +IPv6 is not yet supported. diff --git a/contrib/pf/ftp-proxy/ftp-proxy.c b/contrib/pf/ftp-proxy/ftp-proxy.c new file mode 100644 index 0000000..88b6fd1 --- /dev/null +++ b/contrib/pf/ftp-proxy/ftp-proxy.c @@ -0,0 +1,1320 @@ +/* $OpenBSD: ftp-proxy.c,v 1.33 2003/08/22 21:50:34 david Exp $ */ + +/* + * Copyright (c) 1996-2001 + * Obtuse Systems Corporation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the Obtuse Systems nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY OBTUSE SYSTEMS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL OBTUSE SYSTEMS CORPORATION OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + * ftp proxy, Originally based on juniper_ftp_proxy from the Obtuse + * Systems juniper firewall, written by Dan Boulet + * and Bob Beck + * + * This version basically passes everything through unchanged except + * for the PORT and the * "227 Entering Passive Mode" reply. + * + * A PORT command is handled by noting the IP address and port number + * specified and then configuring a listen port on some very high port + * number and telling the server about it using a PORT message. + * We then watch for an in-bound connection on the port from the server + * and connect to the client's port when it happens. + * + * A "227 Entering Passive Mode" reply is handled by noting the IP address + * and port number specified and then configuring a listen port on some + * very high port number and telling the client about it using a + * "227 Entering Passive Mode" reply. + * We then watch for an in-bound connection on the port from the client + * and connect to the server's port when it happens. + * + * supports tcp wrapper lookups/access control with the -w flag using + * the real destination address - the tcp wrapper stuff is done after + * the real destination address is retrieved from pf + * + */ + +/* + * TODO: + * Plenty, this is very basic, with the idea to get it in clean first. + * + * - IPv6 and EPASV support + * - Content filter support + * - filename filter support + * - per-user rules perhaps. + */ + +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" + +#ifdef LIBWRAP +#include +int allow_severity = LOG_INFO; +int deny_severity = LOG_NOTICE; +#endif /* LIBWRAP */ + +int min_port = IPPORT_HIFIRSTAUTO; +int max_port = IPPORT_HILASTAUTO; + +#define STARTBUFSIZE 1024 /* Must be at least 3 */ + +/* + * Variables used to support PORT mode connections. + * + * This gets a bit complicated. + * + * If PORT mode is on then client_listen_sa describes the socket that + * the real client is listening on and server_listen_sa describes the + * socket that we are listening on (waiting for the real server to connect + * with us). + * + * If PASV mode is on then client_listen_sa describes the socket that + * we are listening on (waiting for the real client to connect to us on) + * and server_listen_sa describes the socket that the real server is + * listening on. + * + * If the socket we are listening on gets a connection then we connect + * to the other side's socket. Similarly, if a connected socket is + * shutdown then we shutdown the other side's socket. + */ + +double xfer_start_time; + +struct sockaddr_in real_server_sa; +struct sockaddr_in client_listen_sa; +struct sockaddr_in server_listen_sa; + +int client_listen_socket = -1; /* Only used in PASV mode */ +int client_data_socket = -1; /* Connected socket to real client */ +int server_listen_socket = -1; /* Only used in PORT mode */ +int server_data_socket = -1; /* Connected socket to real server */ +int client_data_bytes, server_data_bytes; + +int AnonFtpOnly; +int Verbose; +int NatMode; + +char ClientName[NI_MAXHOST]; +char RealServerName[NI_MAXHOST]; +char OurName[NI_MAXHOST]; + +char *User = "proxy"; +char *Group; + +extern int Debug_Level; +extern int Use_Rdns; +extern char *__progname; + +typedef enum { + UNKNOWN_MODE, + PORT_MODE, + PASV_MODE, + EPRT_MODE, + EPSV_MODE +} connection_mode_t; + +connection_mode_t connection_mode; + +extern void debuglog(int debug_level, const char *fmt, ...); +double wallclock_time(void); +void show_xfer_stats(void); +void log_control_command (char *cmd, int client); +int new_dataconn(int server); +void do_client_cmd(struct csiob *client, struct csiob *server); +void do_server_reply(struct csiob *server, struct csiob *client); +static void +usage(void) +{ + syslog(LOG_NOTICE, + "usage: %s [-AnrVw] [-D debuglevel] [-g group] %s %s", + __progname, "[-m minport] [-M maxport] [-t timeout]", + "[-u user]"); + exit(EX_USAGE); +} + +static void +close_client_data(void) +{ + if (client_data_socket >= 0) { + shutdown(client_data_socket, 2); + close(client_data_socket); + client_data_socket = -1; + } +} + +static void +close_server_data(void) +{ + if (server_data_socket >= 0) { + shutdown(server_data_socket, 2); + close(server_data_socket); + server_data_socket = -1; + } +} + +static void +drop_privs(void) +{ + struct passwd *pw; + struct group *gr; + uid_t uid = 0; + gid_t gid = 0; + + if (User != NULL) { + pw = getpwnam(User); + if (pw == NULL) { + syslog(LOG_ERR, "cannot find user %s", User); + exit(EX_USAGE); + } + uid = pw->pw_uid; + gid = pw->pw_gid; + } + + if (Group != NULL) { + gr = getgrnam(Group); + if (gr == NULL) { + syslog(LOG_ERR, "cannot find group %s", Group); + exit(EX_USAGE); + } + gid = gr->gr_gid; + } + + if (gid != 0 && (setegid(gid) == -1 || setgid(gid) == -1)) { + syslog(LOG_ERR, "cannot drop group privs (%m)"); + exit(EX_CONFIG); + } + + if (uid != 0 && (seteuid(uid) == -1 || setuid(uid) == -1)) { + syslog(LOG_ERR, "cannot drop root privs (%m)"); + exit(EX_CONFIG); + } +} + +#ifdef LIBWRAP +/* + * Check a connection against the tcpwrapper, log if we're going to + * reject it, returns: 0 -> reject, 1 -> accept. We add in hostnames + * if we are set to do reverse DNS, otherwise no. + */ +static int +check_host(struct sockaddr_in *client_sin, struct sockaddr_in *server_sin) +{ + char cname[NI_MAXHOST]; + char sname[NI_MAXHOST]; + struct request_info request; + int i; + + request_init(&request, RQ_DAEMON, __progname, RQ_CLIENT_SIN, + client_sin, RQ_SERVER_SIN, server_sin, RQ_CLIENT_ADDR, + inet_ntoa(client_sin->sin_addr), 0); + + if (Use_Rdns) { + /* + * We already looked these up, but we have to do it again + * for tcp wrapper, to ensure that we get the DNS name, since + * the tcp wrapper cares about these things, and we don't + * want to pass in a printed address as a name. + */ + i = getnameinfo((struct sockaddr *) &client_sin->sin_addr, + sizeof(&client_sin->sin_addr), cname, sizeof(cname), + NULL, 0, NI_NAMEREQD); + + if (i != 0 && i != EAI_NONAME && i != EAI_AGAIN) + strlcpy(cname, STRING_UNKNOWN, sizeof(cname)); + + i = getnameinfo((struct sockaddr *)&server_sin->sin_addr, + sizeof(&server_sin->sin_addr), sname, sizeof(sname), + NULL, 0, NI_NAMEREQD); + + if (i != 0 && i != EAI_NONAME && i != EAI_AGAIN) + strlcpy(sname, STRING_UNKNOWN, sizeof(sname)); + } else { + /* + * ensure the TCP wrapper doesn't start doing + * reverse DNS lookups if we aren't supposed to. + */ + strlcpy(cname, STRING_UNKNOWN, sizeof(cname)); + strlcpy(sname, STRING_UNKNOWN, sizeof(sname)); + } + + request_set(&request, RQ_SERVER_ADDR, inet_ntoa(server_sin->sin_addr), + 0); + request_set(&request, RQ_CLIENT_NAME, cname, RQ_SERVER_NAME, sname, 0); + + if (!hosts_access(&request)) { + syslog(LOG_NOTICE, "tcpwrappers rejected: %s -> %s", + ClientName, RealServerName); + return(0); + } + return(1); +} +#endif /* LIBWRAP */ + +double +wallclock_time(void) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + return(tv.tv_sec + tv.tv_usec / 1e6); +} + +/* + * Show the stats for this data transfer + */ +void +show_xfer_stats(void) +{ + char tbuf[1000]; + double delta; + size_t len; + int i; + + if (!Verbose) + return; + + delta = wallclock_time() - xfer_start_time; + + if (delta < 0.001) + delta = 0.001; + + if (client_data_bytes == 0 && server_data_bytes == 0) { + syslog(LOG_INFO, + "data transfer complete (no bytes transferred)"); + return; + } + + len = sizeof(tbuf); + + if (delta >= 60) { + int idelta; + + idelta = delta + 0.5; + if (idelta >= 60*60) { + i = snprintf(tbuf, len, + "data transfer complete (%dh %dm %ds", + idelta / (60*60), (idelta % (60*60)) / 60, + idelta % 60); + if (i >= len) + goto logit; + len -= i; + } else { + i = snprintf(tbuf, len, + "data transfer complete (%dm %ds", idelta / 60, + idelta % 60); + if (i >= len) + goto logit; + len -= i; + } + } else { + i = snprintf(tbuf, len, "data transfer complete (%.1fs", + delta); + if (i >= len) + goto logit; + len -= i; + } + + if (client_data_bytes > 0) { + i = snprintf(&tbuf[strlen(tbuf)], len, + ", %d bytes to server) (%.1fKB/s", client_data_bytes, + (client_data_bytes / delta) / (double)1024); + if (i >= len) + goto logit; + len -= i; + } + if (server_data_bytes > 0) { + i = snprintf(&tbuf[strlen(tbuf)], len, + ", %d bytes to client) (%.1fKB/s", server_data_bytes, + (server_data_bytes / delta) / (double)1024); + if (i >= len) + goto logit; + len -= i; + } + strlcat(tbuf, ")", sizeof(tbuf)); + logit: + syslog(LOG_INFO, "%s", tbuf); +} + +void +log_control_command (char *cmd, int client) +{ + /* log an ftp control command or reply */ + char *logstring; + int level = LOG_DEBUG; + + if (!Verbose) + return; + + /* don't log passwords */ + if (strncasecmp(cmd, "pass ", 5) == 0) + logstring = "PASS XXXX"; + else + logstring = cmd; + if (client) { + /* log interesting stuff at LOG_INFO, rest at LOG_DEBUG */ + if ((strncasecmp(cmd, "user ", 5) == 0) || + (strncasecmp(cmd, "retr ", 5) == 0) || + (strncasecmp(cmd, "cwd ", 4) == 0) || + (strncasecmp(cmd, "stor " ,5) == 0)) + level = LOG_INFO; + } + syslog(level, "%s %s", client ? "client:" : " server:", + logstring); +} + +/* + * set ourselves up for a new data connection. Direction is toward client if + * "server" is 0, towards server otherwise. + */ +int +new_dataconn(int server) +{ + /* + * Close existing data conn. + */ + + if (client_listen_socket != -1) { + close(client_listen_socket); + client_listen_socket = -1; + } + close_client_data(); + + if (server_listen_socket != -1) { + close(server_listen_socket); + server_listen_socket = -1; + } + close_server_data(); + + if (server) { + bzero(&server_listen_sa, sizeof(server_listen_sa)); + server_listen_socket = get_backchannel_socket(SOCK_STREAM, + min_port, max_port, -1, 1, &server_listen_sa); + + if (server_listen_socket == -1) { + syslog(LOG_INFO, "server socket bind() failed (%m)"); + exit(EX_OSERR); + } + if (listen(server_listen_socket, 5) != 0) { + syslog(LOG_INFO, "server socket listen() failed (%m)"); + exit(EX_OSERR); + } + } else { + bzero(&client_listen_sa, sizeof(client_listen_sa)); + client_listen_socket = get_backchannel_socket(SOCK_STREAM, + min_port, max_port, -1, 1, &client_listen_sa); + + if (client_listen_socket == -1) { + syslog(LOG_NOTICE, + "cannot get client listen socket (%m)"); + exit(EX_OSERR); + } + if (listen(client_listen_socket, 5) != 0) { + syslog(LOG_NOTICE, + "cannot listen on client socket (%m)"); + exit(EX_OSERR); + } + } + return(0); +} + +static void +connect_pasv_backchannel(void) +{ + struct sockaddr_in listen_sa; + socklen_t salen; + + /* + * We are about to accept a connection from the client. + * This is a PASV data connection. + */ + debuglog(2, "client listen socket ready"); + + close_server_data(); + close_client_data(); + + salen = sizeof(listen_sa); + client_data_socket = accept(client_listen_socket, + (struct sockaddr *)&listen_sa, &salen); + + if (client_data_socket < 0) { + syslog(LOG_NOTICE, "accept() failed (%m)"); + exit(EX_OSERR); + } + close(client_listen_socket); + client_listen_socket = -1; + memset(&listen_sa, 0, sizeof(listen_sa)); + + server_data_socket = get_backchannel_socket(SOCK_STREAM, min_port, + max_port, -1, 1, &listen_sa); + if (server_data_socket < 0) { + syslog(LOG_NOTICE, "get_backchannel_socket() failed (%m)"); + exit(EX_OSERR); + } + if (connect(server_data_socket, (struct sockaddr *) &server_listen_sa, + sizeof(server_listen_sa)) != 0) { + syslog(LOG_NOTICE, "connect() failed (%m)"); + exit(EX_NOHOST); + } + client_data_bytes = 0; + server_data_bytes = 0; + xfer_start_time = wallclock_time(); +} + +static void +connect_port_backchannel(void) +{ + struct sockaddr_in listen_sa; + socklen_t salen; + + /* + * We are about to accept a connection from the server. + * This is a PORT or EPRT data connection. + */ + debuglog(2, "server listen socket ready"); + + close_server_data(); + close_client_data(); + + salen = sizeof(listen_sa); + server_data_socket = accept(server_listen_socket, + (struct sockaddr *)&listen_sa, &salen); + if (server_data_socket < 0) { + syslog(LOG_NOTICE, "accept() failed (%m)"); + exit(EX_OSERR); + } + close(server_listen_socket); + server_listen_socket = -1; + + if (getuid() != 0) { + /* + * We're not running as root, so we get a backchannel + * socket bound in our designated range, instead of + * getting one bound to port 20 - This is deliberately + * not RFC compliant. + */ + bzero(&listen_sa.sin_addr, sizeof(struct in_addr)); + client_data_socket = get_backchannel_socket(SOCK_STREAM, + min_port, max_port, -1, 1, &listen_sa); + if (client_data_socket < 0) { + syslog(LOG_NOTICE, "get_backchannel_socket() failed (%m)"); + exit(EX_OSERR); + } + + } else { + + /* + * We're root, get our backchannel socket bound to port + * 20 here, so we're fully RFC compliant. + */ + client_data_socket = socket(AF_INET, SOCK_STREAM, 0); + + salen = 1; + listen_sa.sin_family = AF_INET; + bzero(&listen_sa.sin_addr, sizeof(struct in_addr)); + listen_sa.sin_port = htons(20); + + if (setsockopt(client_data_socket, SOL_SOCKET, SO_REUSEADDR, + &salen, sizeof(salen)) == -1) { + syslog(LOG_NOTICE, "setsockopt() failed (%m)"); + exit(EX_OSERR); + } + + if (bind(client_data_socket, (struct sockaddr *)&listen_sa, + sizeof(listen_sa)) == - 1) { + syslog(LOG_NOTICE, "data channel bind() failed (%m)"); + exit(EX_OSERR); + } + } + + if (connect(client_data_socket, (struct sockaddr *) &client_listen_sa, + sizeof(client_listen_sa)) != 0) { + syslog(LOG_INFO, "cannot connect data channel (%m)"); + exit(EX_NOHOST); + } + + client_data_bytes = 0; + server_data_bytes = 0; + xfer_start_time = wallclock_time(); +} + +void +do_client_cmd(struct csiob *client, struct csiob *server) +{ + int i, j, rv; + char tbuf[100]; + char *sendbuf = NULL; + + log_control_command((char *)client->line_buffer, 1); + + /* client->line_buffer is an ftp control command. + * There is no reason for these to be very long. + * In the interest of limiting buffer overrun attempts, + * we catch them here. + */ + if (strlen((char *)client->line_buffer) > 512) { + syslog(LOG_NOTICE, "excessively long control command"); + exit(EX_DATAERR); + } + + /* + * Check the client user provided if needed + */ + if (AnonFtpOnly && strncasecmp((char *)client->line_buffer, "user ", + strlen("user ")) == 0) { + char *cp; + + cp = (char *) client->line_buffer + strlen("user "); + if ((strcasecmp(cp, "ftp\r\n") != 0) && + (strcasecmp(cp, "anonymous\r\n") != 0)) { + /* + * this isn't anonymous - give the client an + * error before they send a password + */ + snprintf(tbuf, sizeof(tbuf), + "500 Only anonymous FTP is allowed\r\n"); + j = 0; + i = strlen(tbuf); + do { + rv = send(client->fd, tbuf + j, i - j, 0); + if (rv == -1 && errno != EAGAIN && + errno != EINTR) + break; + else if (rv != -1) + j += rv; + } while (j >= 0 && j < i); + sendbuf = NULL; + } else + sendbuf = (char *)client->line_buffer; + } else if ((strncasecmp((char *)client->line_buffer, "eprt ", + strlen("eprt ")) == 0)) { + + /* Watch out for EPRT commands */ + char *line = NULL, *q, *p, *result[3], delim; + struct addrinfo hints, *res = NULL; + unsigned long proto; + + j = 0; + line = strdup((char *)client->line_buffer+strlen("eprt ")); + if (line == NULL) { + syslog(LOG_ERR, "insufficient memory"); + exit(EX_UNAVAILABLE); + } + p = line; + delim = p[0]; + p++; + + memset(result,0, sizeof(result)); + for (i = 0; i < 3; i++) { + q = strchr(p, delim); + if (!q || *q != delim) + goto parsefail; + *q++ = '\0'; + result[i] = p; + p = q; + } + + proto = strtoul(result[0], &p, 10); + if (!*result[0] || *p) + goto protounsupp; + + memset(&hints, 0, sizeof(hints)); + if (proto != 1) /* 1 == AF_INET - all we support for now */ + goto protounsupp; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_NUMERICHOST; /*no DNS*/ + if (getaddrinfo(result[1], result[2], &hints, &res)) + goto parsefail; + if (res->ai_next) + goto parsefail; + if (sizeof(client_listen_sa) < res->ai_addrlen) + goto parsefail; + memcpy(&client_listen_sa, res->ai_addr, res->ai_addrlen); + + debuglog(1, "client wants us to use %s:%u", + inet_ntoa(client_listen_sa.sin_addr), + htons(client_listen_sa.sin_port)); + + /* + * Configure our own listen socket and tell the server about it + */ + new_dataconn(1); + connection_mode = EPRT_MODE; + + debuglog(1, "we want server to use %s:%u", + inet_ntoa(server->sa.sin_addr), + ntohs(server_listen_sa.sin_port)); + + snprintf(tbuf, sizeof(tbuf), "EPRT |%d|%s|%u|\r\n", 1, + inet_ntoa(server->sa.sin_addr), + ntohs(server_listen_sa.sin_port)); + debuglog(1, "to server (modified): %s", tbuf); + sendbuf = tbuf; + goto out; +parsefail: + snprintf(tbuf, sizeof(tbuf), + "500 Invalid argument; rejected\r\n"); + sendbuf = NULL; + goto out; +protounsupp: + /* we only support AF_INET for now */ + if (proto == 2) + snprintf(tbuf, sizeof(tbuf), + "522 Protocol not supported, use (1)\r\n"); + else + snprintf(tbuf, sizeof(tbuf), + "501 Protocol not supported\r\n"); + sendbuf = NULL; +out: + if (line) + free(line); + if (res) + freeaddrinfo(res); + if (sendbuf == NULL) { + debuglog(1, "to client (modified): %s", tbuf); + i = strlen(tbuf); + do { + rv = send(client->fd, tbuf + j, i - j, 0); + if (rv == -1 && errno != EAGAIN && + errno != EINTR) + break; + else if (rv != -1) + j += rv; + } while (j >= 0 && j < i); + } + } else if (!NatMode && (strncasecmp((char *)client->line_buffer, + "epsv", strlen("epsv")) == 0)) { + + /* + * If we aren't in NAT mode, deal with EPSV. + * EPSV is a problem - Unlike PASV, the reply from the + * server contains *only* a port, we can't modify the reply + * to the client and get the client to connect to us without + * resorting to using a dynamic rdr rule we have to add in + * for the reply to this connection, and take away afterwards. + * so this will wait until we have the right solution for rule + * additions/deletions in pf. + * + * in the meantime we just tell the client we don't do it, + * and most clients should fall back to using PASV. + */ + + snprintf(tbuf, sizeof(tbuf), + "500 EPSV command not understood\r\n"); + debuglog(1, "to client (modified): %s", tbuf); + j = 0; + i = strlen(tbuf); + do { + rv = send(client->fd, tbuf + j, i - j, 0); + if (rv == -1 && errno != EAGAIN && errno != EINTR) + break; + else if (rv != -1) + j += rv; + } while (j >= 0 && j < i); + sendbuf = NULL; + } else if (strncasecmp((char *)client->line_buffer, "port ", + strlen("port ")) == 0) { + unsigned int values[6]; + char *tailptr; + + debuglog(1, "Got a PORT command"); + + tailptr = (char *)&client->line_buffer[strlen("port ")]; + values[0] = 0; + + i = sscanf(tailptr, "%u,%u,%u,%u,%u,%u", &values[0], + &values[1], &values[2], &values[3], &values[4], + &values[5]); + if (i != 6) { + syslog(LOG_INFO, "malformed PORT command (%s)", + client->line_buffer); + exit(EX_DATAERR); + } + + for (i = 0; i<6; i++) { + if (values[i] > 255) { + syslog(LOG_INFO, + "malformed PORT command (%s)", + client->line_buffer); + exit(EX_DATAERR); + } + } + + client_listen_sa.sin_family = AF_INET; + client_listen_sa.sin_addr.s_addr = htonl((values[0] << 24) | + (values[1] << 16) | (values[2] << 8) | + (values[3] << 0)); + + client_listen_sa.sin_port = htons((values[4] << 8) | + values[5]); + debuglog(1, "client wants us to use %u.%u.%u.%u:%u", + values[0], values[1], values[2], values[3], + (values[4] << 8) | values[5]); + + /* + * Configure our own listen socket and tell the server about it + */ + new_dataconn(1); + connection_mode = PORT_MODE; + + debuglog(1, "we want server to use %s:%u", + inet_ntoa(server->sa.sin_addr), + ntohs(server_listen_sa.sin_port)); + + snprintf(tbuf, sizeof(tbuf), "PORT %u,%u,%u,%u,%u,%u\r\n", + ((u_char *)&server->sa.sin_addr.s_addr)[0], + ((u_char *)&server->sa.sin_addr.s_addr)[1], + ((u_char *)&server->sa.sin_addr.s_addr)[2], + ((u_char *)&server->sa.sin_addr.s_addr)[3], + ((u_char *)&server_listen_sa.sin_port)[0], + ((u_char *)&server_listen_sa.sin_port)[1]); + + debuglog(1, "to server (modified): %s", tbuf); + + sendbuf = tbuf; + } else + sendbuf = (char *)client->line_buffer; + + /* + *send our (possibly modified) control command in sendbuf + * on it's way to the server + */ + if (sendbuf != NULL) { + j = 0; + i = strlen(sendbuf); + do { + rv = send(server->fd, sendbuf + j, i - j, 0); + if (rv == -1 && errno != EAGAIN && errno != EINTR) + break; + else if (rv != -1) + j += rv; + } while (j >= 0 && j < i); + } +} + +void +do_server_reply(struct csiob *server, struct csiob *client) +{ + int code, i, j, rv; + struct in_addr *iap; + static int continuing = 0; + char tbuf[100], *sendbuf, *p; + + log_control_command((char *)server->line_buffer, 0); + + if (strlen((char *)server->line_buffer) > 512) { + /* + * someone's playing games. Have a cow in the syslogs and + * exit - we don't pass this on for fear of hurting + * our other end, which might be poorly implemented. + */ + syslog(LOG_NOTICE, "long FTP control reply"); + exit(EX_DATAERR); + } + + /* + * Watch out for "227 Entering Passive Mode ..." replies + */ + code = strtol((char *)server->line_buffer, &p, 10); + if (isspace(server->line_buffer[0])) + code = 0; + if (!*(server->line_buffer) || (*p != ' ' && *p != '-')) { + if (continuing) + goto sendit; + syslog(LOG_INFO, "malformed control reply"); + exit(EX_DATAERR); + } + if (code <= 0 || code > 999) { + if (continuing) + goto sendit; + syslog(LOG_INFO, "invalid server reply code %d", code); + exit(EX_DATAERR); + } + if (*p == '-') + continuing = 1; + else + continuing = 0; + if (code == 227 && !NatMode) { + unsigned int values[6]; + char *tailptr; + + debuglog(1, "Got a PASV reply"); + debuglog(1, "{%s}", (char *)server->line_buffer); + + tailptr = (char *)strchr((char *)server->line_buffer, '('); + if (tailptr == NULL) { + tailptr = strrchr((char *)server->line_buffer, ' '); + if (tailptr == NULL) { + syslog(LOG_NOTICE, "malformed 227 reply"); + exit(EX_DATAERR); + } + } + tailptr++; /* skip past space or ( */ + + values[0] = 0; + + i = sscanf(tailptr, "%u,%u,%u,%u,%u,%u", &values[0], + &values[1], &values[2], &values[3], &values[4], + &values[5]); + if (i != 6) { + syslog(LOG_INFO, "malformed PASV reply (%s)", + client->line_buffer); + exit(EX_DATAERR); + } + for (i = 0; i<6; i++) + if (values[i] > 255) { + syslog(LOG_INFO, "malformed PASV reply(%s)", + client->line_buffer); + exit(EX_DATAERR); + } + + server_listen_sa.sin_family = AF_INET; + server_listen_sa.sin_addr.s_addr = htonl((values[0] << 24) | + (values[1] << 16) | (values[2] << 8) | (values[3] << 0)); + server_listen_sa.sin_port = htons((values[4] << 8) | + values[5]); + + debuglog(1, "server wants us to use %s:%u", + inet_ntoa(server_listen_sa.sin_addr), (values[4] << 8) | + values[5]); + + new_dataconn(0); + connection_mode = PASV_MODE; + iap = &(server->sa.sin_addr); + + debuglog(1, "we want client to use %s:%u", inet_ntoa(*iap), + htons(client_listen_sa.sin_port)); + + snprintf(tbuf, sizeof(tbuf), + "227 Entering Passive Mode (%u,%u,%u,%u,%u,%u)\r\n", + ((u_char *)iap)[0], ((u_char *)iap)[1], + ((u_char *)iap)[2], ((u_char *)iap)[3], + ((u_char *)&client_listen_sa.sin_port)[0], + ((u_char *)&client_listen_sa.sin_port)[1]); + debuglog(1, "to client (modified): %s", tbuf); + sendbuf = tbuf; + } else { + sendit: + sendbuf = (char *)server->line_buffer; + } + + /* + * send our (possibly modified) control command in sendbuf + * on it's way to the client + */ + j = 0; + i = strlen(sendbuf); + do { + rv = send(client->fd, sendbuf + j, i - j, 0); + if (rv == -1 && errno != EAGAIN && errno != EINTR) + break; + else if (rv != -1) + j += rv; + } while (j >= 0 && j < i); + +} + +int +main(int argc, char *argv[]) +{ + struct csiob client_iob, server_iob; + struct sigaction new_sa, old_sa; + int sval, ch, flags, i; + socklen_t salen; + int one = 1; + long timeout_seconds = 0; + struct timeval tv; +#ifdef LIBWRAP + int use_tcpwrapper = 0; +#endif /* LIBWRAP */ + + while ((ch = getopt(argc, argv, "D:g:m:M:t:u:AnVwr")) != -1) { + char *p; + switch (ch) { + case 'A': + AnonFtpOnly = 1; /* restrict to anon usernames only */ + break; + case 'D': + Debug_Level = strtol(optarg, &p, 10); + if (!*optarg || *p) + usage(); + break; + case 'g': + Group = optarg; + break; + case 'm': + min_port = strtol(optarg, &p, 10); + if (!*optarg || *p) + usage(); + if (min_port < 0 || min_port > USHRT_MAX) + usage(); + break; + case 'M': + max_port = strtol(optarg, &p, 10); + if (!*optarg || *p) + usage(); + if (max_port < 0 || max_port > USHRT_MAX) + usage(); + break; + case 'n': + NatMode = 1; /* pass all passives, we're using NAT */ + break; + case 'r': + Use_Rdns = 1; /* look up hostnames */ + break; + case 't': + timeout_seconds = strtol(optarg, &p, 10); + if (!*optarg || *p) + usage(); + break; + case 'u': + User = optarg; + break; + case 'V': + Verbose = 1; + break; +#ifdef LIBWRAP + case 'w': + use_tcpwrapper = 1; /* do the libwrap thing */ + break; +#endif /* LIBWRAP */ + default: + usage(); + /* NOTREACHED */ + } + } + argc -= optind; + argv += optind; + + if (max_port < min_port) + usage(); + + openlog(__progname, LOG_NDELAY|LOG_PID, LOG_DAEMON); + + setlinebuf(stdout); + setlinebuf(stderr); + + memset(&client_iob, 0, sizeof(client_iob)); + memset(&server_iob, 0, sizeof(server_iob)); + + if (get_proxy_env(0, &real_server_sa, &client_iob.sa) == -1) + exit(EX_PROTOCOL); + + /* + * We may now drop root privs, as we have done our ioctl for + * pf. If we do drop root, we can't make backchannel connections + * for PORT and EPRT come from port 20, which is not strictly + * RFC compliant. This shouldn't cause problems for all but + * the stupidest ftp clients and the stupidest packet filters. + */ + drop_privs(); + + /* + * We check_host after get_proxy_env so that checks are done + * against the original destination endpoint, not the endpoint + * of our side of the rdr. This allows the use of tcpwrapper + * rules to restrict destinations as well as sources of connections + * for ftp. + */ + if (Use_Rdns) + flags = 0; + else + flags = NI_NUMERICHOST | NI_NUMERICSERV; + + i = getnameinfo((struct sockaddr *)&client_iob.sa, + sizeof(client_iob.sa), ClientName, sizeof(ClientName), NULL, 0, + flags); + + if (i != 0 && i != EAI_NONAME && i != EAI_AGAIN) { + debuglog(2, "name resolution failure (client)"); + exit(EX_OSERR); + } + + i = getnameinfo((struct sockaddr *)&real_server_sa, + sizeof(real_server_sa), RealServerName, sizeof(RealServerName), + NULL, 0, flags); + + if (i != 0 && i != EAI_NONAME && i != EAI_AGAIN) { + debuglog(2, "name resolution failure (server)"); + exit(EX_OSERR); + } + +#ifdef LIBWRAP + if (use_tcpwrapper && !check_host(&client_iob.sa, &real_server_sa)) + exit(EX_NOPERM); +#endif + + client_iob.fd = 0; + + syslog(LOG_INFO, "accepted connection from %s:%u to %s:%u", ClientName, + ntohs(client_iob.sa.sin_port), RealServerName, + ntohs(real_server_sa.sin_port)); + + server_iob.fd = get_backchannel_socket(SOCK_STREAM, min_port, max_port, + -1, 1, &server_iob.sa); + + if (connect(server_iob.fd, (struct sockaddr *)&real_server_sa, + sizeof(real_server_sa)) != 0) { + syslog(LOG_INFO, "cannot connect to %s:%u (%m)", RealServerName, + ntohs(real_server_sa.sin_port)); + exit(EX_NOHOST); + } + + /* + * Now that we are connected to the real server, get the name + * of our end of the server socket so we know our IP address + * from the real server's perspective. + */ + salen = sizeof(server_iob.sa); + getsockname(server_iob.fd, (struct sockaddr *)&server_iob.sa, &salen); + + i = getnameinfo((struct sockaddr *)&server_iob.sa, + sizeof(server_iob.sa), OurName, sizeof(OurName), NULL, 0, flags); + + if (i != 0 && i != EAI_NONAME && i != EAI_AGAIN) { + debuglog(2, "name resolution failure (local)"); + exit(EX_OSERR); + } + + debuglog(1, "local socket is %s:%u", OurName, + ntohs(server_iob.sa.sin_port)); + + /* ignore SIGPIPE */ + bzero(&new_sa, sizeof(new_sa)); + new_sa.sa_handler = SIG_IGN; + (void)sigemptyset(&new_sa.sa_mask); + new_sa.sa_flags = SA_RESTART; + if (sigaction(SIGPIPE, &new_sa, &old_sa) != 0) { + syslog(LOG_ERR, "sigaction() failed (%m)"); + exit(EX_OSERR); + } + + if (setsockopt(client_iob.fd, SOL_SOCKET, SO_OOBINLINE, (char *)&one, + sizeof(one)) == -1) { + syslog(LOG_NOTICE, "cannot set SO_OOBINLINE (%m)"); + exit(EX_OSERR); + } + + client_iob.line_buffer_size = STARTBUFSIZE; + client_iob.line_buffer = malloc(client_iob.line_buffer_size); + client_iob.io_buffer_size = STARTBUFSIZE; + client_iob.io_buffer = malloc(client_iob.io_buffer_size); + client_iob.next_byte = 0; + client_iob.io_buffer_len = 0; + client_iob.alive = 1; + client_iob.who = "client"; + client_iob.send_oob_flags = 0; + client_iob.real_sa = client_iob.sa; + + server_iob.line_buffer_size = STARTBUFSIZE; + server_iob.line_buffer = malloc(server_iob.line_buffer_size); + server_iob.io_buffer_size = STARTBUFSIZE; + server_iob.io_buffer = malloc(server_iob.io_buffer_size); + server_iob.next_byte = 0; + server_iob.io_buffer_len = 0; + server_iob.alive = 1; + server_iob.who = "server"; + server_iob.send_oob_flags = MSG_OOB; + server_iob.real_sa = real_server_sa; + + if (client_iob.line_buffer == NULL || client_iob.io_buffer == NULL || + server_iob.line_buffer == NULL || server_iob.io_buffer == NULL) { + syslog (LOG_NOTICE, "insufficient memory"); + exit(EX_UNAVAILABLE); + } + + while (client_iob.alive || server_iob.alive) { + int maxfd = 0; + fd_set *fdsp; + + if (client_iob.fd > maxfd) + maxfd = client_iob.fd; + if (client_listen_socket > maxfd) + maxfd = client_listen_socket; + if (client_data_socket > maxfd) + maxfd = client_data_socket; + if (server_iob.fd > maxfd) + maxfd = server_iob.fd; + if (server_listen_socket > maxfd) + maxfd = server_listen_socket; + if (server_data_socket > maxfd) + maxfd = server_data_socket; + + debuglog(3, "client is %s; server is %s", + client_iob.alive ? "alive" : "dead", + server_iob.alive ? "alive" : "dead"); + + fdsp = (fd_set *)calloc(howmany(maxfd + 1, NFDBITS), + sizeof(fd_mask)); + if (fdsp == NULL) { + syslog(LOG_NOTICE, "insufficient memory"); + exit(EX_UNAVAILABLE); + } + + if (client_iob.alive && telnet_getline(&client_iob, + &server_iob)) { + debuglog(3, "client line buffer is \"%s\"", + (char *)client_iob.line_buffer); + if (client_iob.line_buffer[0] != '\0') + do_client_cmd(&client_iob, &server_iob); + } else if (server_iob.alive && telnet_getline(&server_iob, + &client_iob)) { + debuglog(3, "server line buffer is \"%s\"", + (char *)server_iob.line_buffer); + if (server_iob.line_buffer[0] != '\0') + do_server_reply(&server_iob, &client_iob); + } else { + if (client_iob.alive) { + FD_SET(client_iob.fd, fdsp); + if (client_listen_socket >= 0) + FD_SET(client_listen_socket, fdsp); + if (client_data_socket >= 0) + FD_SET(client_data_socket, fdsp); + } + if (server_iob.alive) { + FD_SET(server_iob.fd, fdsp); + if (server_listen_socket >= 0) + FD_SET(server_listen_socket, fdsp); + if (server_data_socket >= 0) + FD_SET(server_data_socket, fdsp); + } + tv.tv_sec = timeout_seconds; + tv.tv_usec = 0; + + doselect: + sval = select(maxfd + 1, fdsp, NULL, NULL, + (tv.tv_sec == 0) ? NULL : &tv); + if (sval == 0) { + /* + * This proxy has timed out. Expire it + * quietly with an obituary in the syslogs + * for any passing mourners. + */ + syslog(LOG_INFO, + "timeout: no data for %ld seconds", + timeout_seconds); + exit(EX_OK); + } + if (sval == -1) { + if (errno == EINTR || errno == EAGAIN) + goto doselect; + syslog(LOG_NOTICE, + "select() failed (%m)"); + exit(EX_OSERR); + } + if (client_data_socket >= 0 && + FD_ISSET(client_data_socket, fdsp)) { + int rval; + + debuglog(3, "transfer: client to server"); + rval = xfer_data("client to server", + client_data_socket, + server_data_socket, + client_iob.sa.sin_addr, + real_server_sa.sin_addr); + if (rval <= 0) { + close_client_data(); + close_server_data(); + show_xfer_stats(); + } else + client_data_bytes += rval; + } + if (server_data_socket >= 0 && + FD_ISSET(server_data_socket, fdsp)) { + int rval; + + debuglog(3, "transfer: server to client"); + rval = xfer_data("server to client", + server_data_socket, + client_data_socket, + real_server_sa.sin_addr, + client_iob.sa.sin_addr); + if (rval <= 0) { + close_client_data(); + close_server_data(); + show_xfer_stats(); + } else + server_data_bytes += rval; + } + if (server_listen_socket >= 0 && + FD_ISSET(server_listen_socket, fdsp)) { + connect_port_backchannel(); + } + if (client_listen_socket >= 0 && + FD_ISSET(client_listen_socket, fdsp)) { + connect_pasv_backchannel(); + } + if (client_iob.alive && + FD_ISSET(client_iob.fd, fdsp)) { + client_iob.data_available = 1; + } + if (server_iob.alive && + FD_ISSET(server_iob.fd, fdsp)) { + server_iob.data_available = 1; + } + } + free(fdsp); + if (client_iob.got_eof) { + shutdown(server_iob.fd, 1); + shutdown(client_iob.fd, 0); + client_iob.got_eof = 0; + client_iob.alive = 0; + } + if (server_iob.got_eof) { + shutdown(client_iob.fd, 1); + shutdown(server_iob.fd, 0); + server_iob.got_eof = 0; + server_iob.alive = 0; + } + } + + if (Verbose) + syslog(LOG_INFO, "session ended"); + + exit(EX_OK); +} diff --git a/contrib/pf/ftp-proxy/getline.c b/contrib/pf/ftp-proxy/getline.c new file mode 100644 index 0000000..2be3883 --- /dev/null +++ b/contrib/pf/ftp-proxy/getline.c @@ -0,0 +1,259 @@ +/* $OpenBSD: getline.c,v 1.15 2003/06/28 01:04:57 deraadt Exp $ */ + +/* + * Copyright (c) 1985, 1988 Regents of the University of California. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * @(#)ftpcmd.y 5.24 (Berkeley) 2/25/91 + */ + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" + +int refill_buffer(struct csiob *iobp); + +/* + * Refill the io buffer if we KNOW that data is available + * + * Returns 1 if any new data was obtained, 0 otherwise. + */ + +int +refill_buffer(struct csiob *iobp) +{ + int rqlen, rlen; + + if (!(iobp->data_available)) + return(0); + + if (iobp->got_eof) + return(0); + + /* + * The buffer has been entirely consumed if next_byte == io_buffer_len. + * Otherwise, there is some still-to-be-used data in io_buffer. + * Shuffle it to the start of the buffer. + * Note that next_byte will never exceed io_buffer_len. + * Also, note that we MUST use bcopy because the two regions could + * overlap (memcpy isn't defined to work properly with overlapping + * regions). + */ + if (iobp->next_byte < iobp->io_buffer_len) { + int dst_ix = 0; + int src_ix = iobp->next_byte; + int amount = iobp->io_buffer_len - iobp->next_byte; + + bcopy(&iobp->io_buffer[src_ix], &iobp->io_buffer[dst_ix], + amount); + iobp->io_buffer_len = amount; + } else if (iobp->next_byte == iobp->io_buffer_len) + iobp->io_buffer_len = 0; + else { + syslog(LOG_ERR, "next_byte(%d) > io_buffer_len(%d)", + iobp->next_byte, iobp->io_buffer_len); + exit(EX_OSERR); + } + + iobp->next_byte = 0; + + /* don't do tiny reads, grow first if we need to */ + rqlen = iobp->io_buffer_size - iobp->io_buffer_len; + if (rqlen <= 128) { + char *tmp; + + iobp->io_buffer_size += 128; + tmp = realloc(iobp->io_buffer, iobp->io_buffer_size); + if (tmp == NULL) { + syslog(LOG_INFO, "Insufficient memory"); + exit(EX_UNAVAILABLE); + } + iobp->io_buffer = tmp; + rqlen = iobp->io_buffer_size - iobp->io_buffer_len; + } + + /* + * Always leave an unused byte at the end of the buffer + * because the debug output uses that byte from time to time + * to ensure that something that is being printed is \0 terminated. + */ + rqlen -= 1; + + doread: + rlen = read(iobp->fd, &iobp->io_buffer[iobp->io_buffer_len], rqlen); + iobp->data_available = 0; + switch (rlen) { + case -1: + if (errno == EAGAIN || errno == EINTR) + goto doread; + if (errno != ECONNRESET) { + syslog(LOG_INFO, "read() failed on socket from %s (%m)", + iobp->who); + exit(EX_DATAERR); + } + /* fall through to EOF case */ + case 0: + iobp->got_eof = 1; + return(0); + break; + default: + iobp->io_buffer_len += rlen; + break; + } + return(1); +} + +/* + * telnet_getline - a hacked up version of fgets to ignore TELNET escape codes. + * + * This code is derived from the getline routine found in the UC Berkeley + * ftpd code. + * + */ + +int +telnet_getline(struct csiob *iobp, struct csiob *telnet_passthrough) +{ + unsigned char ch; + int ix; + char tbuf[100]; + + iobp->line_buffer[0] = '\0'; + + /* + * If the buffer is empty then refill it right away. + */ + if (iobp->next_byte == iobp->io_buffer_len) + if (!refill_buffer(iobp)) + return(0); + + /* + * Is there a telnet command in the buffer? + */ + ch = iobp->io_buffer[iobp->next_byte]; + if (ch == IAC) { + /* + * Yes - buffer must have at least three bytes in it + */ + if (iobp->io_buffer_len - iobp->next_byte < 3) { + if (!refill_buffer(iobp)) + return(0); + if (iobp->io_buffer_len - iobp->next_byte < 3) + return(0); + } + + iobp->next_byte++; + ch = iobp->io_buffer[iobp->next_byte++]; + + switch (ch) { + case WILL: + case WONT: + case DO: + case DONT: + tbuf[0] = IAC; + tbuf[1] = ch; + tbuf[2] = iobp->io_buffer[iobp->next_byte++]; + (void)send(telnet_passthrough->fd, tbuf, 3, + telnet_passthrough->send_oob_flags); + break; + case IAC: + break; + default: + break; + } + return(1); + } else { + int clen; + + /* + * Is there a newline in the buffer? + */ + for (ix = iobp->next_byte; ix < iobp->io_buffer_len; + ix += 1) { + if (iobp->io_buffer[ix] == '\n') + break; + if (iobp->io_buffer[ix] == '\0') { + syslog(LOG_INFO, + "got NUL byte from %s - bye!", + iobp->who); + exit(EX_DATAERR); + } + } + + if (ix == iobp->io_buffer_len) { + if (!refill_buffer(iobp)) + return(0); + /* + * Empty line returned + * will try again soon! + */ + return(1); + } + + /* + * Expand the line buffer if it isn't big enough. We + * use a fudge factor of 5 rather than trying to + * figure out exactly how to account for the '\0 \r\n' and + * such. The correct fudge factor is 0, 1 or 2 but + * anything higher also works. We also grow it by a + * bunch to avoid having to do this often. Yes this is + * nasty. + */ + if (ix - iobp->next_byte > iobp->line_buffer_size - 5) { + char *tmp; + + iobp->line_buffer_size = 256 + ix - iobp->next_byte; + tmp = realloc(iobp->line_buffer, + iobp->line_buffer_size); + if (tmp == NULL) { + syslog(LOG_INFO, "Insufficient memory"); + exit(EX_UNAVAILABLE); + } + iobp->line_buffer = tmp; + } + + /* +1 is for the newline */ + clen = (ix+1) - iobp->next_byte; + memcpy(iobp->line_buffer, &iobp->io_buffer[iobp->next_byte], + clen); + iobp->next_byte += clen; + iobp->line_buffer[clen] = '\0'; + return(1); + } +} diff --git a/contrib/pf/ftp-proxy/util.c b/contrib/pf/ftp-proxy/util.c new file mode 100644 index 0000000..3c8b20e --- /dev/null +++ b/contrib/pf/ftp-proxy/util.c @@ -0,0 +1,296 @@ +/* $OpenBSD: util.c,v 1.16 2003/06/28 01:04:57 deraadt Exp $ */ + +/* + * Copyright (c) 1996-2001 + * Obtuse Systems Corporation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the Obtuse Systems nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE OBTUSE SYSTEMS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OBTUSE + * SYSTEMS CORPORATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" + +int Debug_Level; +int Use_Rdns; + +void debuglog(int debug_level, const char *fmt, ...); + +void +debuglog(int debug_level, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + + if (Debug_Level >= debug_level) + vsyslog(LOG_DEBUG, fmt, ap); + va_end(ap); +} + +int +get_proxy_env(int connected_fd, struct sockaddr_in *real_server_sa_ptr, + struct sockaddr_in *client_sa_ptr) +{ + struct pfioc_natlook natlook; + int slen, fd; + + slen = sizeof(*real_server_sa_ptr); + if (getsockname(connected_fd, (struct sockaddr *)real_server_sa_ptr, + &slen) != 0) { + syslog(LOG_ERR, "getsockname() failed (%m)"); + return(-1); + } + slen = sizeof(*client_sa_ptr); + if (getpeername(connected_fd, (struct sockaddr *)client_sa_ptr, + &slen) != 0) { + syslog(LOG_ERR, "getpeername() failed (%m)"); + return(-1); + } + + /* + * Build up the pf natlook structure. + * Just for IPv4 right now + */ + memset((void *)&natlook, 0, sizeof(natlook)); + natlook.af = AF_INET; + natlook.saddr.addr32[0] = client_sa_ptr->sin_addr.s_addr; + natlook.daddr.addr32[0] = real_server_sa_ptr->sin_addr.s_addr; + natlook.proto = IPPROTO_TCP; + natlook.sport = client_sa_ptr->sin_port; + natlook.dport = real_server_sa_ptr->sin_port; + natlook.direction = PF_OUT; + + /* + * Open the pf device and lookup the mapping pair to find + * the original address we were supposed to connect to. + */ + fd = open("/dev/pf", O_RDWR); + if (fd == -1) { + syslog(LOG_ERR, "cannot open /dev/pf (%m)"); + exit(EX_UNAVAILABLE); + } + + if (ioctl(fd, DIOCNATLOOK, &natlook) == -1) { + syslog(LOG_INFO, + "pf nat lookup failed %s:%hu (%m)", + inet_ntoa(client_sa_ptr->sin_addr), + ntohs(client_sa_ptr->sin_port)); + close(fd); + return(-1); + } + close(fd); + + /* + * Now jam the original address and port back into the into + * destination sockaddr_in for the proxy to deal with. + */ + memset((void *)real_server_sa_ptr, 0, sizeof(struct sockaddr_in)); + real_server_sa_ptr->sin_port = natlook.rdport; + real_server_sa_ptr->sin_addr.s_addr = natlook.rdaddr.addr32[0]; + real_server_sa_ptr->sin_len = sizeof(struct sockaddr_in); + real_server_sa_ptr->sin_family = AF_INET; + return(0); +} + + +/* + * Transfer one unit of data across a pair of sockets + * + * A unit of data is as much as we get with a single read(2) call. + */ +int +xfer_data(const char *what_read,int from_fd, int to_fd, struct in_addr from, + struct in_addr to) +{ + int rlen, offset, xerrno, mark, flags = 0; + char tbuf[4096]; + + /* + * Are we at the OOB mark? + */ + if (ioctl(from_fd, SIOCATMARK, &mark) < 0) { + xerrno = errno; + syslog(LOG_ERR, "cannot ioctl(SIOCATMARK) socket from %s (%m)", + what_read); + errno = xerrno; + return(-1); + } + if (mark) + flags = MSG_OOB; /* Yes - at the OOB mark */ + +snarf: + rlen = recv(from_fd, tbuf, sizeof(tbuf), flags); + if (rlen == -1 && flags == MSG_OOB && errno == EINVAL) { + /* OOB didn't work */ + flags = 0; + rlen = recv(from_fd, tbuf, sizeof(tbuf), flags); + } + if (rlen == 0) { + debuglog(3, "EOF on read socket"); + return(0); + } else if (rlen == -1) { + if (errno == EAGAIN || errno == EINTR) + goto snarf; + xerrno = errno; + syslog(LOG_ERR, "xfer_data (%s): failed (%m) with flags 0%o", + what_read, flags); + errno = xerrno; + return(-1); + } else { + offset = 0; + debuglog(3, "got %d bytes from socket", rlen); + + while (offset < rlen) { + int wlen; + fling: + wlen = send(to_fd, &tbuf[offset], rlen - offset, + flags); + if (wlen == 0) { + debuglog(3, "zero-length write"); + goto fling; + } else if (wlen == -1) { + if (errno == EAGAIN || errno == EINTR) + goto fling; + xerrno = errno; + syslog(LOG_INFO, "write failed (%m)"); + errno = xerrno; + return(-1); + } else { + debuglog(3, "wrote %d bytes to socket",wlen); + offset += wlen; + } + } + return(offset); + } +} + +/* + * get_backchannel_socket gets us a socket bound somewhere in a + * particular range of ports + */ +int +get_backchannel_socket(int type, int min_port, int max_port, int start_port, + int direction, struct sockaddr_in *sap) +{ + int count; + + /* + * Make sure that direction is 'defined' and that min_port is not + * greater than max_port. + */ + if (direction != -1) + direction = 1; + + /* by default we go up by one port until we find one */ + if (min_port > max_port) { + errno = EINVAL; + return(-1); + } + + count = 1 + max_port - min_port; + + /* + * Pick a port we can bind to from within the range we want. + * If the caller specifies -1 as the starting port number then + * we pick one somewhere in the range to try. + * This is an optimization intended to speedup port selection and + * has NOTHING to do with security. + */ + if (start_port == -1) + start_port = (arc4random() % count) + min_port; + + if (start_port < min_port || start_port > max_port) { + errno = EINVAL; + return(-1); + } + + while (count-- > 0) { + struct sockaddr_in sa; + int one, fd; + + fd = socket(AF_INET, type, 0); + + bzero(&sa, sizeof sa); + sa.sin_family = AF_INET; + if (sap == NULL) + sa.sin_addr.s_addr = INADDR_ANY; + else + sa.sin_addr.s_addr = sap->sin_addr.s_addr; + + /* + * Indicate that we want to reuse a port if it happens that the + * port in question was a listen port recently. + */ + one = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, + sizeof(one)) == -1) + return(-1); + + sa.sin_port = htons(start_port); + + if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) == 0) { + if (sap != NULL) + *sap = sa; + return(fd); + } + + if (errno != EADDRINUSE) + return(-1); + + /* if it's in use, try the next port */ + close(fd); + + start_port += direction; + if (start_port < min_port) + start_port = max_port; + else if (start_port > max_port) + start_port = min_port; + } + errno = EAGAIN; + return(-1); +} diff --git a/contrib/pf/ftp-proxy/util.h b/contrib/pf/ftp-proxy/util.h new file mode 100644 index 0000000..597cd18 --- /dev/null +++ b/contrib/pf/ftp-proxy/util.h @@ -0,0 +1,68 @@ +/* $OpenBSD: util.h,v 1.3 2002/05/23 10:22:14 deraadt Exp $ */ + +/* + * Copyright (c) 1996-2001 + * Obtuse Systems Corporation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 4. Neither the name of the Obtuse Systems nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL OBTUSE SYSTEMS CORPORATION OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +struct proxy_channel { + int pc_to_fd, pc_from_fd; + int pc_alive; + int pc_nextbyte; + int pc_flags; + int pc_length; + int pc_size; + struct sockaddr_in pc_from_sa, pc_to_sa; + int (*pc_filter)( void ** databuf, int datalen); + char *pc_buffer; +}; + +struct csiob { + int fd; + int line_buffer_size, io_buffer_size, io_buffer_len, next_byte; + unsigned char *io_buffer, *line_buffer; + struct sockaddr_in sa, real_sa; + char *who; + char alive, got_eof, data_available; + int send_oob_flags; +}; + +extern int telnet_getline(struct csiob *iobp, + struct csiob *telnet_passthrough); + +extern int get_proxy_env(int fd, struct sockaddr_in *server_sa_ptr, + struct sockaddr_in *client_sa_ptr); + +extern int get_backchannel_socket(int type, int min_port, int max_port, + int start_port, int direction, struct sockaddr_in *sap); + +extern int xfer_data(const char *what_read, int from_fd, int to_fd, + struct in_addr from, struct in_addr to); + +extern char *ProgName; + + -- cgit v1.1