lib_SOURCES = str.c buffer.c common.c epoll.c server.c trie.c file.c rbl.c
tst-trie_SOURCES = tst-trie.c lib.a
+all:
+
include ../mk/common.mk
ARRAY(char)
ARRAY(int)
ARRAY(bool)
+ARRAY(uint16_t)
ARRAY(uint32_t)
PARRAY(void)
void common_sighandler(int sig)
{
switch (sig) {
- case SIGTERM:
+ case SIGTERM:
case SIGINT:
sigint = true;
return;
return;
default:
- err("Killed (got signal %d)...", sig);
+ err("Killed (got signal %d)...", sig);
exit(-1);
}
}
pid = fork();
if (pid < 0) {
return -1;
- }
+ }
if (pid) {
- daemon_process = false;
+ daemon_process = false;
exit(0);
- }
+ }
setsid();
return 0;
int pidfile_open(const char *name)
{
- struct flock lock;
- p_clear(&lock, 1);
- lock.l_type = F_WRLCK;
if (name) {
pidfile = fopen(name, "w");
if (!pidfile)
return -1;
- if (fcntl(fileno(pidfile), F_SETLK, &lock) == -1) {
- crit("program already started");
- fclose(pidfile);
- pidfile = NULL;
- return -1;
- }
- fprintf(pidfile, "%d\n", getpid());
+ fprintf(pidfile, "%d\n", getpid());
return fflush(pidfile);
}
return 0;
static void pidfile_close(void)
{
- struct flock lock;
- p_clear(&lock, 1);
- lock.l_type = F_UNLCK;
if (pidfile) {
- rewind(pidfile);
- ftruncate(fileno(pidfile), 0);
- fcntl(fileno(pidfile), F_SETLK, &lock);
- fclose(pidfile);
+ if (daemon_process) {
+ rewind(pidfile);
+ ftruncate(fileno(pidfile), 0);
+ }
+ fclose(pidfile);
pidfile = NULL;
}
}
int common_setup(const char* pidfilename, bool unsafe, const char* runas_user,
const char* runas_group, bool daemonize)
{
+ if (pidfile_open(pidfilename) < 0) {
+ crit("unable to write pidfile %s", pidfilename);
+ return EXIT_FAILURE;
+ }
+
if (!unsafe && drop_privileges(runas_user, runas_group) < 0) {
crit("unable to drop privileges");
return EXIT_FAILURE;
return EXIT_FAILURE;
}
- if (pidfile_open(pidfilename) < 0) {
- crit("unable to write pidfile %s", pidfilename);
- return EXIT_FAILURE;
- }
-
pidfile_refresh();
return EXIT_SUCCESS;
}
static void common_shutdown(void)
{
- if (daemon_process) {
- info("stopping...");
- }
- pidfile_close();
+ if (daemon_process) {
+ info("stopping...");
+ }
+ pidfile_close();
for (int i = -1; __madexit[i]; i--) {
(*__madexit[i])();
}
*/
if (argc > 1) {
trie = create_trie_from_file(argv[1]);
- trie_inspect(trie, true);
+ trie_inspect(trie, false);
trie_delete(&trie);
}
return 0;
# declare a file to load. If lock is given, the klist is locked into the
# RAM. The weight is a number giving the weight of this blaclist file in the
# score of the IP
+# - rbldns: (no)?lock:weight:filename
+# this is an alias for file.
# - soft_threshold: score (default: 1)
# minimum score to match the soft_match return value
# - hard_threshold: score (default: 1)
# * a file that contains "postmaster@" in "partial-prefix" mode will match all
# postmaster emails.
# * a file open without "partial-" modifier match exact strings.
+# - rbldns: (no)?lock:weight:filename
+# declare a rbldns zone file to load. This is exactly the same as file excepted that it wraps
+# parsing of hostname to split them into 2 categories:
+# * names beginning with '*' are sorted as 'domains' and are matched as suffix
+# * names starting with an alphanumirical character are sorted as 'hostnames' and are
+# process via exact matching.
# - soft_threshold: score (default: 1)
# minimum score to match the soft_match return value
# - hard_threshold: score (default: 1)
# configuration
file = lock:1:suffix:/var/spool/postlicyd/client_whitelist;
- fields = client_name;
+ rbldns = lock:1:/va/spool/postlicyd/abuse.rfc-ignorant.org;
+ fields = client_name,sender_domain,helo_name;
# hooks
on_hard_match = postfix:OK;
pfix-srsd_SOURCES = main-srsd.c ../common/lib.a
pfix-srsd_LIBADD = -lsrs2
+all:
+
include ../mk/common.mk
filter_tokens.h filter_tokens.c \
hook_tokens.h hook_tokens.c \
param_tokens.h param_tokens.c
-TESTS = test-rbl tst-filters
+TESTS = tst-rbl tst-filters
FILTERS = iplist.c greylist.c strlist.c match.c
postlicyd_SOURCES = main-postlicyd.c ../common/lib.a filter.c config.c query.c $(FILTERS) $(GENERATED)
postlicyd_LIBADD = $(TC_LIBS)
-tst-rbl_SOURCES = tst-rbl.c
+tst-rbl_SOURCES = tst-rbl.c ../common/lib.a filter.c config.c query.c iplist.c $(GENERATED)
tst-filters_SOURCES = tst-filters.c ../common/lib.a config.c filter.c query.c $(FILTERS) $(GENERATED)
tst-filters_LIBADD = $(TC_LIBS)
hook_tokens.h hook_tokens.c: $(FILTERS)
param_tokens.c param_tokens.h: $(FILTERS) config.c
+all:
+
include ../mk/common.mk
};
struct rbldb_t {
- A(uint32_t) ips;
+ A(uint16_t) ips[1 << 16];
};
ARRAY(rbldb_t)
rbldb_t *db;
file_map_t map;
const char *p, *end;
+ uint32_t ips = 0;
if (!file_map_open(&map, file, false)) {
return NULL;
if (parse_ipv4(p, &p, &ip) < 0) {
p = (char *)memchr(p, '\n', end - p) + 1;
} else {
- array_add(db->ips, ip);
+ array_add(db->ips[ip >> 16], ip & 0xffff);
+ ++ips;
}
}
file_map_close(&map);
/* Lookup may perform serveral I/O, so avoid swap.
*/
- array_adjust(db->ips);
- if (lock && !array_lock(db->ips)) {
- UNIXERR("mlock");
- }
-
- if (db->ips.len) {
-# define QSORT_TYPE uint32_t
-# define QSORT_BASE db->ips.data
-# define QSORT_NELT db->ips.len
+ for (int i = 0 ; i < 1 << 16 ; ++i) {
+ array_adjust(db->ips[i]);
+ if (lock && !array_lock(db->ips[i])) {
+ UNIXERR("mlock");
+ }
+ if (db->ips[i].len) {
+# define QSORT_TYPE uint16_t
+# define QSORT_BASE db->ips[i].data
+# define QSORT_NELT db->ips[i].len
# define QSORT_LT(a,b) *a < *b
# include "qsort.c"
+ }
}
- info("rbl %s loaded, %d IPs", file, db->ips.len);
+ info("rbl %s loaded, %d IPs", file, ips);
return db;
}
static void rbldb_wipe(rbldb_t *db)
{
- array_wipe(db->ips);
+ for (int i = 0 ; i < 1 << 16 ; ++i) {
+ array_wipe(db->ips[i]);
+ }
}
void rbldb_delete(rbldb_t **db)
uint32_t rbldb_stats(const rbldb_t *rbl)
{
- return rbl->ips.len;
+ uint32_t ips = 0;
+ for (int i = 0 ; i < 1 << 16 ; ++i) {
+ ips += array_len(rbl->ips[i]);
+ }
+ return ips;
}
bool rbldb_ipv4_lookup(const rbldb_t *db, uint32_t ip)
{
- int l = 0, r = db->ips.len;
+ const uint16_t hip = ip >> 16;
+ const uint16_t lip = ip & 0xffff;
+ int l = 0, r = db->ips[hip].len;
while (l < r) {
int i = (r + l) / 2;
- if (array_elt(db->ips, i) == ip)
+ if (array_elt(db->ips[hip], i) == lip)
return true;
- if (ip < array_elt(db->ips, i)) {
+ if (lip < array_elt(db->ips[hip], i)) {
r = i;
} else {
l = i + 1;
* the file pointed by filename MUST be a valid ip list issued from
* the rsync (or equivalent) service of a (r)bl.
*/
- case ATK_FILE: {
+ case ATK_FILE: case ATK_RBLDNS: {
bool lock = false;
int weight = 0;
rbldb_t *rbl = NULL;
}
} break;
- /* host parameter.
+ /* dns parameter.
* weight:hostname.
* define a RBL to use through DNS resolution.
*/
- case ATK_HOST: {
+ case ATK_DNS: {
int weight = 0;
const char *current = param->value;
const char *p = m_strchrnul(param->value, ':');
/* Parameters.
*/
(void)filter_param_register(type, "file");
- (void)filter_param_register(type, "host");
+ (void)filter_param_register(type, "rbldns");
+ (void)filter_param_register(type, "dns");
(void)filter_param_register(type, "hard_threshold");
(void)filter_param_register(type, "soft_threshold");
return 0;
return EXIT_FAILURE;
}
+ if (pidfile_open(pidfile) < 0) {
+ crit("unable to write pidfile %s", pidfile);
+ return EXIT_FAILURE;
+ }
+
if (drop_privileges(RUNAS_USER, RUNAS_GROUP) < 0) {
crit("unable to drop privileges");
return EXIT_FAILURE;
config->port = port;
}
- if (common_setup(pidfile, true, NULL, NULL, daemonize) != EXIT_SUCCESS
- || start_listener(config->port) < 0) {
+ if (daemonize && daemon_detach() < 0) {
+ crit("unable to fork");
+ return EXIT_FAILURE;
+ }
+
+ pidfile_refresh();
+
+ if (start_listener(config->port) < 0) {
return EXIT_FAILURE;
} else {
return server_loop(query_starter, (delete_client_t)query_delete,
return db;
}
+static bool strlist_create_from_rhbl(const char *file, bool lock,
+ trie_t **phosts, trie_t **pdomains)
+{
+ trie_t *hosts, *domains;
+ uint32_t host_count, domain_count;
+ file_map_t map;
+ const char *p, *end;
+ char line[BUFSIZ];
+
+ if (!file_map_open(&map, file, false)) {
+ return false;
+ }
+ p = map.map;
+ end = map.end;
+ while (end > p && end[-1] != '\n') {
+ --end;
+ }
+ if (end != map.end) {
+ warn("file %s miss a final \\n, ignoring last line",
+ file);
+ }
+
+ hosts = trie_new();
+ host_count = 0;
+ domains = trie_new();
+ domain_count = 0;
+ while (p < end && p != NULL) {
+ const char *eol = (char *)memchr(p, '\n', end - p);
+ if (eol == NULL) {
+ eol = end;
+ }
+ if (eol - p >= BUFSIZ) {
+ err("unreasonnable long line");
+ file_map_close(&map);
+ trie_delete(&hosts);
+ trie_delete(&domains);
+ return false;
+ }
+ if (*p != '#') {
+ const char *eos = eol;
+ while (p < eos && isspace(*p)) {
+ ++p;
+ }
+ while (p < eos && isspace(eos[-1])) {
+ --eos;
+ }
+ if (p < eos) {
+ if (isalnum(*p)) {
+ strlist_copy(line, p, eos - p, true);
+ trie_insert(hosts, line);
+ ++host_count;
+ } else if (*p == '*') {
+ ++p;
+ strlist_copy(line, p, eos - p, true);
+ trie_insert(domains, line);
+ ++domain_count;
+ }
+ }
+ }
+ p = eol + 1;
+ }
+ file_map_close(&map);
+ if (host_count > 0) {
+ trie_compile(hosts, lock);
+ *phosts = hosts;
+ } else {
+ trie_delete(&hosts);
+ *phosts = NULL;
+ }
+ if (domain_count > 0) {
+ trie_compile(domains, lock);
+ *pdomains = domains;
+ } else {
+ trie_delete(&domains);
+ *pdomains = NULL;
+ }
+ return hosts != NULL || domains != NULL;
+
+}
+
static bool strlist_filter_constructor(filter_t *filter)
{
foreach (filter_param_t *param, filter->params) {
switch (param->type) {
/* file parameter is:
- * [no]lock:(prefix|suffix):weight:filename
+ * [no]lock:(partial-)(prefix|suffix):weight:filename
* valid options are:
* - lock: memlock the database in memory.
* - nolock: don't memlock the database in memory.
}
} break;
+ /* rbldns parameter is:
+ * [no]lock::weight:filename
+ * valid options are:
+ * - lock: memlock the database in memory.
+ * - nolock: don't memlock the database in memory.
+ * - \d+: a number describing the weight to give to the match
+ * the given list [mandatory]
+ * directly import a file issued from a rhbl in rbldns format.
+ */
+ case ATK_RBLDNS: {
+ bool lock = false;
+ int weight = 0;
+ trie_t *trie_hosts = NULL;
+ trie_t *trie_domains = NULL;
+ const char *current = param->value;
+ const char *p = m_strchrnul(param->value, ':');
+ char *next = NULL;
+ for (int i = 0 ; i < 3 ; ++i) {
+ PARSE_CHECK(i == 2 || *p,
+ "file parameter must contains a locking state "
+ "and a weight option");
+ switch (i) {
+ case 0:
+ if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
+ lock = true;
+ } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
+ lock = false;
+ } else {
+ PARSE_CHECK(false, "illegal locking state %.*s",
+ p - current, current);
+ }
+ break;
+
+ case 1:
+ weight = strtol(current, &next, 10);
+ PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
+ "illegal weight value %.*s",
+ (p - current), current);
+ break;
+
+ case 2:
+ PARSE_CHECK(strlist_create_from_rhbl(current, lock,
+ &trie_hosts, &trie_domains),
+ "cannot load string list from rhbl %s", current);
+ if (trie_hosts != NULL) {
+ array_add(config->tries, trie_hosts);
+ array_add(config->weights, weight);
+ array_add(config->reverses, true);
+ array_add(config->partiales, false);
+ }
+ if (trie_domains != NULL) {
+ array_add(config->tries, trie_domains);
+ array_add(config->weights, weight);
+ array_add(config->reverses, true);
+ array_add(config->partiales, true);
+ }
+ config->is_hostname = true;
+ break;
+ }
+ if (i != 2) {
+ current = p + 1;
+ p = m_strchrnul(current, ':');
+ }
+ }
+ } break;
+
/* hard_threshold parameter is an integer.
* If the matching score is greater or equal than this threshold,
* the hook "hard_match" is called.
/* Parameters.
*/
(void)filter_param_register(type, "file");
+ (void)filter_param_register(type, "rbldns");
(void)filter_param_register(type, "hard_threshold");
(void)filter_param_register(type, "soft_threshold");
(void)filter_param_register(type, "fields");
__FILE__, __LINE__, __func__, ##__VA_ARGS__)
#include "common.h"
-#include "iplist.c"
+#include "iplist.h"
+#include "array.h"
int main(int argc, char *argv[])
{
if (argc > 1) {
rbldb_t *db = rbldb_create(argv[1], false);
printf("loaded: %s, %d ips, %d o\n", argv[1], rbldb_stats(db),
- rbldb_stats(db) * 4);
+ rbldb_stats(db) * 1 + 65536 * sizeof(A(uint16_t)));
+
+ time_t now = time(NULL);
+ for (uint32_t i = 0 ; i < 1000000000 ; ++i) {
+ rbldb_ipv4_lookup(db, (88 << 24) | (170 << 16) | (239 << 8) | (132));
+ }
+ printf("%ld request per second\n", 1000000000 / (time(NULL) - now));
rbldb_delete(&db);
}
return 0;
--- /dev/null
+#!/usr/bin/env python
+##############################################################################
+# pfixtools: a collection of postfix related tools #
+# ~~~~~~~~~ #
+# ________________________________________________________________________ #
+# #
+# Redistribution and use in source and binary forms, with or without #
+# modification, are permitted provided that the following conditions #
+# are met: #
+# #
+# 1. Redistributions of source code must retain the above copyright #
+# notice, this list of conditions and the following disclaimer. #
+# 2. Redistributions in binary form must reproduce the above copyright #
+# notice, this list of conditions and the following disclaimer in the #
+# documentation and/or other materials provided with the distribution. #
+# 3. The names of its contributors may not be used to endorse or promote #
+# products derived from this software without specific prior written #
+# permission. #
+# #
+# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND #
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE #
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR #
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS #
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR #
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS #
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN #
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) #
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF #
+# THE POSSIBILITY OF SUCH DAMAGE. #
+##############################################################################
+
+# Copyright (c) 2008 Aymeric Augustin
+
+
+# Convert the postgrey_whitelist_clients file to a format
+# suitable for use with the postlicyd Postfix policy daemon
+
+
+import os, re, sys
+
+def process(infile, outfile):
+
+ # Write headers
+ file.write("# Do not edit, file autogenerated by %s\n" % sys.argv[0])
+ if len(sys.argv) > 1 and sys.argv[1] != '-':
+ file.write("# This file has been generated from %s\n" % sys.argv[1])
+
+ re_domain_name = re.compile(r'[a-z0-9.\-]+\.[a-z]+')
+ re_ip_address = re.compile(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
+
+ # Store each entry to avoid duplicates
+ entries = []
+
+ for line in infile:
+ # Comments: keep them
+ if line == '\n' or line[0] == '#':
+ outfile.write(line)
+ # IP addresses: keep as is
+ elif re_ip_address.match(line):
+ outfile.write(line)
+ # Regexps: extract final constant part
+ elif line[0] == '/':
+ line = line.rstrip(r'$/').replace(r'\.', r'.')
+ host = re_domain_name.findall(line)[-1]
+ result = host + '\n'
+ if result not in entries:
+ entries.append(result)
+ outfile.write(result)
+ # Domain names: prepend a dot if the domain name contains only one dot
+ elif re_domain_name.match(line):
+ if line.count('.') < 2:
+ result = '.' + line
+ else:
+ result = line
+ if result not in entries:
+ entries.append(result)
+ outfile.write(result)
+ # Unrecognized: report on stderr and comment in output
+ else:
+ outfile.write('# IGNORED: ' + line)
+ sys.stderr.write("Couldn't process line: %s" % line)
+
+
+if __name__ == '__main__':
+
+ # Check number of arguments
+ if len(sys.argv) > 3:
+ print "Usage: %s [input] [output]" % sys.argv[0]
+ print "If input/output is omitted or -, stdin/stdout is used."
+ sys.exit(1)
+
+ # Parse first argument
+ if len(sys.argv) > 1 and sys.argv[1] != '-':
+ infile = open(sys.argv[1], 'r')
+ else:
+ infile = sys.stdin
+
+ # Parse second argument
+ if len(sys.argv) > 2 and sys.argv[2] != '-':
+ if sys.argv[1] == sys.argv[2]:
+ print "Source file and destination file are identical, aborting"
+ sys.exit(1)
+ if os.path.exists(sys.argv[2]):
+ print "Destination file %s already exists, aborting" % sys.argv[2]
+ sys.exit(1)
+ outfile = open(sys.argv[2], 'w')
+ else:
+ outfile = sys.stdout
+
+ # Do the processing
+ process(infile, outfile)
--- /dev/null
+#!/usr/bin/env python
+##############################################################################
+# pfixtools: a collection of postfix related tools #
+# ~~~~~~~~~ #
+# ________________________________________________________________________ #
+# #
+# Redistribution and use in source and binary forms, with or without #
+# modification, are permitted provided that the following conditions #
+# are met: #
+# #
+# 1. Redistributions of source code must retain the above copyright #
+# notice, this list of conditions and the following disclaimer. #
+# 2. Redistributions in binary form must reproduce the above copyright #
+# notice, this list of conditions and the following disclaimer in the #
+# documentation and/or other materials provided with the distribution. #
+# 3. The names of its contributors may not be used to endorse or promote #
+# products derived from this software without specific prior written #
+# permission. #
+# #
+# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND #
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE #
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR #
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS #
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR #
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS #
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN #
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) #
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF #
+# THE POSSIBILITY OF SUCH DAMAGE. #
+##############################################################################
+
+# Copyright (c) 2008 Florent Bruneau
+
+import os, re, sys
+
+def headers(file, source, type):
+ file.write("# Do not edit, file autogenerated by %s\n" % sys.argv[0])
+ file.write("# This file has been generated from %s blacklist\n" % source)
+ file.write("# it contains %s matching rules for postlicyd\n" % type)
+
+def convert(rbl):
+ source = open(rbl, "r")
+ hosts = open("%s_hosts" % rbl, "w")
+ domains = open("%s_domains" % rbl, "w")
+
+ headers(hosts, rbl, "hosts")
+ headers(domains, rbl, "domains")
+
+ for line in source:
+ if line[0].isalnum():
+ hosts.write(line);
+ elif line[0] == '*':
+ domains.write(line[1:]);
+
+if __name__ == '__main__':
+ if len(sys.argv) == 1:
+ print "Usage: %s [input] ..." % sys.argv[0]
+ sys.exit(1)
+
+ map(convert, sys.argv[1:])
+
+# vim:set syntax=python: