X-Git-Url: https://git.distorted.org.uk/~mdw/fwd/blobdiff_plain/07d71f34ab3887b63c6ff2d635fce07368f90295..48bb1f76fd805b19aa32a374a8a9d8106ab7f420:/socket.c diff --git a/socket.c b/socket.c new file mode 100644 index 0000000..941bcd5 --- /dev/null +++ b/socket.c @@ -0,0 +1,698 @@ +/* -*-c-*- + * + * $Id: socket.c,v 1.1 1999/07/26 23:33:32 mdw Exp $ + * + * Socket source and target definitions + * + * (c) 1999 Straylight/Edgeware + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of the `fw' port forwarder. + * + * `fw' is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * `fw' is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with `fw'; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/*----- Revision history --------------------------------------------------* + * + * $Log: socket.c,v $ + * Revision 1.1 1999/07/26 23:33:32 mdw + * New sources and targets. + * + */ + +/*----- Header files ------------------------------------------------------*/ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "addr.h" +#include "conf.h" +#include "endpt.h" +#include "fw.h" +#include "scan.h" +#include "socket.h" +#include "target.h" + +#include "inet.h" +#include "un.h" + +/*----- Data structures ---------------------------------------------------*/ + +/* --- Socket source options --- */ + +typedef struct ssource_opts { + unsigned conn; +} ssource_opts; + +static ssource_opts ssgo = { 256 }; + +/* --- Socket source --- */ + +typedef struct ssource { + source s; + addr *a; + target *t; + addr_opts *ao; + ssource_opts o; + sel_file r; +} ssource; + +/* --- Socket target --- */ + +typedef struct starget { + target t; + addr *a; + addr_opts *ao; +} starget; + +/* --- Socket target endpoint --- */ + +typedef struct stept { + endpt e; + conn c; + const char *desc; +} stept; + +/* --- Socket source endpoint --- */ + +typedef struct ssept { + endpt e; + ssource *s; +} ssept; + +#define SKF_CONN 16u +#define SKF_BROKEN 32u + +/*----- Protocol table ----------------------------------------------------*/ + +static addr_ops *addrs[] = { &inet_ops, &un_ops, 0 }; + +/*----- Other persistent variables ----------------------------------------*/ + +static addr_opts gao = { 0 }; + +/*----- Parsing address types ---------------------------------------------*/ + +/* --- @getaddrtype@ --- * + * + * Arguments: @scanner *sc@ = pointer to scanner (for error reporting) + * @const char *p@ = pointer to protocol name + * @int abbrev@ = nonzero to allow abbreviations + * + * Returns: Pointer to address operations table or null. + * + * Use: Looks up a protocol name. Handy when parsing addresses and + * other bits of configuration. Returns null if no matching + * address was found. + */ + +static addr_ops *getaddrtype(scanner *sc, const char *p, int abbrev) +{ + addr_ops **ops; + addr_ops *chosen = 0; + size_t sz = strlen(p); + + for (ops = addrs; *ops; ops++) { + if (strncmp((*ops)->name, p, sz) == 0) { + if ((*ops)->name[sz] == 0) + return (*ops); + else if (chosen && abbrev) + error(sc, "ambiguous socket address type `%s'", p); + chosen = *ops; + } + } + if (!abbrev) + return (0); + return (chosen); +} + +/* --- @getaddr@ --- * + * + * Arguments: @scanner *sc@ = pointer to scanner to read from + * @unsigned type@ = address type (@ADDR_SRC@ or @ADDR_DEST@) + * + * Returns: Pointer to an address successfully read. + * + * Use: Reads an optionally qualified address. + */ + +static addr *getaddr(scanner *sc, unsigned type) +{ + addr_ops *ops = 0; + int abbrev = 0; + + if (sc->t == ':') { + token(sc); + abbrev = 1; + } + if (sc->t == CTOK_WORD) + ops = getaddrtype(sc, sc->d.buf, abbrev); + if (ops) + token(sc); + else if (abbrev) + error(sc, "unknown socket address type `%s'", sc->d.buf); + else + ops = &inet_ops; + if (sc->t == ':') + token(sc); + + return (ops->read(sc, type)); +} + +/*----- Socket endpoints --------------------------------------------------*/ + +/* --- @wclose@ --- */ + +static void sept_wclose(endpt *e) +{ + shutdown(e->out->fd, 1); +} + +/* --- @close@ (source) --- */ + +static void ss_listen(ssource */*ss*/); + +static void ssept_close(endpt *e) +{ + ssept *ee = (ssept *)e; + + if (!ee->s->o.conn) { + ee->s->o.conn++; + ss_listen(ee->s); + } else + ee->s->o.conn++; + REFFD_DEC(ee->e.in); + REFFD_DEC(ee->e.out); + fw_dec(); + DESTROY(ee); +} + +/* --- @close@ (target) --- */ + +static void stept_close(endpt *e) +{ + stept *ee = (stept *)e; + + if (ee->e.f & EPF_PENDING) { + if (ee->e.f & SKF_CONN) + conn_kill(&ee->c); + } else { + REFFD_DEC(ee->e.in); + REFFD_DEC(ee->e.out); + } + + fw_dec(); + DESTROY(ee); +} + +/* --- @stept_go@ --- * + * + * Arguments: @int fd@ = file descriptor now ready for use + * @void *p@ = pointer to an endpoint structure + * + * Returns: --- + * + * Use: Handles successful connection of the target endpoint. + */ + +static void stept_go(int fd, void *p) +{ + stept *e = p; + + /* --- Complicated and subtle --- * + * + * This code interacts quite closely with @starget_create@, mainly through + * flags in the endpoint block. + * + * If the connection failed, I log a message (that's easy enough). The + * behaviour then depends on whether the endpoints have been joined yet. + * If not, I set @SKF_BROKEN@ and return, so that @starget_create@ can + * clean up the mess and return an immediate failure. If they have, I kill + * the connection and everything ought to work. + * + * If the connection worked, I clear @EPF_PENDING@ (as expected, because + * my endpoint is now ready), and @SKF_CONN@ (to let @starget_create@ know + * that the connection is already going). Then, only if this isn't the + * first attempt, I rejoin this endpoint to its partner. + */ + + if (fd == -1) { + fw_log(-1, "[%s] connection failed: %s", e->desc, strerror(errno)); + e->e.f &= ~SKF_CONN; + if (e->e.f & EPF_PENDING) + endpt_kill(&e->e); + else + e->e.f |= SKF_BROKEN; + } else { + reffd *r = reffd_init(fd); + REFFD_INC(r); + e->e.in = e->e.out = r; + e->e.f &= ~(EPF_PENDING | SKF_CONN); + if (e->e.other) + endpt_join(&e->e, e->e.other); + } +} + +/* --- Socket endpoint definition --- */ + +static endpt_ops ssept_ops = { + 0, sept_wclose, ssept_close +}; + +static endpt_ops stept_ops = { + 0, sept_wclose, stept_close +}; + +/*----- Source definition -------------------------------------------------*/ + +/* --- @option@ --- */ + +static int ssource_option(source *s, scanner *sc) +{ + ssource *ss = (ssource *)s; + ssource_opts *sso = ss ? &ss->o : &ssgo; + + CONF_BEGIN(sc, "socket", "socket") + + /* --- Make sure the next token is a word --- */ + + if (sc->t != CTOK_WORD) + error(sc, "parse error, option keyword expected"); + + /* --- Handle options at this level --- */ + + if (strcmp(sc->d.buf, "conn") == 0) { + token(sc); + if (sc->t == '=') + token(sc); + if (sc->t != CTOK_WORD || !isdigit((unsigned char)sc->d.buf[0])) + error(sc, "parse error, argument of `conn' must be a number"); + sso->conn = atoi(sc->d.buf); + if (sso->conn == 0) + error(sc, "argument of `conn' must be positive"); + token(sc); + CONF_ACCEPT; + } + + if (strcmp(sc->d.buf, "logging") == 0 || + strcmp(sc->d.buf, "log") == 0) { + addr_opts *ao = ss ? ss->ao : &gao; + token(sc); + if (sc->t == '=') + token(sc); + if (conf_enum(sc, "no,yes", ENUM_ABBREV, "logging status")) + ao->f &= ~ADDRF_NOLOG; + else + ao->f |= ADDRF_NOLOG; + CONF_ACCEPT; + } + + /* --- Pass the option around the various address types --- */ + + if (ss) { + if (ss->a->ops->option && ss->a->ops->option(sc, ss ? ss->ao : 0)) + CONF_ACCEPT; + } else { + addr_ops **a; + for (a = addrs; *a; a++) { + if ((*a)->option && (*a)->option(sc, 0)) + CONF_ACCEPT; + } + } + + /* --- Nobody understood the option --- */ + + CONF_END; +} + +/* --- @read@ --- */ + +static source *ssource_read(scanner *sc) +{ + ssource *ss; + + (void)(conf_prefix(sc, "socket") || conf_prefix(sc, "sk")); + ss = CREATE(ssource); + ss->s.ops = &ssource_ops; + ss->s.desc = 0; + ss->t = 0; + ss->a = getaddr(sc, ADDR_SRC); + if (ss->a->ops->initopts) + ss->ao = ss->a->ops->initopts(); + else + ss->ao = CREATE(addr_opts); + *ss->ao = gao; + ss->o = ssgo; + return (&ss->s); +} + +/* --- @ss_accept@ --- * + * + * Arguments: @int fd@ = file descriptor to accept from + * @unsigned mode@ = what's ready with the descriptor + * @void *p@ = pointer to the source definition + * + * Returns: --- + * + * Use: Accepts an incoming connection and attaches it to a target + * endpoint. + */ + +static void ss_accept(int fd, unsigned mode, void *p) +{ + ssource *ss = p; + ssept *e; + endpt *ee; + reffd *r; + + /* --- Make the file descriptor --- */ + + { + int opt = 1; + if ((r = ss->a->ops->accept(fd, ss->ao, ss->s.desc)) == 0) + return; + setsockopt(r->fd, SOL_SOCKET, SO_OOBINLINE, &opt, sizeof(opt)); + fdflags(r->fd, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC); + } + + /* --- Make an endpoint --- */ + + e = CREATE(ssept); + e->e.ops = &ssept_ops; + e->e.other = 0; + e->e.f = EPF_FILE; + e->e.t = 0; + e->e.in = e->e.out = r; + e->s = ss; + REFFD_INC(r); + + /* --- Obtain the target endpoint and let rip --- */ + + if ((ee = ss->t->ops->create(ss->t, ss->s.desc)) == 0) { + REFFD_DEC(r); + REFFD_DEC(r); + DESTROY(e); + return; + } + + /* --- Remove the listening socket if necessary --- */ + + ss->o.conn--; + if (!ss->o.conn) { + fw_log(-1, "[%s] maximum connections reached", ss->s.desc); + sel_rmfile(&ss->r); + close(ss->r.fd); + if (ss->a->ops->unbind) + ss->a->ops->unbind(ss->a); + } + + /* --- Let everything else happen --- */ + + fw_inc(); + endpt_join(&e->e, ee); +} + +/* --- @ss_listen@ --- * + * + * Arguments: @ssource *ss@ = source to listen on + * + * Returns: --- + * + * Use: Sets the socket to listen again, if it stopped for some + * reason. This is a copy of the code in the @read@ function, + * because it has different (wildly different) error handling + * behaviour. + */ + +static void ssource_destroy(source */*s*/); + +static void ss_listen(ssource *ss) +{ + gen_addr *ga = (gen_addr *)ss->a; + int fd; + + fw_log(-1, "[%s] reattaching listener", ss->s.desc); + + /* --- Make the socket --- */ + + if ((fd = socket(ga->a.ops->pf, SOCK_STREAM, 0)) < 0) { + fw_log(-1, "[%s] couldn't create socket: %s", + ss->s.desc, strerror(errno)); + goto fail_0; + } + + /* --- Set it to allow address reuse --- */ + + { + int opt = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + fdflags(fd, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC); + } + + /* --- Bind it to the right port --- */ + + if (bind(fd, &ga->sa, ga->a.sz)) { + fw_log(-1, "[%s] couldn't bind to %s: %s", ss->s.desc, strerror(errno)); + goto fail_1; + } + if (ga->a.ops->bound) + ga->a.ops->bound(&ga->a, ss->ao); + + /* --- Set it to listen for connections --- */ + + if (listen(fd, 5)) { + fw_log(-1, "[%s] couldn't listen on socket: %s", + ss->s.desc, strerror(errno)); + goto fail_1; + } + + /* --- Set the listener up again --- */ + + ss->r.fd = fd; + sel_addfile(&ss->r); + return; + + /* --- Tidy up if it failed --- * + * + * I'll just remove the entire source. + */ + +fail_1: + close(fd); +fail_0: + ss->o.conn = 0; + ssource_destroy(&ss->s); +} + +/* --- @attach@ --- */ + +static void ssource_attach(source *s, scanner *sc, target *t) +{ + ssource *ss = (ssource *)s; + int fd; + + ss->t = t; + + /* --- Initialize the description string --- */ + + { + dstr d = DSTR_INIT; + dstr_puts(&d, "socket."); + ss->a->ops->print(ss->a, ADDR_SRC, &d); + dstr_puts(&d, " -> "); + dstr_puts(&d, ss->t->desc); + ss->s.desc = xstrdup(d.buf); + dstr_destroy(&d); + } + + /* --- Initialize the socket for listening --- */ + + { + gen_addr *ga = (gen_addr *)ss->a; + + /* --- Make the socket --- */ + + if ((fd = socket(ga->a.ops->pf, SOCK_STREAM, 0)) < 0) + error(sc, "couldn't create socket: %s", strerror(errno)); + + /* --- Set it to allow address reuse --- */ + + { + int opt = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + fdflags(fd, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC); + } + + /* --- Bind it to the right port --- */ + + if (bind(fd, &ga->sa, ga->a.sz)) + error(sc, "couldn't bind to %s: %s", ss->s.desc, strerror(errno)); + if (ga->a.ops->bound) + ga->a.ops->bound(&ga->a, ss->ao); + + /* --- Set it to listen for connections --- */ + + if (listen(fd, 5)) + error(sc, "couldn't listen on socket: %s", strerror(errno)); + } + + /* --- We're ready to go now --- */ + + sel_initfile(sel, &ss->r, fd, SEL_READ, ss_accept, ss); + sel_addfile(&ss->r); + source_add(&ss->s); + fw_inc(); +} + +/* --- @destroy@ --- */ + +static void ssource_destroy(source *s) +{ + ssource *ss = (ssource *)s; + + if (ss->o.conn) { + sel_rmfile(&ss->r); + close(ss->r.fd); + if (ss->a->ops->unbind) + ss->a->ops->unbind(ss->a); + } + if (ss->a->ops->freeopts) + ss->a->ops->freeopts(ss->ao); + else + DESTROY(ss->ao); + /* free(ss->s.desc); */ + ss->a->ops->destroy(ss->a); + ss->t->ops->destroy(ss->t); + source_remove(&ss->s); + DESTROY(ss); + fw_dec(); +} + +/* --- Source definition block --- */ + +source_ops ssource_ops = { + "socket", + ssource_option, ssource_read, ssource_attach, ssource_destroy +}; + +/*----- Target definition -------------------------------------------------*/ + +/* --- @read@ --- */ + +static target *starget_read(scanner *sc) +{ + starget *st; + dstr d = DSTR_INIT; + + (void)(conf_prefix(sc, "socket") || conf_prefix(sc, "sk")); + st = CREATE(starget); + st->t.ops = &starget_ops; + st->a = getaddr(sc, ADDR_DEST); + dstr_puts(&d, "socket."); + st->a->ops->print(st->a, ADDR_DEST, &d); + st->t.desc = xstrdup(d.buf); + dstr_destroy(&d); + return (&st->t); +} + +/* --- @create@ --- * + * + * Arguments: @target *t@ = pointer to target + * @const char *desc@ = description of connection + * + * Returns: Pointer to a created endpoint. + * + * Use: Generates a target endpoint for communication. + */ + +static endpt *starget_create(target *t, const char *desc) +{ + starget *st = (starget *)t; + stept *e = CREATE(stept); + int fd; + gen_addr *ga = (gen_addr *)st->a; + int opt; + + if ((fd = socket(st->a->ops->pf, SOCK_STREAM, 0)) < 0) + return (0); + setsockopt(fd, SOL_SOCKET, SO_OOBINLINE, &opt, sizeof(opt)); + fdflags(fd, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC); + e->e.ops = &stept_ops; + e->e.other = 0; + e->e.f = EPF_FILE | SKF_CONN; + e->e.t = 0; + e->desc = desc; + + /* --- Pay attention --- * + * + * This bit is quite subtle. The connect can succeed or fail later: that's + * fine. The problem comes if it makes its mind up right now. The flag + * @SKF_CONN@ signifies that I'm trying to connect. I set it up to begin + * with and @stept_go@ turns it off when it's done: @stept_close@ uses it + * to decide whether to kill the connection. The flag @EPF_PENDING@ is + * only set after @conn_init@ returns and @SKF_CONN@ is still set (meaning + * that the connection is still in progress). That's used to let + * @stept_go@ know whether to kill the other endpoint. The flag + * @SKF_BROKEN@ is used to signify an immediate failure. + */ + + conn_init(&e->c, sel, fd, &ga->sa, ga->a.sz, stept_go, e); + if (e->e.f & SKF_BROKEN) { + DESTROY(e); + return (0); + } + if (e->e.f & SKF_CONN) + e->e.f |= EPF_PENDING; + fw_inc(); + return (&e->e); +} + +/* --- @destroy@ --- */ + +static void starget_destroy(target *t) +{ + starget *st = (starget *)t; + st->a->ops->destroy(st->a); + /* free(st->t.desc); */ + DESTROY(st); +} + +/* --- Socket target definition block --- */ + +target_ops starget_ops = { + "socket", + 0, starget_read, starget_create, starget_destroy +}; + +/*----- That's all, folks -------------------------------------------------*/