diff options
author | Camiel Dobbelaar <camield@cvs.openbsd.org> | 2005-05-26 04:38:36 +0000 |
---|---|---|
committer | Camiel Dobbelaar <camield@cvs.openbsd.org> | 2005-05-26 04:38:36 +0000 |
commit | f3bca145d07bf82861c2a131167ae8c5b8792e6d (patch) | |
tree | d22c40bdc30c9a03069adbe25987a73e5eddac97 | |
parent | e926c786ee7effb5dbbcb62123460110959befa5 (diff) |
Import new FTP proxy. Handles IPv6 and all FTP modes. It was
previously known as pftpx.
Not connected to the builds yet.
ok beck
-rw-r--r-- | usr.sbin/ftp-proxy/Makefile | 10 | ||||
-rw-r--r-- | usr.sbin/ftp-proxy/filter.c | 380 | ||||
-rw-r--r-- | usr.sbin/ftp-proxy/filter.h | 29 | ||||
-rw-r--r-- | usr.sbin/ftp-proxy/ftp-proxy.8 | 167 | ||||
-rw-r--r-- | usr.sbin/ftp-proxy/ftp-proxy.c | 1085 |
5 files changed, 1671 insertions, 0 deletions
diff --git a/usr.sbin/ftp-proxy/Makefile b/usr.sbin/ftp-proxy/Makefile new file mode 100644 index 00000000000..2d620c976bd --- /dev/null +++ b/usr.sbin/ftp-proxy/Makefile @@ -0,0 +1,10 @@ +PROG= ftp-proxy +SRCS= ftp-proxy.c filter.c +MAN= ftp-proxy.8 + +CFLAGS+= -I${.CURDIR} +CFLAGS+= -Wall -Wstrict-prototypes -Wmissing-prototypes -Wpointer-arith \ + -Wno-uninitialized +LDADD+= -levent + +.include <bsd.prog.mk> diff --git a/usr.sbin/ftp-proxy/filter.c b/usr.sbin/ftp-proxy/filter.c new file mode 100644 index 00000000000..b4e958aa64f --- /dev/null +++ b/usr.sbin/ftp-proxy/filter.c @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2004, 2005 Camiel Dobbelaar, <cd@sentia.nl> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/ioctl.h> +#include <sys/types.h> +#include <sys/socket.h> + +#include <net/if.h> +#include <net/pfvar.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> + +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include "filter.h" + +/* From netinet/in.h, but only _KERNEL_ gets them. */ +#define satosin(sa) ((struct sockaddr_in *)(sa)) +#define satosin6(sa) ((struct sockaddr_in6 *)(sa)) + +enum { TRANS_FILTER = 0, TRANS_NAT, TRANS_RDR, TRANS_SIZE }; + +int prepare_rule(u_int32_t, int, struct sockaddr *, struct sockaddr *, + u_int16_t); +int server_lookup4(struct sockaddr_in *, struct sockaddr_in *, + struct sockaddr_in *); +int server_lookup6(struct sockaddr_in6 *, struct sockaddr_in6 *, + struct sockaddr_in6 *); + +static struct pfioc_pooladdr pfp; +static struct pfioc_rule pfr; +static struct pfioc_trans pft; +static struct pfioc_trans_e pfte[TRANS_SIZE]; +static int dev; +static char *qname; + +int +add_filter(u_int32_t id, u_int8_t dir, struct sockaddr *src, + struct sockaddr *dst, u_int16_t d_port) +{ + if (!src || !dst || !d_port) { + errno = EINVAL; + return (-1); + } + + if (prepare_rule(id, PF_RULESET_FILTER, src, dst, d_port) == -1) + return (-1); + + pfr.rule.direction = dir; + if (ioctl(dev, DIOCADDRULE, &pfr) == -1) + return (-1); + + return (0); +} + +int +add_nat(u_int32_t id, struct sockaddr *src, struct sockaddr *dst, + u_int16_t d_port, struct sockaddr *nat, u_int16_t nat_range_low, + u_int16_t nat_range_high) +{ + if (!src || !dst || !d_port || !nat || !nat_range_low || + (src->sa_family != nat->sa_family)) { + errno = EINVAL; + return (-1); + } + + if (prepare_rule(id, PF_RULESET_NAT, src, dst, d_port) == -1) + return (-1); + + if (nat->sa_family == AF_INET) { + memcpy(&pfp.addr.addr.v.a.addr.v4, + &satosin(nat)->sin_addr.s_addr, 4); + memset(&pfp.addr.addr.v.a.mask.addr8, 255, 4); + } else { + memcpy(&pfp.addr.addr.v.a.addr.v6, + &satosin6(nat)->sin6_addr.s6_addr, 16); + memset(&pfp.addr.addr.v.a.mask.addr8, 255, 16); + } + if (ioctl(dev, DIOCADDADDR, &pfp) == -1) + return (-1); + + pfr.rule.rpool.proxy_port[0] = nat_range_low; + pfr.rule.rpool.proxy_port[1] = nat_range_high; + if (ioctl(dev, DIOCADDRULE, &pfr) == -1) + return (-1); + + return (0); +} + +int +add_rdr(u_int32_t id, struct sockaddr *src, struct sockaddr *dst, + u_int16_t d_port, struct sockaddr *rdr, u_int16_t rdr_port) +{ + if (!src || !dst || !d_port || !rdr || !rdr_port || + (src->sa_family != rdr->sa_family)) { + errno = EINVAL; + return (-1); + } + + if (prepare_rule(id, PF_RULESET_RDR, src, dst, d_port) == -1) + return (-1); + + if (rdr->sa_family == AF_INET) { + memcpy(&pfp.addr.addr.v.a.addr.v4, + &satosin(rdr)->sin_addr.s_addr, 4); + memset(&pfp.addr.addr.v.a.mask.addr8, 255, 4); + } else { + memcpy(&pfp.addr.addr.v.a.addr.v6, + &satosin6(rdr)->sin6_addr.s6_addr, 16); + memset(&pfp.addr.addr.v.a.mask.addr8, 255, 16); + } + if (ioctl(dev, DIOCADDADDR, &pfp) == -1) + return (-1); + + pfr.rule.rpool.proxy_port[0] = rdr_port; + if (ioctl(dev, DIOCADDRULE, &pfr) == -1) + return (-1); + + return (0); +} + +int +do_commit(void) +{ + if (ioctl(dev, DIOCXCOMMIT, &pft) == -1) + return (-1); + + return (0); +} + +int +do_rollback(void) +{ + if (ioctl(dev, DIOCXROLLBACK, &pft) == -1) + return (-1); + + return (0); +} + +void +init_filter(char *opt_qname) +{ + struct pf_status status; + + qname = opt_qname; + + dev = open("/dev/pf", O_RDWR); + if (dev == -1) + err(1, "/dev/pf"); + if (ioctl(dev, DIOCGETSTATUS, &status) == -1) + err(1, "DIOCGETSTATUS"); + if (!status.running) + errx(1, "pf is disabled"); +} + +int +prepare_commit(u_int32_t id) +{ + char an[PF_ANCHOR_NAME_SIZE]; + int i; + + memset(&pft, 0, sizeof pft); + pft.size = TRANS_SIZE; + pft.esize = sizeof pfte[0]; + pft.array = pfte; + + snprintf(an, PF_ANCHOR_NAME_SIZE, "%s/%d.%d", FTP_PROXY_ANCHOR, + getpid(), id); + for (i = 0; i < TRANS_SIZE; i++) { + memset(&pfte[i], 0, sizeof pfte[0]); + strlcpy(pfte[i].anchor, an, PF_ANCHOR_NAME_SIZE); + switch (i) { + case TRANS_FILTER: + pfte[i].rs_num = PF_RULESET_FILTER; + break; + case TRANS_NAT: + pfte[i].rs_num = PF_RULESET_NAT; + break; + case TRANS_RDR: + pfte[i].rs_num = PF_RULESET_RDR; + break; + default: + errno = EINVAL; + return (-1); + } + } + + if (ioctl(dev, DIOCXBEGIN, &pft) == -1) + return (-1); + + return (0); +} + +int +prepare_rule(u_int32_t id, int rs_num, struct sockaddr *src, + struct sockaddr *dst, u_int16_t d_port) +{ + char an[PF_ANCHOR_NAME_SIZE]; + + if ((src->sa_family != AF_INET && src->sa_family != AF_INET6) || + (src->sa_family != dst->sa_family)) { + errno = EPROTONOSUPPORT; + return (-1); + } + + memset(&pfp, 0, sizeof pfp); + memset(&pfr, 0, sizeof pfr); + snprintf(an, PF_ANCHOR_NAME_SIZE, "%s/%d.%d", FTP_PROXY_ANCHOR, + getpid(), id); + strlcpy(pfp.anchor, an, PF_ANCHOR_NAME_SIZE); + strlcpy(pfr.anchor, an, PF_ANCHOR_NAME_SIZE); + + switch (rs_num) { + case PF_RULESET_FILTER: + pfr.ticket = pfte[TRANS_FILTER].ticket; + break; + case PF_RULESET_NAT: + pfr.ticket = pfte[TRANS_NAT].ticket; + break; + case PF_RULESET_RDR: + pfr.ticket = pfte[TRANS_RDR].ticket; + break; + default: + errno = EINVAL; + return (-1); + } + if (ioctl(dev, DIOCBEGINADDRS, &pfp) == -1) + return (-1); + pfr.pool_ticket = pfp.ticket; + + /* Generic for all rule types. */ + pfr.rule.af = src->sa_family; + pfr.rule.proto = IPPROTO_TCP; + pfr.rule.src.addr.type = PF_ADDR_ADDRMASK; + pfr.rule.dst.addr.type = PF_ADDR_ADDRMASK; + if (src->sa_family == AF_INET) { + memcpy(&pfr.rule.src.addr.v.a.addr.v4, + &satosin(src)->sin_addr.s_addr, 4); + memset(&pfr.rule.src.addr.v.a.mask.addr8, 255, 4); + memcpy(&pfr.rule.dst.addr.v.a.addr.v4, + &satosin(dst)->sin_addr.s_addr, 4); + memset(&pfr.rule.dst.addr.v.a.mask.addr8, 255, 4); + } else { + memcpy(&pfr.rule.src.addr.v.a.addr.v6, + &satosin6(src)->sin6_addr.s6_addr, 16); + memset(&pfr.rule.src.addr.v.a.mask.addr8, 255, 16); + memcpy(&pfr.rule.dst.addr.v.a.addr.v6, + &satosin6(dst)->sin6_addr.s6_addr, 16); + memset(&pfr.rule.dst.addr.v.a.mask.addr8, 255, 16); + } + pfr.rule.dst.port_op = PF_OP_EQ; + pfr.rule.dst.port[0] = htons(d_port); + + switch (rs_num) { + case PF_RULESET_FILTER: + /* + * pass quick log inet[6] proto tcp \ + * from $src to $dst port = $d_port flags S/SAFR keep state + * (max 1) [queue qname] + */ + pfr.rule.action = PF_PASS; + pfr.rule.quick = 1; + pfr.rule.log = 1; + pfr.rule.keep_state = 1; + pfr.rule.flags = TH_SYN; + pfr.rule.flagset = (TH_SYN|TH_ACK|TH_FIN|TH_RST); + pfr.rule.max_states = 1; + if (qname != NULL) + strlcpy(pfr.rule.qname, qname, sizeof pfr.rule.qname); + break; + case PF_RULESET_NAT: + /* + * nat inet[6] proto tcp from $src to $dst port $d_port -> $nat + */ + pfr.rule.action = PF_NAT; + break; + case PF_RULESET_RDR: + /* + * rdr inet[6] proto tcp from $src to $dst port $d_port -> $rdr + */ + pfr.rule.action = PF_RDR; + break; + default: + errno = EINVAL; + return (-1); + } + + return (0); +} + +int +server_lookup(struct sockaddr *client, struct sockaddr *proxy, + struct sockaddr *server) +{ + if (client->sa_family == AF_INET) + return (server_lookup4(satosin(client), satosin(proxy), + satosin(server))); + + if (client->sa_family == AF_INET6) + return (server_lookup6(satosin6(client), satosin6(proxy), + satosin6(server))); + + errno = EPROTONOSUPPORT; + return (-1); +} + +int +server_lookup4(struct sockaddr_in *client, struct sockaddr_in *proxy, + struct sockaddr_in *server) +{ + struct pfioc_natlook pnl; + + memset(&pnl, 0, sizeof pnl); + pnl.direction = PF_OUT; + pnl.af = AF_INET; + pnl.proto = IPPROTO_TCP; + memcpy(&pnl.saddr.v4, &client->sin_addr.s_addr, sizeof pnl.saddr.v4); + memcpy(&pnl.daddr.v4, &proxy->sin_addr.s_addr, sizeof pnl.daddr.v4); + pnl.sport = client->sin_port; + pnl.dport = proxy->sin_port; + + if (ioctl(dev, DIOCNATLOOK, &pnl) == -1) + return (-1); + + memset(server, 0, sizeof(struct sockaddr_in)); + server->sin_len = sizeof(struct sockaddr_in); + server->sin_family = AF_INET; + memcpy(&server->sin_addr.s_addr, &pnl.rdaddr.v4, + sizeof server->sin_addr.s_addr); + server->sin_port = pnl.rdport; + + return (0); +} + +int +server_lookup6(struct sockaddr_in6 *client, struct sockaddr_in6 *proxy, + struct sockaddr_in6 *server) +{ + struct pfioc_natlook pnl; + + memset(&pnl, 0, sizeof pnl); + pnl.direction = PF_OUT; + pnl.af = AF_INET6; + pnl.proto = IPPROTO_TCP; + memcpy(&pnl.saddr.v6, &client->sin6_addr.s6_addr, sizeof pnl.saddr.v6); + memcpy(&pnl.daddr.v6, &proxy->sin6_addr.s6_addr, sizeof pnl.daddr.v6); + pnl.sport = client->sin6_port; + pnl.dport = proxy->sin6_port; + + if (ioctl(dev, DIOCNATLOOK, &pnl) == -1) + return (-1); + + memset(server, 0, sizeof(struct sockaddr_in6)); + server->sin6_len = sizeof(struct sockaddr_in6); + server->sin6_family = AF_INET6; + memcpy(&server->sin6_addr.s6_addr, &pnl.rdaddr.v6, + sizeof server->sin6_addr); + server->sin6_port = pnl.rdport; + + return (0); +} diff --git a/usr.sbin/ftp-proxy/filter.h b/usr.sbin/ftp-proxy/filter.h new file mode 100644 index 00000000000..cfb75b77dbf --- /dev/null +++ b/usr.sbin/ftp-proxy/filter.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2004, 2005 Camiel Dobbelaar, <cd@sentia.nl> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#define FTP_PROXY_ANCHOR "ftp-proxy" + +int add_filter(u_int32_t, u_int8_t, struct sockaddr *, struct sockaddr *, + u_int16_t); +int add_nat(u_int32_t, struct sockaddr *, struct sockaddr *, u_int16_t, + struct sockaddr *, u_int16_t, u_int16_t); +int add_rdr(u_int32_t, struct sockaddr *, struct sockaddr *, u_int16_t, + struct sockaddr *, u_int16_t); +int do_commit(void); +int do_rollback(void); +void init_filter(char *); +int prepare_commit(u_int32_t); +int server_lookup(struct sockaddr *, struct sockaddr *, struct sockaddr *); diff --git a/usr.sbin/ftp-proxy/ftp-proxy.8 b/usr.sbin/ftp-proxy/ftp-proxy.8 new file mode 100644 index 00000000000..d85c49541c5 --- /dev/null +++ b/usr.sbin/ftp-proxy/ftp-proxy.8 @@ -0,0 +1,167 @@ +.\" +.\" Copyright (c) 2004, 2005 Camiel Dobbelaar, <cd@sentia.nl> +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd November 28, 2004 +.Dt FTP-PROXY 8 +.Os +.Sh NAME +.Nm ftp-proxy +.Nd Internet File Transfer Protocol proxy daemon +.Sh SYNOPSIS +.Nm ftp-proxy +.Op Fl 6Adr +.Op Fl a Ar address +.Op Fl b Ar address +.Op Fl D Ar level +.Op Fl m Ar maxsessions +.Op Fl P Ar port +.Op Fl p Ar port +.Op Fl q Ar queue +.Op Fl R Ar address +.Op Fl t Ar timeout +.Sh DESCRIPTION +.Nm +is a proxy for the Internet File Transfer Protocol. +FTP control connections should be redirected into the proxy using the +.Xr pf 4 +.Ar rdr +command, after which the proxy connects to the server on behalf of +the client. +.Pp +The proxy allows data connections to pass, rewriting and redirecting +them so that the right addresses are used. +All connections from the client to the server have their source +address rewritten so they appear to come from the proxy. +Consequently, all connections from the server to the proxy have +their destination address rewritten, so they are redirected to the +client. +The proxy uses the +.Xr pf 4 +.Ar anchor +facility for this. +.Pp +Assuming the FTP control connection is from $client to $server, the +proxy connected to the server using the $proxy source address, and +$port is negotiated, then +.Nm ftp-proxy +adds the following rules to the various anchors. +(These example rules use inet, but the proxy also supports inet6.) +.Pp +In case of active mode (PORT or EPRT): +.Bd -literal -offset 2n +rdr from $server to $proxy port $port -> $client +pass log quick inet proto tcp \e + from $server to $client port $port flags S/SAFR keep state +.Ed +.Pp +In case of passive mode (PASV or EPSV): +.Bd -literal -offset 2n +nat from $client to $server port $port -> $proxy +pass in log quick inet proto tcp \e + from $client to $server port $port flags S/SAFR keep state +pass out log quick inet proto tcp \e + from $proxy to $server port $port flags S/SAFR keep state +.Ed +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl 6 +IPv6 mode. The proxy will expect and use IPv6 addresses for all +communication. +Only the extended FTP modes EPSV and EPRT are allowed with IPv6. +The proxy is in IPv4 mode by default. +.It Fl A +Only permit anonymous FTP connections. +Either user "ftp" or user "anonymous" is allowed. +.It Fl a Ar address +The proxy will use this as the source address for the control +connection to a server. +.It Fl b Ar address +Address where the proxy will listen for redirected control connections. +The default is 127.0.0.1, or ::1 in IPv6 mode. +.It Fl D Ar level +Debug level, ranging from 0 to 7. +Higher is more verbose. +The default is 5. +(These levels correspond to the +.Xr syslog 3 +levels.) +.It Fl d +Do not daemonize. +The process will stay in the foreground, logging to standard error. +.It Fl m Ar maxsessions +Maximum number of concurrent FTP sessions. +When the proxy reaches this limit, new connections are denied. +The default is 100. +.It Fl P Ar port +Fixed server port. +Only used in combination with -R. +The default is port 21. +.It Fl p Ar port +Port where the proxy will listen for redirected connections. +The default is port 8021. +.It Fl q Ar queue +Create rules with queue +.Ar queue +appended, so that data connections can be queued. +.It Fl R Ar address +Fixed server address, also known as reverse mode. +The proxy will always connect to the same server, regardless of +where the client wanted to connect to (before it was redirected). +Use this option to proxy for a server behind NAT, or to forward all +connections to another proxy. +.It Fl r +Rewrite sourceport to 20 in active mode to suit antique clients that insist +on this RFC property. +.It Fl t Ar timeout +Number of seconds that the control connection can be idle, before the +proxy will disconnect. +The default is 24 hours. +Do not set this too low, because the control connection is usually +idle when large data transfers are taking place. +.El +.Sh CONFIGURATION +To make use of the proxy, +.Xr pf.conf 5 +needs the following rules. +All anchors are mandatory. +Adjust the rules as needed. +.Pp +In the NAT section: +.Bd -literal -offset 2n +nat-anchor "ftp-proxy/*" +rdr-anchor "ftp-proxy/*" +rdr pass on $int_if proto tcp from $lan to any port 21 -> \e + 127.0.0.1 port 8021 +.Ed +.Pp +In the rule section: +.Bd -literal -offset 2n +anchor "ftp-proxy/*" +pass out proto tcp from $proxy to any port 21 keep state +.Ed +.Sh SECURITY +Negotiated data connection ports below 1024 are not allowed. +.Pp +The negotiated IP address for active modes is ignored for security +reasons. +This makes third party file transfers impossible. +.Pp +.Nm ftp-proxy +chroots to "/var/empty" and changes to user "proxy" to drop privileges. +.Sh SEE ALSO +.Xr ftp 1 , +.Xr pf 4 , +.Xr pf.conf 5 , diff --git a/usr.sbin/ftp-proxy/ftp-proxy.c b/usr.sbin/ftp-proxy/ftp-proxy.c new file mode 100644 index 00000000000..eec6925dd75 --- /dev/null +++ b/usr.sbin/ftp-proxy/ftp-proxy.c @@ -0,0 +1,1085 @@ +/* + * Copyright (c) 2004, 2005 Camiel Dobbelaar, <cd@sentia.nl> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/queue.h> +#include <sys/types.h> +#include <sys/time.h> +#include <sys/resource.h> +#include <sys/socket.h> + +#include <net/if.h> +#include <net/pfvar.h> +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <netdb.h> +#include <pwd.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> +#include <vis.h> + +#include "filter.h" + +#define CONNECT_TIMEOUT 30 +#define MIN_PORT 1024 +#define MAX_LINE 500 +#define MAX_LOGLINE 300 +#define NTOP_BUFS 3 +#define TCP_BACKLOG 10 + +#define CHROOT_DIR "/var/empty" +#define NOPRIV_USER "proxy" + +/* pfctl standard NAT range. */ +#define PF_NAT_PROXY_PORT_LOW 50001 +#define PF_NAT_PROXY_PORT_HIGH 65535 + +#define sstosa(ss) ((struct sockaddr *)(ss)) + +enum { CMD_NONE = 0, CMD_PORT, CMD_EPRT, CMD_PASV, CMD_EPSV }; + +struct session { + u_int32_t id; + struct sockaddr_storage client_ss; + struct sockaddr_storage proxy_ss; + struct sockaddr_storage server_ss; + struct sockaddr_storage orig_server_ss; + struct bufferevent *client_bufev; + struct bufferevent *server_bufev; + int client_fd; + int server_fd; + char cbuf[MAX_LINE]; + size_t cbuf_valid; + char sbuf[MAX_LINE]; + size_t sbuf_valid; + int cmd; + u_int16_t port; + u_int16_t proxy_port; + LIST_ENTRY(session) entry; +}; + +LIST_HEAD(, session) sessions = LIST_HEAD_INITIALIZER(sessions); + +void catch_signal(int); +void client_error(struct bufferevent *, short, void *); +int client_parse(struct session *s); +int client_parse_anon(struct session *s); +int client_parse_cmd(struct session *s); +void client_read(struct bufferevent *, void *); +void client_write(struct bufferevent *, void *); +int drop_privs(void); +void end_session(struct session *); +int exit_daemon(void); +int getline(char *, size_t *); +void handle_connection(const int, short, void *); +struct session * init_session(void); +void logmsg(int, const char *, ...); +u_int16_t parse_port(int); +u_int16_t pick_proxy_port(void); +void proxy_reply(int, struct sockaddr *, u_int16_t); +void server_error(struct bufferevent *, short, void *); +int server_parse(struct session *s); +void server_read(struct bufferevent *, void *); +void server_write(struct bufferevent *, void *); +const char *sock_ntop(struct sockaddr *); +void usage(void); + +char linebuf[MAX_LINE + 1]; +size_t linelen; + +char ntop_buf[NTOP_BUFS][INET6_ADDRSTRLEN]; + +struct sockaddr_storage fixed_server_ss, fixed_proxy_ss; +char *fixed_server, *fixed_server_port, *fixed_proxy, *listen_ip, *listen_port, + *qname; +int anonymous_only, caught_sig, daemonize, id_count, ipv6_mode, loglevel, + max_sessions, rfc_mode, session_count, timeout; +extern char *__progname; + +void +catch_signal(int sig) +{ + extern int event_gotsig; + + event_gotsig = 1; + caught_sig = sig; +} + +void +client_error(struct bufferevent *bufev, short what, void *arg) +{ + struct session *s = arg; + + if (what & EVBUFFER_EOF) + logmsg(LOG_INFO, "#%d client close", s->id); + else if (what == (EVBUFFER_ERROR | EVBUFFER_READ)) + logmsg(LOG_ERR, "#%d client reset connection", s->id); + else if (what & EVBUFFER_TIMEOUT) + logmsg(LOG_ERR, "#%d client timeout", s->id); + else if (what & EVBUFFER_WRITE) + logmsg(LOG_ERR, "#%d client write error: %d", s->id, what); + else + logmsg(LOG_ERR, "#%d abnormal client error: %d", s->id, what); + + end_session(s); +} + +int +client_parse(struct session *s) +{ + /* Reset any previous command. */ + s->cmd = CMD_NONE; + s->port = 0; + + /* Commands we are looking for are at least 4 chars long. */ + if (linelen < 4) + return (1); + + if (linebuf[0] == 'P' || linebuf[0] == 'p' || + linebuf[0] == 'E' || linebuf[0] == 'e') + return (client_parse_cmd(s)); + + if (anonymous_only && (linebuf[0] == 'U' || linebuf[0] == 'u')) + return (client_parse_anon(s)); + + return (1); +} + +int +client_parse_anon(struct session *s) +{ + if (strcasecmp("USER ftp\r\n", linebuf) != 0 && + strcasecmp("USER anonymous\r\n", linebuf) != 0) { + linelen = snprintf(linebuf, sizeof linebuf, + "500 Only anonymous FTP allowed\r\n"); + logmsg(LOG_DEBUG, "#%d proxy: %s", s->id, linebuf); + + /* Talk back to the client ourself. */ + bufferevent_write(s->client_bufev, linebuf, linelen); + + /* Clear buffer so it's not sent to the server. */ + linebuf[0] = '\0'; + linelen = 0; + } + + return (1); +} + +int +client_parse_cmd(struct session *s) +{ + if (strncasecmp("PASV", linebuf, 4) == 0) + s->cmd = CMD_PASV; + else if (strncasecmp("PORT ", linebuf, 5) == 0) + s->cmd = CMD_PORT; + else if (strncasecmp("EPSV", linebuf, 4) == 0) + s->cmd = CMD_EPSV; + else if (strncasecmp("EPRT ", linebuf, 5) == 0) + s->cmd = CMD_EPRT; + else + return (1); + + if (ipv6_mode && (s->cmd == CMD_PASV || s->cmd == CMD_PORT)) { + logmsg(LOG_CRIT, "PASV and PORT not allowed with IPv6"); + return (0); + } + + if (s->cmd == CMD_PORT || s->cmd == CMD_EPRT) { + s->port = parse_port(s->cmd); + if (s->port < MIN_PORT) { + logmsg(LOG_CRIT, "#%d bad port in '%s'", s->id, + linebuf); + return (0); + } + s->proxy_port = pick_proxy_port(); + proxy_reply(s->cmd, sstosa(&s->proxy_ss), s->proxy_port); + logmsg(LOG_DEBUG, "#%d proxy: %s", s->id, linebuf); + } + + return (1); +} + +void +client_read(struct bufferevent *bufev, void *arg) +{ + struct session *s = arg; + size_t buf_avail, read; + int n; + + do { + buf_avail = sizeof s->cbuf - s->cbuf_valid; + read = bufferevent_read(bufev, s->cbuf + s->cbuf_valid, + buf_avail); + s->cbuf_valid += read; + + while ((n = getline(s->cbuf, &s->cbuf_valid)) > 0) { + logmsg(LOG_DEBUG, "#%d client: %s", s->id, linebuf); + if (!client_parse(s)) { + end_session(s); + return; + } + bufferevent_write(s->server_bufev, linebuf, linelen); + } + + if (n == -1) { + logmsg(LOG_ERR, "#%d client command too long", s->id); + end_session(s); + return; + } + } while (read == buf_avail); +} + +void +client_write(struct bufferevent *bufev, void *arg) +{ + return; +} + +int +drop_privs(void) +{ + struct passwd *pw; + + pw = getpwnam(NOPRIV_USER); + if (pw == NULL) + return (0); + + tzset(); + if (chroot(CHROOT_DIR) != 0 || chdir("/") != 0 || + setgroups(1, &pw->pw_gid) != 0 || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) != 0 || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) != 0) + return (0); + + return (1); +} + +void +end_session(struct session *s) +{ + int err; + + logmsg(LOG_INFO, "#%d ending session", s->id); + + if (s->client_bufev) + bufferevent_free(s->client_bufev); + if (s->server_bufev) + bufferevent_free(s->server_bufev); + + if (s->client_fd != -1) + while (close(s->client_fd) != 0 && errno == EINTR) + ; + if (s->server_fd != -1) + while (close(s->server_fd) != 0 && errno == EINTR) + ; + + /* Remove rulesets by commiting empty ones. */ + err = 0; + if (prepare_commit(s->id) == -1) + err = errno; + else if (do_commit() == -1) { + err = errno; + do_rollback(); + } + if (err) + logmsg(LOG_ERR, "#%d pf rule removal failed: %s", s->id, + strerror(err)); + + LIST_REMOVE(s, entry); + free(s); + session_count--; +} + +int +exit_daemon(void) +{ + struct session *s, *next; + + logmsg(LOG_ERR, "%s exiting on signal %d", __progname, caught_sig); + + for (s = LIST_FIRST(&sessions); s != LIST_END(&sessions); s = next) { + next = LIST_NEXT(s, entry); + end_session(s); + } + + if (daemonize) + closelog(); + + exit(0); + + /* NOTREACHED */ + return (-1); +} + +int +getline(char *buf, size_t *valid) +{ + size_t i; + + if (*valid > MAX_LINE) + return (-1); + + /* Copy to linebuf while searching for a newline. */ + for (i = 0; i < *valid; i++) + if ((linebuf[i] = buf[i]) == '\n') + break; + + if (i == *valid) { + /* No newline found. */ + linebuf[0] = '\0'; + linelen = 0; + if (i < MAX_LINE) + return (0); + return (-1); + } + + linelen = i + 1; + linebuf[linelen] = '\0'; + *valid -= linelen; + + /* Move leftovers to the start. */ + if (*valid != 0) + bcopy(buf + linelen, buf, *valid); + + return ((int)linelen); +} + +void +handle_connection(const int listen_fd, short event, void *ev) +{ + struct sockaddr_storage tmp_ss; + struct sockaddr *client_sa, *server_sa, *fixed_server_sa; + struct sockaddr *client_to_proxy_sa, *proxy_to_server_sa; + struct session *s; + socklen_t len; + int client_fd, fc, on; + + /* + * We _must_ accept the connection, otherwise libevent will keep + * coming back, and we will chew up all CPU. + */ + client_sa = sstosa(&tmp_ss); + len = sizeof(struct sockaddr_storage); + if ((client_fd = accept(listen_fd, client_sa, &len)) < 0) { + logmsg(LOG_CRIT, "accept failed: %s", strerror(errno)); + return; + } + + /* Refuse connection if the maximum is reached. */ + if (session_count >= max_sessions) { + logmsg(LOG_ERR, "client limit (%d) reached, refusing " + "connection from %s", max_sessions, sock_ntop(client_sa)); + close(client_fd); + return; + } + + /* Allocate session and copy back the info from the accept(). */ + s = init_session(); + if (s == NULL) { + logmsg(LOG_CRIT, "init_session failed"); + close(client_fd); + return; + } + s->client_fd = client_fd; + memcpy(sstosa(&s->client_ss), client_sa, client_sa->sa_len); + + /* Cast it once, and be done with it. */ + client_sa = sstosa(&s->client_ss); + server_sa = sstosa(&s->server_ss); + client_to_proxy_sa = sstosa(&tmp_ss); + proxy_to_server_sa = sstosa(&s->proxy_ss); + fixed_server_sa = sstosa(&fixed_server_ss); + + /* Log id/client early to ease debugging. */ + logmsg(LOG_DEBUG, "#%d accepted connection from %s", s->id, + sock_ntop(client_sa)); + + /* + * Find out the real server and port that the client wanted. + */ + len = sizeof(struct sockaddr_storage); + if ((getsockname(s->client_fd, client_to_proxy_sa, &len)) < 0) { + logmsg(LOG_CRIT, "#%d getsockname failed: %s", s->id, + strerror(errno)); + goto fail; + } + if (server_lookup(client_sa, client_to_proxy_sa, server_sa) != 0) { + logmsg(LOG_CRIT, "#%d server lookup failed (no rdr?)", s->id); + goto fail; + } + if (fixed_server) { + memcpy(sstosa(&s->orig_server_ss), server_sa, + server_sa->sa_len); + memcpy(server_sa, fixed_server_sa, fixed_server_sa->sa_len); + } + + /* XXX: check we are not connecting to ourself. */ + + /* + * Setup socket and connect to server. + */ + if ((s->server_fd = socket(server_sa->sa_family, SOCK_STREAM, + IPPROTO_TCP)) < 0) { + logmsg(LOG_CRIT, "#%d server socket failed: %s", s->id, + strerror(errno)); + goto fail; + } + if (fixed_proxy && bind(s->server_fd, sstosa(&fixed_proxy_ss), + fixed_proxy_ss.ss_len) != 0) { + logmsg(LOG_CRIT, "#%d cannot bind fixed proxy address: %s", + s->id, strerror(errno)); + goto fail; + } + + /* Use non-blocking connect(), see CONNECT_TIMEOUT below. */ + if ((fc = fcntl(s->server_fd, F_GETFL)) == -1 || + fcntl(s->server_fd, F_SETFL, fc | O_NONBLOCK) == -1) { + logmsg(LOG_CRIT, "#%d cannot mark socket non-blocking: %s", + s->id, strerror(errno)); + goto fail; + } + if (connect(s->server_fd, server_sa, server_sa->sa_len) < 0 && + errno != EINPROGRESS) { + logmsg(LOG_CRIT, "#%d proxy cannot connect to server %s: %s", + s->id, sock_ntop(server_sa), strerror(errno)); + goto fail; + } + + len = sizeof(struct sockaddr_storage); + if ((getsockname(s->server_fd, proxy_to_server_sa, &len)) < 0) { + logmsg(LOG_CRIT, "#%d getsockname failed: %s", s->id, + strerror(errno)); + goto fail; + } + + logmsg(LOG_INFO, "#%d FTP session %d/%d started: client %s to server " + "%s via proxy %s ", s->id, session_count, max_sessions, + sock_ntop(client_sa), sock_ntop(server_sa), + sock_ntop(proxy_to_server_sa)); + + /* Keepalive is nice, but don't care if it fails. */ + on = 1; + setsockopt(s->client_fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&on, + sizeof on); + setsockopt(s->server_fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&on, + sizeof on); + + /* + * Setup buffered events. + */ + s->client_bufev = bufferevent_new(s->client_fd, &client_read, + &client_write, &client_error, s); + if (s->client_bufev == NULL) { + logmsg(LOG_CRIT, "#%d bufferevent_new client failed", s->id); + goto fail; + } + bufferevent_settimeout(s->client_bufev, timeout, 0); + bufferevent_enable(s->client_bufev, EV_READ | EV_TIMEOUT); + + s->server_bufev = bufferevent_new(s->server_fd, &server_read, + &server_write, &server_error, s); + if (s->server_bufev == NULL) { + logmsg(LOG_CRIT, "#%d bufferevent_new server failed", s->id); + goto fail; + } + bufferevent_settimeout(s->server_bufev, CONNECT_TIMEOUT, 0); + bufferevent_enable(s->server_bufev, EV_READ | EV_TIMEOUT); + + return; + + fail: + end_session(s); +} + +struct session * +init_session(void) +{ + struct session *s; + + s = calloc(1, sizeof(struct session)); + if (s == NULL) + return (NULL); + + s->id = id_count++; + s->client_fd = -1; + s->server_fd = -1; + s->cbuf[0] = '\0'; + s->cbuf_valid = 0; + s->sbuf[0] = '\0'; + s->sbuf_valid = 0; + s->client_bufev = NULL; + s->server_bufev = NULL; + s->cmd = CMD_NONE; + s->port = 0; + + LIST_INSERT_HEAD(&sessions, s, entry); + session_count++; + + return (s); +} + +void +logmsg(int pri, const char *message, ...) +{ + va_list ap; + va_start(ap, message); + + if (pri > loglevel) + return; + + if (daemonize) + /* syslog does its own vissing. */ + vsyslog(pri, message, ap); + else { + char buf[MAX_LOGLINE]; + char visbuf[2 * MAX_LOGLINE]; + + /* We don't care about truncation. */ + vsnprintf(buf, sizeof buf, message, ap); + strnvis(visbuf, buf, sizeof visbuf, VIS_CSTYLE | VIS_NL); + fprintf(stderr, "%s\n", visbuf); + } + + va_end(ap); +} + +int +main(int argc, char *argv[]) +{ + extern int (*event_sigcb)(void); + struct rlimit rlp; + struct addrinfo hints, *res; + struct event ev; + int ch, error, listenfd, on; + + /* Defaults. */ + anonymous_only = 0; + daemonize = 1; + fixed_proxy = NULL; + fixed_server = NULL; + fixed_server_port = "21"; + ipv6_mode = 0; + listen_ip = NULL; + listen_port = "8021"; + loglevel = LOG_NOTICE; + max_sessions = 100; + qname = NULL; + rfc_mode = 0; + timeout = 24 * 3600; + + /* Other initialization. */ + id_count = 1; + session_count = 0; + + while ((ch = getopt(argc, argv, "6Aa:b:D:dm:P:p:q:R:rt:")) != -1) { + switch (ch) { + case '6': + ipv6_mode = 1; + break; + case 'A': + anonymous_only = 1; + break; + case 'a': + fixed_proxy = optarg; + break; + case 'b': + listen_ip = optarg; + break; + case 'D': + loglevel = atoi(optarg); + if (loglevel < LOG_EMERG || loglevel > LOG_DEBUG) + errx(1, "bad loglevel"); + break; + case 'd': + daemonize = 0; + break; + case 'm': + max_sessions = atoi(optarg); + if (max_sessions < 0) + errx(1, "bad max sessions"); + break; + case 'P': + fixed_server_port = optarg; + break; + case 'p': + listen_port = optarg; + break; + case 'q': + if (strlen(optarg) >= PF_QNAME_SIZE) + errx(1, "queuename too long"); + qname = optarg; + break; + case 'R': + fixed_server = optarg; + break; + case 'r': + rfc_mode = 1; + break; + case 't': + timeout = atoi(optarg); + if (timeout < 0) + errx(1, "bad timeout"); + break; + default: + usage(); + } + } + + if (listen_ip == NULL) + listen_ip = ipv6_mode ? "::1" : "127.0.0.1"; + + /* Check for root to save the user from cryptic failure messages. */ + if (getuid() != 0) + errx(1, "needs to start as root"); + + /* Raise max. open files limit to satisfy max. sessions. */ + rlp.rlim_cur = rlp.rlim_max = (2 * max_sessions) + 10; + if (setrlimit(RLIMIT_NOFILE, &rlp) == -1) + err(1, "setrlimit"); + + if (fixed_proxy) { + memset(&hints, 0, sizeof hints); + hints.ai_flags = AI_NUMERICHOST; + hints.ai_family = ipv6_mode ? AF_INET6 : AF_INET; + hints.ai_socktype = SOCK_STREAM; + error = getaddrinfo(fixed_proxy, NULL, &hints, &res); + if (error) + errx(1, "getaddrinfo fixed proxy address failed: %s", + gai_strerror(error)); + memcpy(&fixed_proxy_ss, res->ai_addr, res->ai_addrlen); + logmsg(LOG_INFO, "using %s to connect to servers", + sock_ntop(sstosa(&fixed_proxy_ss))); + freeaddrinfo(res); + } + + if (fixed_server) { + memset(&hints, 0, sizeof hints); + hints.ai_family = ipv6_mode ? AF_INET6 : AF_INET; + hints.ai_socktype = SOCK_STREAM; + error = getaddrinfo(fixed_server, fixed_server_port, &hints, + &res); + if (error) + errx(1, "getaddrinfo fixed server address failed: %s", + gai_strerror(error)); + memcpy(&fixed_server_ss, res->ai_addr, res->ai_addrlen); + logmsg(LOG_INFO, "using fixed server %s", + sock_ntop(sstosa(&fixed_server_ss))); + freeaddrinfo(res); + } + + /* Setup listener. */ + memset(&hints, 0, sizeof hints); + hints.ai_flags = AI_NUMERICHOST | AI_PASSIVE; + hints.ai_family = ipv6_mode ? AF_INET6 : AF_INET; + hints.ai_socktype = SOCK_STREAM; + error = getaddrinfo(listen_ip, listen_port, &hints, &res); + if (error) + errx(1, "getaddrinfo listen address failed: %s", + gai_strerror(error)); + if ((listenfd = socket(res->ai_family, SOCK_STREAM, IPPROTO_TCP)) == -1) + errx(1, "socket failed"); + on = 1; + if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void *)&on, + sizeof on) != 0) + err(1, "setsockopt failed"); + if (bind(listenfd, (struct sockaddr *)res->ai_addr, + (socklen_t)res->ai_addrlen) != 0) + err(1, "bind failed"); + if (listen(listenfd, TCP_BACKLOG) != 0) + err(1, "listen failed"); + freeaddrinfo(res); + + /* Initialize pf. */ + init_filter(qname); + + if (daemonize) { + if (daemon(0, 0) == -1) + err(1, "cannot daemonize"); + openlog(__progname, LOG_PID | LOG_NDELAY, LOG_DAEMON); + } + + /* Use logmsg for output from here on. */ + + if (!drop_privs()) { + logmsg(LOG_ERR, "cannot drop privileges: %s", strerror(errno)); + exit(1); + } + + event_init(); + event_set(&ev, listenfd, EV_READ | EV_PERSIST, handle_connection, &ev); + event_add(&ev, NULL); + + signal(SIGHUP, catch_signal); + signal(SIGINT, catch_signal); + signal(SIGTERM, catch_signal); + event_sigcb = exit_daemon; + + logmsg(LOG_NOTICE, "listening on %s port %s", listen_ip, listen_port); + + /* Vroom, vroom. */ + event_dispatch(); + + logmsg(LOG_ERR, "event_dispatch error: %s", strerror(errno)); + exit_daemon(); + + /* NOTREACHED */ + return (1); +} + +u_int16_t +parse_port(int mode) +{ + unsigned int port, v[6]; + int n; + char *p; + + /* Find the last space or left-parenthesis. */ + for (p = linebuf + linelen; p > linebuf; p--) + if (*p == ' ' || *p == '(') + break; + if (p == linebuf) + return (0); + + switch (mode) { + case CMD_PORT: + n = sscanf(p, " %u,%u,%u,%u,%u,%u", &v[0], &v[1], &v[2], + &v[3], &v[4], &v[5]); + if (n == 6 && v[0] < 256 && v[1] < 256 && v[2] < 256 && + v[3] < 256 && v[4] < 256 && v[5] < 256) + return ((v[4] << 8) | v[5]); + break; + case CMD_PASV: + n = sscanf(p, "(%u,%u,%u,%u,%u,%u)", &v[0], &v[1], &v[2], + &v[3], &v[4], &v[5]); + if (n == 6 && v[0] < 256 && v[1] < 256 && v[2] < 256 && + v[3] < 256 && v[4] < 256 && v[5] < 256) + return ((v[4] << 8) | v[5]); + break; + case CMD_EPSV: + n = sscanf(p, "(|||%u|)", &port); + if (n == 1 && port < 65536) + return (port); + break; + case CMD_EPRT: + n = sscanf(p, " |1|%u.%u.%u.%u|%u|", &v[0], &v[1], &v[2], + &v[3], &port); + if (n == 5 && v[0] < 256 && v[1] < 256 && v[2] < 256 && + v[3] < 256 && port < 65536) + return (port); + n = sscanf(p, " |2|%*[a-fA-F0-9:]|%u|", &port); + if (n == 1 && port < 65536) + return (port); + break; + default: + return (0); + } + + return (0); +} + +u_int16_t +pick_proxy_port(void) +{ + /* Random should be good enough for avoiding port collisions. */ + return (IPPORT_HIFIRSTAUTO + (arc4random() % + (IPPORT_HILASTAUTO - IPPORT_HIFIRSTAUTO))); +} + +void +proxy_reply(int cmd, struct sockaddr *sa, u_int16_t port) +{ + int i, r; + + switch (cmd) { + case CMD_PORT: + r = snprintf(linebuf, sizeof linebuf, + "PORT %s,%u,%u\r\n", sock_ntop(sa), port / 256, + port % 256); + break; + case CMD_PASV: + r = snprintf(linebuf, sizeof linebuf, + "227 Entering Passive Mode (%s,%u,%u)\r\n", sock_ntop(sa), + port / 256, port % 256); + break; + case CMD_EPRT: + if (sa->sa_family == AF_INET) + r = snprintf(linebuf, sizeof linebuf, + "EPRT |1|%s|%u|\r\n", sock_ntop(sa), port); + else if (sa->sa_family == AF_INET6) + r = snprintf(linebuf, sizeof linebuf, + "EPRT |2|%s|%u|\r\n", sock_ntop(sa), port); + break; + case CMD_EPSV: + r = snprintf(linebuf, sizeof linebuf, + "229 Entering Extended Passive Mode (|||%u|)\r\n", port); + break; + } + + if (r < 0 || r >= sizeof linebuf) { + logmsg(LOG_ERR, "proxy_reply failed: %d", r); + linebuf[0] = '\0'; + linelen = 0; + return; + } + linelen = (size_t)r; + + if (cmd == CMD_PORT || cmd == CMD_PASV) { + /* Replace dots in IP address with commas. */ + for (i = 0; i < linelen; i++) + if (linebuf[i] == '.') + linebuf[i] = ','; + } +} + +void +server_error(struct bufferevent *bufev, short what, void *arg) +{ + struct session *s = arg; + + if (what & EVBUFFER_EOF) + logmsg(LOG_INFO, "#%d server close", s->id); + else if (what == (EVBUFFER_ERROR | EVBUFFER_READ)) + logmsg(LOG_ERR, "#%d server refused connection", s->id); + else if (what & EVBUFFER_WRITE) + logmsg(LOG_ERR, "#%d server write error: %d", s->id, what); + else if (what & EVBUFFER_TIMEOUT) + logmsg(LOG_NOTICE, "#%d server timeout", s->id); + else + logmsg(LOG_ERR, "#%d abnormal server error: %d", s->id, what); + + end_session(s); +} + +int +server_parse(struct session *s) +{ + struct sockaddr *client_sa, *orig_sa, *proxy_sa, *server_sa; + int prepared = 0; + + if (s->cmd == CMD_NONE || linelen < 4 || linebuf[0] != '2') + goto out; + + /* + * The pf rules below do quite some NAT rewriting, to keep up + * appearances. Points to keep in mind: + * 1) The client must think it's talking to the real server, + * for both control and data connections. Transparently. + * 2) The server must think that the proxy is the client. + * 3) Source and destination ports are rewritten to minimize + * port collisions, to aid security (some systems pick weak + * ports) or to satisfy RFC requirements (source port 20). + */ + + /* Cast this once, to make code below it more readable. */ + client_sa = sstosa(&s->client_ss); + server_sa = sstosa(&s->server_ss); + proxy_sa = sstosa(&s->proxy_ss); + if (fixed_server) + /* Fixed server: data connections must appear to come + from / go to the original server, not the fixed one. */ + orig_sa = sstosa(&s->orig_server_ss); + else + /* Server not fixed: orig_server == server. */ + orig_sa = sstosa(&s->server_ss); + + /* Passive modes. */ + if ((s->cmd == CMD_PASV && strncmp("227 ", linebuf, 4) == 0) || + (s->cmd == CMD_EPSV && strncmp("229 ", linebuf, 4) == 0)) { + s->port = parse_port(s->cmd); + if (s->port < MIN_PORT) { + logmsg(LOG_CRIT, "#%d bad port in '%s'", s->id, + linebuf); + return (0); + } + s->proxy_port = pick_proxy_port(); + logmsg(LOG_INFO, "#%d passive: client to server port %d" + " via port %d", s->id, s->port, s->proxy_port); + + if (prepare_commit(s->id) == -1) + goto fail; + prepared = 1; + + proxy_reply(s->cmd, orig_sa, s->proxy_port); + logmsg(LOG_DEBUG, "#%d proxy: %s", s->id, linebuf); + + /* rdr from $client to $orig_server port $proxy_port -> $server + port $port */ + if (add_rdr(s->id, client_sa, orig_sa, s->proxy_port, + server_sa, s->port) == -1) + goto fail; + + /* nat from $client to $server port $port -> $proxy */ + if (add_nat(s->id, client_sa, server_sa, s->port, proxy_sa, + PF_NAT_PROXY_PORT_LOW, PF_NAT_PROXY_PORT_HIGH) == -1) + goto fail; + + /* pass in from $client to $server port $port */ + if (add_filter(s->id, PF_IN, client_sa, server_sa, + s->port) == -1) + goto fail; + + /* pass out from $proxy to $server port $port */ + if (add_filter(s->id, PF_OUT, proxy_sa, server_sa, + s->port) == -1) + goto fail; + } + + /* Active modes. */ + if ((s->cmd == CMD_PORT || s->cmd == CMD_EPRT) && + strncmp("200 ", linebuf, 4) == 0) { + logmsg(LOG_INFO, "#%d active: server to client port %d" + " via port %d", s->id, s->port, s->proxy_port); + + if (prepare_commit(s->id) == -1) + goto fail; + prepared = 1; + + /* rdr from $server to $proxy port $proxy_port -> $client port + $port */ + if (add_rdr(s->id, server_sa, proxy_sa, s->proxy_port, + client_sa, s->port) == -1) + goto fail; + + /* nat from $server to $client port $port -> $orig_server port + $natport */ + if (rfc_mode && s->cmd == CMD_PORT) { + /* Rewrite sourceport to RFC mandated 20. */ + if (add_nat(s->id, server_sa, client_sa, s->port, + orig_sa, 20, 20) == -1) + goto fail; + } else { + /* Let pf pick a source port from the standard range. */ + if (add_nat(s->id, server_sa, client_sa, s->port, + orig_sa, PF_NAT_PROXY_PORT_LOW, + PF_NAT_PROXY_PORT_HIGH) == -1) + goto fail; + } + + /* pass in from $server to $client port $port */ + if (add_filter(s->id, PF_IN, server_sa, client_sa, s->port) == + -1) + goto fail; + + /* pass out from $orig_server to $client port $port */ + if (add_filter(s->id, PF_OUT, orig_sa, client_sa, s->port) == + -1) + goto fail; + } + + /* Commit rules if they were prepared. */ + if (prepared && (do_commit() == -1)) { + if (errno != EBUSY) + goto fail; + /* One more try if busy. */ + usleep(5000); + if (do_commit() == -1) + goto fail; + } + + out: + s->cmd = CMD_NONE; + s->port = 0; + + return (1); + + fail: + logmsg(LOG_CRIT, "#%d pf operation failed: %s", s->id, strerror(errno)); + if (prepared) + do_rollback(); + return (0); +} + +void +server_read(struct bufferevent *bufev, void *arg) +{ + struct session *s = arg; + size_t buf_avail, read; + int n; + + bufferevent_settimeout(bufev, timeout, 0); + + do { + buf_avail = sizeof s->sbuf - s->sbuf_valid; + read = bufferevent_read(bufev, s->sbuf + s->sbuf_valid, + buf_avail); + s->sbuf_valid += read; + + while ((n = getline(s->sbuf, &s->sbuf_valid)) > 0) { + logmsg(LOG_DEBUG, "#%d server: %s", s->id, linebuf); + if (!server_parse(s)) { + end_session(s); + return; + } + bufferevent_write(s->client_bufev, linebuf, linelen); + } + + if (n == -1) { + logmsg(LOG_ERR, "#%d server reply too long", s->id); + end_session(s); + return; + } + } while (read == buf_avail); +} + +void +server_write(struct bufferevent *bufev, void *arg) +{ + return; +} + +const char * +sock_ntop(struct sockaddr *sa) +{ + static int n = 0; + + /* Cycle to next buffer. */ + n = (n + 1) % NTOP_BUFS; + ntop_buf[n][0] = '\0'; + + if (sa->sa_family == AF_INET) { + struct sockaddr_in *sin = (struct sockaddr_in *)sa; + + return (inet_ntop(AF_INET, &sin->sin_addr, ntop_buf[n], + sizeof ntop_buf[0])); + } + + if (sa->sa_family == AF_INET6) { + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)sa; + + return (inet_ntop(AF_INET6, &sin6->sin6_addr, ntop_buf[n], + sizeof ntop_buf[0])); + } + + return (NULL); +} + +void +usage(void) +{ + fprintf(stderr, "usage: %s [-6Adr] [-a address] [-b address]" + " [-D level] [-m maxsessions]\n [-P port]" + " [-p port] [-q queue] [-R address] [-t timeout]\n", __progname); + exit(1); +} |