diff options
Diffstat (limited to 'src/qmail-remote')
| -rw-r--r-- | src/qmail-remote/deps-exe/qmail-remote | 6 | ||||
| -rw-r--r-- | src/qmail-remote/deps-lib/qmailr | 6 | ||||
| -rw-r--r-- | src/qmail-remote/dns.c | 315 | ||||
| -rw-r--r-- | src/qmail-remote/qmail-remote.c | 86 | ||||
| -rw-r--r-- | src/qmail-remote/qmail-remote.h | 46 | ||||
| -rw-r--r-- | src/qmail-remote/qmailr.h | 66 | ||||
| -rw-r--r-- | src/qmail-remote/qmailr_control.c | 82 | ||||
| -rw-r--r-- | src/qmail-remote/qmailr_error.c | 38 | ||||
| -rw-r--r-- | src/qmail-remote/qmailr_tcpto.c | 157 | ||||
| -rw-r--r-- | src/qmail-remote/qmailr_tls.c | 47 | ||||
| -rw-r--r-- | src/qmail-remote/qmailr_utils.c | 60 | ||||
| -rw-r--r-- | src/qmail-remote/smtproutes.c | 240 |
12 files changed, 1149 insertions, 0 deletions
diff --git a/src/qmail-remote/deps-exe/qmail-remote b/src/qmail-remote/deps-exe/qmail-remote new file mode 100644 index 0000000..4484f9b --- /dev/null +++ b/src/qmail-remote/deps-exe/qmail-remote @@ -0,0 +1,6 @@ +dns.o +smtproutes.o +libqmailr.a.xyzzy +-lskadns +-ls6dns +-lskarnet diff --git a/src/qmail-remote/deps-lib/qmailr b/src/qmail-remote/deps-lib/qmailr new file mode 100644 index 0000000..c1095ed --- /dev/null +++ b/src/qmail-remote/deps-lib/qmailr @@ -0,0 +1,6 @@ +qmailr_control.o +qmailr_error.o +qmailr_tcpto.o +qmailr_tls.o +qmailr_utils.o +-lskarnet diff --git a/src/qmail-remote/dns.c b/src/qmail-remote/dns.c new file mode 100644 index 0000000..4649fc7 --- /dev/null +++ b/src/qmail-remote/dns.c @@ -0,0 +1,315 @@ +/* ISC license. */ + +#include <string.h> +#include <stdint.h> +#include <limits.h> +#include <stdlib.h> +#include <errno.h> + +#include <skalibs/tai.h> +#include <skalibs/stralloc.h> +#include <skalibs/genalloc.h> +#include <skalibs/iopause.h> +#include <skalibs/ip46.h> +#include <skalibs/random.h> + +#include <s6-dns/s6dns.h> +#include <s6-dns/skadns.h> + +#include "qmailr.h" +#include "qmail-remote.h" + +typedef struct cnameinfo_s cnameinfo, *cnameinfo_ref ; +struct cnameinfo_s +{ + stralloc sa ; + size_t atpos ; + uint16_t id ; + uint16_t count ; +} ; + +typedef struct mxipinfo_s mxipinfo, mxipinfo_ref ; +struct mxipinfo_s +{ + stralloc ip4 ; + stralloc ip6 ; + uint16_t id4 ; + uint16_t id6 ; +} ; +#define MXIPINFO_ZERO { .ip4 = STRALLOC_ZERO, .ip6 = STRALLOC_ZERO, .id4 = UINT16_MAX, .id6 = UINT16_MAX } + +static int mx_cmp (void const *a, void const *b) +{ + s6dns_message_rr_mx_t const *aa = a ; + s6dns_message_rr_mx_t const *bb = b ; + return aa->preference < bb-> preference ? -1 : aa->preference > bb->preference ; +} + +static unsigned int use_host_as_mx (skadns_t *a, char const *host, genalloc *mxip, tain const *deadline) +{ + unsigned int newreqs = 0 ; + mxipinfo info = MXIPINFO_ZERO ; + s6dns_domain_t q ; + if (!s6dns_domain_fromstring_noqualify_encode(&q, host, strlen(host))) + qmailr_tempsys("Unable to DNS-encode host domain") ; + if (!skadns_send_g(a, &info.id4, &q, S6DNS_T_A, deadline, deadline)) + qmailr_tempsys("Unable to send A DNS query") ; + newreqs++ ; +#ifdef SKALIBS_IPV6_ENABLED + if (!skadns_send_g(a, &info.id6, &q, S6DNS_T_AAAA, deadline, deadline)) + qmailr_tempsys("Unable to send AAAA DNS query") ; + newreqs++ ; +#endif + if (!genalloc_catb(mxipinfo, mxip, &info, 1)) dienomem() ; + return newreqs ; +} + + /* + The point of this monster here is to do all the DNS resolutions in parallel, + to avoid compounding network latency. One of the many things that could never + be done by patching qmail-remote. + 1 sender + n-1 recipients are given in eaddr. + - loop around CNAME until we get the canonical name, for the n eaddrs + - either lookup the MX for the host then find all the A and AAAAs of all the MXes, + or get the A and AAAAs of the host directly (if smtproutes) + - do not keep the As and AAAAs listed in ipme + - sort the set of IPs by MX preference + When done, quote all the boxnames in eaddr. + Shove everything in storage and return the indices: + in eaddrpos for sender+recipients, in mxipind for the IPs to connect to. + + Also, fuck DNS for requiring so many small allocations and data copies. + + Also, fuck DNS. + */ + +void dns_stuff (char const *host, char const *const *eaddr, unsigned int n, size_t *eaddrpos, genalloc *mxipind, stralloc *storage, unsigned int timeoutdns, char const *ipme4, unsigned int n4, char const *ipme6, unsigned int n6, uint32_t flags) +{ + skadns_t a = SKADNS_ZERO ; + genalloc mxipi = GENALLOC_ZERO ; /* mxipinfo */ + tain deadline ; + unsigned int pending = 0 ; + uint16_t mxn = 0 ; + uint16_t mxid = UINT16_MAX ; + cnameinfo cnames[n] ; + + tain_addsec_g(&deadline, timeoutdns) ; + if (!skadns_startf_g(&a, &deadline)) + qmailr_tempsys("Unable to start asynchronous DNS helper") ; + + for (unsigned int i = 0 ; i < n ; i++) + { + char const *at = strrchr(eaddr[i], '@') ; + cnames[i].sa = stralloc_zero ; + if (at) + { + s6dns_domain_t q ; + size_t len = strlen(at+1) ; + cnames[i].atpos = at - eaddr[i] ; + if (!stralloc_catb(&cnames[i].sa, at+1, len)) dienomem() ; + if (!s6dns_domain_fromstring_noqualify_encode(&q, at+1, len)) + qmailr_tempsys("Unable to DNS-encode recipient domain") ; + if (!skadns_send_g(&a, &cnames[i].id, &q, S6DNS_T_CNAME, &deadline, &deadline)) + qmailr_tempsys("Unable to send CNAME DNS query") ; + cnames[i].count = 1 ; + pending++ ; + } + else + { + cnames[i].id = UINT16_MAX ; + cnames[i].count = 0 ; + cnames[i].atpos = strlen(eaddr[i]) ; + } + } + + if (flags & 1) + { + s6dns_domain_t q ; + if (!s6dns_domain_fromstring_noqualify_encode(&q, host, strlen(host))) + qmailr_tempsys("Unable to DNS-encode host domain") ; + if (!skadns_send_g(&a, &mxid, &q, S6DNS_T_MX, &deadline, &deadline)) + qmailr_tempsys("Unable to send MX DNS query") ; + pending++ ; + } + else + { + mxn = 1 ; + pending += use_host_as_mx(&a, host, &mxipi, &deadline) ; + } + + while (pending) + { + uint16_t *ids ; + iopause_fd x = { .fd = skadns_fd(&a), .events = IOPAUSE_READ } ; + int r = iopause_g(&x, 1, &deadline) ; + if (r == -1) qmailr_tempsys("Unable to iopause") ; + if (!r) qmailr_tempsys("Timed out waiting for DNS") ; + r = skadns_update(&a) ; + if (r == -1) qmailr_tempsys("Unable to read DNS answers") ; + ids = genalloc_s(uint16_t, &a.list) ; + for (size_t j = 0 ; j < genalloc_len(uint16_t, &a.list) ; j++) + { + char const *packet = skadns_packet(&a, ids[j]) ; + uint16_t packetlen = skadns_packetlen(&a, ids[j]) ; + if (!packet) qmailr_tempsys("DNS packet reading error") ; + + if (ids[j] == mxid) /* return from MX query */ + { + s6dns_message_header_t h ; + genalloc mxes = GENALLOC_ZERO ; /* s6dns_message_rr_mx_t */ + r = s6dns_message_parse(&h, packet, packetlen, &s6dns_message_parse_answer_mx, &mxes) ; + if (r == -1) qmailr_tempsys("DNS packet parsing error") ; + if (!r) + { + if (errno == EBUSY || errno == EIO) qmailr_temp("Temporary DNS error while resolving MX") ; + else qmailr_perm("DNS CNAME resolution error") ; + } + skadns_release(&a, ids[j]) ; + pending-- ; + mxid = UINT16_MAX ; + if (r >= 2) /* we have MXes, ask for their IPs */ + { + s6dns_message_rr_mx_t *mxs = genalloc_s(s6dns_message_rr_mx_t, &mxes) ; + mxn = genalloc_len(s6dns_message_rr_mx_t, &mxes) ; + if (!genalloc_readyplus(mxipinfo, &mxipi, mxn)) dienomem() ; + qsort(mxs, mxn, sizeof(s6dns_message_rr_mx_t), &mx_cmp) ; + for (unsigned int i = 0 ; i < mxn ; i++) + { + mxipinfo *p = genalloc_s(mxipinfo, &mxipi) + i ; + p->ip4 = p->ip6 = stralloc_zero ; + if (!skadns_send_g(&a, &p->id4, &mxs[i].exchange, S6DNS_T_A, &deadline, &deadline)) + qmailr_tempsys("Unable to send A DNS query") ; + pending++ ; +#ifdef SKALIBS_IPV6_ENABLED + if (!skadns_send_g(&a, &p->id6, &mxs[i].exchange, S6DNS_T_AAAA, &deadline, &deadline)) + qmailr_tempsys("Unable to send AAAA DNS query") ; + pending++ ; +#endif + } + genalloc_free(s6dns_message_rr_mx_t, &mxes) ; + } + else + { + mxn = 1 ; + pending += use_host_as_mx(&a, host, &mxipi, &deadline) ; + } + continue ; + } + + for (unsigned int i = 0 ; i < n ; i++) if (ids[j] == cnames[i].id) /* return from CNAME query */ + { + s6dns_message_header_t h ; + s6dns_dpag_t dlist = { .ds = GENALLOC_ZERO, .rtype = S6DNS_T_CNAME } ; + r = s6dns_message_parse(&h, packet, packetlen, &s6dns_message_parse_answer_domain, &dlist) ; + if (r == -1) qmailr_tempsys("DNS packet parsing error") ; + if (!r) + { + if (errno == EBUSY || errno == EIO) qmailr_temp("Temporary DNS error while resolving CNAME") ; + else qmailr_perm("DNS CNAME resolution error") ; + } + skadns_release(&a, ids[j]) ; + pending-- ; + if (r >= 2) /* it's a CNAME, loop on it */ + { + s6dns_domain_t *domain = genalloc_s(s6dns_domain_t, &dlist.ds) ; + if (cnames[i].count++ >= 100) qmailr_temp("DNS CNAME loop") ; + if (!skadns_send_g(&a, &cnames[i].id, domain, S6DNS_T_CNAME, &deadline, &deadline)) + qmailr_tempsys("Unable to send CNAME DNS query") ; + pending++ ; + if (!stralloc_ready(&cnames[i].sa, 256)) dienomem() ; + s6dns_domain_decode(domain) ; + cnames[i].sa.len = s6dns_domain_tostring(cnames[i].sa.s, 256, domain) ; + genalloc_free(s6dns_domain_t, &dlist.ds) ; + } + else cnames[i].id = UINT16_MAX ; /* that's the canonical host in cnames[i].sa */ + continue ; + } + + for (unsigned int i = 0 ; i < mxn ; i++) + { + mxipinfo *p = genalloc_s(mxipinfo, &mxipi) + i ; + if (ids[j] == p->id4) + { + s6dns_message_header_t h ; + r = s6dns_message_parse(&h, packet, packetlen, &s6dns_message_parse_answer_a, &p->ip4) ; + if (r == -1) qmailr_tempsys("DNS packet parsing error") ; + if (!r) + { + if (errno == EBUSY || errno == EIO) qmailr_temp("Temporary DNS error while resolving A") ; + else qmailr_perm("DNS A resolution error") ; + } + skadns_release(&a, ids[j]) ; + pending-- ; + p->id4 = UINT16_MAX ; + for (unsigned int k = 0 ; k < p->ip4.len ; k += 4) + { + if (bsearch(p->ip4.s + k, ipme4, n4, 4, &qmailr_memcmp4)) + { + memmove(p->ip4.s + k, p->ip4.s + p->ip4.len - 4, 4) ; + p->ip4.len -= 4 ; + k -= 4 ; + } + } + } +#ifdef SKALIBS_IPV6_ENABLED + else if (ids[j] == p->id6) + { + s6dns_message_header_t h ; + r = s6dns_message_parse(&h, packet, packetlen, &s6dns_message_parse_answer_aaaa, &p->ip6) ; + if (r == -1) qmailr_tempsys("DNS packet parsing error") ; + if (!r) + { + if (errno == EBUSY || errno == EIO) qmailr_temp("Temporary DNS error while resolving AAAA") ; + else qmailr_perm("DNS AAAA resolution error") ; + } + skadns_release(&a, ids[j]) ; + pending-- ; + p->id6 = UINT16_MAX ; + for (unsigned int k = 0 ; k < p->ip6.len ; k += 16) + { + if (bsearch(p->ip6.s + k, ipme6, n6, 16, &qmailr_memcmp16)) + { + memmove(p->ip6.s + k, p->ip6.s + p->ip6.len - 16, 16) ; + p->ip6.len -= 16 ; + k -= 16 ; + } + } + } +#endif + } + } + } + skadns_end(&a) ; /* we done buddy */ + + for (unsigned int i = 0 ; i < n ; i++) + { + eaddrpos[i] = storage->len ; + if (!qmailr_box_encode(eaddr[i], cnames[i].atpos, storage)) dienomem() ; + if (cnames[i].count) + { + if (!stralloc_catb(storage, "@", 1)) dienomem() ; + if (!stralloc_catb(storage, cnames[i].sa.s, cnames[i].sa.len)) dienomem() ; + stralloc_free(&cnames[i].sa) ; + } + if (!stralloc_0(storage)) dienomem() ; + } + + if (!genalloc_readyplus(mxip, mxipind, mxn)) dienomem() ; + for (unsigned int i = 0 ; i < mxn ; i++) + { + mxip data ; + mxipinfo *p = genalloc_s(mxipinfo, &mxipi) + i ; + data.n4 = p->ip4.len >> 2 ; + data.pos4 = storage->len ; + if (!stralloc_catb(storage, p->ip4.s, p->ip4.len)) dienomem() ; + stralloc_free(&p->ip4) ; +#ifdef SKALIBS_IPV6_ENABLED + data.n6 = p->ip6.len >> 4 ; + data.pos6 = storage->len ; + if (!stralloc_catb(storage, p->ip6.s, p->ip6.len)) dienomem() ; + stralloc_free(&p->ip6) ; + genalloc_catb(mxip, mxipind, &data, 1) ; +#endif + } + genalloc_free(mxipinfo, &mxipi) ; +} diff --git a/src/qmail-remote/qmail-remote.c b/src/qmail-remote/qmail-remote.c new file mode 100644 index 0000000..870c797 --- /dev/null +++ b/src/qmail-remote/qmail-remote.c @@ -0,0 +1,86 @@ +/* ISC license. */ + +#include <string.h> +#include <stdint.h> +#include <unistd.h> + +#include <skalibs/cdb.h> +#include <skalibs/stralloc.h> +#include <skalibs/sig.h> +#include <skalibs/tai.h> +#include <skalibs/ip46.h> + +#include <smtpd-starttls-proxy/config.h> +#include "qmailr.h" +#include "qmail-remote.h" + +#define dieusage() qmailr_perm("qmail-remote was invoked improperly") + +int main (int argc, char const *const *argv) +{ + stralloc storage = STRALLOC_ZERO ; + stralloc ipme4 = STRALLOC_ZERO ; + stralloc ipme6 = STRALLOC_ZERO ; + qmailr_tls qt = QMAILR_TLS_ZERO ; + smtproutes routes = SMTPROUTES_ZERO ; + unsigned int timeoutconnect = 60, timeoutremote = 1200 ; + char const *host ; + size_t mepos, helopos, hostpos = 0 ; + uint16_t port = 25 ; + int r ; + + if (argc-- < 4) dieusage() ; + argv++ ; + if (chdir(SMTPD_STARTTLS_PROXY_QMAIL_HOME) == -1) qmailr_temp("Unable to chdir to " SMTPD_STARTTLS_PROXY_QMAIL_HOME) ; + if (sig_altignore(SIGPIPE) == -1) qmailr_tempsys("Unable to ignore SIGPIPE") ; + host = *argv++ ; argc-- ; + tain_now_set_stopwatch_g() ; + + + /* init control */ + + r = qmailr_control_read("control/me", &storage, &mepos) ; + if (r == -1) qmailr_tempsys("Unable to read control/me") ; + else if (!r) qmailr_temp("Invalid control/me") ; + + r = qmailr_control_read("control/helohost", &storage, &helopos) ; + if (r == -1) qmailr_tempsys("Unable to read control/helohost") ; + else if (!r) helopos = mepos ; + + r = qmailr_control_readint("control/timeoutconnect", &timeoutconnect, &storage) ; + if (r == -1) qmailr_tempsys("Unable to read control/timeoutconnect") ; + r = qmailr_control_readint("control/timeoutremote", &timeoutremote, &storage) ; + if (r == -1) qmailr_tempsys("Unable to read control/timeoutremote") ; + + if (!qmailr_control_readiplist("control/ipme", &ipme4, &ipme6)) + qmailr_tempsys("Unable to read control/ipme") ; + stralloc_shrink(&ipme4) ; + stralloc_shrink(&ipme6) ; + qsort(ipme4.s, ipme4.len >> 2, 4, &qmailr_memcmp4) ; + qsort(ipme6.s, ipme6.len >> 4, 16, &qmailr_memcmp16) ; + + if (!qmailr_tls_init(&qt, &storage)) + qmailr_tempsys("Unable to read TLS control files") ; + + if (smtproutes_init(&routes)) + { + if (!smtproutes_match(&routes, host, &storage, &hostpos, &port)) + { + size_t hostlen = strlen(host) ; + for (size_t i = 0 ; i < hostlen ; i++) if (host[i] == '.') + if (smtproutes_match(&routes, argv[1], &storage, &hostpos, &port)) break ; + if (!hostpos) smtproutes_match(&routes, "", &storage, &hostpos, &port) ; + } + smtproutes_free(&routes) ; + } + + { + genalloc mxipind = GENALLOC_ZERO ; + size_t eaddrpos[argc] ; + dns_stuff(hostpos ? storage.s + hostpos : host, argv, argc, eaddrpos, &mxipind, &storage, timeoutconnect, ipme4.s, ipme4.len >> 2, ipme6.s, ipme6.len >> 4, !hostpos) ; + + } + + + _exit(0) ; +} diff --git a/src/qmail-remote/qmail-remote.h b/src/qmail-remote/qmail-remote.h new file mode 100644 index 0000000..8059344 --- /dev/null +++ b/src/qmail-remote/qmail-remote.h @@ -0,0 +1,46 @@ +/* ISC license. */ + +#ifndef QMAIL_REMOTE_H +#define QMAIL_REMOTE_H + +#include <stddef.h> +#include <stdint.h> + +#include <skalibs/cdb.h> +#include <skalibs/stralloc.h> +#include <skalibs/genalloc.h> + +#include "qmailr.h" + +#define dienomem() qmailr_tempsys("Unable to grow stralloc") + + +/* dns */ + +typedef struct mxip_s mxip, *mxip_ref ; +struct mxip_s +{ + size_t pos4 ; + size_t pos6 ; + uint16_t n4 ; + uint16_t n6 ; +} ; +#define MXIP_ZERO { 0 } + +extern void dns_stuff (char const *, char const *const *, unsigned int, size_t *, genalloc *, stralloc *, unsigned int, char const *, unsigned int, char const *, unsigned int, uint32_t) ; + + +/* smtproutes */ + +typedef struct smtproutes_s smtproutes ; +struct smtproutes_s +{ + cdb map ; +} ; +#define SMTPROUTES_ZERO { .map = CDB_ZERO } + +extern int smtproutes_init (smtproutes *) ; +extern int smtproutes_match (smtproutes const *, char const *, stralloc *, size_t *, uint16_t *) ; +extern void smtproutes_free (smtproutes *) ; + +#endif diff --git a/src/qmail-remote/qmailr.h b/src/qmail-remote/qmailr.h new file mode 100644 index 0000000..08c4f41 --- /dev/null +++ b/src/qmail-remote/qmailr.h @@ -0,0 +1,66 @@ +/* ISC license. */ + +#ifndef QMAILR_H +#define QMAILR_H + +#include <stddef.h> +#include <stdint.h> + +#include <skalibs/gccattributes.h> +#include <skalibs/tai.h> +#include <skalibs/stralloc.h> + + +/* qmailr_error */ + +extern void qmailr_diev (int, char const *const *, unsigned int) gccattr_noreturn ; +extern void qmailr_dievsys (char const *const *, unsigned int) gccattr_noreturn ; +extern void qmailr_die (int, char const *) gccattr_noreturn ; +extern void qmailr_diesys (char const *) gccattr_noreturn ; + +#define qmailr_temp(s) qmailr_die(0, (s)) +#define qmailr_tempv(v, n) qmailr_diev(0, (v), n) +#define qmailr_tempsys(s) qmailr_diesys(s) +#define qmailr_tempvsys(v, n) qmailr_dievsys(v, n) +#define qmailr_perm(s) qmailr_die(1, (s)) +#define qmailr_permv(v, n) qmailr_diev(1, (v), n) + + +/* qmailr_utils */ + +extern int qmailr_memcmp4 (void const *, void const *) ; +extern int qmailr_memcmp16 (void const *, void const *) ; +extern int qmailr_box_encode (char const *, size_t, stralloc *) ; + + +/* qmailr_tcpto */ + +extern int qmailr_tcpto_match (char const *, int) ; +extern int qmailr_tcpto_update (char const *, int, int) ; + + +/* qmailr_control */ + +extern int qmailr_control_read (char const *, stralloc *, size_t *) ; +extern int qmailr_control_readint (char const *file, unsigned int *, stralloc *) ; +extern int qmailr_control_readiplist (char const *, stralloc *, stralloc *) ; + + +/* qmailr_tls */ + +typedef struct qmailr_tls_s qmailr_tls, *qmailr_tls_ref ; +struct qmailr_tls_s +{ + size_t tapos ; + size_t certpos ; + size_t keypos ; + uint8_t strictness : 2 ; + uint8_t flagtls : 1 ; + uint8_t flagtadir : 1 ; + uint8_t flagclientcert : 1 ; +} ; +#define QMAILR_TLS_ZERO { 0 } + +extern int qmailr_tls_init (qmailr_tls *, stralloc *) ; + +#endif diff --git a/src/qmail-remote/qmailr_control.c b/src/qmail-remote/qmailr_control.c new file mode 100644 index 0000000..706b0d9 --- /dev/null +++ b/src/qmail-remote/qmailr_control.c @@ -0,0 +1,82 @@ +/* ISC license. */ + +#include <stddef.h> +#include <errno.h> + +#include <skalibs/types.h> +#include <skalibs/allreadwrite.h> +#include <skalibs/buffer.h> +#include <skalibs/fmtscan.h> +#include <skalibs/stralloc.h> +#include <skalibs/djbunix.h> +#include <skalibs/skamisc.h> + +#include "qmailr.h" + +#include <skalibs/posixishard.h> + +int qmailr_control_read (char const *file, stralloc *sa, size_t *pos) +{ + int fd = openc_readb(file) ; + if (fd == -1) return errno == ENOENT ? (errno = 0, 0) : -1 ; + + char buf[4096] ; + size_t r = allread(fd, buf, 4096) ; + fd_close(fd) ; + if (r == 4096) return (errno = ENAMETOOLONG, -1) ; + if (!r) return 0 ; + if (buf[r-1] == '\n') r-- ; + if (!r) return 0 ; + if (!stralloc_readyplus(sa, r+1)) return -1 ; + *pos = sa->len ; + stralloc_catb(sa, buf, r) ; stralloc_0(sa) ; + return 1 ; +} + +int qmailr_control_readint (char const *file, unsigned int *x, stralloc *sa) +{ + size_t pos ; + int r = qmailr_control_read(file, sa, &pos) ; + if (r <= 0) return r ; + sa->len = pos ; + if (!uint0_scan(sa->s + sa->len, x)) return (errno = EPROTO, 0) ; + return 1 ; +} + +int qmailr_control_readiplist (char const *file, stralloc *ip4, stralloc *ip6) +{ + int fd = openc_readb(file) ; + if (fd == -1) return errno == ENOENT ? (errno = 0, 0) : -1 ; + + size_t pos4 = ip4->len, pos6 = ip6->len ; + char buf[4096] ; + buffer b = BUFFER_INIT(&buffer_read, fd, buf, 4096) ; + + for (;;) + { + char line[128] ; + char ip[16] ; + size_t len = 0 ; + int r = getlnmax(&b, line, 127, &len, '\n') ; + if (r == -1) goto err ; + if (!r) break ; + if (!len) continue ; + if (line[len-1] != '\n') line[len++] = '\n' ; + if (ip6_scan(line, ip) == len-1) + { + if (!stralloc_catb(ip6, ip, 16)) goto err ; + } + else if (ip4_scan(line, ip) == len-1) + { + if (!stralloc_catb(ip4, ip, 4)) goto err ; + } + else goto errinval ; + } + return 1 ; + + errinval: + errno = EINVAL ; + err: + ip4->len = pos4 ; ip6->len = pos6 ; + return 0 ; +} diff --git a/src/qmail-remote/qmailr_error.c b/src/qmail-remote/qmailr_error.c new file mode 100644 index 0000000..fcdce54 --- /dev/null +++ b/src/qmail-remote/qmailr_error.c @@ -0,0 +1,38 @@ +/* ISC license. */ + +#include <string.h> +#include <unistd.h> +#include <errno.h> + +#include <skalibs/buffer.h> + +#include <smtpd-starttls-proxy/config.h> + +void qmailr_diev (int permanent, char const *const *v, unsigned int n) +{ + buffer_put(buffer_1small, permanent ? "D" : "Z", 1) ; + while (n--) buffer_puts(buffer_1small, *v++) ; + buffer_putflush(buffer_1small, "\n", 2) ; + _exit(0) ; +} + +void qmailr_dievsys (char const *const *v, unsigned int n) +{ + char const *se = strerror(errno) ; + buffer_put(buffer_1small, "Z", 1) ; + while (n--) buffer_puts(buffer_1small, *v++) ; + buffer_put(buffer_1small, ": ", 2) ; + buffer_puts(buffer_1small, se) ; + buffer_putflush(buffer_1small, "\n", 2) ; + _exit(0) ; +} + +void qmailr_die (int permanent, char const *msg) +{ + qmailr_diev(permanent, &msg, 1) ; +} + +void qmailr_diesys (char const *msg) +{ + qmailr_dievsys(&msg, 1) ; +} diff --git a/src/qmail-remote/qmailr_tcpto.c b/src/qmail-remote/qmailr_tcpto.c new file mode 100644 index 0000000..f90d1d0 --- /dev/null +++ b/src/qmail-remote/qmailr_tcpto.c @@ -0,0 +1,157 @@ +/* ISC license. */ + +#include <stdint.h> +#include <string.h> +#include <unistd.h> +#include <stdlib.h> +#include <errno.h> + +#include <skalibs/uint32.h> +#include <skalibs/uint64.h> +#include <skalibs/allreadwrite.h> +#include <skalibs/cdb.h> +#include <skalibs/tai.h> +#include <skalibs/djbunix.h> + +#include <smtpd-starttls-proxy/config.h> +#include "qmailr.h" + +#include <skalibs/posixishard.h> + + +/* + tcpto implementation, should be compatible with qmail-tcpto. + Assumes the 4 unused bytes at the end of a record are there to + accommodate 64-bit time_t. Which we use. But qmail-tcpto does + not, so you should patch that before 2038. + Has ipv6 support, storing v6 records in a different file. + Unlike qmail's tcpto, we assume the records are sorted by IP. + This makes it easy to look for a record with bsearch. We + keep records sorted with every modification, and we aggressively + cut empty ones from the file. + For the match function, not sure what is faster between mmapping and + simple reading. Currently we mmap to save private/dirty RAM, but + that holds the lock longer; it should be ok because we switched to + a shared lock for this (unsure why djb didn't). +*/ + +int qmailr_tcpto_match (char const *ip, int is6) +{ + char const *file = is6 ? SMTPD_STARTTLS_PROXY_QMAIL_HOME "/run/qmail-remote/tcpto6" : SMTPD_STARTTLS_PROXY_QMAIL_HOME "/queue/lock/tcpto" ; + uint32_t iplen = is6 ? 16 : 4 ; + uint32_t width = iplen + 12 ; + int r = 0 ; + char const *p ; + cdb c ; /* XXX: not a cdb, we're just using the mmap wrapper */ + int fd = openc_read(file) ; + + if (fd == -1) return -1 ; + if (fd_lock(fd, 0, 0) == -1) goto err ; + if (!cdb_init_fromfd(&c, fd)) goto err ; + if (c.size % width) goto errproto ; + p = bsearch(ip, c.map, c.size / width, width, is6 ? &qmailr_memcmp16 : &qmailr_memcmp4) ; + if (p) + { + if (p[iplen] >= 2) + { + tai when ; + uint64_t x ; + uint64_unpack(p + iplen + 4, &x) ; + tai_u64(&when, x) ; + tai_sub(&when, tain_secp(&STAMP), &when) ; + r = tai_sec(&when) < ((60 + (getpid() & 31)) << 6) ; /* don't ask me, ask djb */ + } + } + cdb_free(&c) ; + fd_close(fd) ; + return r ; + + errproto: + errno = EPROTO ; + err: + fd_close(fd) ; + return -1 ; +} + +int qmailr_tcpto_update (char const *ip, int is6, int problem) +{ + char const *file = is6 ? SMTPD_STARTTLS_PROXY_QMAIL_HOME "/run/qmail-remote/tcpto6" : SMTPD_STARTTLS_PROXY_QMAIL_HOME "/queue/lock/tcpto" ; + uint32_t iplen = is6 ? 16 : 4 ; + uint32_t width = iplen + 12 ; + uint32_t n ; + char *p = 0 ; + struct stat st ; + int fdr ; + int fdw = openc_create(file) ; + + if (fdw == -1) return 0 ; + if (fd_lock(fdw, 1, 0) == -1) goto err ; + fdr = openc_read(file) ; + if (fdr == -1) goto err ; + if (fstat(fdr, &st) == -1) goto err0 ; + if (st.st_size % width) goto errproto ; + n = st.st_size / width ; + + { + char buf[(n+1) * width] ; /* relax, it won't bite */ + if (n) + { + if (allread(fdr, buf, st.st_size) < st.st_size) goto err0 ; + memset(buf + st.st_size, 0, width) ; + p = bsearch(ip, buf, n, width, is6 ? &qmailr_memcmp16 : &qmailr_memcmp4) ; + if (p) + { + if (problem) + { + tai when ; + uint64_t x ; + uint64_unpack(p + iplen + 4, &x) ; + tai_u64(&when, x) ; + tai_sub(&when, tain_secp(&STAMP), &when) ; + if (tai_sec(&when) < 120) p = 0 ; + else + { + if (++p[iplen] > 10) p[iplen] = 10 ; + x = tai_sec(tain_secp(&STAMP)) - TAI_MAGIC ; + uint64_pack(p + iplen + 4, x) ; + } + } + else p[iplen] = 0 ; + } + } + else if (problem) + { + uint64_t x = tai_sec(tain_secp(&STAMP)) - TAI_MAGIC ; + p = buf + n++ * width ; + memcpy(p, ip, iplen) ; + p[iplen] = 1 ; + memset(p + iplen + 1, 0, 3) ; + uint64_pack(p + iplen + 4, x) ; + } + fd_close(fdr) ; + + if (p) + { + for (uint32_t i = 0 ; i < n ; i++) + if (!buf[i * width + iplen]) + memcpy(buf + i * width, buf + --n * width, width) ; + if (n) + { + qsort(buf, n, width, is6 ? &qmailr_memcmp16 : &qmailr_memcmp4) ; + if (allwrite(fdw, buf, n * width) < n * width) goto err ; + } + if (ftruncate(fdw, n * width) == -1) goto err ; + } + } + + fd_close(fdw) ; + return 1 ; + + errproto: + errno = EPROTO ; + err0: + fd_close(fdr) ; + err: + fd_close(fdw) ; + return 0 ; +} diff --git a/src/qmail-remote/qmailr_tls.c b/src/qmail-remote/qmailr_tls.c new file mode 100644 index 0000000..6e09a82 --- /dev/null +++ b/src/qmail-remote/qmailr_tls.c @@ -0,0 +1,47 @@ +/* ISC license. */ + +#include <stddef.h> + +#include <smtpd-starttls-proxy/config.h> +#include "qmailr.h" + +int qmailr_tls_init (qmailr_tls *qt, stralloc *sa) +{ + static char const *tafile = SMTPD_STARTTLS_PROXY_QMAIL_HOME "/control/trustanchors" ; + static char const *certfile = SMTPD_STARTTLS_PROXY_QMAIL_HOME "/control/clientcert" ; + static char const *keyfile = SMTPD_STARTTLS_PROXY_QMAIL_HOME "/control/clientkey" ; + static char const *strictfile = SMTPD_STARTTLS_PROXY_QMAIL_HOME "/control/tlsstrictness" ; + + qmailr_tls tmp = QMAILR_TLS_ZERO ; + size_t sabase = sa->len ; + int r = qmailr_control_read(tafile, sa, &tmp.tapos) ; + if (r == -1) return 0 ; + if (r) + { + unsigned int strictness = 0 ; + tmp.flagtls = 1 ; + if (sa->s[sa->len - 2] == '/') + { + sa->s[--sa->len - 1] = 0 ; + tmp.flagtadir = 1 ; + } + r = qmailr_control_read(certfile, sa, &tmp.certpos) ; + if (r == -1) goto err ; + if (r) + { + r = qmailr_control_read(keyfile, sa, &tmp.keypos) ; + if (r == -1) goto err ; + if (r) tmp.flagclientcert = 1 ; + } + r = qmailr_control_readint(strictfile, &strictness, sa) ; + if (r == -1) goto err ; + tmp.strictness = strictness & 3 ; + } + + *qt = tmp ; + return 1 ; + + err: + sa->len = sabase ; + return 0 ; +} diff --git a/src/qmail-remote/qmailr_utils.c b/src/qmail-remote/qmailr_utils.c new file mode 100644 index 0000000..deeff46 --- /dev/null +++ b/src/qmail-remote/qmailr_utils.c @@ -0,0 +1,60 @@ +/* ISC license. */ + +#include <string.h> + +#include <skalibs/bitarray.h> +#include <skalibs/stralloc.h> + +#include "qmailr.h" + +int qmailr_memcmp4 (void const *a, void const *b) +{ + return memcmp(a, b, 4) ; +} + +int qmailr_memcmp16 (void const *a, void const *b) +{ + return memcmp(a, b, 16) ; +} + +static inline int needsquoting (char const *s, size_t len) +{ + static unsigned char const badchar[32] = + { + 0xff, 0xff, 0xff, 0xff, + 0x05, 0x13, 0x00, 0x5c, + 0x01, 0x00, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x80, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + } ; + if (!len) return 1 ; + if (s[0] == '.' || s[len - 1] == '.') return 1 ; + for (size_t i = 0 ; i < len ; i++) + { + if (bitarray_peek(badchar, (unsigned char)s[i])) return 1 ; + if (i + 2 < len && s[i] == '.' && s[i+1] == '.') return 1 ; + } + return 0 ; +} + +int qmailr_box_encode (char const *s, size_t len, stralloc *storage) +{ + if (needsquoting(s, len)) + { + size_t j = storage->len ; + if (!stralloc_readyplus(storage, 2 + (len << 1))) return 0 ; + storage->s[j++] = '"' ; + for (size_t i = 0 ; i < len ; i++) + { + if (strchr("\"\\\r\n", s[i])) storage->s[j++] = '\\' ; + storage->s[j++] = s[i] ; + } + storage->s[j++] = '"' ; + storage->len = j ; + } + else if (!stralloc_catb(storage, s, len)) return 0 ; + return 1 ; +} diff --git a/src/qmail-remote/smtproutes.c b/src/qmail-remote/smtproutes.c new file mode 100644 index 0000000..7467031 --- /dev/null +++ b/src/qmail-remote/smtproutes.c @@ -0,0 +1,240 @@ +/* ISC license. */ + +#include <skalibs/bsdsnowflake.h> + +#include <string.h> +#include <stdint.h> +#include <unistd.h> +#include <stdlib.h> +#include <errno.h> +#include <stdio.h> + +#include <skalibs/stat.h> +#include <skalibs/posixplz.h> +#include <skalibs/uint16.h> +#include <skalibs/buffer.h> +#include <skalibs/cdb.h> +#include <skalibs/cdbmake.h> +#include <skalibs/stralloc.h> +#include <skalibs/djbtime.h> +#include <skalibs/djbunix.h> + +#include <smtpd-starttls-proxy/config.h> +#include "qmailr.h" +#include "qmail-remote.h" + + +/* + qmail-remote uses a "constmap" for smtproutes, which is + basically a cdb in RAM. Every instance of qmail-remote + parses control/smtproutes to make the constmap. + We replace it with a real cdb, stored in the filesystem. + It saves CPU (N-1 instances of qmail-remote use the cdb + directly) and RAM (the cdb is read-only and shared). + The cdb is updated whenever control/smtproutes is newer. + We have to lock around the test to avoid several + concurrent compilations; the current lock feels too big, + the crit section can probably be made smaller, but the + behaviour is safe and avoids retry heuristics. +*/ + +/* + Key to the control/smtproutes parser + [host]:[relay[:port]] + An ip in square brackets is acceptable in host and relay, even ipv6 + + + 0 1 2 3 4 5 6 7 8 9 +st\ev EOF # \n : [ ] 0-9 a-f other special + +0 h n n n +START END COMMENT START RELAY QHOST X HOST HOST HOST X + +1 +COMMENT END COMMENT START COMMENT COMMENT COMMENT COMMENT COMMENT COMMENT COMMENT + +2 n n n +QHOST X X X QHOST X EHOST QHOST QHOST X X + +3 n h n n n +HOST X HOST X RELAY X X HOST HOST HOST X + +4 h +EHOST X X X RELAY X X X X X X + +5 ra n ra r n n n +RELAY END INRELAY START PORT QRELAY X INRELAY INRELAY INRELAY X + +6 n n n +QRELAY X X X QRELAY X ERELAY QRELAY QRELAY X X + +7 ra ra r n n n +INRELAY END X START PORT X X INRELAY INRELAY INRELAY X + +8 ra ra r +ERELAY END X START PORT X X X X X X + +9 pa pa n +PORT END X START X X X PORT X X X + +END=a, X=b + +0x0100 n push character +0x0200 h compute host length +0x0400 r compute relay length +0x0800 p compute port +0x1000 a add route entry +*/ + +static inline uint8_t cclass (char c) +{ + static uint8_t const table[128] = "0999999999299999999999999999999998918889999898786666666666399898877777788888888888888888884958898888888888888888888888888899999" ; + return c & 0x80 ? 9 : table[(uint8_t)c] - '0' ; +} + +static inline char getnext (buffer *b) +{ + char c ; + ssize_t r = buffer_get(b, &c, 1) ; + if (r == -1) qmailr_tempsys("Unable to read from control/smtproutes") ; + return r ? c : 0 ; +} + +static inline void smtproutes_compile (int fdr, int fdw) +{ + static uint16_t const table[10][10] = + { + { 0x000a, 0x0001, 0x0000, 0x0205, 0x0002, 0x000b, 0x0103, 0x0103, 0x0103, 0x000b }, + { 0x000a, 0x0001, 0x0000, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001 }, + { 0x000b, 0x000b, 0x000b, 0x0102, 0x000b, 0x0004, 0x0102, 0x0102, 0x0102, 0x000b }, + { 0x000b, 0x0103, 0x000b, 0x0205, 0x000b, 0x000b, 0x0103, 0x0103, 0x0103, 0x000b }, + { 0x000b, 0x000b, 0x000b, 0x0205, 0x000b, 0x000b, 0x000b, 0x000b, 0x000b, 0x000b }, + { 0x140a, 0x0107, 0x1400, 0x0409, 0x0006, 0x000b, 0x0107, 0x0107, 0x0107, 0x000b }, + { 0x000b, 0x000b, 0x000b, 0x0106, 0x000b, 0x0008, 0x0106, 0x0106, 0x0106, 0x000b }, + { 0x140a, 0x000b, 0x1400, 0x0409, 0x000b, 0x000b, 0x0107, 0x0107, 0x0107, 0x000b }, + { 0x140a, 0x000b, 0x1400, 0x0409, 0x000b, 0x000b, 0x000b, 0x000b, 0x000b, 0x000b }, + { 0x180a, 0x000b, 0x1800, 0x000b, 0x000b, 0x000b, 0x0109, 0x000b, 0x000b, 0x000b } + } ; + cdbmaker cm = CDBMAKER_ZERO ; + stralloc sa = STRALLOC_ZERO ; + char buf[2048] ; + buffer b = BUFFER_INIT(&buffer_read, fdr, buf, 2048) ; + uint32_t relaypos = 0, relayend = 0 ; + uint8_t state = 0 ; + if (!cdbmake_start(&cm, fdw)) qmailr_tempsys("Unable to cdbmake_start") ; + + while (state < 0x0a) + { + char c = getnext(&b) ; + uint16_t val = table[state][cclass(c)] ; + state = val & 0x000f ; + if (val & 0x0100) + { + if (!stralloc_catb(&sa, &c, 1)) qmailr_tempsys("Unable to grow stralloc") ; + } + if (val & 0x0200) + { + relaypos = sa.len + 1 ; + if (!stralloc_catb(&sa, "\0\0\31", 3)) qmailr_tempsys("Unable to grow stralloc") ; + } + if (val & 0x0400) + { + if (!stralloc_0(&sa)) qmailr_tempsys("Unable to grow stralloc") ; + relayend = sa.len ; + } + if (val & 0x0800) + { + uint16_t port ; + if (!stralloc_0(&sa)) qmailr_tempsys("Unable to grow stralloc") ; + if (!uint160_scan(sa.s + relayend, &port)) qmailr_temp("Invalid port in control/smtproutes") ; + uint16_pack_big(sa.s + relaypos, port) ; + } + if (val & 0x0100) + { + if (relaypos > 1 || relayend > 2 + relaypos) + if (!cdbmake_add(&cm, sa.s, relaypos, sa.s + relaypos, relayend - relaypos)) + qmailr_tempsys("Unable to cdbmake_add") ; + sa.len = 0 ; + } + } + if (state != 0x0a) qmailr_temp("Syntax error in control/smtproutes") ; + stralloc_free(&sa) ; + if (!cdbmake_finish(&cm)) qmailr_tempsys("Unable to cdbmake_finish") ; +} + +int smtproutes_init (smtproutes *routes) +{ + static char const *cdbfile = SMTPD_STARTTLS_PROXY_QMAIL_HOME "/run/qmail-remote/smtproutes.cdb" ; + static char const *lckfile = SMTPD_STARTTLS_PROXY_QMAIL_HOME "/run/qmail-remote/smtproutes.lock" ; + static char const *txtfile = SMTPD_STARTTLS_PROXY_QMAIL_HOME "/control/smtproutes" ; + static size_t const cdblen = sizeof(cdbfile) - 1 ; + int fdl = openc_create(lckfile) ; + if (fdl == -1) qmailr_tempsys("Unable to open run/qmail-remote/smtproutes.lock") ; + if (fd_lock(fdl, 1, 0) == -1) qmailr_tempsys("Unable to lock run/qmail-remote/smtproutes.lock") ; + + int fdc = openc_read(cdbfile) ; + if (fdc >= 0) + { + struct stat stc, str ; + if (fstat(fdc, &stc) == -1) qmailr_tempsys("Unable to fstat run/qmail-remote/smtproutes.cdb") ; + if (stat(txtfile, &str) == -1) + { + if (errno != ENOENT) qmailr_tempsys("Unable to fstat control/smtproutes") ; + unlink_void(cdbfile) ; + fd_close(fdc) ; + goto zero ; + } + if (timespec_cmp(&stc.st_mtim, &str.st_mtim) > 0) goto useit ; + fd_close(fdc) ; + } + + int fdr = openc_read(txtfile) ; + if (fdr == -1) + { + if (errno != ENOENT) qmailr_tempsys("Unable to open control/smtproutes") ; + goto zero ; + } + + { + char tmp[cdblen + 8] ; + memcpy(tmp, cdbfile, cdblen) ; + memcpy(tmp + cdblen, ":XXXXXX", 8) ; + fdc = mkstemp(tmp) ; + if (fdc == -1) qmailr_tempsys("Unable to mkstemp") ; + smtproutes_compile(fdr, fdc) ; + if (lseek(fdc, 0, SEEK_SET) == -1) qmailr_tempsys("Unable to lseek") ; + if (fsync(fdc) == -1) qmailr_tempsys("Unable to fsync run/qmail-remote/smtproutes.cdb") ; + fd_close(fdr) ; + if (rename(tmp, cdbfile) == -1) unlink_void(tmp) ; + } + + useit: + if (!cdb_init_fromfd(&routes->map, fdc)) qmailr_tempsys("Unable to mmap run/qmail-remote/smtproutes.cdb") ; + fd_close(fdc) ; + fd_close(fdl) ; + return 1 ; + + zero: + fd_close(fdl) ; + errno = 0 ; + return 0 ; +} + +int smtproutes_match (smtproutes const *routes, char const *s, stralloc *sa, size_t *pos, uint16_t *port) +{ + cdb_data data ; + int r = cdb_find(&routes->map, &data, s, strlen(s)) ; + if (r == -1) qmailr_temp("Invalid run/qmail-remote/smtproutes.cdb") ; + if (!r) return 0 ; + if (data.len < 3) return 0 ; + if (data.s[data.len - 1]) qmailr_temp("Invalid run/qmail-remote/smtproutes.cdb") ; + *pos = sa->len ; + uint16_unpack_big(data.s, port) ; + if (!stralloc_catb(sa, data.s + 2, data.len - 2)) qmailr_tempsys("Unable to grow stralloc") ; + return 1 ; +} + +void smtproutes_free (smtproutes *routes) +{ + cdb_free(&routes->map) ; +} |
