/* This file is in the public domain. */ #define CLIENTS_PER_PROCESS 64 #define MIN_CLIENT_SLOTS 5 #define DNSL_TIMER_INC 60 #define DEF_SLEEP 0 #define PARENT_TIMER_INC 600 #define MAX_DNSL_WORKERS 128 #define MAXILINELEN 65536 #define REAL_SMTP_SERVER "backend-mx.example.net" /* LISTEN_ON can be 0, to listen on all available addresses, or a space-separated list of addresses to listen on, suitable for passing to getaddrinfo(). */ #define LISTEN_ON 0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "checkip.h" #include "sbb-hook.h" #include "validaddr.h" extern const char *__progname; /* This is because client counts are sent around in unsigned chars. */ #if CLIENTS_PER_PROCESS >= 256 #error "CLIENTS_PER_PROCESS must be less than 256" #endif /* I think if this is true it will go wild spawning listeners. */ #if MIN_CLIENT_SLOTS >= CLIENTS_PER_PROCESS #warning "MIN_CLIENT_SLOTS >= CLIENTS_PER_PROCESS probably won't work well" #endif /* * The protocols between the various processes are relatively simple. * * Listeners can be in any of these states: * * LS_LISTENING * The listener is accepting new connections and managing * those it already has. * * LS_FULL * LS_STOPPING * LS_DYING * The listener is managing existing connections but not * listening for new connections. * * When the parent forks a new listener it comes up in state * LS_LISTENING. When a listener acquires a new client or loses an * existing client, it sends the parent an LM_LISTENING message giving * the number of client slots it has available - or, if none, it * switches to LS_FULL and sends LM_FULL (with no following count). * * When the parent wants to kill off a listener, it sends it an LM_STOP * byte. The listener then switches from LS_LISTENING or LS_FULL (as * the case may be) to LS_STOPPING. * * When the parent dies, the listener notices the connection to the * parent break and switches to LS_DYING. This state is just like * LS_STOPPING except that the listener does not try to interact * further with the parent. * * When an LS_STOPPING listener has no clients, it sends the parent an * LM_DYING byte and exits; LS_DYING is the same except that the * listener doesn't send anything before exiting. * * When a listener sees an SMTP command that changes global state, it * sends the parent an LM_STATE message. The second byte is one of * the STATE_* constants, and any later bytes depend on which one. * The parent then echoes this message to all listeners, so they can * change their state correspondingly. * * When an LS_LISTENING listener sees a new connection, it accepts it * and starts speaking SMTP to it. When it gets a valid RCPT, it does * DNSBL checks (see below), and if they pass, opens a connection to * the real SMTP server, does initial protocol with it, and (except * for filtering invalid RCPTs) transparently passes the rest of the * SMTP protocol between the two. If it gets an error trying to talk * to the real SMTP server, it starts 4xxing all SMTP protocol * commands. * * The parent tries to keep no fewer than MIN_LISTENERS and no more * than MAX_LISTENERS listeners in state LS_LISTENING. If there are * fewer, it forks a new one; if there are more, it tells one to stop. * * DNSBL lookups are also supported (actually, filtering based on * source IP address; it's designed for DNSBLs, but could actually * filter based on any criterion you care to code). Since the SMTP * workers cannot afford to block, and (while possible) it's * relatively difficult to do DNS lookups without blocking, this is * handled by separate processes. There is a DNSL overseer, which has * a number of DNSL worker children. When an SMTP worker wants to do * a DNSL lookup, it sends the necessary information to the DNSL * overseer, along with a pipe to send the response back through; the * DNSL overseer chooses a non-busy worker process and sends the * request to it. The worker process sends its response back and then * tells the overseer that it's available to handle requests again. * * The DNSL-lookup parent process is separate from the listener parent * so that the listener parent can be killed to shut down operation * without killing off the DNSL-lookup parent. * * Many of the pipes between the DNSL pieces are duplicated. This is * primarily because AF_LOCAL SOCK_DGRAM sockets do not, it appears, * wake up a poll waiting for read when their peer disconnects. So we * have a SOCK_DGRAM socket for data and a SOCK_STREAM socket for * death detection. This applies to the pipe between the connection * handlers and the DNSL parent and to the pipes between the DNSL * parent and the DNSL workers. * * DNSL requests pass through the overseer rather than going directly * to the DNSL workers. This does not have to be so; we could instead * oave the DNSL workers pick up the requests directly. But in that * case the DNSL workers would have to tell the overseer when they're * busy, so the overseer can know when to fork a new one and when to * kill off an existing one. It seems slightly cleaner to me to have * the overseer handle requests than to have the workers constantly * reporting their status, though it's admittedly a close call. * * When compiled with DOSTATS defined, the protocol between the * listeners and the parent becomes significantly chattier. In this * case, every connection accept(), every SMTP command, every address * checked, and every DATA body handled, each of these things produces * a message to the parent, for statistics-keeping. (The messages are * SM_ACCEPT, SM_ADDR_OK, SM_ADDR_BAD, SM_ADDR_UNK, SM_DNSL_OK, * SM_DNSL_BAD, SM_DNSL_FAIL, SM_BODY, and one message per SMTP * command in the smtpcmds[] array below.) Of course, these are not * sent when the listener is in state LS_DYING. Addresses tested with * XRT are not counted towards any of the SM_ADDR_* counts. Message * bodies not completed (eg, client drops the connection before the * dot) are not counted. Statistics are printed as part of the * response to SIGUSR1. * * The SMTP listener imposes comparatively few checks. It does check * all command lines to ensure they don't contain octets in the 0-31 * or 128-159 ranges, and it imposes a limit of 64K on each SMTP * command line. (It imposes neither of these restrictions on * DATA-phase data.) It requires that commands be syntactic in a few * of the most rudimentary ways. A line that attempts to exceed the * 64K limit produces an abrupt and ungraceful dropping of the * connection; command lines with forbidden octets produce 500 syntax * error replies. * * VRFY and EXPN are implemented, but as dummy routines, the effect of * which is that they are completely disabled. * * Source routes in RCPT commands have all but the part after the colon * silently disacarded before being checked or passed along. * * The state machine for a CLIENT looks like this. Each state has * flags in []; these flags include: * R There is a recipient hanging fire * S There is a server connection * F There is a failure reason * L Can't be in this state unless logging is on * * startup: * CS_SBB * CS_SBB: [] wait for banner_sleep seconds * 220->client, start DNSL lookup, CS_GREET * CS_GREET: [] read from client * get HELO: 250->client, CS_HELO * get RSET: 250->client, CS_GREET * get QUIT: 221->client, CS_DEAD * get other: error->client * CS_HELO: [] read from client * get HELO: 250->client, CS_HELO * get MAIL: * already did DNSL? * if yes 250->client, CS_MAIL * if no 250->client, request DNSL lookup, CS_DNSL * get RSET: 250->client, CS_HELO * get QUIT: 221->client, CS_DEAD * get other: error->client * CS_DNSL: [] wait for DNSL response, CS_MAIL * CS_MAIL: [] read from client * get HELO: 250->client, CS_HELO * get DATA: 503->client, CS_MAIL * get RSET: 250->client, CS_HELO * get QUIT: 221->client, CS_DEAD * get RCPT: * if to postmaster: start server connect, CS_SCONN * DNSL? * if bad logging? * if yes save rcpt, 250->client, why, CS_FAIL * if no drop rcpt, 550->client, CS_MAIL * if unk drop rcpt, 451->client, CS_MAIL * if ok valid? * if yes start server connect, CS_SCONN * if no logging? * if yes save rcpt, 250->client, why, CS_IRCPTS * if no drop rcpt, 550->client, CS_MAIL * if unk drop rcpt, 451->client, CS_MAIL * get other: error->client * CS_IRCPTS: [FL] read from client * get HELO: 250->client, nowhy, CS_HELO * get DATA: 354->client, CS_IDATA * get RSET: 250->client, nowhy, CS_HELO * get QUIT: 221->client, nowhy, CS_DEAD * get RCPT: * valid? * if yes drop rcpt, 451->client, CS_IRCPTS * if unk drop rcpt, 451->client, CS_IRCPTS * if no save rcpt, 250->client, CS_IRCPTS * get other: error->client * CS_IDATA: [FL] copy data client->file; on dot, * error->client, log, nowhy, CS_HELO * CS_SCONN: [RS] wait for server connect * success: CS_SGREET * failure: close server, drop rcpt, 451->client, CS_CLOSING * CS_SGREET: [RS] read from server * get 2xx: HELO->server, CS_SHELO * get other: close server, drop rcpt, 451->client, CS_CLOSING * CS_SHELO: [RS] read from server * get 2xx: MAIL->server, CS_SMAIL * get other: close server, drop rcpt, 451->client, CS_CLOSING * CS_SMAIL: [RS] read from server * get 2xx: RCPT->server, CS_SRCPT * get 4xx: close server, drop rcpt, echo->client, CS_TEMPFAIL * get 5xx: * logging? * if yes close server, 250->client, save rcpt, why, CS_FAIL * if no close server, echo->client, drop rcpt, CS_HARDFAIL * get other: close server, drop rcpt, 451->client, CS_TEMPFAIL * CS_SRCPT: [RS] read from server * get 2xx: save rcpt, 250->client, CS_SGRCPTS * get 4xx: echo->client, drop rcpt, CS_SNRCPTS * get 5xx: * logging? * if yes save rcpt, 250->client, why, CS_SBRCPTS * if no drop rcpt, echo->client, CS_SNRCPTS * get other: close server, drop rcpt, 451->client, CS_TEMPFAIL * CS_SNRCPTS: [S] read from client * get HELO: close server, 250->client, CS_HELO * get RSET: close server, 250->client, CS_HELO * get QUIT: close server, 221->client, CS_DEAD * get DATA: 503->client, CS_SNRCPTS * get RCPT: * valid? * if yes RCPT->server, CS_SRCPT * if no logging? * if yes drop rcpt, 451->client, CS_SNRCPTS * if no drop rcpt, 550->client, CS_SNRCPTS * if unk drop rcpt, 450->client, CS_SNRCPTS * get other: error->client * CS_SGRCPTS: [S] read from client * get HELO: close server, 250->client, CS_HELO * get RSET: close server, 250->client, CS_HELO * get QUIT: close server, 221->client, CS_DEAD * get DATA: DATA->server, CS_SDATA * get RCPT: * valid? * if yes RCPT->server, CS_SGRCPT * if no logging? * if yes drop rcpt, 451->client, CS_SGRCPTS * if no drop rcpt, 550->client, CS_SGRCPTS * if unk drop rcpt, 450->client, CS_SGRCPTS * get other: error->client * CS_SGRCPT: [RS] read from server * get 2xx: save rcpt, 250->client, CS_SGRCPTS * get 4xx: drop rcpt, echo->client, CS_SGRCPTS * get 5xx: * logging? * if yes drop rcpt, 451->client, CS_SGRCPTS * if no drop rcpt, echo->client, CS_SGRCPTS * get other: close server, drop rcpt, 451->client, CS_TEMPFAIL * CS_SBRCPTS: [SFL] read from client * get HELO: close server, 250->client, nowhy, CS_HELO * get RSET: close server, 250->client, nowhy, CS_HELO * get QUIT: close server, 221->client, nowhy, CS_DEAD * get DATA: close server, 354->client, CS_BDATA * get RCPT: * valid? * if yes RCPT->server, CS_SBRCPT * if no drop rcpt, 451->client, CS_SBRCPTS * if unk drop rcpt, 450->client, CS_SBRCPTS * get other: error->client * CS_SBRCPT: [RSFL] read from server * get 2xx: drop rcpt, 451->client, CS_SBRCPTS * get 4xx: drop rcpt, echo->client, CS_SBRCPTS * get 5xx: save rcpt, 250->client, CS_SBRCPTS * get other: close server, drop rcpt, 451->client, nowhy, CS_TEMPFAIL * CS_BDATA: [FL] copy data client->file; on dot, * error->client, log, nowhy, CS_HELO * CS_SDATA: [S] read from server * get 3xx: 354->client, CS_SDCOPY * get other: * logging? * if yes close server, 354->client, why, CS_BDATA * if no echo->client, CS_SGRCPTS * CS_SDCOPY: [S] copy data client->server(&file); on dot, CS_SDOT * CS_SDOT: [S] read from server * get 2xx: echo->client, close server, CS_HELO * get 4xx: echo->client, close server, CS_HELO * get 5xx: * logging? * if yes 250->client, log, close server, CS_HELO * if no echo->client, close server, CS_HELO * get other: close server, error->client, CS_TEMPFAIL * CS_CLOSING: [] read from client * get QUIT: 221->client, CS_DEAD * get other: 421->client * CS_FAIL: [FL] read from client * get HELO: 250->client, nowhy, CS_HELO * get DATA: 354->client, CS_BDATA * get RSET: 250->client, nowhy, CS_HELO * get QUIT: 221->client, nowhy, CS_DEAD * get RCPT: save rcpt, 250->client, CS_FAIL * get other: error->client * CS_TEMPFAIL: [] read from client * get HELO: 250->client, CS_HELO * get RSET: 250->client, CS_HELO * get QUIT: 221->client, CS_DEAD * get RCPT: 450->client, CS_TEMPFAIL * get other: error->client * CS_HARDFAIL: [] read from client * get HELO: 250->client, CS_HELO * get RSET: 250->client, CS_HELO * get QUIT: 221->client, CS_DEAD * get RCPT: 550->client, CS_HARDFAIL * get other: error->client * * Some SMTP commands are valid in any "read from client" state: * * get NOOP: 250->client * get VRFY: 553->client * * Also, EHLO counts as HELO. */ /* * There are multiple distinct namespaces here; I use different numbers * for all of them to get slightly better error checking - if, for * example, I use LS_DEAD where I mean CS_DEAD, this sort of numbering * will help catch it. */ /* Listener State */ /* These are used both by the parent, to record a listener's state in the structure for that listener, and by the listener, in ownstate. */ #define LS_LISTENING 1 #define LS_FULL 2 #define LS_STOPPING 3 #define LS_DYING 4 /* LS_DEAD is not truly a listener state; it is used to mark a listener data structure corresponding to a listener that's died. (Or, to put it another way, it's a listener state in the parent, but not in a listener.) */ #define LS_DEAD 5 /* Client State */ #define CS_SBB 6 /* delaying before sending greeting */ #define CS_GREET 7 /* sent greeting, expecting HELO */ #define CS_HELO 8 /* post-HELO, want MAIL */ #define CS_DNSL 9 /* waiting for DNSL lookup */ #define CS_MAIL 10 /* post-MAIL, want first RCPT */ #define CS_IRCPTS 11 /* collecting invalid RCPTs */ #define CS_IDATA 12 /* collecting data for to-be-rejected message */ #define CS_SCONN 13 /* awaiting server connection */ #define CS_SGREET 14 /* awaiting server greeting */ #define CS_SHELO 15 /* awaiting server response to HELO */ #define CS_SMAIL 16 /* awaiting server response to MAIL */ #define CS_SRCPT 17 /* awaiting server response to RCPT */ #define CS_SNRCPTS 18 /* collecting RCPTs, server 4xxed all we've seen */ #define CS_SGRCPTS 19 /* collecting RCPTs the server likes */ #define CS_SGRCPT 20 /* awaiting server response to RCPT */ #define CS_SBRCPTS 21 /* collecting RCPTs the server doesn't like */ #define CS_SBRCPT 22 /* awaiting server response to RCPT */ #define CS_BDATA 23 /* collecting data for to-be-rejected message */ #define CS_SDATA 24 /* awaiting server response to DATA */ #define CS_SDCOPY 25 /* collecting data to be sent to server */ #define CS_SDOT 26 /* awaiting server response to closing dot */ #define CS_CLOSING 27 /* waiting for QUIT, 421ing all else */ #define CS_FAIL 28 /* collecting RCPTs, don't care about validity */ #define CS_TEMPFAIL 29 /* softfailing all recipients */ #define CS_HARDFAIL 30 /* hardfailing all recipients */ #define CS_DEAD 31 /* transient state during client shutdown */ #define CS_BURIED 32 /* like CS_DEAD only more so */ #define CS_SCLOSE 33 /* transient state during server closedown */ /* Listener Message */ #define LM_FULL 34 #define LM_LISTENING 35 #define LM_STOP 36 #define LM_DYING 37 #define LM_STATE 38 #define LM_STATS 39 #define LM_STATS_DATA 40 /* never actually sent - used internally */ /* State-change values */ #define STATE_LOG 41 /* one byte further data, zero or nonzero */ #define STATE_TIMEOUT 42 /* one byte further data, timeout in seconds */ /* Statistics Message */ #define SM_FIRST_STATS (SM_ACCEPT) #define SM_ACCEPT 43 #define SM_ADDR_OK 44 #define SM_ADDR_BAD 45 #define SM_ADDR_UNK 46 #define SM_DNSL_OK 47 #define SM_DNSL_BAD 48 #define SM_DNSL_FAIL 49 #define SM_BODY 50 #define SM__CMD 51 #define SM_CMD_HELO (SM__CMD+CMD_HELO) #define SM_CMD_EHLO (SM__CMD+CMD_EHLO) #define SM_CMD_MAIL (SM__CMD+CMD_MAIL) #define SM_CMD_RCPT (SM__CMD+CMD_RCPT) #define SM_CMD_DATA (SM__CMD+CMD_DATA) #define SM_CMD_RSET (SM__CMD+CMD_RSET) #define SM_CMD_NOOP (SM__CMD+CMD_NOOP) #define SM_CMD_QUIT (SM__CMD+CMD_QUIT) #define SM_CMD_VRFY (SM__CMD+CMD_VRFY) #define SM_CMD_XRT (SM__CMD+CMD_XRT) #define SM_CMD_XCTL (SM__CMD+CMD_XCTL) #define SM_CMD_POST (SM__CMD+CMD_POST) #define SM_LAST_STATS (SM_CMD_POST) #define STATS_N (SM_LAST_STATS+1-SM_FIRST_STATS) /* Switch case pseudo-values. The are designed so you can write things like switch (...) { case DIGITS: ... } */ #define DIGITS '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9' #define UCLETTERS 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z' #define LCLETTERS 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z' /* * Data structures. * * LSOCK is a listening socket. There is one of these for each port on * which the shim accepts conenctions. They are set up by the parent * and inherited by each SMTP worker forked; it's the SMTP workers * that actually use them. * * LISTENER is used by the parent to record information about an SMTP * worker process. (These processes were originally thought of as * listeners for incoming SMTP connections, which is whence the name.) * * CLIENT is used by the SMTP workers to represent an SMTP client * connection and the stuff attached thereto. * * IOQ is an input/output queue; it contains both queued blocks of data * to be written and an input data buffer. * * OB is an output block of data - an IOQ uses these to record output * data. * * DNSLPROC is used by the DNSL overseer to represent a DNSL worker. * * RCPT representes a recipient as a local-part and a domain. The * local-part is never nil; the domain is for the bare-postmaster * case. * * MSG encapsulates the state necessary to accumulate listener * messages, since this is wanted in multiple places. * * DNSRQ encapsulates a request from an SMTP worker to the DNSL * subsystem; the DNSL parent queues these under some circumstances * (which is why this struct is designed to form a queue). */ typedef struct lsock LSOCK; typedef struct listener LISTENER; typedef struct client CLIENT; typedef struct ioq IOQ; typedef struct ob OB; typedef struct dnslproc DNSLPROC; typedef struct rcpt RCPT; typedef struct msg MSG; typedef struct dnsrq DNSRQ; struct dnsrq { DNSRQ *link; int fd; char *data; int len; } ; #define MAX_LM_LEN (1+8+1+128) struct msg { int len; int fill; unsigned char buf[MAX_LM_LEN]; } ; struct rcpt { char *lp; char *dom; } ; #define NORCPT ((RCPT){.lp=0,.dom=0}) struct dnslproc { DNSLPROC *link; unsigned int flags; #define DPF_DEAD 0x00000001 /* either exited or sick; transient */ #define DPF_BUSY 0x00000002 /* busy handling a request */ int datafd; int datapx; int deathfd; int deathpx; pid_t kid; } ; struct ob { OB *link; const char *data; char *tofree; int len; int ptr; } ; struct ioq { int fd; OB *q; OB **qt; char ibuf[8192]; int ibfill; int ibptr; } ; struct client { CLIENT *link; unsigned long long int serial;/* overall serial number */ int state; /* one of the CS_* values */ int scstate; /* saved state value when in CS_SCLOSE */ /* lbuf/lalloc/llen form an input line buffer, for input from wherever input is being read from - the client or the server. lalloc is the amount of memory allocated; llen is the amount used. */ unsigned char *lbuf; int lalloc; int llen; unsigned int statstate; /* state for LM_STATS response */ #define CSS_ATNL 0x00000001 int termstate; /* state machine for data termination */ int px; /* poll index */ time_t timeout; /* time at which timeout expires */ struct sockaddr *peeraddr; /* peer's address */ int peeralen; /* length of peeraddr */ char *peertext; /* text form of peeraddr */ /* values from the SMTP dialogue. helo_as is from the HELO, env_from from the MAIL; each is nil if that hasn't happened yet. rcpt records a RCPT address while the shim holds onto it (which is only until it's been dealt with - eg during starting up a backend connection). */ char *helo_as; char *env_from; RCPT rcpt; struct timeval starttime; /* when connection came up */ struct timeval banner_at; /* when to emit banner */ int dnsl_status; /* DNSL status of this peer IP */ #define DNSLSTAT_UNK 1 /* don't yet know */ #define DNSLSTAT_OK 2 /* we like it */ #define DNSLSTAT_BAD 3 /* we don't like it */ #define DNSLSTAT_FAIL 4 /* we can't tell */ char *dnsl_msg; /* long message if BAD or FAIL */ char *dnsl_tag; /* log-entry tag if BAD or FAIL */ int dnsl_fd; /* fd to DNSL worker while query live */ int data_fd; /* fd of file holding message body */ #define NO_FD (-1) #define BAD_FD (-2) char *data_file; /* name of file holding message body */ int logging; /* logging state for this client */ char *fail_reason; /* reason message isn't accepted */ int nrcpts; /* number of saved recipients */ int arcpts; /* number of recipients rcpts points to */ RCPT *rcpts; /* saved recipients */ int baddata; /* just following a rejected DATA? */ int unrecog; /* unrecognized-command count */ /* When doing I/O, the state is looked at and something is chosen to add to the poll() list. checkfn is set to a function to call if the poll succeeds on that fd. If this is I/O to the client or server, ioq is set to point to either cq or sq (below) and iostr is set to a string for use in log messages, indicating what it's talking to. */ void (*checkfn)(CLIENT *); IOQ *ioq; const char *iostr; char *cid; /* connection ID, nil if not yet set */ unsigned int xid_serial; /* per-connection transaction serial number */ char *xid; /* transaction ID, nil if not yet set */ IOQ cq; /* I/O to client */ IOQ sq; /* I/O to backend server */ } ; struct lsock { LSOCK *link; int fd; int px; } ; struct listener { LISTENER *link; pid_t proc; int pipe; int state; int px; MSG msg; int fcs; } ; /* Parent->listener message, in the listener */ static MSG parentmsg; /* myhostname holds my hostname, per gethostname(); it's used various places, wherever this machine's hostname is needed. */ static char myhostname[MAXHOSTNAMELEN]; /* listeners is where the parent keeps its list of the SMTP workers. */ static LISTENER *listeners; /* accs is a list of sockets to accept SMTP connections on. As remarked above, this is set up by the parent at startup and inherited by the SMTP workers, which are the processes that actually use it. */ static LSOCK *accs; /* dnslprocs is where the DNSL overseer keeps the list of DNSL workers. */ static DNSLPROC *dnslprocs; /* dnslfree is a pointer to a non-busy DNSLPROC, to save unnecessary list walks in some places. */ static DNSLPROC *dnslfree; /* This records a low-water mark for the number of free DNSL workers. See dnsl_timeout() for more. */ static int dnsl_minfree; /* The pipe between the DNSL overseer, on one end, and the parent and SMTP workers, on the other. Two sockets (one for data and one for death), as sketched above. */ static int dnsl_data; static int dnsl_death; /* A pipe between the DNSL overseer and (only) the parent, specifically so the overseer can notice parent death. This variable is also used to record whether the overseer has seen parent death - it's set to -1 when the parent dies. */ static int dnsl_parent_death; /* Next time at which we want to call dnsl_timeout(). See dnsl_timeout() for more. */ static unsigned long int dnsl_timer; /* Last DNSL request/length, to help humans notice connection bombs. */ static struct sockaddr_storage last_dnsl_req; static int last_dnsl_rql; /* Next time at which we want to call parent_timeout(). See parent_timeout() for more. */ static unsigned long int parent_timer; /* This records a low-water mark for the number of free client slots. See parent_timeout() for more. */ static int client_minfree; /* List of fds to poll. Used by most processes. pfds is the list itself; apfds is the size of the memory allocated to pfds, with npfds the current number of pollfds set. */ static struct pollfd *pfds = 0; static int apfds = 0; static int npfds; /* Flag to notice SIGUSR1. */ static volatile int got_usr1; /* In the DNSL overseer, the queue of requests waiting for a DNS worker. */ static DNSRQ *dnsrqq; static DNSRQ **dnsrqt; /* In an SMTP worker, the pipe between it and the parent. */ static int parentpipe; /* Poll index for parentpipe. */ static int pppx; /* An SMTP worker's own state (LS_*). */ static int ownstate; /* The state value corresponding to our current process title. */ static int titlestate; /* The number of clients we last told our parent we had. */ static int toldclients; /* A listener's list of clients. */ static CLIENT *clients; /* See xid() for what this is. */ static unsigned int cid_serial = 0; /* Global logging state, copied to new clients' logging state. */ static int logging; /* avoid 0 on general principles; avoid 1 to help find bugs - 1 was used to mean `true' when logging was a boolean. */ #define LOG_OFF 2 #define LOG_REJ 3 #define LOG_ALL 4 /* Current sleep before banner. 0 -> no sleep. */ static int banner_sleep; /* Cached getpid() result (<0 -> nothing cached) */ static int cached_pid = -1; /* Next client serial number - used by listeners */ static unsigned long long int client_serial; /* * List of SMTP commands we recognize. XRT and XCTL are private * extensions. POST is not really an SMTP command; we recognize it in * order to do special handling for attempts to talk to us through * open Web proxies. */ static const char *smtpcmds[] = { "HELO", #define CMD_HELO 0 "EHLO", #define CMD_EHLO 1 "MAIL", #define CMD_MAIL 2 "RCPT", #define CMD_RCPT 3 "DATA", #define CMD_DATA 4 "RSET", #define CMD_RSET 5 "NOOP", #define CMD_NOOP 6 "QUIT", #define CMD_QUIT 7 "VRFY", #define CMD_VRFY 8 "XRT", #define CMD_XRT 9 "XCTL", #define CMD_XCTL 10 "POST", #define CMD_POST 11 0 }; #define CMD__N 12 #ifdef DOSTATS static time_t starttt; static char starttext[256]; static unsigned int counters[STATS_N]; #endif #if SM_LAST_STATS != SM__CMD+CMD__N-1 #error Statistics table disagrees with commands table #endif /* * We really should use CMSG_SPACE, CMSG_LEN, and CMSG_DATA. But we * want this same code to run on both a 1.4T sparc and a 1.6.1 i386, * and 1.4T is before the ABI change for SCM_RIGHTS. * * However, on i386, __cmsg_alignbytes() is 3, so the old way produces * the same data layout as the new. So we use the old way, but we * have these defines against the possibility of changing this in the * future. */ #ifdef OLD_CMSG_INTERFACE #define CMSPACE(x) (sizeof(struct cmsghdr)+(x)) #define CMLEN(x) (sizeof(struct cmsghdr)+(x)) #define CMSKIP(x) (sizeof(struct cmsghdr)) #else #define CMSPACE(x) (CMSG_SPACE((x))) #define CMLEN(x) (CMSG_LEN((x))) /* It's gross to have to do this, but it's forced upon us by the botched design of the the CMSG_* interface. This code is not as portable as I'd like - it depends on using a pointer past the end of an object, on some architectures - but I believe it's the least horrible of the available alternatives. */ #define CMSKIP(x) ((char *)CMSG_DATA((x))-(char *)(x)) #endif /* * It's not possible to do proper const poisoning in a program that * uses writev() without either extreme gyrations like mallocking * copies of every const string, or some kind of const-stripper. (The * abstractly correct way to handle this is to have one struct iovec * variant with non-const iov_base, for read, and one with const * iov_base, for write. That's not possible, for compatability * reasons if nothing else; it simply isn't how the interfaces are * currently designed.) * * We, of course, go the const-stripper route. Here it is. It may be * possible to do better than this, but I don't know of anything * better. This is admittedly not fully conformant C, in that it does * arithmetic on nil pointers, but it's the overall best alternative * I've seen. */ static void *deconst(const void *cvp) { return((((const char *)cvp)-(const char *)0)+(char *)0); } /* * Just like getpid(), except the result is cached, to save syscalls. */ static int mypid(void) { if (cached_pid < 0) cached_pid = getpid(); return(cached_pid); } /* * Some can't-happen has tripped. Gripe and core. */ static void panic_(const char *, int, const char *, ...) __attribute__((__format__(__printf__,3,4),__noreturn__)); static void panic_(const char *file, int line, const char *fmt, ...) #define panic(fmt,args...) panic_(__FILE__,__LINE__,fmt , ## args) { FILE *f; va_list ap; static int w(void *cookie __attribute__((__unused__)), const char *data, int len) { write(1,data,len); write(2,data,len); return(len); } f = fwopen(0,&w); fprintf(f,"[%d] PANIC (\"%s\", line %d): ",mypid(),file,line); va_start(ap,fmt); vfprintf(f,fmt,ap); va_end(ap); fprintf(f,"\n"); fclose(f); fflush(0); abort(); } /* * Write function for use by msgopen, below. */ static int mow(void *cookie __attribute__((__unused__)), const char *b, int l) { fwrite(b,1,l,stdout); fwrite(b,1,l,stderr); return(l); } /* * Close function for use by msgopen, below. */ static int moc(void *cookie __attribute__((__unused__))) { putc('\n',stderr); putc('\n',stdout); return(0); } /* * Return a FILE * that writes to stdout and stderr both; also, print * appropriate prefixes to those streams, and when the FILE * is * closed, append newlines. */ static FILE *msgopen(void) { printf("[%d] ",mypid()); fprintf(stderr,"%s: ",__progname); return(funopen(0,0,&mow,0,&moc)); } /* * Some serious error has occurred. Print the message to stdout * (prefixed with our PID) and to stderr (prefixed with __progname) * and exit(1). If fatal is set, then exit(1). */ static void serious(int, const char *, va_list) __attribute__((__format__(__printf__,2,0))); static void serious(int fatal, const char *fmt, va_list ap) { FILE *f; f = msgopen(); vfprintf(f,fmt,ap); fclose(f); fflush(0); if (fatal) exit(1); } /* * A fatal error has occurred. This is for errors that aren't * can't-happens but which are definitely fatal, like socketpair() * failing. */ static void fatal(const char *, ...) __attribute__((__format__(__printf__,1,2),__noreturn__)); static void fatal(const char *fmt, ...) { va_list ap; va_start(ap,fmt); serious(1,fmt,ap); va_end(ap); exit(1); /* actually can't be reached, but shut up gcc */ } /* * Some nonfatal but serious error has occurred. This is for serious * errors that we can nevertheless recover from. */ static void nonfatal(const char *, ...) __attribute__((__format__(__printf__,1,2))); static void nonfatal(const char *fmt, ...) { va_list ap; va_start(ap,fmt); serious(0,fmt,ap); va_end(ap); } /* * Wrappers for malloc-family routines, so we can get a trace of malloc * actions. */ static void mlog(const char *, ...) __attribute__((__format__(__printf__,1,2))); static void mlog(const char *fmt, ...) { static int initted = 0; static FILE *mlogf; va_list ap; if (! initted) { int fd; initted = 1; fd = open("shim-log.malloc-log",O_WRONLY|O_APPEND,0); if (fd < 0) { mlogf = 0; } else { mlogf = fdopen(fd,"w"); } } if (! mlogf) return; va_start(ap,fmt); vfprintf(mlogf,fmt,ap); va_end(ap); fflush(mlogf); } static const char *tstamp(void) { struct timeval tv; time_t tt; struct tm *tm; static char rbuf[64]; gettimeofday(&tv,0); tt = tv.tv_sec; tm = localtime(&tt); sprintf(&rbuf[0],"%02d:%02d:%02d.%06lu [%d]",tm->tm_hour,tm->tm_min,tm->tm_sec,(unsigned long int)tv.tv_usec,mypid()); return(&rbuf[0]); } static void *xmalloc(int nb, const char *f, int l) { void *rv; mlog("%s [%s, line %d] malloc(%d) -> ",tstamp(),f,l,nb); rv = malloc(nb); mlog("%p\n",rv); return(rv); } static void *xrealloc(void *data, int nb, const char *f, int l) { void *rv; mlog("%s [%s, line %d] realloc(%p,%d) -> ",tstamp(),f,l,data,nb); rv = realloc(data,nb); mlog("%p\n",rv); return(rv); } static void xfree(void *data, const char *f, int l) { mlog("%s [%s, line %d] free(%p)\n",tstamp(),f,l,data); free(data); } #define malloc(n) xmalloc((n),__FUNCTION__,__LINE__) #define realloc(x,n) xrealloc((x),(n),__FUNCTION__,__LINE__) #define free(x) xfree((x),__FUNCTION__,__LINE__) /* * When something happens that can be tolerated if it happens only * occasionally but should be a fatal error if it happens too often, * we use RATELIMIT(). This keeps a counter; each time the event * happens, it's incremented by "inc", and it's decremented by "dec" * per second, with a clamp to prevent it from going below 0. If it * ever reaches "max", we exit. */ #define RATELIMIT(inc,dec,max) \ do { static int c = 0; static time_t t = 0; ratelimit(&c,&t,(inc),(dec),(max)); } while (0) static void ratelimit(int *cp, time_t *tp, int inc, int dec, int max) { time_t now; if (*tp == 0) { time(tp); *cp = inc; return; } time(&now); if (now != *tp) { inc -= dec * (now - *tp); *tp = now; } inc += *cp; if (inc >= max) exit(1); if (inc < 0) inc = 0; *cp = inc; } /* * Initialize a new IOQ. */ static void ioq_init(IOQ *q, int fd) { q->fd = fd; q->q = 0; q->qt = &q->q; q->ibfill = 0; q->ibptr = 0; } /* * Queue a block of data for output. There are three variants of this, * depending on the exact semantics: * * ioq_oq_point Queues a pointer to the data; the data block * must remain valid until the write completes. * Nothing in particular is done when the block is * fully written. * * ioq_oq_copy A copy is made of the data; the data pointer * passed in need not remain valid once * ioq_oq_copy returns. The copy is freed once it * is completely written. * * ioq_oq_free Queues a pointer to the data, like * ioq_oq_point, but once the write is complete, * frees the data. This is semantically like * ioq_oq_copy followed immediately by free, * except it doesn't actually bother making a * copy. Of course, the data should not be * considered to remain valid after the call. */ static void ioq_oq_point(IOQ *q, const void *data, int len) { OB *b; if (len < 0) len = strlen(data); if (len < 1) return; b = malloc(sizeof(OB)); b->data = data; b->tofree = 0; b->len = len; b->ptr = 0; b->link = 0; *q->qt = b; q->qt = &b->link; } static void ioq_oq_copy(IOQ *q, const void *data, int len) { OB *b; if (len < 0) len = strlen(data); if (len < 1) return; b = malloc(sizeof(OB)+len); bcopy(data,b+1,len); b->data = (void *) (b+1); b->tofree = 0; b->len = len; b->ptr = 0; b->link = 0; *q->qt = b; q->qt = &b->link; } static void ioq_oq_free(IOQ *q, void *data, int len) { OB *b; if (len < 0) len = strlen(data); if (len < 1) { free(data); return; } b = malloc(sizeof(OB)); b->data = data; b->tofree = data; b->len = len; b->ptr = 0; b->link = 0; *q->qt = b; q->qt = &b->link; } /* * Flush all queued output blocks. This disposes of the blocks as if * they were successfully written, but does not actually write * anything anywhere. It's used when a connection is being torn down * in circumstances under which we don't care about queued data * getting flushed (such as after a write error). */ static void ioq_flush(IOQ *q) { OB *b; while (q->q) { b = q->q; q->q = b->link; if (b->tofree) free(b->tofree); free(b); } q->qt = &q->q; } /* * Try to write queued data for a client. Which connection the write * is attempted on depends on the client's ioq pointer. * * This accumulates multiple data blocks into a vector of struct * iovecs, finally calling writev to perform the write. It tries the * write as soon as either it's run out of data blocks or it's * accumulated min(kern.iov_max,64) blocks. If it actually writes * anything, it then flushes that many bytes, including handling any * completely-written blocks. */ static int try_write(CLIENT *c) { static int maxiov = 0; static struct iovec *iov = 0; static int aiov = 0; int niov; OB *b; int w; IOQ *q; if (maxiov == 0) { int mib[2]; size_t vallen; mib[0] = CTL_KERN; mib[1] = KERN_IOV_MAX; vallen = sizeof(maxiov); if (sysctl(&mib[0],2,&maxiov,&vallen,0,0) < 0) fatal("sysctl kern.iov_max: %s",strerror(errno)); if (maxiov > 64) maxiov = 64; } niov = 0; q = c->ioq; b = q->q; while (b && (niov < maxiov)) { if (niov >= aiov) iov = realloc(iov,(aiov=niov+8)*sizeof(*iov)); iov[niov].iov_base = deconst(b->data+b->ptr); iov[niov].iov_len = b->len - b->ptr; niov ++; b = b->link; } w = writev(q->fd,iov,niov); if (w < 0) { switch (errno) { case EWOULDBLOCK: case EINTR: return(0); } printf("[%d] write error to %s: %s\n",mypid(),c->iostr,strerror(errno)); return(1); } while ((b=q->q) && (w >= b->len-b->ptr)) { w -= b->len - b->ptr; q->q = b->link; if (b->tofree) free(b->tofree); free(b); } if (! b) q->qt = &q->q; if (w) { if (! b) { nonfatal("write overrun (leftover=%d) to %s",w,c->iostr); return(1); } b->ptr += w; } return(0); } /* * Try to fill an IOQ's input buffer. */ static int ioq_read(IOQ *q) { int r; r = read(q->fd,&q->ibuf[0],sizeof(q->ibuf)); if (r < 0) { switch (errno) { case EWOULDBLOCK: case EINTR: return(0); } return(errno); } if (r == 0) return(-1); q->ibfill = r; q->ibptr = 0; return(0); } /* * Send our SMTP greeting banner to a CLIENT's client connection. */ static void send_greeting(CLIENT *c) { ioq_oq_point(&c->cq,"220 ",-1); ioq_oq_point(&c->cq,&myhostname[0],-1); ioq_oq_point(&c->cq," SMTP listener ready.\r\n",-1); } /* * Set a file descriptor nonblocking. Or rather, try to; we ignore * errors from the set attempt. (F_GETFL cannot fail for any reason * that won't make the F_SETFL fail equally hard, so we don't need to * check for F_GETFL failures.) */ static void set_nonblock(int fd) { fcntl(fd,F_SETFL,fcntl(fd,F_GETFL,0)|O_NONBLOCK); } /* * Record that a CLIENT has had something happen, so its inactivity * timeout should be updated. */ static void fresh_timeout(CLIENT *c) { time(&c->timeout); c->timeout += 3600; } /* * Send a message to our parent. We deliberately ignore write errors * here; the only plausible error is EPIPE, which can happen if the * parent dies just as we do something that makes us want to write to * it - but if the parent is gone, doing nothing is the right thing. */ static void tellparent(int nb, ...) { va_list ap; int i; unsigned char buf[MAX_LM_LEN+1]; if ((nb < 1) || (nb > MAX_LM_LEN)) panic("tellparent: len %d not in [1..%d]",nb,MAX_LM_LEN); va_start(ap,nb); buf[0] = nb; for (i=0;idnsl_fd); c->dnsl_fd = NO_FD; c->state = CS_MAIL; } /* * Common routine called when a DNSL lookup fails for some reason that * does not allow us to tell whether the address is listed. */ static void dnsl_fail(CLIENT *c, const char *msg, const char *tag) { c->dnsl_status = DNSLSTAT_FAIL; c->dnsl_msg = strdup(msg); c->dnsl_tag = strdup(tag); dnsl_over(c); } /* * Called to request a DNSL lookup for CLIENT's client address. We * create a pipe for the DNSL worker to respond to us through, then * compose a query and send it off to the overseer to pass along to a * worker. */ static void request_dnslookup(CLIENT *c) { int p[2]; int n; struct iovec iov; struct msghdr mh; struct cmsghdr cmh; char ctlbuf[CMSPACE(sizeof(int))]; if (socketpair(AF_LOCAL,SOCK_STREAM,0,&p[0]) < 0) { printf("[%d] client DNSL socketpair: %s\n",mypid(),strerror(errno)); dnsl_fail(c,"Internal error","socketpair"); return; } iov.iov_base = c->peeraddr; iov.iov_len = c->peeralen; cmh.cmsg_len = CMLEN(sizeof(int)); cmh.cmsg_level = SOL_SOCKET; cmh.cmsg_type = SCM_RIGHTS; bcopy(&cmh,&ctlbuf[0],sizeof(struct cmsghdr)); bcopy(&p[1],&ctlbuf[CMSKIP(&cmh)],sizeof(int)); mh.msg_name = 0; mh.msg_namelen = 0; mh.msg_iov = &iov; mh.msg_iovlen = 1; mh.msg_control = &ctlbuf[0]; mh.msg_controllen = sizeof(ctlbuf); n = sendmsg(dnsl_data,&mh,0); if (n < 0) { printf("[%d] client DNSL sendmsg: %s\n",mypid(),strerror(errno)); dnsl_fail(c,"Internal error","sendmsg"); close(p[0]); close(p[1]); return; } close(p[1]); c->dnsl_fd = p[0]; } /* * Return true if the client is local. Such clients are different from * network clients in various ways; at this writing, (a) they never * have sleep-before-banner imposed on them and (b) they are permitted * to use XCTL commands. */ static int client_is_local(CLIENT *c) { switch (c->peeraddr->sa_family) { case AF_INET: return(ntohl(((struct sockaddr_in *)c->peeraddr)->sin_addr.s_addr) == 0x7f000001); break; case AF_INET6: return(IN6_IS_ADDR_LOOPBACK(&((struct sockaddr_in6 *)c->peeraddr)->sin6_addr)); break; } return(0); } /* * Convert a large number to base 62. n contains the number, with nn * giving its length in bytes; the least significant 8 bits are in * n[0]. t and nt similarly describe the space for the converted * number. n may include leading zeros, which will be ignored. nt * must be large enough to hold the result, or a panic occurs. */ static void base62(unsigned char *n, int nn, char *t, int nt) { int z; int tx; int dr; for (z=nn;(z>0)&&!n[z-1];z--) ; tx = 0; while (z > 0) { int i; dr = 0; for (i=z-1;i>=0;i--) { dr = (dr * 256) + n[i]; n[i] = dr / 62; dr %= 62; } if (tx >= nt) panic("base62 overflow"); t[tx++] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[dr]; if (! n[z-1]) z --; } if (tx >= nt) panic("base62 overflow"); t[tx++] = '\0'; } /* * Do a multiply-and-add step. n and nn describe the accumulator, with * the least significant 8 bits in n[0] and nn giving the number of * bytes; mul is the multiplier and val is the addend. The operation * can be sumamrized as n=(n*mul)+val. * * This is used with zero for mul when the current value of n is to be * ignored. */ static void ninc(unsigned char *n, int nn, unsigned long long int val, unsigned long long int mul) { int i; for (i=0;i>= 8; } } /* * Return the cid string for a client. * * This is a base-62 rendition of a large number formed from * - a per-SMTP-worker serial number * - the SMTP worker's process ID * - a timestamp, in seconds and microseconds */ static char *cid(CLIENT *c) { unsigned char nbuf[14]; char tbuf[20]; if (! c->cid) { struct timeval tv; pid_t me; gettimeofday(&tv,0); me = mypid(); ninc(&nbuf[0],sizeof(nbuf),cid_serial++,0); ninc(&nbuf[0],sizeof(nbuf),me,30000); ninc(&nbuf[0],sizeof(nbuf),tv.tv_sec,0x100000000ULL); ninc(&nbuf[0],sizeof(nbuf),tv.tv_usec,1000000); base62(&nbuf[0],sizeof(nbuf),&tbuf[0],sizeof(tbuf)); c->cid = strdup(&tbuf[0]); } return(c->cid); } /* * Return the xid string for a client. * * This is simply the client's cid string with a suffix formed from a * per-connection serial number. */ static char *xid(CLIENT *c) { unsigned char nbuf[14]; char tbuf[20]; if (! c->xid) { ninc(&nbuf[0],sizeof(nbuf),c->xid_serial++,0); base62(&nbuf[0],sizeof(nbuf),&tbuf[0],sizeof(tbuf)); asprintf(&c->xid,"%s.%s",c->cid,&tbuf[0]); } return(c->xid); } /* * Accept a new connection on the given LSOCK. If we get EINTR or * EWOULDBLOCK, ignore it (if there's a real connection, we'll pick it * up next time around the main loop); other errors get reported, and * if they occur too fast, we die. If we succeed, set up a new * CLIENT and toss it into the pot. */ static void accept_acc(LSOCK *a) { int new; struct sockaddr_storage ss; socklen_t sslen; CLIENT *c; char hnbuf[NI_MAXHOST]; char pnbuf[NI_MAXSERV]; sslen = sizeof(ss); new = accept(a->fd,(struct sockaddr *)&ss,&sslen); if (new < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return; break; } nonfatal("accept: %s",strerror(errno)); RATELIMIT(1,1,60); } STATS_RECORD(SM_ACCEPT); set_nonblock(new); c = malloc(sizeof(CLIENT)); /* link done below */ c->serial = client_serial ++; /* state done below */ c->lbuf = 0; c->lalloc = 0; c->llen = 0; /* statstate needs no init */ c->termstate = 0; /* px needs no init */ fresh_timeout(c); c->peeraddr = malloc(sslen); bcopy(&ss,c->peeraddr,sslen); c->peeralen = sslen; if (getnameinfo((const void *)&ss,sslen,&hnbuf[0],NI_MAXHOST,&pnbuf[0],NI_MAXSERV,NI_NUMERICHOST|NI_NUMERICSERV)) { asprintf(&c->peertext,"[getnameinfo failed; %s]",strerror(errno)); } else { asprintf(&c->peertext,"%s/%s",&hnbuf[0],&pnbuf[0]); } c->helo_as = 0; c->env_from = 0; c->rcpt = NORCPT; gettimeofday(&c->starttime,0); /* banner_at done below */ c->dnsl_status = DNSLSTAT_UNK; c->dnsl_msg = 0; c->dnsl_tag = 0; c->dnsl_fd = NO_FD; c->data_fd = NO_FD; c->data_file = 0; c->logging = logging; c->fail_reason = 0; c->nrcpts = 0; c->arcpts = 0; c->rcpts = 0; c->baddata = 0; c->unrecog = 0; /* checkfn/ioq/iostr need no init */ c->cid = 0; c->xid_serial = 1; c->xid = 0; ioq_init(&c->cq,new); ioq_init(&c->sq,-1); printf("[%d] connection from %s, cid %s\n",mypid(),c->peertext,cid(c)); if ((banner_sleep < 1) || client_is_local(c)) { c->state = CS_GREET; send_greeting(c); } else { c->state = CS_SBB; c->banner_at.tv_sec = c->starttime.tv_sec + banner_sleep; c->banner_at.tv_usec = c->starttime.tv_usec; } c->link = clients; clients = c; } /* * Add a file descriptor to the poll vector, showing interest in the * given event(s). */ static int add_pfd(int fd, short int ev) { if (npfds >= apfds) pfds = realloc(pfds,(apfds=npfds+8)*sizeof(struct pollfd)); pfds[npfds] = ((struct pollfd){.fd=fd,.events=ev,.revents=0}); return(npfds++); } /* * Append some data to a client's input line. */ static int iline_append(CLIENT *c, const void *data, int len) { if (c->llen+len > MAXILINELEN) return(1); if (c->llen+len > c->lalloc) c->lbuf = realloc(c->lbuf,c->lalloc=c->llen+len); bcopy(data,c->lbuf+c->llen,len); c->llen += len; return(0); } /* * Make a mallocked NUL-terminated string from a pointer-and-length * block of data. */ static char *blk_to_malloc_str(const void *data, int len) { char *t; t = malloc(len+1); if (len) bcopy(data,t,len); t[len] = '\0'; return(t); } /* * Look up our server, returning the struct addrinfo * chain of * addresses for it. If called multiple times, we either return the * same chain (if CACHE_SERVER is defined) or look it up afresh each * time (if not). This does mean that if CACHE_SERVER is not defined, * the resulting chain remains valid only until the next time * lookup_server() is called. * * This is done synchronously; all other interaction locks up while the * lookup is happening (this is why CACHE_SERVER exists). * * If the lookup fails, (*fail)() is called with a printf-style * argument list. It may throw out; if it returns, lookup_server() * returns nil. */ static struct addrinfo *lookup_server(void (*fail)(const char *, ...)) { struct addrinfo hints; static struct addrinfo *ai0 = 0; int e; if (ai0) #ifdef CACHE_SERVER return(ai0); #else freeaddrinfo(ai0); #endif hints.ai_flags = 0; hints.ai_family = PF_INET/*PF_UNSPEC*/; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; hints.ai_addrlen = 0; hints.ai_canonname = 0; hints.ai_addr = 0; hints.ai_next = 0; e = getaddrinfo(REAL_SMTP_SERVER,"smtp",&hints,&ai0); if (e) { (*fail)("can't getaddrinfo for %s/smtp: %s",REAL_SMTP_SERVER,gai_strerror(e)); return(0); } if (! ai0) { (*fail)("%s/smtp: successful lookup but no addresses?",REAL_SMTP_SERVER); return(0); } return(ai0); } /* * Start opening a connection to the server for the given client. * Loops over the list of server addresses, trying to connect to each * one, until it gets success, gets EINPROGRESS, or runs out of * addresses. In the first two cases, it leavse the client in state * SCONN or SGREET and returns 0; in the last case, it leaves the * client state unchanged and returns 1. */ static int open_server_conn(CLIENT *c) { struct addrinfo *ai0; struct addrinfo *ai; int e; int s; static void fail(const char *fmt, ...) { va_list ap; printf("[%d] ",mypid()); va_start(ap,fmt); vprintf(fmt,ap); va_end(ap); printf("\n"); } ai0 = lookup_server(&fail); for (ai=ai0;ai;ai=ai->ai_next) { s = socket(ai->ai_family,ai->ai_socktype,ai->ai_protocol); if (s < 0) { printf("[%d] socket (af %d): %s\n",mypid(),ai->ai_family,strerror(errno)); continue; } set_nonblock(s); if (connect(s,ai->ai_addr,ai->ai_addrlen) < 0) { char hnbuf[NI_MAXHOST]; char pnbuf[NI_MAXSERV]; if (errno == EINPROGRESS) { c->sq.fd = s; c->state = CS_SCONN; return(0); } e = errno; if (getnameinfo(ai->ai_addr,ai->ai_addrlen,&hnbuf[0],NI_MAXHOST,&pnbuf[0],NI_MAXSERV,NI_NUMERICHOST|NI_NUMERICSERV)) { printf("[%d] %s/smtp: connect failed [%s], can't get numeric hostname info [%s]\n",mypid(),REAL_SMTP_SERVER,strerror(e),strerror(errno)); } else { if (ai0->ai_next) { printf("[%d] connect %s [%s/%s]: %s\n",mypid(),REAL_SMTP_SERVER,&hnbuf[0],&pnbuf[0],strerror(e)); } else { printf("[%d] connect %s/%s: %s\n",mypid(),&hnbuf[0],&pnbuf[0],strerror(e)); } } close(s); continue; } c->sq.fd = s; c->state = CS_SGREET; return(0); } printf("[%d] can't connect to %s\n",mypid(),REAL_SMTP_SERVER); return(1); } /* * Send an RCPT command to a client's server connection. The address * sent is the recipient currently hanging fire for this client. */ static void send_rcpt(CLIENT *c, RCPT r) { ioq_oq_point(&c->sq,"RCPT To:<",-1); ioq_oq_copy(&c->sq,r.lp,-1); if (r.dom) { ioq_oq_point(&c->sq,"@",-1); ioq_oq_copy(&c->sq,r.dom,-1); } ioq_oq_point(&c->sq,">\r\n",-1); } /* * Pick apart the forward-path from an RCPT command. Returns 0 if all * went well, nonzero on error. On success, stores the local-part and * domain through *lp and *dom. */ static int parse_path(const char *buf, int len, char **lp, char **dom) { int x; int lpbeg; int lpend; int dombeg; int domend; static char get(int i) { return(((i < 0) || (i >= len)) ? '\0' : buf[i]); } static int skip_domain(int x) { if (get(x) == '[') { while (1) { x ++; switch (get(x)) { case '\\': x ++; break; case ']': goto out1; /* break 2, grrr */ break; case '\0': return(-1); break; } } out1:; x ++; } else { /* This is more lenient than 2822's grammar calls for; this code accepts zero-length labels, dots at the beginning or end of the string, and dashes at the beginning or end of labels. */ while (1) { switch (get(x)) { case '-': case '.': case DIGITS: case UCLETTERS: case LCLETTERS: x ++; break; default: goto out2; /* break 2, grrr */ break; } } out2:; } return(x); } /* This routine does not quite parse an RFC2821 "Path"; it parses the part of a path between the < and >. */ /* Ugh. There is no way to skip over a source route without parsing most of it, if only because domains can contain arbitrary characters in their source-text form (via domain -> address-literal -> general-address-literal -> dcontent -> quoted-pair). */ /* Fortunately, whether by chance or design, the grammar is relatively parser-friendly. In particular, minimal lookahead is called for. */ x = 0; /* First, skip any a-d-l: portion. */ if (get(x) == '@') { while (1) { x ++; x = skip_domain(x); if (x < 0) return(1); switch (get(x++)) { case ',': break; case ':': goto out3; /* break 2, grrr */ break; default: return(1); break; } } out3:; } /* What's left must be a mailbox - or an error. */ lpbeg = x; if (get(x) == '"') { /* quoted-string local-part */ while (1) { x ++; switch (get(x)) { case '\\': x ++; break; case '"': goto out4; /* break 2, grrr */ break; case '\0': return(1); break; } } out4:; x ++; } else { /* dot-string local-part */ /* This code is more lenient than 2822's grammar. In particular, 2822 requires that dots not be first or last, that no two dots be adjacent internally, and imposes some constraints on where - can appear. */ while (1) { switch (get(x)) { case '.': case DIGITS: case UCLETTERS: case LCLETTERS: case '!': case '#': case '$': case '%': case '&': case '\'': case '*': case '+': case '-': case '/': case '=': case '?': case '^': case '_': case '`': case '{': case '|': case '}': case '~': x ++; break; default: goto out5; /* break 2, grrr */ break; } } out5:; } lpend = x; if (get(x) != '@') return(1); x ++; dombeg = x; x = skip_domain(x); if (x < 0) return(1); domend = x; if (x != len) return(1); *lp = blk_to_malloc_str(buf+lpbeg,lpend-lpbeg); *dom = blk_to_malloc_str(buf+dombeg,domend-dombeg); return(0); } /* * Free the strings pointed to by a RCPT. */ static void free_rcpt(RCPT r) { free(r.lp); free(r.dom); } /* * Do common cleanup for RSET and similar commands. Most places that * call this do additional state-epsecific cleanup. */ static void do_rset(CLIENT *c) { int i; free(c->env_from); c->env_from = 0; for (i=c->nrcpts-1;i>=0;i--) free_rcpt(c->rcpts[i]); c->nrcpts = 0; if (c->data_fd >= 0) close(c->data_fd); c->data_fd = NO_FD; } /* * Process a HELO/EHLO. (Since we support no ESMTP extensions, our * response is the same for EHLO as for HELO.) */ static void do_helo(CLIENT *c, const char *arg, int arglen) { free(c->helo_as); c->helo_as = blk_to_malloc_str(arg,arglen); ioq_oq_point(&c->cq,"250 ",-1); ioq_oq_point(&c->cq,&myhostname[0],-1); ioq_oq_point(&c->cq,"\r\n",-1); do_rset(c); } /* * Handle RCPT. Perform rudimentary syntax checks and notice the * bare-postmaster special case, then hand the hard work off to * parse_path. */ static int do_rcpt(CLIENT *c, const char *arg, int arglen) { char *lp; char *dom; const char *abeg; int alen; static void syntax(void) { ioq_oq_point(&c->cq,"500 Syntax: RCPT To:\r\n",-1); printf("[%d] %s (%.*s): 500 Syntax: RCPT To:\n",mypid(),c->iostr,c->llen,c->lbuf); } if ( (arglen < 5) || (arg[arglen-1] != '>') || strncasecmp(arg,"to:",3) ) { syntax(); return(1); } abeg = &arg[3]; alen = arglen - 4; if (*abeg != '<') { syntax(); return(1); } abeg ++; alen --; if ((alen == 10) && !strncasecmp(abeg,"postmaster",10)) { lp = strdup("postmaster"); dom = 0; } else { if (parse_path(abeg,alen,&lp,&dom)) { syntax(); return(1); } } c->rcpt.lp = lp; c->rcpt.dom = dom; return(0); } /* * Handle MAIL. Perform rudimentary syntax checks, then remember the * sender for passing on to the backend server if/when we open a * connection to it. We don't syntax-check the argument further; we * leave that up to the backend. (If we don't ever talk to the * backend, we don't care if it's invalid, and if we do, syntactically * invalid envelope senders are rare enough we don't mind the extra * cost of bothering the backend.) */ static int do_mail(CLIENT *c, const char *arg, int arglen) { const char *abeg; int alen; static void syntax(void) { ioq_oq_point(&c->cq,"500 Syntax: MAIL From:\r\n",-1); printf("[%d] %s (%.*s): 500 Syntax: MAIL From:\n",mypid(),c->iostr,c->llen,c->lbuf); } if ( (arglen < 7) || (arg[arglen-1] != '>') || strncasecmp(arg,"from:",5) ) { syntax(); return(1); } abeg = &arg[5]; alen = arglen - 6; if (*abeg != '<') { syntax(); return(1); } abeg ++; alen --; free(c->env_from); c->env_from = blk_to_malloc_str(abeg,alen); return(0); } /* * Signal-handler for SIGUSR1. Don't try to do anything here; just set * a flag which is noticed in the main loop. */ static void handle_usr1(int sig __attribute__((__unused__))) { got_usr1 = 1; } /* * Set up SIGUSR1 handling. */ static void setup_usr1(void) { struct sigaction sa; got_usr1 = 0; sa.sa_handler = &handle_usr1; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1,&sa,0); } /* * Ignore SIGPIPE. (We prefer to get EPIPE from writes.) */ static void ignore_pipe(void) { struct sigaction sa; sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGPIPE,&sa,0); } /* * Initialize the statistics counters. */ static void stats_init(void) { #ifdef DOSTATS int i; struct tm *tm; time(&starttt); tm = localtime(&starttt); strftime(&starttext[0],sizeof(starttext),"%Y-%m-%d %H:%M:%S",tm); for (i=0;i= 0) && (dnsl_minfree > 2)) { if (!dnslfree || (dnslfree->flags & (DPF_BUSY|DPF_DEAD))) panic("dnsl_timeout: impossible free"); #if 0 printf("[%d] dnsl_timeout: killing %d\n",mypid(),(int)dnslfree->kid); #endif dnslfree->flags |= DPF_DEAD; } dnsl_minfree = 50; } /* * Called by the DNSL overseer to fork a new DNSL worker. Called * whenever we have no free DNSL workers. */ static int new_dnslproc(int nlive) { pid_t kid; DNSLPROC *p; int dp[2]; int sp[2]; if (socketpair(AF_LOCAL,SOCK_DGRAM,0,&dp[0]) < 0) fatal("new DNSL dgram socketpair: %s",strerror(errno)); if (socketpair(AF_LOCAL,SOCK_STREAM,0,&sp[0]) < 0) fatal("new DNSL stream socketpair: %s",strerror(errno)); kid = fork(); if (kid < 0) fatal("new DNSL fork: %s",strerror(errno)); if (kid == 0) { cached_pid = -1; while (dnslprocs) { p = dnslprocs; dnslprocs = p->link; close(p->datafd); close(p->deathfd); free(p); } close(dp[0]); close(sp[0]); close(dnsl_data); close(dnsl_death); dnsl_data = dp[1]; dnsl_death = sp[1]; return(1); } #if 0 printf("[%d] forked DNSL process %d",mypid(),(int)kid); if (last_dnsl_rql) { int e; char hnbuf[NI_MAXHOST]; e = getnameinfo((const void *)&last_dnsl_req,last_dnsl_rql,&hnbuf[0],NI_MAXHOST,0,0,NI_NUMERICHOST); if (e) { printf(" [getnameinfo error: %s]",strerror(e)); } else { printf(" [%s]",&hnbuf[0]); } } printf(" (%d)\n",nlive); #else nlive = nlive; #endif close(dp[1]); close(sp[1]); p = malloc(sizeof(DNSLPROC)); p->link = dnslprocs; dnslprocs = p; p->flags = 0; p->datafd = dp[0]; p->deathfd = sp[0]; p->kid = kid; return(0); } /* * Called in the overseer to handle a DNSL request from an SMTP worker. * We just receive it, do some minimal error checking, and queue it * for handling in the main loop. */ static void read_dnsl_request(void) { struct sockaddr_storage ss; int len; DNSRQ *rq; struct iovec iov; struct msghdr mh; struct cmsghdr cmh; char ctlbuf[CMSPACE(sizeof(int))]; iov.iov_base = &ss; iov.iov_len = sizeof(ss); mh.msg_name = 0; mh.msg_namelen = 0; mh.msg_iov = &iov; mh.msg_iovlen = 1; mh.msg_control = &ctlbuf[0]; mh.msg_controllen = sizeof(ctlbuf); len = recvmsg(dnsl_data,&mh,0); if (len < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return; } printf("[%d] DNSL request recvmsg: %s\n",mypid(),strerror(errno)); exit(1); } if (len == 0) { printf("[%d] DNSL empty request\n",mypid()); exit(0); } if (mh.msg_controllen == 0) { printf("[%d] request without control data\n",mypid()); } else if (mh.msg_controllen != CMSPACE(sizeof(int))) { printf("[%d] request controllen %d (wanted %d)\n",mypid(),mh.msg_controllen,(int)CMSPACE(sizeof(int))); } else { bcopy(&ctlbuf[0],&cmh,sizeof(struct cmsghdr)); if (cmh.cmsg_len != CMLEN(sizeof(int))) { printf("[%d] cmsg_len %d (wanted %d)\n",mypid(),cmh.cmsg_len,(int)CMLEN(sizeof(int))); } else if (cmh.cmsg_level != SOL_SOCKET) { printf("[%d] cmsg_level %d (wanted %d)\n",mypid(),cmh.cmsg_level,(int)SOL_SOCKET); } else if (cmh.cmsg_type != SCM_RIGHTS) { printf("[%d] cmsg_type %d (wanted %d)\n",mypid(),cmh.cmsg_type,(int)SCM_RIGHTS); } else { rq = malloc(sizeof(DNSRQ)+len); rq->data = (void *)(rq+1); bcopy(&ss,rq+1,len); bcopy(&ctlbuf[CMSKIP(&cmh)],&rq->fd,sizeof(int)); rq->len = len; rq->link = 0; *dnsrqt = rq; dnsrqt = &rq->link; } } } /* * Called in the DNSL overseer, to send a queued request to a worker. * Sends the first request on the queue to the DNSLPROC named as an * argument. Returns false on success. On error, returns true, with * a complaint logged; in this case, it should be called again, with a * different DNSLPROC, because the request was not sent anywhere. */ static int send_dnsl_request(DNSLPROC *p) { int sent; DNSRQ *rq; struct iovec iov; struct msghdr mh; struct cmsghdr cmh; char ctlbuf[CMSPACE(sizeof(int))]; rq = dnsrqq; if (! rq) panic("send_dnsl_request: no request"); last_dnsl_rql = rq->len; bcopy(rq->data,&last_dnsl_req,last_dnsl_rql); iov.iov_base = rq->data; iov.iov_len = rq->len; mh.msg_name = 0; mh.msg_namelen = 0; mh.msg_iov = &iov; mh.msg_iovlen = 1; mh.msg_control = &ctlbuf[0]; mh.msg_controllen = sizeof(ctlbuf); cmh.cmsg_len = CMLEN(sizeof(int)); cmh.cmsg_level = SOL_SOCKET; cmh.cmsg_type = SCM_RIGHTS; bcopy(&cmh,&ctlbuf[0],sizeof(struct cmsghdr)); bcopy(&rq->fd,&ctlbuf[CMSKIP(&cmh)],sizeof(int)); sent = sendmsg(p->datafd,&mh,0); if (sent == rq->len) { if (! (dnsrqq = rq->link)) dnsrqt = &dnsrqq; close(rq->fd); free(rq); p->flags |= DPF_BUSY; return(0); } if (sent < 0) { printf("[%d] DNSL request sendmsg to %d: %s\n",mypid(),(int)p->kid,strerror(errno)); } else { printf("[%d] DNSL request sendmsg to %d sent %d, wanted %d\n",mypid(),(int)p->kid,sent,rq->len); } p->flags |= DPF_DEAD; return(1); } /* * Called in the overseer to read from a DNSL worker. This is how the * overseer finds out that a worker is done with a request. Also, in * a DOSTATS build, the byte is a statistics message and is passed on * to the parent for statistics-keeping. Some dead DNSL worker * detection is done here, but it's not reliable (which is why the * death pipes between workers and the overseer exist). */ static void dnsl_read(DNSLPROC *p) { unsigned char status; int n; n = recv(p->datafd,&status,1,0); if (n < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return; } printf("[%d] DNSL child recv: %s\n",mypid(),strerror(errno)); p->flags |= DPF_DEAD; return; } if (n == 0) { p->flags |= DPF_DEAD; return; } p->flags &= ~DPF_BUSY; #ifdef DOSTATS send(dnsl_data,&status,1,0); #endif } /* * The DNSL overseer main loop. Nothing especially noteworthy here: we * just reap dead children, build the poll list, poll, and handle * possible I/O. (Note that I/O possible on the death pipes always * indicates a dead peer, since they are not used for I/O.) */ static int dnsl_tick(void) { struct timeval now; DNSLPROC *p; DNSLPROC **pp; int nfree; int nlive; int datapx; int deathpx; int pdeathpx; int prv; int wstat; pid_t kid; while (1) { kid = wait3(&wstat,WNOHANG,0); if (kid <= 0) break; if (WIFEXITED(wstat)) { if (WEXITSTATUS(wstat) != 0) { printf("[%d] DNSL child %d exited %d\n",mypid(),(int)kid,WEXITSTATUS(wstat)); } } else if (WIFSIGNALED(wstat)) { printf("[%d] DNSL child %d died on %d (%s)%s\n",mypid(),(int)kid,WTERMSIG(wstat),strsignal(WTERMSIG(wstat)),WCOREDUMP(wstat)?" (core dumped)":""); } else { printf("[%d] DNSL child %d cryptic wait status %#x\n",mypid(),(int)kid,wstat); } } fflush(0); npfds = 0; nfree = 0; nlive = 0; deathpx = add_pfd(dnsl_death,POLLIN|POLLRDNORM); if (dnsl_parent_death >= 0) pdeathpx = add_pfd(dnsl_parent_death,POLLIN|POLLRDNORM); pp = &dnslprocs; dnslfree = 0; while ((p = *pp)) { if (p->flags & DPF_DEAD) { *pp = p->link; close(p->datafd); close(p->deathfd); free(p); } else { pp = &p->link; if (! (p->flags & DPF_BUSY)) { nfree ++; dnslfree = p; } p->datapx = add_pfd(p->datafd,POLLIN|POLLRDNORM); p->deathpx = add_pfd(p->deathfd,POLLIN|POLLRDNORM); nlive ++; } } if ( (dnsl_parent_death < 0) && (nlive > 2) && dnslfree ) { dnslfree->flags |= DPF_DEAD; return(0); } if (dnslfree && dnsrqq) { send_dnsl_request(dnslfree); return(0); } datapx = add_pfd(dnsl_data,POLLIN|POLLRDNORM); gettimeofday(&now,0); if (now.tv_sec >= dnsl_timer) { dnsl_timeout(); return(0); } if (dnsl_parent_death >= 0) { if ((nfree < 1) && (nlive < MAX_DNSL_WORKERS)) return(new_dnslproc(nlive)); if (nfree < dnsl_minfree) dnsl_minfree = nfree; } prv = poll(pfds,npfds,((dnsl_timer-now.tv_sec)*1000)+100-(now.tv_usec/1000)); if (prv < 0) { if (errno == EINTR) return(0); printf("[%d] DNSL poll: %s\n",mypid(),strerror(errno)); exit(1); } if (prv == 0) return(0); if (pfds[deathpx].revents & (POLLIN|POLLRDNORM)) { printf("[%d] DNSL overseer dying\n",mypid()); exit(0); } if ((dnsl_parent_death >= 0) && pfds[pdeathpx].revents & (POLLIN|POLLRDNORM)) { close(dnsl_parent_death); dnsl_parent_death = -1; setproctitle("DNSL overseer (dying)"); return(0); } if (pfds[datapx].revents & (POLLIN|POLLRDNORM)) { read_dnsl_request(); return(0); } for (p=dnslprocs;p;p=p->link) { if (p->flags & DPF_DEAD) continue; if (pfds[p->deathpx].revents & (POLLIN|POLLRDNORM)) { printf("[%d] DNSL child %d died\n",mypid(),(int)p->kid); p->flags |= DPF_DEAD; continue; } if (pfds[p->datapx].revents & (POLLIN|POLLRDNORM)) dnsl_read(p); } return(0); } /* * A DNSL worker's main loop. The only thing of note here is how the * response is constructed with the resp_* functions. * * We don't yet support DNSL checks for address families other than * AF_INET, though most of the infrastructure is in place. */ static int dnslkid_tick(void) { struct pollfd pfd[2]; struct sockaddr_storage ss; int len; int fd; int niov; int resplen; struct iovec iov[8]; unsigned char statcode; int ni; int ival[4]; struct msghdr mh; struct cmsghdr cmh; char ctlbuf[CMSPACE(sizeof(int))]; static struct iovec *resp_iov(void) { if (niov >= sizeof(iov)/sizeof(iov[0])) panic("dnslkid_tick: iov overflow"); return(&iov[niov++]); } static void resp_add_int(int iv) { struct iovec *v; if (ni >= sizeof(ival)/sizeof(ival[0])) panic("dnslkid_tick: ival overflow"); v = resp_iov(); v->iov_base = &ival[ni]; v->iov_len = sizeof(int); ival[ni++] = iv; resplen += sizeof(int); } static void resp_add_block(const void *data, int len) { struct iovec *v; v = resp_iov(); v->iov_base = deconst(data); v->iov_len = len; resplen += len; } static void resp_init(void) { niov = 0; resplen = 0; ni = 0; resp_add_block(&resplen,sizeof(int)); } static void resp_send(void) { int w; w = writev(fd,&iov[0],niov); if (w < 0) { printf("[%d] error writing reply data: %s\n",mypid(),strerror(errno)); fflush(0); exit(0); } if (w != resplen) { printf("[%d] error writing reply data: sent %d, wanted %d\n",mypid(),w,resplen); exit(0); } } auto void fail(const char *, ...) __attribute__((__format__(__printf__,1,2))); auto void fail(const char *fmt, ...) { va_list ap; char *t; int tl; resp_add_int(DNSLSTAT_FAIL); statcode = SM_DNSL_FAIL; va_start(ap,fmt); tl = vasprintf(&t,fmt,ap); va_end(ap); resp_add_int(tl); resp_add_int(tl); resp_add_block(t,tl); resp_add_block(t,tl); resp_send(); free(t); } pfd[0].fd = dnsl_data; pfd[0].events = POLLIN | POLLRDNORM; pfd[1].fd = dnsl_death; pfd[1].events = POLLIN | POLLRDNORM; setproctitle("DNSL worker (idle)"); ni = poll(&pfd[0],2,INFTIM); if (pfd[1].revents) { #if 0 printf("[%d] DNSL child exiting\n",mypid()); #endif exit(0); } iov[0].iov_base = &ss; iov[0].iov_len = sizeof(ss); mh.msg_name = 0; mh.msg_namelen = 0; mh.msg_iov = &iov[0]; mh.msg_iovlen = 1; mh.msg_control = &ctlbuf[0]; mh.msg_controllen = sizeof(ctlbuf); len = recvmsg(dnsl_data,&mh,0); if (len < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return(0); } printf("[%d] DNSL child recvmsg: %s\n",mypid(),strerror(errno)); exit(1); } if (len == 0) { printf("[%d] DNSL child empty request\n",mypid()); exit(0); } if (mh.msg_controllen == 0) { printf("[%d] request without control data\n",mypid()); } else if (mh.msg_controllen != CMSPACE(sizeof(int))) { printf("[%d] request controllen %d (wanted %d)\n",mypid(),mh.msg_controllen,(int)CMSPACE(sizeof(int))); } else { bcopy(&ctlbuf[0],&cmh,sizeof(struct cmsghdr)); if (cmh.cmsg_len != CMLEN(sizeof(int))) { printf("[%d] cmsg_len %d (wanted %d)\n",mypid(),cmh.cmsg_len,(int)CMLEN(sizeof(int))); } else if (cmh.cmsg_level != SOL_SOCKET) { printf("[%d] cmsg_level %d (wanted %d)\n",mypid(),cmh.cmsg_level,(int)SOL_SOCKET); } else if (cmh.cmsg_type != SCM_RIGHTS) { printf("[%d] cmsg_type %d (wanted %d)\n",mypid(),cmh.cmsg_type,(int)SCM_RIGHTS); } else { bcopy(&ctlbuf[CMSKIP(&cmh)],&fd,sizeof(int)); resp_init(); if (ss.ss_len != len) { printf("[%d] ss_len %d, len %d\n",mypid(),ss.ss_len,len); fail("Internal error (ss_len %d, len %d)",ss.ss_len,len); } else { int e; char hnbuf[NI_MAXHOST]; e = getnameinfo((const void *)&ss,len,&hnbuf[0],NI_MAXHOST,0,0,NI_NUMERICHOST); if (e) { setproctitle("DNSL worker (running [%s])",strerror(e)); } else { setproctitle("DNSL worker (%s)",&hnbuf[0]); } switch (ss.ss_family) { case AF_INET: { int status; const char *lmsg; const char *smsg; int ll; int sl; check_ipv4(&((struct sockaddr_in *)&ss)->sin_addr,&status,&lmsg,&smsg); switch (status) { case IPSTAT_OK: resp_add_int(DNSLSTAT_OK); statcode = SM_DNSL_OK; break; case IPSTAT_BAD: resp_add_int(DNSLSTAT_BAD); statcode = SM_DNSL_BAD; if (0) { case IPSTAT_FAIL: resp_add_int(DNSLSTAT_FAIL); statcode = SM_DNSL_FAIL; } ll = strlen(lmsg); sl = strlen(smsg); resp_add_int(ll); resp_add_int(sl); resp_add_block(lmsg,ll); resp_add_block(smsg,sl); break; default: panic("dnslkid_tick: check_ipv4 status %d",status); break; } resp_send(); } break; default: printf("[%d] ss_family %d\n",mypid(),ss.ss_family); fail("Internal error (ss_family %d)",ss.ss_family); break; } } close(fd); send(dnsl_data,&statcode,1,0); } } return(0); } /* * Setup the LSOCKs. */ static void setup_accs(void) { struct addrinfo hints; const char *listen_on; static void add_listeners(const char *arg) { struct addrinfo *ai0; struct addrinfo *ai; int e; int s; int v; LSOCK *ls; e = getaddrinfo(arg,"smtp",&hints,&ai0); if (e) fatal("can't getaddrinfo for %s%ssmtp: %s",arg?:"",arg?"/":"",gai_strerror(e)); if (! ai0) fatal("successful %s%ssmtp lookup but no addresses?",arg?:"",arg?"/":""); for (ai=ai0;ai;ai=ai->ai_next) { s = socket(ai->ai_family,ai->ai_socktype,ai->ai_protocol); if (s < 0) { nonfatal("socket (af %d): %s",ai->ai_family,strerror(errno)); continue; } v = 1; setsockopt(s,SOL_SOCKET,SO_REUSEADDR,&v,sizeof(v)); /* ignore errors */ if (bind(s,ai->ai_addr,ai->ai_addrlen) < 0) { nonfatal("bind (af %d): %s",ai->ai_family,strerror(errno)); close(s); continue; } set_nonblock(s); listen(s,10); ls = malloc(sizeof(LSOCK)); ls->fd = s; ls->link = accs; accs = ls; } } hints.ai_flags = AI_PASSIVE; hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; hints.ai_addrlen = 0; hints.ai_canonname = 0; hints.ai_addr = 0; hints.ai_next = 0; listen_on = LISTEN_ON; if (listen_on) { const char *b; const char *e; char *on; int n; int l; on = 0; n = 0; b = listen_on; while (1) { while (*b == ' ') b ++; if (! *b) break; for (e=b;*e&&(*e!=' ');e++) ; l = e - b; if (l >= n) { free(on); n = l + 1; on = malloc(n); } bcopy(b,on,l); on[l] = '\0'; add_listeners(on); b = e; } free(on); } else { add_listeners(0); } if (! accs) fatal("no listening sockets"); } /* * Dump out the parent's state. */ static void dump_parent_state(FILE *to) { LISTENER *l; time_t now; time(&now); for (l=listeners;l;l=l->link) { fprintf(to,"listener proc=%d pipe=%d state=",(int)l->proc,l->pipe); switch (l->state) { case LS_LISTENING: fprintf(to,"LISTENING"); break; case LS_FULL: fprintf(to,"FULL"); break; case LS_STOPPING: fprintf(to,"STOPPING"); break; case LS_DYING: fprintf(to,"DYING"); break; case LS_DEAD: fprintf(to,"DEAD"); break; default: fprintf(to,"%d?",l->state); break; } fprintf(to,"\n"); } #ifdef DOSTATS fprintf(to,"Statistics:\n"); fprintf(to,"Connections accepted: %u\n",counters[SM_ACCEPT-SM_FIRST_STATS]); fprintf(to,"Recipients accepted: %u\n",counters[SM_ADDR_OK-SM_FIRST_STATS]); fprintf(to,"Recipients rejected: %u\n",counters[SM_ADDR_BAD-SM_FIRST_STATS]); fprintf(to,"Recipients uncheckable: %u\n",counters[SM_ADDR_UNK-SM_FIRST_STATS]); fprintf(to,"DNSL lookups passed: %u\n",counters[SM_DNSL_OK-SM_FIRST_STATS]); fprintf(to,"DNSL lookups rejected: %u\n",counters[SM_DNSL_BAD-SM_FIRST_STATS]); fprintf(to,"DNSL lookups failed: %u\n",counters[SM_DNSL_FAIL-SM_FIRST_STATS]); fprintf(to,"Message bodies: %u\n",counters[SM_BODY-SM_FIRST_STATS]); { int i; for (i=0;i= 86400) { fprintf(to,"%lud",up/86400); up %= 86400; } if (up >= 3600) { fprintf(to,"%luh",up/3600); up %= 3600; } if (up >= 60) { fprintf(to,"%lum",up/60); up %= 60; } if (up) fprintf(to,"%lus",up); fprintf(to,")\n"); } #endif } /* * Return a text name for a client state (CS_*). */ static const char *clientstatename(int cs) { static char badbuf[64]; switch (cs) { case CS_GREET: return("GREET"); break; case CS_HELO: return("HELO"); break; case CS_DNSL: return("DNSL"); break; case CS_MAIL: return("MAIL"); break; case CS_IRCPTS: return("IRCPTS"); break; case CS_IDATA: return("IDATA"); break; case CS_SCONN: return("SCONN"); break; case CS_SGREET: return("SGREET"); break; case CS_SHELO: return("SHELO"); break; case CS_SMAIL: return("SMAIL"); break; case CS_SRCPT: return("SRCPT"); break; case CS_SNRCPTS: return("SNRCPTS"); break; case CS_SGRCPTS: return("SGRCPTS"); break; case CS_SGRCPT: return("SGRCPT"); break; case CS_SBRCPTS: return("SBRCPTS"); break; case CS_SBRCPT: return("SBRCPT"); break; case CS_BDATA: return("BDATA"); break; case CS_SDATA: return("SDATA"); break; case CS_SDCOPY: return("SDCOPY"); break; case CS_SDOT: return("SDOT"); break; case CS_CLOSING: return("CLOSING"); break; case CS_FAIL: return("FAIL"); break; case CS_TEMPFAIL: return("TEMPFAIL"); break; case CS_HARDFAIL: return("HARDFAIL"); break; case CS_DEAD: return("DEAD"); break; case CS_BURIED: return("BURIED"); break; case CS_SCLOSE: return("SCLOSE"); break; } sprintf(&badbuf[0],"%d?",cs); return(&badbuf[0]); } /* * Dump out an SMTP worker's internal state. */ static void dump_listener_state(FILE *to) { LSOCK *a; CLIENT *c; time_t now; time(&now); fprintf(to,"state = "); switch (ownstate) { case LS_LISTENING: fprintf(to,"LISTENING"); break; case LS_FULL: fprintf(to,"FULL"); break; case LS_STOPPING: fprintf(to,"STOPPING"); break; case LS_DYING: fprintf(to,"DYING"); break; default: fprintf(to,"%d?",ownstate); break; } fprintf(to,"\n"); if (ownstate != LS_DYING) { for (a=accs;a;a=a->link) { fprintf(to,"listening fd %d\n",a->fd); } } for (c=clients;c;c=c->link) { fprintf(to,"client %s state=%s",c->peertext,clientstatename(c->state)); if (c->helo_as) fprintf(to," helo=%s",c->helo_as); if (c->env_from) fprintf(to," from=%s",c->env_from); fprintf(to," cq.fd=%d",c->cq.fd); if (c->sq.fd >= 0) fprintf(to," sq.fd=%d",c->sq.fd); fprintf(to," timeout=%d",(int)(c->timeout-now)); fprintf(to,"\n"); } } /* * A client's DNSL response descriptor is readable. See what the DNSL * lookup produced. */ static void check_dnsl_done(CLIENT *c) { if (pfds[c->px].revents) { int resplen; int n; int rcode; int llen; int slen; unsigned char *resp; n = recv(c->dnsl_fd,&resplen,sizeof(int),MSG_WAITALL); if (n < 0) { printf("[%d] DNSL response length recv: %s\n",mypid(),strerror(errno)); dnsl_fail(c,"Internal error","length recv"); return; } if (n != sizeof(int)) { printf("[%d] DNSL response length recv: got %d, wanted %d\n",mypid(),n,(int)sizeof(int)); dnsl_fail(c,"Internal error","incomplete length"); return; } if ((resplen < 2*sizeof(int)) || (resplen > 8192)) { printf("[%d] insane DNSL response length %d\n",mypid(),resplen); dnsl_fail(c,"Internal error","insane length"); return; } resplen -= sizeof(int); resp = malloc(resplen); n = recv(c->dnsl_fd,resp,resplen,MSG_WAITALL); if (n < 0) { printf("[%d] DNSL response body recv: %s\n",mypid(),strerror(errno)); dnsl_fail(c,"Internal error","body recv"); free(resp); return; } if (n != resplen) { printf("[%d] DNSL response body recv: got %d, wanted %d\n",mypid(),n,resplen); dnsl_fail(c,"Internal error","incomplete body"); free(resp); return; } bcopy(resp,&rcode,sizeof(int)); switch (rcode) { const char *kindstr; case DNSLSTAT_OK: if (resplen != sizeof(int)) { printf("[%d] DNSL response body length wrong for OK (is %d, wanted %d)\n",mypid(),resplen,2*(int)sizeof(int)); dnsl_fail(c,"Internal error","body length wrong"); free(resp); return; } c->dnsl_status = DNSLSTAT_OK; dnsl_over(c); break; case DNSLSTAT_BAD: kindstr = "BAD"; if (0) { case DNSLSTAT_FAIL: kindstr = "FAIL"; } bcopy(resp+sizeof(int),&llen,sizeof(int)); bcopy(resp+(2*sizeof(int)),&slen,sizeof(int)); if ( (llen > resplen) || (slen > resplen) || (resplen != (3*sizeof(int))+llen+slen) ) { printf("[%d] DNSL response body length wrong for %s (is %d, wanted %d)\n",mypid(),kindstr,resplen,(3*(int)sizeof(int))+llen+slen); dnsl_fail(c,"Internal error","body length wrong"); free(resp); return; } c->dnsl_status = rcode; c->dnsl_msg = malloc(llen+1); bcopy(resp+(3*sizeof(int)),c->dnsl_msg,llen); c->dnsl_msg[llen] = '\0'; c->dnsl_tag = malloc(slen+1); bcopy(resp+(3*sizeof(int))+llen,c->dnsl_tag,slen); c->dnsl_tag[slen] = '\0'; dnsl_over(c); break; default: printf("[%d] DNSL response type bad (%d)\n",mypid(),rcode); dnsl_fail(c,"Internal error","bad rcode"); free(resp); return; break; } free(resp); } } /* * Drop the current recipient. */ static void drop_rcpt(CLIENT *c) { free_rcpt(c->rcpt); c->rcpt = NORCPT; } /* * A client's backend server connect() has completed. Check whether it * worked or not and handle it, either way. */ static void check_poll_conn(CLIENT *c) { if (pfds[c->px].revents) { int e; socklen_t elen; elen = sizeof(e); if (getsockopt(c->sq.fd,SOL_SOCKET,SO_ERROR,&e,&elen) < 0) { nonfatal("can't getsockopt(SO_ERROR): %s",strerror(errno)); e = 1; } else if (e) { nonfatal("connect to server: %s",strerror(e)); } if (e) { close(c->sq.fd); c->sq.fd = -1; ioq_oq_point(&c->cq,"451 Backend error\r\n",-1); drop_rcpt(c); c->state = CS_CLOSING; } else { c->state = CS_SGREET; } } } /* * The client's fd shows writeable. Try to write. On failure, mark * the client very dead (CS_DEAD tries to flush first - wrong here). */ static void check_poll_out(CLIENT *c) { if (pfds[c->px].revents && try_write(c)) c->state = CS_BURIED; } /* * Return the client's data-file fd. This opens the file if it's not * yet open, leaving the fd in c->data_fd as well as returning it. * Can return BAD_FD, but not NO_FD. */ static int client_data_fd(CLIENT *c) { switch (c->data_fd) { case NO_FD: { struct timeval tv; int serial; char *sp; char fn[64]; gettimeofday(&tv,0); sp = &fn[sprintf(&fn[0],"%lu.%06lu",(unsigned long int)tv.tv_sec,(unsigned long int)tv.tv_usec)]; serial = 0; while (1) { sprintf(sp,".%d",serial++); c->data_fd = open(&fn[0],O_RDWR|O_CREAT|O_EXCL,0600); if (c->data_fd >= 0) { if (unlink(&fn[0]) < 0) { printf("[%d] can't remove body file %s (for %s): %s\n",mypid(),&fn[0],c->peertext,strerror(errno)); close(c->data_fd); c->data_fd = BAD_FD; } else { c->data_file = strdup(&fn[0]); } break; } if (errno == EEXIST) continue; printf("[%d] can't open body file %s (for %s): %s\n",mypid(),&fn[0],c->peertext,strerror(errno)); c->data_fd = BAD_FD; break; } } break; case BAD_FD: break; } return(c->data_fd); } /* * Save some DATA-phase data. The data buffer passed in here includes * the terminating dot (for the end-of-data call) and has not had * hidden dots stripped, nor CRLFs turned into NLs - it's the data as * it is on the wire. */ static void data_data(CLIENT *c, const void *data, int len) { int fd; int w; if (len < 0) len = strlen(data); fd = client_data_fd(c); if (fd >= 0) { w = write(fd,data,len); if (w < 0) { printf("[%d] write to %s (for %s): %s\n",mypid(),c->data_file,c->peertext,strerror(errno)); close(c->data_fd); c->data_fd = BAD_FD; } else if (w < len) { printf("[%d] write to %s (for %s): wanted %d, wrote %d\n",mypid(),c->data_file,c->peertext,len,w); close(c->data_fd); c->data_fd = BAD_FD; } } if (c->state == CS_SDCOPY) ioq_oq_copy(&c->sq,data,len); } static void base26(char *to, unsigned long long int val, int width) { int i; for (i=width-1;i>=0;i--) { to[i] = "abcdefghijklmnopqrstuvwxyz"[val%26]; val /= 26; } if (val) printf("[%d] base26: overflow (leftover %llu)\n",mypid(),val); } /* * Log a message body with envelope information. */ static void log_message(CLIENT *c) { int serial; char *np; int fd; int i; int j; struct timeval tv; char bsp[13]; char name[64]; struct stat stb; char rbuf[8192]; gettimeofday(&tv,0); base26(&bsp[0],(tv.tv_sec*1000000ULL)+tv.tv_usec,12); bsp[12] = '\0'; np = &name[0]; bcopy("save/",np,5); np += 5; *np++ = bsp[0]; *np++ = bsp[1]; *np++ = bsp[2]; *np++ = '/'; *np++ = bsp[3]; *np++ = bsp[4]; *np++ = '/'; *np++ = bsp[5]; *np++ = bsp[6]; *np++ = '/'; np[0] = '.'; np[1] = '\0'; /* Ugh. This has to build under compilers without labeled control structure, hence the gotos instead of what would normally be labeled do-while(0)s. */ retry1:; if (stat(&name[0],&stb) >= 0) goto have3; if (errno == ENOENT) { np[-1] = '\0'; retry2:; if (mkdir(&name[0],0700) >= 0) goto have2; switch (errno) { case EEXIST: np[-1] = '/'; goto retry1; break; case ENOENT: np[-4] = '\0'; retry3:; if (mkdir(&name[0],0700) >= 0) goto have1; switch (errno) { case EEXIST: np[-4] = '/'; goto retry2; break; case ENOENT: np[-7] = '\0'; if (mkdir(&name[0],0700) < 0) { switch (errno) { case EEXIST: np[-7] = '/'; goto retry3; break; } goto cant_mkdir; } np[-7] = '/'; if (mkdir(&name[0],0700) >= 0) goto have1; break; } goto cant_mkdir; have1:; np[-4] = '/'; if (mkdir(&name[0],0700) >= 0) goto have2; break; } goto cant_mkdir; have2:; np[-1] = '/'; goto have3; } cant_mkdir:; printf("[%d] can't log to %s (for %s): can't mkdir %s: %s\n",mypid(),&bsp[0],c->peertext,&name[0],strerror(errno)); return; have3:; bcopy(&bsp[0],np,12); np += 12; *np++ = '.'; serial = 1; while (1) { sprintf(np,"%d",serial++); fd = open(&name[0],O_WRONLY|O_CREAT|O_EXCL,0600); if (fd >= 0) break; if (errno == EEXIST) continue; printf("[%d] can't log to %s (for %s): %s\n",mypid(),&name[0],c->peertext,strerror(errno)); return; } write(fd,"reason: ",8); write(fd,c->fail_reason,strlen(c->fail_reason)); write(fd,"\nefrom: ",8); write(fd,c->env_from,strlen(c->env_from)); write(fd,"\n",1); for (i=0;inrcpts;i++) { write(fd,"eto: ",5); write(fd,c->rcpts[i].lp,strlen(c->rcpts[i].lp)); if (c->rcpts[i].dom) { write(fd,"@",1); write(fd,c->rcpts[i].dom,strlen(c->rcpts[i].dom)); } write(fd,"\n",1); } write(fd,"\n",1); lseek(c->data_fd,0,SEEK_SET); while (1) { i = read(c->data_fd,&rbuf[0],sizeof(rbuf)); if (i < 0) { printf("[%d] read from data fd for %s: %s\n",mypid(),c->peertext,strerror(errno)); } if (i <= 0) break; j = write(fd,&rbuf[0],i); if (j < 0) { printf("[%d] write to %s, logging for %s: %s\n",mypid(),&name[0],c->peertext,strerror(errno)); break; } if (j != i) { printf("[%d] write to %s, logging for %s: wanted %d, did %d\n",mypid(),&name[0],c->peertext,i,j); break; } } if (close(fd) < 0) { printf("[%d] closing %s, logging for %s: %s\n",mypid(),&name[0],c->peertext,strerror(errno)); } printf("[%d] message from %s logged to %s: %s\n",mypid(),c->peertext,&name[0],c->fail_reason); fd = open("save/.list",O_WRONLY|O_APPEND|O_CREAT|O_EXLOCK,0600); if (fd < 0) { printf("[%d] open save/.list: %s\n",mypid(),strerror(errno)); } else { struct iovec iov[2]; i = strlen(&name[0]); iov[0].iov_base = &name[0]; iov[0].iov_len = i; iov[1].iov_base = deconst("\n"); iov[1].iov_len = 1; j = writev(fd,&iov[0],2); if (j < 0) { printf("[%d] write save/.list: %s\n",mypid(),strerror(errno)); } else if (j != i+1) { printf("[%d] write save/.list: wanted %d, did %d\n",mypid(),i+1,j); } close(fd); } } /* * Set the failure reason for a client. */ static void set_fail_reason(CLIENT *, const char *, ...) __attribute__((__format__(__printf__,2,3))); static void set_fail_reason(CLIENT *c, const char *fmt, ...) { va_list ap; if (c->fail_reason) panic("set_fail_reason: already set"); va_start(ap,fmt); vasprintf(&c->fail_reason,fmt,ap); va_end(ap); } /* * Clear the failure reason for a client. */ static void clear_fail_reason(CLIENT *c) { if (! c->fail_reason) panic("clear_fail_reason: already clear"); free(c->fail_reason); c->fail_reason = 0; } /* * We just got the . closing a message body. Check state * and do appropriate things. */ static void body_ends(CLIENT *c) { switch (c->state) { case CS_IDATA: case CS_BDATA: switch (c->nrcpts) { case 1: ioq_oq_point(&c->cq,"554 This recipient is not valid.\r\n",-1); break; default: ioq_oq_point(&c->cq,"554 These recipients are not valid.\r\n",-1); break; } log_message(c); do_rset(c); clear_fail_reason(c); c->state = CS_HELO; break; case CS_SDCOPY: c->state = CS_SDOT; break; } } /* * Save the current recipient. */ static void save_rcpt(CLIENT *c) { if (c->nrcpts >= c->arcpts) c->rcpts = realloc(c->rcpts,(c->arcpts=c->nrcpts+8)*sizeof(RCPT)); c->rcpts[c->nrcpts++] = c->rcpt; c->rcpt = NORCPT; } /* * Initiate a server connection closedown. Send a QUIT and switch to * CS_SCLOSE. Note that this must always be called *after* setting * ->state to the desired new state, so that scstate is correct. */ static void close_server(CLIENT *c) { ioq_oq_point(&c->sq,"QUIT\r\n",-1); c->scstate = c->state; c->state = CS_SCLOSE; } /* * Create a Received: header and treat it as if it had just been * received on the client connection (thereby inserting it before the * data the client sends). */ static void generate_received(CLIENT *c) { int e; int i; time_t nowtt; struct tm *nowtm; char *t; char hnbuf[NI_MAXHOST]; char tbuf[64]; e = getnameinfo(c->peeraddr,c->peeralen,&hnbuf[0],NI_MAXHOST,0,0,NI_NUMERICHOST); if (e) { data_data(c,"Received: from UNKNOWN (getnameinfo error: ",-1); data_data(c,strerror(e),-1); data_data(c,", sockaddr",-1); for (i=0;ipeeralen;i++) { sprintf(&tbuf[0]," %02x",((const unsigned char *)c->peeraddr)[i]); data_data(c,&tbuf[0],3); } data_data(c,")\r\n\t",-1); } else { data_data(c,"Received: from ",-1); data_data(c,&hnbuf[0],-1); data_data(c," ",-1); } data_data(c,"(helo: ",-1); t = malloc(strlen(c->helo_as)); i = 0; while (1) { int ch; ch = ((const unsigned char *)c->helo_as)[i]; if (! ch) break; if ((ch < 32) || (ch > 126) || (ch == '(') || (ch == ')')) ch = '?'; t[i] = ch; i ++; } data_data(c,t,i); data_data(c,")\r\n",-1); data_data(c,"\tby ",-1); data_data(c,&myhostname[0],-1); data_data(c," with SMTP id ",-1); data_data(c,xid(c),-1); data_data(c,";\r\n\t",-1); time(&nowtt); nowtm = localtime(&nowtt); sprintf(&tbuf[0],"%02d %.3s %04d %02d:%02d:%02d %c%02ld%02ld (%s, ", nowtm->tm_mday, "JanFebMarAprMayJunJulAugSepOctNovDec"+(nowtm->tm_mon*3), nowtm->tm_year + 1900, nowtm->tm_hour, nowtm->tm_min, nowtm->tm_sec, (nowtm->tm_gmtoff<0)?'-':'+', labs(nowtm->tm_gmtoff)/3600, (labs(nowtm->tm_gmtoff)/60)%60, nowtm->tm_zone ); data_data(c,&tbuf[0],-1); nowtm = gmtime(&nowtt); sprintf(&tbuf[0],"%02d:%02d:%02d UTC)", nowtm->tm_hour, nowtm->tm_min, nowtm->tm_sec ); data_data(c,&tbuf[0],-1); data_data(c,"\r\n",-1); } /* * Switch into a data-accepting state. This exists to centralize * things that must be done when starting to accept message data. */ static void begin_data(CLIENT *c, int datastate) { c->state = datastate; generate_received(c); } /* * Return true if a RCPT is for postmaster. This is so we can * special-case more acceptance for mail to postmaster. */ static int rcpt_is_postmaster(RCPT *r) { return(!r->dom||!strcasecmp(r->lp,"postmaster")); } /* * Tell a client about the global logging state. */ static void tell_state_logging(CLIENT *c, char sep, int val) { char *dbuf; ioq_oq_point(&c->cq,"250",-1); ioq_oq_copy(&c->cq,&sep,1); ioq_oq_point(&c->cq,"log ",-1); switch (val) { case LOG_OFF: ioq_oq_point(&c->cq,"off",-1); break; case LOG_REJ: ioq_oq_point(&c->cq,"rej",-1); break; case LOG_ALL: ioq_oq_point(&c->cq,"all",-1); break; default: asprintf(&dbuf,"?%d",val); ioq_oq_free(&c->cq,dbuf,-1); break; } ioq_oq_point(&c->cq,"\r\n",-1); } /* * Tell a client about the global sleep-before-banner state. */ static void tell_state_sbb(CLIENT *c, char sep, int val) { char *dbuf; ioq_oq_point(&c->cq,"250",-1); ioq_oq_copy(&c->cq,&sep,1); ioq_oq_point(&c->cq,"sbb ",-1); asprintf(&dbuf,"%d",val); ioq_oq_free(&c->cq,dbuf,-1); ioq_oq_point(&c->cq,"\r\n",-1); } /* * Handle an SMTP command from a client. First we perform a gross * syntax check, checking that no C0 or C1 control characters appear * in the command (arguably, we should enforce the restriction to * ASCII as well), then extract the command verb and search our table * of commands for it. If it's not there, we return an error, and, if * the client is issuing too many unrecognized commands without * recognized commands (like ratware which blats a whole transaction * at us and ignores all return codes), summarily drop the connection. * * Otherwise, update the statistics counter for this command. Then * check to see if it's a command with no arguments that was given an * argument, or one with an argument that was given none, and return * an error in those cases. Then check for a few commands (like NOOP) * that are valid and do the same thing in all states, and handle * them. Else, do a big switch on the current state and handle things * appropriately for the state. */ static void smtp_command(CLIENT *c) { int i; int cl; int al; int cmd; static int smtpcmdlens[sizeof(smtpcmds)/sizeof(smtpcmds[0])] = { -1 }; if (smtpcmdlens[0] < 0) for (i=0;smtpcmds[i];i++) smtpcmdlens[i] = strlen(smtpcmds[i]); for (i=0;illen;i++) { if ((c->lbuf[i]&0x7f) < 32) { ioq_oq_point(&c->cq,"500 Syntax error: invalid character\r\n",-1); printf("[%d] %s (octet 0x%02x): 500 Syntax error: invalid character\n",mypid(),c->iostr,c->lbuf[i]); return; } } for (i=0;(illen)&&(c->lbuf[i]!=' ');i++) ; cl = i; for (;(illen)&&(c->lbuf[i]==' ');i++) ; al = c->llen - i; for (cmd=0;smtpcmds[cmd];cmd++) if ((cl == smtpcmdlens[cmd]) && !strncasecmp(c->lbuf,smtpcmds[cmd],cl)) break; if (! smtpcmds[cmd]) { c->unrecog ++; if (c->unrecog > (c->baddata?5:25)) { ioq_oq_point(&c->cq,"500-Command `",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq,"' not recognized\r\n",-1); ioq_oq_point(&c->cq,"500-Come back when you learn SMTP\r\n",-1); ioq_oq_point(&c->cq,"500 Goodbye.\r\n",-1); if (c->baddata) { printf("[%d] %s: 500 Bad DATA unrecognized stream\n",mypid(),c->iostr); } else { printf("[%d] %s (%.*s): 500 Come back when you learn SMTP\n",mypid(),c->iostr,cl,c->lbuf); } c->state = CS_DEAD; } else { ioq_oq_point(&c->cq,"500 Command `",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq,"' not recognized\r\n",-1); if (! c->baddata) { printf("[%d] %s (%.*s): 500 Command unrecognized\n",mypid(),c->iostr,cl,c->lbuf); } } return; } else { if (c->unrecog > 10) c->unrecog -= 10; else c->unrecog = 0; c->baddata = 0; } STATS_RECORD(SM__CMD+cmd); switch (cmd) { case CMD_HELO: case CMD_EHLO: case CMD_MAIL: case CMD_RCPT: case CMD_VRFY: case CMD_XRT: if (i >= c->llen) { ioq_oq_point(&c->cq,"501 Syntax error: ",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq," requires an argument\r\n",-1); printf("[%d] %s (%.*s): 501 Requires an argument\n",mypid(),c->iostr,cl,c->lbuf); return; } break; case CMD_DATA: case CMD_RSET: case CMD_QUIT: if (i < c->llen) { ioq_oq_point(&c->cq,"501 Syntax error: ",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq," takes no argument\r\n",-1); printf("[%d] %s (%.*s): 501 Takes no argument\n",mypid(),c->iostr,c->llen,c->lbuf); return; } break; case CMD_XCTL: break; } switch (cmd) { case CMD_NOOP: if (i < c->llen) { printf("[%d] %s NOOP argument: %.*s\n",mypid(),xid(c),c->llen-i,c->lbuf+i); } ioq_oq_point(&c->cq,"250 Nothing done.\r\n",-1); return; break; case CMD_VRFY: ioq_oq_point(&c->cq,"553 VRFY disabled to you.\r\n",-1); printf("[%d] %s (%.*s): 553 VRFY disabled\n",mypid(),c->iostr,cl,c->lbuf); return; break; case CMD_XRT: ioq_oq_point(&c->cq,"050 Testing...\r\n",-1); if (! do_rcpt(c,c->lbuf+i,al)) { switch (valid_address(c->rcpt.lp,c->rcpt.dom)) { case VA_GOOD: ioq_oq_point(&c->cq,"250 Valid recipient\r\n",-1); break; case VA_BAD: ioq_oq_point(&c->cq,"250 Not a valid recipient\r\n",-1); break; case VA_UNKNOWN: ioq_oq_point(&c->cq,"250 Can't determine recipient validity\r\n",-1); break; default: panic("smtp_command: bad address validity"); break; } drop_rcpt(c); } return; break; case CMD_XCTL: if (client_is_local(c)) { if ((al == 1) && (c->lbuf[i] == '?')) { tell_state_logging(c,'-',logging); tell_state_sbb(c,' ',banner_sleep); } else if ((al > 3) && !strncasecmp(c->lbuf+i,"log",3)) { i += 3; al -= 3; while ((al > 0) && (c->lbuf[i] == ' ')) { i ++; al --; } if ((al == 1) && (c->lbuf[i] == '+')) { printf("[%d] log + [%s]\n",mypid(),c->peertext); tellparent(3,LM_STATE,STATE_LOG,LOG_REJ); ioq_oq_point(&c->cq,"250-\"log+\" has been replaced by \"log rej\"\r\n250 \"log rej\" done\r\n",-1); } else if ((al == 1) && (c->lbuf[i] == '-')) { printf("[%d] log - [%s]\n",mypid(),c->peertext); tellparent(3,LM_STATE,STATE_LOG,LOG_OFF); ioq_oq_point(&c->cq,"250-\"log-\" has been replaced by \"log off\"\r\n250 \"log off\" done\r\n",-1); } else if ((al == 3) && !strncasecmp(c->lbuf+i,"off",3)) { printf("[%d] log off [%s]\n",mypid(),c->peertext); tellparent(3,LM_STATE,STATE_LOG,LOG_OFF); ioq_oq_point(&c->cq,"250 Done\r\n",-1); } else if ((al == 3) && !strncasecmp(c->lbuf+i,"rej",3)) { printf("[%d] log rej [%s]\n",mypid(),c->peertext); tellparent(3,LM_STATE,STATE_LOG,LOG_REJ); ioq_oq_point(&c->cq,"250 Done\r\n",-1); } else if ((al == 3) && !strncasecmp(c->lbuf+i,"all",3)) { printf("[%d] log all [%s]\n",mypid(),c->peertext); tellparent(3,LM_STATE,STATE_LOG,LOG_ALL); ioq_oq_point(&c->cq,"250 Done\r\n",-1); } } else if ((al > 3) && !strncasecmp(c->lbuf+i,"sbb",3)) { int v; v = atoi(c->lbuf+i+3); if ((v < 0) || (v > 255)) { ioq_oq_point(&c->cq,"504 Value must be 0..255\r\n",-1); } else { banner_sleep = v; printf("[%d] banner sleep = %d [%s]\n",mypid(),v,c->peertext); tellparent(3,LM_STATE,STATE_TIMEOUT,v); ioq_oq_point(&c->cq,"250 OK\r\n",-1); } } else if ((al == 5) && !strncasecmp(c->lbuf+i,"stats",5)) { tellparent(9,LM_STATS, (int)((c->serial )&0xff), (int)((c->serial>> 8)&0xff), (int)((c->serial>>16)&0xff), (int)((c->serial>>24)&0xff), (int)((c->serial>>32)&0xff), (int)((c->serial>>40)&0xff), (int)((c->serial>>48)&0xff), (int)((c->serial>>56)&0xff)); c->statstate = CSS_ATNL; } else { ioq_oq_point(&c->cq,"501 Use `?', `log [off|rej|all]', `sbb N', or `stats'\r\n",-1); } } else { ioq_oq_point(&c->cq,"500 Command `",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq,"' not recognized\r\n",-1); printf("[%d] %s tried bad XCTL (%.*s)\n",mypid(),c->peertext,c->llen,c->lbuf); } return; break; case CMD_POST: ioq_oq_point(&c->cq,"500 Command `",-1); ioq_oq_copy(&c->cq,c->lbuf,cl); ioq_oq_point(&c->cq,"' recognized as a Web-proxy-abuse signature\r\n",-1); printf("[%d] %s (%.*s): 500 tried to POST\n",mypid(),c->iostr,c->llen,c->lbuf); c->state = CS_DEAD; return; break; } /* The reason the control structure here is upside down relative to what you might conventionally expect (which would be a switch out to command handlers here, with the command handlers checking the state) is that this way the code matches the state machine description more closely. */ #define SEND250() simpleresp(0,"250 OK") switch (c->state) { static void simpleresp(int logit, const char *str) { ioq_oq_point(&c->cq,str,-1); ioq_oq_point(&c->cq,"\r\n",-1); if (logit) printf("[%d] %s (%.*s): %s\n",mypid(),c->iostr,c->llen,c->lbuf,str); } case CS_GREET: switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); c->state = CS_HELO; break; case CMD_RSET: SEND250(); break; case CMD_QUIT: simpleresp(0,"221 OK"); c->state = CS_DEAD; break; default: simpleresp(1,"503 Must HELO/EHLO first"); break; } break; case CS_HELO: free(c->xid); c->xid = 0; switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); break; case CMD_MAIL: if (! do_mail(c,c->lbuf+i,al)) { SEND250(); if (c->dnsl_status == DNSLSTAT_UNK) { request_dnslookup(c); c->state = CS_DNSL; } else { c->state = CS_MAIL; } } break; case CMD_RSET: SEND250(); break; case CMD_QUIT: simpleresp(0,"221 OK"); c->state = CS_DEAD; break; default: simpleresp(1,"503 Must MAIL first"); break; } break; case CS_MAIL: switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); c->state = CS_HELO; break; case CMD_DATA: simpleresp(1,"503 Need an accepted RCPT first"); c->baddata = 1; break; case CMD_RSET: do_rset(c); SEND250(); c->state = CS_HELO; break; case CMD_QUIT: simpleresp(0,"221 OK"); c->state = CS_DEAD; break; case CMD_RCPT: if (! do_rcpt(c,c->lbuf+i,al)) { if (rcpt_is_postmaster(&c->rcpt)) { STATS_RECORD(SM_ADDR_OK); printf("[%d] RCPT accepted %s%s%s from %s efrom %s xid %s\n",mypid(),c->rcpt.lp,c->rcpt.dom?"@":"",c->rcpt.dom?:"",c->peertext,c->env_from,xid(c)); if (open_server_conn(c)) { drop_rcpt(c); simpleresp(0,"451 Backend error"); } break; } switch (c->dnsl_status) { case DNSLSTAT_BAD: switch (c->logging) { case LOG_OFF: drop_rcpt(c); ioq_oq_point(&c->cq,"550 ",-1); ioq_oq_point(&c->cq,c->dnsl_msg,-1); ioq_oq_point(&c->cq,"\r\n",-1); break; case LOG_REJ: case LOG_ALL: save_rcpt(c); SEND250(); set_fail_reason(c,"DNSL reject: %s",c->dnsl_msg); c->state = CS_FAIL; break; default: abort(); break; } break; case DNSLSTAT_FAIL: drop_rcpt(c); ioq_oq_point(&c->cq,"451 ",-1); ioq_oq_point(&c->cq,c->dnsl_msg,-1); ioq_oq_point(&c->cq,"\r\n",-1); break; case DNSLSTAT_OK: switch (valid_address(c->rcpt.lp,c->rcpt.dom)) { case VA_GOOD: STATS_RECORD(SM_ADDR_OK); printf("[%d] RCPT accepted %s%s%s from %s efrom %s xid %s\n",mypid(),c->rcpt.lp,c->rcpt.dom?"@":"",c->rcpt.dom?:"",c->peertext,c->env_from,xid(c)); if (open_server_conn(c)) { drop_rcpt(c); simpleresp(0,"451 Backend error"); } break; case VA_BAD: STATS_RECORD(SM_ADDR_BAD); switch (c->logging) { case LOG_OFF: printf("[%d] RCPT rejected %s%s%s from %s efrom %s xid %s\n",mypid(),c->rcpt.lp,c->rcpt.dom?"@":"",c->rcpt.dom?:"",c->peertext,c->env_from,xid(c)); drop_rcpt(c); simpleresp(0,"550 No such user, or relaying refused"); break; case LOG_REJ: case LOG_ALL: save_rcpt(c); set_fail_reason(c,"bad recipients"); SEND250(); c->state = CS_IRCPTS; break; default: abort(); break; } break; case VA_UNKNOWN: STATS_RECORD(SM_ADDR_UNK); drop_rcpt(c); simpleresp(1,"451 Can't check address validity"); break; default: panic("smtp_command: bad address validity"); break; } break; default: panic("DNSL status %d in state %s",c->dnsl_status,clientstatename(c->state)); break; } } break; case CMD_MAIL: simpleresp(1,"503 Already did MAIL"); break; default: panic("impossible command %d in state %s",cmd,clientstatename(c->state)); break; } break; case CS_IRCPTS: switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); clear_fail_reason(c); c->state = CS_HELO; break; case CMD_DATA: simpleresp(0,"354 OK"); begin_data(c,CS_IDATA); break; case CMD_RSET: do_rset(c); SEND250(); clear_fail_reason(c); c->state = CS_HELO; break; case CMD_QUIT: simpleresp(0,"221 OK"); clear_fail_reason(c); c->state = CS_DEAD; break; case CMD_RCPT: if (! do_rcpt(c,c->lbuf+i,al)) { switch (valid_address(c->rcpt.lp,c->rcpt.dom)) { case VA_GOOD: STATS_RECORD(SM_ADDR_OK); drop_rcpt(c); simpleresp(0,"451 Please use another transaction for this address"); break; case VA_UNKNOWN: STATS_RECORD(SM_ADDR_UNK); drop_rcpt(c); simpleresp(1,"450 Can't check address validity"); break; case VA_BAD: STATS_RECORD(SM_ADDR_BAD); save_rcpt(c); SEND250(); break; default: panic("smtp_command: bad address validity"); break; } } break; case CMD_MAIL: simpleresp(1,"503 Already did MAIL"); break; default: panic("impossible command %d in state %s\n",cmd,clientstatename(c->state)); break; } break; case CS_SNRCPTS: case CS_SGRCPTS: case CS_SBRCPTS: switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); if (c->state == CS_SBRCPTS) clear_fail_reason(c); c->state = CS_HELO; close_server(c); break; case CMD_RSET: do_rset(c); SEND250(); if (c->state == CS_SBRCPTS) clear_fail_reason(c); c->state = CS_HELO; close_server(c); break; case CMD_QUIT: simpleresp(0,"221 OK"); if (c->state == CS_SBRCPTS) clear_fail_reason(c); c->state = CS_DEAD; close_server(c); break; case CMD_DATA: switch (c->state) { case CS_SNRCPTS: simpleresp(0,"503 Need an accepted RCPT first"); break; case CS_SGRCPTS: ioq_oq_point(&c->sq,"DATA\r\n",-1); c->state = CS_SDATA; break; case CS_SBRCPTS: simpleresp(0,"354 OK"); begin_data(c,CS_BDATA); close_server(c); break; default: panic("bad RCPT state"); break; } break; case CMD_RCPT: if (! do_rcpt(c,c->lbuf+i,al)) { switch (valid_address(c->rcpt.lp,c->rcpt.dom)) { case VA_GOOD: STATS_RECORD(SM_ADDR_OK); send_rcpt(c,c->rcpt); switch (c->state) { case CS_SNRCPTS: c->state = CS_SRCPT; break; case CS_SGRCPTS: c->state = CS_SGRCPT; break; case CS_SBRCPTS: c->state = CS_SBRCPT; break; default: panic("bad RCPT state"); break; } break; case VA_BAD: STATS_RECORD(SM_ADDR_BAD); switch (c->logging) { case LOG_OFF: drop_rcpt(c); simpleresp(0,"550 No such user, or relaying refused"); break; case LOG_REJ: case LOG_ALL: drop_rcpt(c); simpleresp(0,"451 Please use another transaction for this address"); break; default: abort(); break; } break; case VA_UNKNOWN: STATS_RECORD(SM_ADDR_UNK); drop_rcpt(c); simpleresp(1,"450 Can't check address validity"); break; default: abort(); break; } } break; case CMD_MAIL: simpleresp(1,"503 Already did MAIL"); break; default: panic("impossible command %d in state %s",cmd,clientstatename(c->state)); break; } break; case CS_CLOSING: switch (cmd) { case CMD_QUIT: simpleresp(0,"221 OK"); c->state = CS_DEAD; break; default: simpleresp(0,"421 Backend error"); break; } break; case CS_FAIL: switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); clear_fail_reason(c); c->state = CS_HELO; break; case CMD_DATA: simpleresp(0,"354 OK"); begin_data(c,CS_BDATA); break; case CMD_RSET: do_rset(c); SEND250(); clear_fail_reason(c); c->state = CS_HELO; break; case CMD_QUIT: simpleresp(0,"221 OK"); clear_fail_reason(c); c->state = CS_DEAD; break; case CMD_RCPT: if (! do_rcpt(c,c->lbuf+i,al)) { save_rcpt(c); SEND250(); break; } break; case CMD_MAIL: simpleresp(1,"503 Already did MAIL"); break; default: panic("impossible command %d in state %s",cmd,clientstatename(c->state)); break; } break; case CS_TEMPFAIL: switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); c->state = CS_HELO; break; case CMD_RSET: do_rset(c); SEND250(); c->state = CS_HELO; break; case CMD_QUIT: simpleresp(0,"221 OK"); c->state = CS_DEAD; break; default: simpleresp(0,"450 Backend error"); break; } break; case CS_HARDFAIL: switch (cmd) { case CMD_HELO: case CMD_EHLO: do_helo(c,c->lbuf+i,al); c->state = CS_HELO; break; case CMD_RSET: do_rset(c); SEND250(); c->state = CS_HELO; break; case CMD_QUIT: simpleresp(0,"221 OK"); c->state = CS_DEAD; break; case CMD_MAIL: simpleresp(0,"503 Already did MAIL"); break; case CMD_RCPT: case CMD_DATA: simpleresp(0,"550 Mail not accepted from this sender"); break; default: panic("impossible command %d in state %s",cmd,clientstatename(c->state)); break; } break; default: panic("impossible state %d",c->state); break; } #undef SEND250 } /* * Called when we've accumulated a reply from the backend server. * Check the state and reply code and step through the state machine. */ static void server_reply(CLIENT *c) { switch (c->state) { case CS_SGREET: if ((c->llen >= 1) && (c->lbuf[0] == '2')) { ioq_oq_point(&c->sq,"HELO ",-1); ioq_oq_point(&c->sq,&myhostname[0],-1); ioq_oq_point(&c->sq,"\r\n",-1); c->state = CS_SHELO; } else { drop_rcpt(c); ioq_oq_point(&c->cq,"451 Backend error\r\n",-1); c->state = CS_CLOSING; close_server(c); } break; case CS_SHELO: if ((c->llen >= 1) && (c->lbuf[0] == '2')) { ioq_oq_point(&c->sq,"MAIL From:<",-1); ioq_oq_point(&c->sq,c->env_from,-1); ioq_oq_point(&c->sq,">\r\n",-1); c->state = CS_SMAIL; } else { drop_rcpt(c); ioq_oq_point(&c->cq,"451 Backend error\r\n",-1); c->state = CS_CLOSING; close_server(c); } break; case CS_SMAIL: if ((c->llen >= 1) && (c->lbuf[0] == '2')) { send_rcpt(c,c->rcpt); c->state = CS_SRCPT; } else { printf("[%d] %s backend rejected MAIL From:<%s> with %.*s\n",mypid(),c->iostr,c->env_from,c->llen,c->lbuf); if ((c->llen >= 1) && (c->lbuf[0] == '4')) { drop_rcpt(c); ioq_oq_copy(&c->cq,c->lbuf,c->llen); ioq_oq_point(&c->cq,"\r\n",2); c->state = CS_TEMPFAIL; close_server(c); } else if ((c->llen >= 1) && (c->lbuf[0] == '5')) { switch (c->logging) { case LOG_OFF: drop_rcpt(c); ioq_oq_copy(&c->cq,c->lbuf,c->llen); ioq_oq_point(&c->cq,"\r\n",2); c->state = CS_HARDFAIL; break; case LOG_REJ: case LOG_ALL: save_rcpt(c); ioq_oq_point(&c->cq,"250 OK\r\n",-1); set_fail_reason(c,"server rejected MAIL (%s): %.*s",c->env_from,c->llen,c->lbuf); c->state = CS_FAIL; break; default: abort(); break; } close_server(c); } else { drop_rcpt(c); ioq_oq_point(&c->cq,"451 Backend error\r\n",-1); c->state = CS_TEMPFAIL; close_server(c); } } break; case CS_SRCPT: if ((c->llen >= 1) && (c->lbuf[0] == '2')) { save_rcpt(c); ioq_oq_point(&c->cq,"250 OK\r\n",-1); c->state = CS_SGRCPTS; } else if ((c->llen >= 1) && (c->lbuf[0] == '4')) { drop_rcpt(c); ioq_oq_copy(&c->cq,c->lbuf,c->llen); ioq_oq_point(&c->cq,"\r\n",2); c->state = CS_SNRCPTS; } else if ((c->llen >= 1) && (c->lbuf[0] == '5')) { switch (c->logging) { case LOG_OFF: drop_rcpt(c); ioq_oq_copy(&c->cq,c->lbuf,c->llen); ioq_oq_point(&c->cq,"\r\n",2); c->state = CS_SNRCPTS; break; case LOG_REJ: case LOG_ALL: save_rcpt(c); ioq_oq_point(&c->cq,"250 OK\r\n",-1); set_fail_reason(c,"server rejected recipient(s)"); c->state = CS_SBRCPTS; break; default: abort(); break; } } else { drop_rcpt(c); ioq_oq_point(&c->cq,"451 Backend error\r\n",-1); c->state = CS_TEMPFAIL; close_server(c); } break; case CS_SGRCPT: if ((c->llen >= 1) && (c->lbuf[0] == '2')) { save_rcpt(c); ioq_oq_point(&c->cq,"250 OK\r\n",-1); c->state = CS_SGRCPTS; } else if ((c->llen >= 1) && (c->lbuf[0] == '4')) { drop_rcpt(c); ioq_oq_copy(&c->cq,c->lbuf,c->llen); ioq_oq_point(&c->cq,"\r\n",2); c->state = CS_SGRCPTS; } else if ((c->llen >= 1) && (c->lbuf[0] == '5')) { drop_rcpt(c); switch (c->logging) { case LOG_OFF: ioq_oq_copy(&c->cq,c->lbuf,c->llen); ioq_oq_point(&c->cq,"\r\n",2); break; case LOG_REJ: case LOG_ALL: ioq_oq_point(&c->cq,"451 Please use another transaction for this address\r\n",-1); break; default: abort(); break; } c->state = CS_SGRCPTS; } else { drop_rcpt(c); ioq_oq_point(&c->cq,"451 Backend error\r\n",-1); c->state = CS_TEMPFAIL; close_server(c); } break; case CS_SBRCPT: if ((c->llen >= 1) && (c->lbuf[0] == '2')) { drop_rcpt(c); ioq_oq_point(&c->cq,"451 Please use another transaction for this address\r\n",-1); c->state = CS_SBRCPTS; } else if ((c->llen >= 1) && (c->lbuf[0] == '4')) { drop_rcpt(c); ioq_oq_copy(&c->cq,c->lbuf,c->llen); ioq_oq_point(&c->cq,"\r\n",2); c->state = CS_SBRCPTS; } else if ((c->llen >= 1) && (c->lbuf[0] == '5')) { save_rcpt(c); ioq_oq_point(&c->cq,"250 OK\r\n",-1); c->state = CS_SBRCPTS; } else { drop_rcpt(c); ioq_oq_point(&c->cq,"451 Backend error\r\n",-1); clear_fail_reason(c); c->state = CS_TEMPFAIL; close_server(c); } break; case CS_SDATA: if ((c->llen >= 1) && (c->lbuf[0] == '3')) { ioq_oq_point(&c->cq,"354 OK\r\n",-1); begin_data(c,CS_SDCOPY); } else { switch (c->logging) { case LOG_OFF: ioq_oq_copy(&c->cq,c->lbuf,c->llen); ioq_oq_point(&c->cq,"\r\n",2); c->state = CS_SGRCPTS; break; case LOG_REJ: case LOG_ALL: ioq_oq_point(&c->cq,"354 OK\r\n",-1); set_fail_reason(c,"server rejected DATA: %.*s",c->llen,c->lbuf); begin_data(c,CS_BDATA); close_server(c); break; default: abort(); break; } } break; case CS_SDOT: if ((c->llen >= 1) && ((c->lbuf[0] == '2') || (c->lbuf[0] == '4'))) { ioq_oq_copy(&c->cq,c->lbuf,c->llen); ioq_oq_point(&c->cq,"\r\n",2); if (c->logging == LOG_ALL) { set_fail_reason(c,"logging all"); log_message(c); clear_fail_reason(c); } do_rset(c); c->state = CS_HELO; close_server(c); } else if ((c->llen >= 1) && (c->lbuf[0] == '5')) { switch (c->logging) { case LOG_OFF: ioq_oq_copy(&c->cq,c->lbuf,c->llen); ioq_oq_point(&c->cq,"\r\n",2); break; case LOG_REJ: case LOG_ALL: ioq_oq_point(&c->cq,"250 OK\r\n",-1); set_fail_reason(c,"server rejected body: %.*s",c->llen,c->lbuf); log_message(c); clear_fail_reason(c); break; default: abort(); break; } do_rset(c); c->state = CS_HELO; close_server(c); } else { printf("[%d] bad backend response to DATA: %.*s\n",mypid(),c->llen,c->lbuf); ioq_oq_point(&c->cq,"451 Backend error\r\n",-1); c->state = CS_TEMPFAIL; close_server(c); } break; default: panic("impossible state %d",c->state); break; } } /* * Return the time in seconds from the client's starttime to now. */ static double sbb_delay(CLIENT *c) { struct timeval now; gettimeofday(&now,0); if (now.tv_usec >= c->starttime.tv_usec) { return( (now.tv_sec - c->starttime.tv_sec) + ((now.tv_usec - c->starttime.tv_usec) * 1e-6) ); } else { return( (now.tv_sec - 1 - c->starttime.tv_sec) + ((now.tv_usec + 1000000 - c->starttime.tv_usec) * 1e-6) ); } } /* * Process an input line. Where it's input from depends on the state. */ static void client_process_line(CLIENT *c) { double d; if ((c->llen < 2) || (c->lbuf[c->llen-1] != '\n') || (c->lbuf[c->llen-2] != '\r')) abort(); c->llen -= 2; c->lbuf[c->llen] = '\0'; switch (c->state) { case CS_SBB: d = sbb_delay(c); sbb_hook(c->peeraddr,d,c->lbuf,c->llen); printf("[%d] %s (%s) sent data before banner, delay %.6f (%.*s%s)\n",mypid(),cid(c),c->peertext,d,(c->llen<32)?c->llen:32,c->lbuf,(c->llen<32)?"":"..."); mlog("%s: SMTP <<< %.*s\n",c->peertext,c->llen,c->lbuf); ioq_oq_point(&c->cq,"500 SMTP dialog phase error\r\n",-1); c->state = CS_DEAD; break; case CS_GREET: case CS_HELO: case CS_MAIL: case CS_IRCPTS: case CS_SNRCPTS: case CS_SGRCPTS: case CS_SBRCPTS: case CS_CLOSING: case CS_FAIL: case CS_TEMPFAIL: case CS_HARDFAIL: mlog("%s: SMTP <<< %.*s\n",c->peertext,c->llen,c->lbuf); smtp_command(c); break; case CS_SGREET: case CS_SHELO: case CS_SMAIL: case CS_SRCPT: case CS_SGRCPT: case CS_SBRCPT: case CS_SDATA: case CS_SDOT: mlog("%s: server <<< %.*s\n",c->peertext,c->llen,c->lbuf); server_reply(c); break; default: nonfatal("client_process_line: state %d",c->state); c->state = CS_DEAD; break; } c->llen = 0; } /* * Accept input that's already in the client's read buffer. Crank * through the terminator state machine appropriate to the client's * state, and when done, either switch state (for DATA) or handle an * input line (for others). */ static void check_buffered_in(CLIENT *c) { int i; IOQ *q; int ts; q = c->ioq; switch (c->state) { case CS_IDATA: case CS_BDATA: case CS_SDCOPY: ts = c->termstate; for (i=q->ibptr;iibfill;i++) { switch (ts) { case 0: /* after CRLF */ switch (q->ibuf[i]) { case '.': ts = 1; break; case '\r': ts = 4; break; default: ts = 3; break; } break; case 1: /* after CRLF dot */ ts = (q->ibuf[i] == '\r') ? 2 : 3; break; case 2: /* after CRLF dot CR */ if (q->ibuf[i] == '\n') { i ++; data_data(c,&q->ibuf[q->ibptr],i-q->ibptr); q->ibptr = i; c->termstate = 0; STATS_RECORD(SM_BODY); body_ends(c); return; } ts = 4; break; case 3: /* middle of line, after nothing special */ if (q->ibuf[i] == '\r') ts = 4; break; case 4: /* after CR */ switch (q->ibuf[i]) { case '\n': ts = 0; break; case '\r': break; default: ts = 3; break; } break; default: abort(); break; } } c->termstate = ts; data_data(c,&q->ibuf[q->ibptr],i-q->ibptr); q->ibptr = i; break; default: ts = c->termstate; for (i=q->ibptr;iibfill;i++) { if (ts) { if (q->ibuf[i] == '\n') { i ++; if (iline_append(c,&q->ibuf[q->ibptr],i-q->ibptr)) { c->state = CS_DEAD; return; } q->ibptr = i; c->termstate = 0; client_process_line(c); return; } ts = 0; } else { if (q->ibuf[i] == '\r') ts = 1; } } if (iline_append(c,&q->ibuf[q->ibptr],i-q->ibptr)) { c->state = CS_DEAD; return; } c->termstate = ts; break; } } /* * The client's input fd - whichever one that is - has become readable. * Read input. (Don't bother actually processing it; let * check_buffered_in handle that next time around the loop.) */ static void check_poll_in(CLIENT *c) { double d; int e; if (pfds[c->px].revents) { e = ioq_read(c->ioq); if (e < 0) { /* EOF */ if (c->state == CS_SBB) { d = sbb_delay(c); sbb_hook(c->peeraddr,d,0,-1); printf("[%d] %s (%s) dropped connection before banner, delay %.6f\n",mypid(),cid(c),c->peertext,d); } c->state = CS_DEAD; } else if (e) { /* error; ECONNRESET isn't worth mentioning, but otherwise... */ if (errno != ECONNRESET) { nonfatal("read error [%s]: %s",c->peertext,strerror(errno)); } c->state = CS_DEAD; } else { fresh_timeout(c); } } } /* * Close the file descriptors for all LSOCKs. Used by SMTP worksers * when the parent dies, to release port 25. (We don't actually need * to set the fd struct elements to anything invalid; the setting of * ownstate to LS_DYING carrise the information that we shouldn't do * anything with them. But it's a cheap way to ensure that even if we * do weird out and try to access them, we don't end up scribbling on * something else by mistake.) */ static void close_accs(void) { LSOCK *a; for (a=accs;a;a=a->link) { close(a->fd); a->fd = -1; } } /* * Initialize a MSG before first use. */ static void msg_init(MSG *m) { m->len = -1; } /* * Process an incoming byte for a MSG. When a complete message is * accumulated, calls (*done)() and may take further action (see * below); on error, calls (*err)() with the message. err may throw * out; done must not. done's return value indicates what msg_input * should do after calling it: * MI_RESET * Reset for the next message as normal. * MI_CONTINUE * Any necessary resetting for more input has been done * already; just continue accumulating input. */ #define MI_RESET 1 #define MI_CONTINUE 2 static void msg_input(MSG *m, unsigned char b, int (*done)(void), void (*err)(const char *, ...)) { if (m->len < 0) { if ((b < 1) || (b > MAX_LM_LEN)) { (*err)("msglen %d",b); return; } m->len = b; m->fill = 0; } else { m->buf[m->fill++] = b; if (m->fill >= m->len) { switch ((*done)()) { case MI_RESET: m->len = -1; break; case MI_CONTINUE: break; default: abort(); break; } } } } /* * Return a completed message's length. To be called only from within * a done function called by msg_input(). */ static int msg_len(MSG *m) { return(m->len); } /* * Return one of a completed message's bytes. To be called only from * within a done function called by msg_input(). */ static unsigned char msg_byte(MSG *m, int n) { return(m->buf[n]); } /* * Some data from the response to LM_STATS has been received. Handle * it. (The whole message is passed in so we can easily get the * serial number of the relevant client.) */ static void stats_response_data(MSG *m) { unsigned long long int serial; CLIENT *c; int i; int i0; unsigned int ch; serial = (m->buf[1] * 0x0000000000000001ULL) | (m->buf[2] * 0x0000000000000100ULL) | (m->buf[3] * 0x0000000000010000ULL) | (m->buf[4] * 0x0000000001000000ULL) | (m->buf[5] * 0x0000000100000000ULL) | (m->buf[6] * 0x0000010000000000ULL) | (m->buf[7] * 0x0001000000000000ULL) | (m->buf[8] * 0x0100000000000000ULL); for (c=clients;c;c=c->link) { if (c->serial != serial) continue; i0 = -1; for (i=0;ibuf[9];i++) { ch = m->buf[10+i]; if (i0 < 0) i0 = i; if (ch == '\n') { if (c->statstate & CSS_ATNL) ioq_oq_point(&c->cq,"250-",-1); if (i > i0) ioq_oq_copy(&c->cq,&m->buf[10+i0],i-i0); ioq_oq_point(&c->cq,"\r\n",-1); i0 = -1; c->statstate |= CSS_ATNL; } } if (i0 >= 0) { if (c->statstate & CSS_ATNL) ioq_oq_point(&c->cq,"250-",-1); if (i > i0) ioq_oq_copy(&c->cq,&m->buf[10+i0],i-i0); c->statstate &= ~CSS_ATNL; } } } /* * The end-of-data marker from the response to LM_STATS has been * received. Handle it. (See stats_resposne_data, above, for more.) */ static void stats_response_done(MSG *m) { unsigned long long int serial; CLIENT *c; serial = (m->buf[1] * 0x0000000000000001ULL) | (m->buf[2] * 0x0000000000000100ULL) | (m->buf[3] * 0x0000000000010000ULL) | (m->buf[4] * 0x0000000001000000ULL) | (m->buf[5] * 0x0000000100000000ULL) | (m->buf[6] * 0x0000010000000000ULL) | (m->buf[7] * 0x0001000000000000ULL) | (m->buf[8] * 0x0100000000000000ULL); for (c=clients;c;c=c->link) { if (c->serial != serial) continue; if (! (c->statstate & CSS_ATNL)) ioq_oq_point(&c->cq,"\r\n",-1); ioq_oq_point(&c->cq,"250 Done.\r\n",-1); } } /* * Read from the parent pipe in an SMTP worker. */ static void parent_read(void) { int i; int r; unsigned char rbuf[64]; r = read(parentpipe,&rbuf[0],sizeof(rbuf)); if (r < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return; break; } fatal("parent pipe read: %s",strerror(errno)); } if (r == 0) { ownstate = LS_DYING; close(parentpipe); close_accs(); return; } for (i=0;i= the second. */ static unsigned int tvsub_ms(struct timeval a, struct timeval b) { if (a.tv_usec >= b.tv_usec) { return(((a.tv_sec-b.tv_sec)*1000)+((a.tv_usec-b.tv_usec)/1000)+1); } else { return(((a.tv_sec-b.tv_sec-1)*1000)+((a.tv_usec+1000000-b.tv_usec)/1000)+1); } } /* * A stdio wrapper which prefixes each line with mypid(), [%d] style. */ /* BEGIN pidprefix wrapper internals */ typedef struct pidprefix_priv PIDPREFIX_PRIV; struct pidprefix_priv { FILE *sub; int atbol; } ; static int pidprefix_w(void *pv, const char *buf, int len) { PIDPREFIX_PRIV *p; int i; p = pv; for (i=0;iatbol) fprintf(p->sub,"[%d] ",mypid()); putc(buf[i],p->sub); p->atbol = (buf[i] == '\n'); } return(len); } static int pidprefix_c(void *pv) { free(pv); return(0); } /* END pidprefix wrapper internals */ static FILE *fopen_pidprefix(FILE *sub) { PIDPREFIX_PRIV *p; p = malloc(sizeof(*p)); p->sub = sub; p->atbol = 1; return(funopen(p,0,&pidprefix_w,0,&pidprefix_c)); } /* BEGIN packetized wrapper internals */ static int packetized_w(void *pv, const char *buf, int len0) { int len; unsigned char n; struct iovec iov[2]; len = len0; while (len > 0) { /* The max value here is 128 so that the pseudo-messages involved are never over 255 bytes long. This really doesn't matter, since the pseudo-messages don't really have length bytes, but it lets us keep MAX_LM_LEN below 255, which avoids a compiler warning when comparing an unsigned char to it in msg_input. */ n = (len > 128) ? 128 : len; iov[0].iov_base = &n; iov[0].iov_len = 1; iov[1].iov_base = deconst(buf); iov[1].iov_len = n; writev(*(int *)pv,&iov[0],2); len -= n; buf += n; } return(len0); } static int packetized_c(void *pv) { write(*(int *)pv,"",1); free(pv); return(0); } /* END packetized wrapper internals */ static FILE *fopen_packetized(int fd) { int *v; v = malloc(sizeof(int)); *v = fd; return(funopen(v,0,&packetized_w,0,&packetized_c)); } /* * The SMTP worker main loop. This is called repeatedly by an SMTP * worker. Nothing particularly notable here. Update our process * title if it's changed. Walk the list of clients, checking each * one's state and adding the appropriate stuff to the poll vector. * Possibly shift state based on client count. Do the poll and handle * any I/O it indicates is possible. */ static void listener_tick(void) { LSOCK *a; int n; int nclients; CLIENT *c; CLIENT **cp; IOQ *q; int timeout; struct timeval now; unsigned long int tmo; if (titlestate != ownstate) { switch (ownstate) { case LS_LISTENING: setproctitle("SMTP worker (listening)"); break; case LS_FULL: setproctitle("SMTP worker (full)"); break; case LS_STOPPING: setproctitle("SMTP worker (stopping)"); break; case LS_DYING: setproctitle("SMTP worker (dying)"); break; default: setproctitle("SMTP worker (%d?)",ownstate); break; } titlestate = ownstate; } fflush(0); if (got_usr1) { FILE *f; got_usr1 = 0; f = fopen_pidprefix(stdout); dump_listener_state(f); fclose(f); return; } timeout = INFTIM; gettimeofday(&now,0); npfds = 0; if (ownstate != LS_DYING) pppx = add_pfd(parentpipe,POLLIN|POLLRDNORM); nclients = 0; for (cp=&clients;(c=*cp);) { if (c->timeout && (c->timeout < now.tv_sec)) c->state = CS_BURIED; switch (c->state) { case CS_SBB: case CS_GREET: case CS_HELO: case CS_DNSL: case CS_MAIL: case CS_IRCPTS: case CS_IDATA: case CS_SNRCPTS: case CS_SGRCPTS: case CS_SBRCPTS: case CS_BDATA: case CS_SDATA: case CS_SDCOPY: case CS_SDOT: case CS_CLOSING: case CS_FAIL: case CS_TEMPFAIL: case CS_HARDFAIL: if (c->rcpt.lp || c->rcpt.dom) panic("recipient in state %s",clientstatename(c->state)); break; case CS_SCONN: case CS_SGREET: case CS_SHELO: case CS_SMAIL: case CS_SRCPT: case CS_SGRCPT: case CS_SBRCPT: if (! c->rcpt.lp) panic("no recipient in state %s",clientstatename(c->state)); break; case CS_DEAD: case CS_BURIED: case CS_SCLOSE: break; default: panic("impossible state %d",c->state); break; } switch (c->state) { case CS_SBB: case CS_GREET: case CS_HELO: case CS_DNSL: case CS_MAIL: case CS_IRCPTS: case CS_IDATA: case CS_BDATA: case CS_CLOSING: case CS_FAIL: case CS_TEMPFAIL: case CS_HARDFAIL: if (c->sq.fd >= 0) panic("server connection in state %s",clientstatename(c->state)); break; case CS_SCONN: case CS_SGREET: case CS_SHELO: case CS_SMAIL: case CS_SRCPT: case CS_SNRCPTS: case CS_SGRCPTS: case CS_SGRCPT: case CS_SBRCPTS: case CS_SBRCPT: case CS_SDATA: case CS_SDCOPY: case CS_SDOT: case CS_SCLOSE: if (c->sq.fd < 0) panic("no server connection in state %s",clientstatename(c->state)); break; case CS_DEAD: case CS_BURIED: break; default: panic("impossible state %d",c->state); break; } switch (c->state) { case CS_SBB: case CS_GREET: case CS_HELO: case CS_DNSL: case CS_MAIL: case CS_SCONN: case CS_SGREET: case CS_SHELO: case CS_SMAIL: case CS_SRCPT: case CS_SNRCPTS: case CS_SGRCPTS: case CS_SGRCPT: case CS_SDATA: case CS_SDCOPY: case CS_SDOT: case CS_CLOSING: case CS_TEMPFAIL: case CS_HARDFAIL: if (c->fail_reason) panic("failure reason in state %s",clientstatename(c->state)); break; case CS_IRCPTS: case CS_IDATA: case CS_SBRCPTS: case CS_SBRCPT: case CS_BDATA: case CS_FAIL: if (! c->fail_reason) panic("no failure reason in state %s",clientstatename(c->state)); break; case CS_DEAD: case CS_BURIED: case CS_SCLOSE: break; default: panic("impossible state %d",c->state); break; } switch (c->state) { case CS_IRCPTS: case CS_IDATA: case CS_SBRCPTS: case CS_SBRCPT: case CS_BDATA: case CS_FAIL: if (c->logging == LOG_OFF) panic("logging off in state %s",clientstatename(c->state)); break; } switch (c->state) { case CS_SBB: q = &c->cq; if (tvle(c->banner_at,now)) { c->state = CS_GREET; send_greeting(c); } else { tmo = tvsub_ms(c->banner_at,now); if ((timeout == INFTIM) || (tmo < timeout)) timeout = tmo; } break; case CS_GREET: case CS_HELO: case CS_MAIL: case CS_IRCPTS: case CS_SNRCPTS: case CS_SGRCPTS: case CS_SBRCPTS: case CS_CLOSING: case CS_FAIL: case CS_TEMPFAIL: case CS_HARDFAIL: q = &c->cq; break; case CS_SGREET: case CS_SHELO: case CS_SMAIL: case CS_SRCPT: case CS_SGRCPT: case CS_SBRCPT: case CS_SDATA: case CS_SDOT: q = &c->sq; break; case CS_DNSL: q = 0; c->checkfn = &check_dnsl_done; c->px = add_pfd(c->dnsl_fd,POLLIN|POLLRDNORM); break; case CS_IDATA: case CS_BDATA: q = &c->cq; break; case CS_SCONN: q = 0; c->checkfn = &check_poll_conn; c->px = add_pfd(c->sq.fd,POLLOUT|POLLWRNORM); break; case CS_SDCOPY: q = (c->cq.q || !c->sq.q) ? &c->cq : &c->sq; break; default: fatal("listener_tick: state %d",c->state); if (0) { case CS_SCLOSE: if (c->sq.q) { q = &c->sq; break; } close(c->sq.fd); ioq_flush(&c->sq); ioq_init(&c->sq,-1); c->state = c->scstate; continue; } case CS_DEAD: if (c->cq.q) { q = &c->cq; break; } /* fall through */ case CS_BURIED: do_rset(c); *cp = c->link; free(c->lbuf); free(c->peeraddr); free(c->peertext); free(c->helo_as); /* env_from done by do_rset above */ free_rcpt(c->rcpt); free(c->dnsl_msg); free(c->dnsl_tag); if (c->dnsl_fd >= 0) close(c->dnsl_fd); if (c->data_fd >= 0) close(c->data_fd); free(c->data_file); free(c->fail_reason); /* rcpts themselves done by do_rset above, but not rcpts vector */ free(c->rcpts); free(c->cid); free(c->xid); close(c->cq.fd); ioq_flush(&c->cq); if (c->sq.fd >= 0) close(c->sq.fd); ioq_flush(&c->sq); free(c); continue; break; } nclients ++; if (q) { c->ioq = q; c->iostr = (q == &c->cq) ? c->peertext : REAL_SMTP_SERVER; if (q->q) { c->checkfn = &check_poll_out; c->px = add_pfd(q->fd,POLLOUT|POLLWRNORM); } else if (q->ibptr < q->ibfill) { c->checkfn = &check_buffered_in; timeout = 0; } else { c->checkfn = &check_poll_in; c->px = add_pfd(q->fd,POLLIN|POLLRDNORM); if (c->timeout) { if (c->timeout < now.tv_sec) panic("impossible c->timeout 1"); if (c->timeout-now.tv_sec > 10000) panic("impossible c->timeout 2"); tmo = (c->timeout + 1 - now.tv_sec) * 1000; if (tmo > 0x70000000) panic("impossible tmo"); if ((timeout == INFTIM) || (tmo < timeout)) timeout = tmo; } } } cp = &c->link; } switch (ownstate) { case LS_LISTENING: if (nclients >= CLIENTS_PER_PROCESS) { ownstate = LS_FULL; tellparent(1,LM_FULL); } else if (nclients != toldclients) { tellparent(2,LM_LISTENING,CLIENTS_PER_PROCESS-nclients); toldclients = nclients; } break; case LS_FULL: if (nclients < CLIENTS_PER_PROCESS) { ownstate = LS_LISTENING; tellparent(2,LM_LISTENING,CLIENTS_PER_PROCESS-nclients); toldclients = nclients; } break; case LS_STOPPING: if (nclients < 1) { tellparent(1,LM_DYING); exit(0); } break; case LS_DYING: if (nclients < 1) exit(0); break; default: abort(); break; } if (ownstate == LS_LISTENING) { for (a=accs;a;a=a->link) a->px = add_pfd(a->fd,POLLIN|POLLRDNORM); } if ((timeout != INFTIM) && ((timeout < 0) || (timeout > 100000000))) panic("impossible timeout"); n = poll(pfds,npfds,timeout); if (n < 0) { if (errno == EINTR) return; fatal("poll: %s",strerror(errno)); exit(1); } /* Do clients before accs - accept_acc can add a new client, and if so, it won't have a checkfn set up yet! Similarly do accs before parentpipe, since reading from parentpipe can change ownstate. */ for (c=clients;c;c=c->link) (*c->checkfn)(c); if (ownstate == LS_LISTENING) for (a=accs;a;a=a->link) if (pfds[a->px].revents) accept_acc(a); if ((ownstate != LS_DYING) && pfds[pppx].revents) parent_read(); } /* * Called by the parent to fork a new SMTP worker. */ static int make_new_listener(void) { pid_t kid; int p[2]; LISTENER *l; if (socketpair(AF_LOCAL,SOCK_STREAM,0,p) < 0) fatal("socketpair: %s",strerror(errno)); set_nonblock(p[0]); set_nonblock(p[1]); kid = fork(); if (kid < 0) fatal("fork: %s",strerror(errno)); if (kid == 0) { cached_pid = -1; while (listeners) { l = listeners; listeners = l->link; close(l->pipe); free(l); } close(p[0]); parentpipe = p[1]; close(dnsl_parent_death); ownstate = LS_LISTENING; titlestate = LS_DEAD; toldclients = -1; client_serial = 0x0123456789abcdefULL; msg_init(&parentmsg); return(1); } close(p[1]); l = malloc(sizeof(LISTENER)); l->proc = kid; l->pipe = p[0]; l->state = LS_LISTENING; l->fcs = CLIENTS_PER_PROCESS; msg_init(&l->msg); l->link = listeners; listeners = l; printf("[%d] forked new listener %d\n",mypid(),(int)kid); return(0); } /* * Send a message to a listener. We deliberately ignore write errors * here; the only plausible error is EPIPE, and simply dropping the * message is the appropriate thing to do. */ static void tell_listener(LISTENER *l, int nb, ...) { va_list ap; int i; unsigned char buf[MAX_LM_LEN+1]; if ((nb < 1) || (nb > MAX_LM_LEN)) panic("tell_listener: len %d not in [1..%d]",nb,MAX_LM_LEN); va_start(ap,nb); buf[0] = nb; for (i=0;ipipe,&buf[0],nb+1); } /* * The equivalent of tell_listener for all listeners. */ static void tell_all_listeners(int nb, ...) { va_list ap; LISTENER *l; int i; unsigned char buf[MAX_LM_LEN+1]; if ((nb < 1) || (nb > MAX_LM_LEN)) panic("tell_listener: len %d not in [1..%d]",nb,MAX_LM_LEN); va_start(ap,nb); buf[0] = nb; for (i=0;ilink) write(l->pipe,&buf[0],nb+1); } /* * Called by the parent to tell an SMTP worker to go away. Just find a * LISTENING worker and send it an LM_STOP message. (We go to the * trouble to find the _oldest_ LISTENING worker just to keep rolling * processes over rather than creating and destroying some while * others last forever.) */ static void stop_a_listener(void) { LISTENER *l; LISTENER *ll; ll = 0; for (l=listeners;l;l=l->link) if (l->state == LS_LISTENING) ll = l; if (ll) { printf("[%d] stopping listener %d\n",mypid(),(int)ll->proc); tell_listener(ll,1,LM_STOP); ll->state = LS_STOPPING; } } #ifdef DOSTATS /* * Called by the parent to receive DNSL stats messages. */ static void dnsl_stats(void) { unsigned char status; int r; r = recv(dnsl_data,&status,1,0); if (r < 0) { switch (errno) { case EWOULDBLOCK: case EINTR: return; break; } printf("[%d] recv stats from DNSL: %s\n",mypid(),strerror(errno)); exit(1); } if (r == 0) { printf("[%d] recv EOF from DNSL\n",mypid()); exit(1); } switch (status) { case SM_DNSL_OK: case SM_DNSL_BAD: case SM_DNSL_FAIL: counters[status-SM_FIRST_STATS] ++; break; default: printf("[%d] got bad stats byte 0x%02x from DNSL overseer\n",mypid(),status); break; } } #endif /* * Parent read from an SMTP worker. Track state changes and accumulate * stats. */ static void listener_read(LISTENER *l) { __label__ _ret; unsigned char rbuf[64]; int r; int i; auto void killoff(const char *, ...) __attribute__((__format__(__printf__,1,2))); auto void killoff(const char *fmt, ...) { va_list ap; FILE *f; f = msgopen(); fprintf(f,"listener %d: ",(int)l->proc); va_start(ap,fmt); vfprintf(f,fmt,ap); va_end(ap); fclose(f); l->state = LS_DEAD; goto _ret; } r = read(l->pipe,&rbuf[0],sizeof(rbuf)); if (r == 0) killoff("EOF"); if (r < 0) { switch (errno) { case EWOULDBLOCK: case EINTR: return; break; } killoff("read: %s",strerror(errno)); } for (i=0;imsg,0)) { default: killoff("message %02x?",msg_byte(&l->msg,0)); break; case LM_FULL: if (msg_len(&l->msg) != 1) killoff("LM_FULL message len %d?",msg_len(&l->msg)); l->state = LS_FULL; break; case LM_LISTENING: if (msg_len(&l->msg) != 2) killoff("LM_LISTENING message len %d?",msg_len(&l->msg)); l->state = LS_LISTENING; l->fcs = msg_byte(&l->msg,1); break; case LM_DYING: if (msg_len(&l->msg) != 1) killoff("LM_DYING message len %d?",msg_len(&l->msg)); l->state = LS_DEAD; return(MI_RESET); break; case LM_STATE: if (msg_len(&l->msg) < 2) killoff("LM_STATE message len %d?",msg_len(&l->msg)); switch (msg_byte(&l->msg,1)) { case STATE_LOG: if (msg_len(&l->msg) != 3) killoff("LM_STATE STATE_LOG message len %d?",msg_len(&l->msg)); switch (msg_byte(&l->msg,2)) { case LOG_OFF: case LOG_REJ: case LOG_ALL: logging = msg_byte(&l->msg,2); tell_all_listeners(3,LM_STATE,STATE_LOG,logging); break; default: killoff("LM_STATE STATE_LOG %d?",msg_byte(&l->msg,2)); break; } break; case STATE_TIMEOUT: if (msg_len(&l->msg) != 3) killoff("LM_STATE STATE_TIMEOUT message len %d?",msg_len(&l->msg)); banner_sleep = msg_byte(&l->msg,2); tell_all_listeners(3,LM_STATE,STATE_TIMEOUT,banner_sleep); break; default: killoff("unknown state change %02x",msg_byte(&l->msg,1)); break; } return(MI_RESET); break; case LM_STATS: { FILE *f; FILE *g; tell_listener(l,9,LM_STATS, msg_byte(&l->msg,1), msg_byte(&l->msg,2), msg_byte(&l->msg,3), msg_byte(&l->msg,4), msg_byte(&l->msg,5), msg_byte(&l->msg,6), msg_byte(&l->msg,7), msg_byte(&l->msg,8) ); f = fopen_packetized(l->pipe); g = fopen_pidprefix(f); dump_parent_state(g); fclose(g); fclose(f); } break; #ifdef DOSTATS case SM_FIRST_STATS ... SM_LAST_STATS: if (msg_len(&l->msg) != 1) killoff("%d message len %d?",msg_byte(&l->msg,0),msg_len(&l->msg)); counters[rbuf[i]-SM_FIRST_STATS] ++; break; #endif } return(MI_RESET); } msg_input(&l->msg,rbuf[i],&done,&killoff); } _ret:; } /* * Called by the parent when the time passes parent_timer. This is * here to prune unnecessary SMTP listeners: if we have had at least * two listeners' worth of free client slots over the whole of the * last interval, we tell one to go away. */ static void parent_timeout(void) { parent_timer += PARENT_TIMER_INC; if (client_minfree >= 2*CLIENTS_PER_PROCESS) stop_a_listener(); client_minfree = CLIENTS_PER_PROCESS * 10; } /* * The parent process's main loop. Check for SIGUSR1, reap dead * children, build a list of SMTP workers, do the poll...nothing * particularly noteworthy. */ static int parent_tick(void) { LISTENER *l; LISTENER **lp; int fcs; int n; struct timeval now; #ifdef DOSTATS int dnslpx; #endif fflush(0); if (got_usr1) { FILE *f; got_usr1 = 0; f = fopen_pidprefix(stdout); dump_parent_state(f); fclose(f); return(0); } while (1) { pid_t dead; int status; dead = wait3(&status,WNOHANG,0); if (dead <= 0) break; if (WIFEXITED(status)) { if (WEXITSTATUS(status) == 0) { printf("[%d] child %d done\n",mypid(),(int)dead); } else { nonfatal("child %d: exit %d",(int)dead,WEXITSTATUS(status)); } } else if (WIFSIGNALED(status)) { nonfatal("child %d: %s%s",(int)dead,strsignal(WTERMSIG(status)),WCOREDUMP(status)?" (core dumped)":""); } else { nonfatal("child %d: undecodable status %#x",(int)dead,status); } } gettimeofday(&now,0); if (now.tv_sec >= parent_timer) { parent_timeout(); return(0); } npfds = 0; #ifdef DOSTATS dnslpx = add_pfd(dnsl_data,POLLIN|POLLRDNORM); #endif fcs = 0; for (lp=&listeners;(l=*lp);) { switch (l->state) { case LS_LISTENING: fcs += l->fcs; break; case LS_DEAD: close(l->pipe); *lp = l->link; free(l); continue; break; } l->px = add_pfd(l->pipe,POLLIN|POLLRDNORM); lp = &l->link; } if (fcs < MIN_CLIENT_SLOTS) return(make_new_listener()); if (fcs < client_minfree) client_minfree = fcs; n = poll(pfds,npfds,((parent_timer-now.tv_sec)*1000)+100-(now.tv_usec/1000)); if (n < 0) { if (errno == EINTR) return(0); fatal("poll: %s",strerror(errno)); } if (n == 0) return(0); #ifdef DOSTATS if (pfds[dnslpx].revents) dnsl_stats(); #endif for (l=listeners;l;l=l->link) if (pfds[l->px].revents) listener_read(l); return(0); } /* * Initialize global parent state. */ static void parent_init(void) { struct timeval tv; client_minfree = 0; gettimeofday(&tv,0); parent_timer = tv.tv_sec + PARENT_TIMER_INC - (tv.tv_sec % PARENT_TIMER_INC); logging = LOG_OFF; banner_sleep = DEF_SLEEP; } /* * main(). Fairly boring. The only thing of note here is that all * processes' main loops are here. This is an attempt to keep the * stack clean, largely for aesthetics. */ int main(void); int main(void) { setup_usr1(); ignore_pipe(); stats_init(); gethostname(&myhostname[0],sizeof(myhostname)); printf("[%d] startup\n",mypid()); if (run_dnsl()) { dnsl_init(); setproctitle("DNSL overseer"); while (! dnsl_tick()) ; while (! dnslkid_tick()) ; exit(0); } listeners = 0; setup_accs(); parent_init(); setproctitle("parent"); while (! parent_tick()) ; while (1) listener_tick() ; }