commit f570931b5be5c92fefc8547ccac41233c5dbf337 Author: Gregory Colpart Date: Fri Aug 21 01:28:17 2015 +0200 init diff --git a/.depend b/.depend new file mode 100644 index 0000000..44bd1eb --- /dev/null +++ b/.depend @@ -0,0 +1,11 @@ +policy.cmo: policy.cmi +policy.cmx: policy.cmi +rules.cmo: spf.cmi policy.cmi rules.cmi +rules.cmx: spf.cmx policy.cmx rules.cmi +server.cmo: server.cmi +server.cmx: server.cmi +spf.cmo: spf.cmi +spf.cmx: spf.cmi +whitelister.cmo: server.cmi rules.cmi policy.cmi +whitelister.cmx: server.cmx rules.cmx policy.cmx +rules.cmi: policy.cmi diff --git a/.pc/.quilt_patches b/.pc/.quilt_patches new file mode 100644 index 0000000..6857a8d --- /dev/null +++ b/.pc/.quilt_patches @@ -0,0 +1 @@ +debian/patches diff --git a/.pc/.quilt_series b/.pc/.quilt_series new file mode 100644 index 0000000..c206706 --- /dev/null +++ b/.pc/.quilt_series @@ -0,0 +1 @@ +series diff --git a/.pc/.version b/.pc/.version new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/.pc/.version @@ -0,0 +1 @@ +2 diff --git a/.pc/applied-patches b/.pc/applied-patches new file mode 100644 index 0000000..d12d092 --- /dev/null +++ b/.pc/applied-patches @@ -0,0 +1,2 @@ +reject_unknown_client.patch +disable_spf.patch diff --git a/.pc/disable_spf.patch/Makefile b/.pc/disable_spf.patch/Makefile new file mode 100644 index 0000000..5c063f4 --- /dev/null +++ b/.pc/disable_spf.patch/Makefile @@ -0,0 +1,95 @@ +########################################################################## +# # +# whitelister : a Whitelister Policy Daemon for Postfix # +# ~~~~~~~~~~~ # +# # +# Copyright (C) 2005 AAEGE.org # +# Author : Pierre Habouzit # +# ____________________________________________________________________ # +# # +# This program is free software; you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation; either version 2 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program; if not, write to the Free Software # +# Foundation, Inc., # +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # +# # +########################################################################## + +VERSION = 0.8 +PROGRAM = whitelister +PKG_DIST = $(PROGRAM)-$(VERSION) +REPO = $(shell svn info | grep URL | sed -e 's,URL *: *,,') + +PKGS = -package syslog +CFLAGS = -O2 -Wall -fPIC +CCLIB = -cclib -lspf +CCOPT = $(foreach opt,$(CFLAGS),-ccopt $(opt)) + +OCAMLC = ocamlfind ocamlc $(PKGS) $(CCOPT) $(CCLIB) +OCAMLOPT= ocamlfind ocamlopt $(PKGS) $(CCOPT) $(CCLIB) +OCAMLDEP= ocamlfind ocamldep $(PKGS) + +BIB = str.cmxa unix.cmxa syslog.cmxa +CMX = spf.cmx policy.cmx rules.cmx server.cmx whitelister.cmx +COB = spfstubs.o + +############################################################## + +all: $(PROGRAM) + +whitelister: $(COB) $(CMX) + $(OCAMLOPT) -o $@ $(BIB) $^ + +headers: Makefile *.ml *.mli + headache -h tpl/header $^ + +dist: + @rm -rf $(PKG_DIST) $(PKG_DIST).tar.gz + @svn export $(REPO) $(PKG_DIST) + @rm -rf $(PKG_DIST)/debian + @tar czf $(PKG_DIST).tar.gz $(PKG_DIST) + @rm -rf $(PKG_DIST) + @echo -e "\ndistribution built in $(PKG_DIST).tar.gz\n" + + +############################################################## + +.SUFFIXES: .mli .ml .cmi .cmo .cmx .mll .mly .c .o + +.c.o: + $(OCAMLC) $(CCOPT) -c $< + +.mli.cmi: + $(OCAMLC) -c $< + +.ml.cmo: + $(OCAMLC) -c $< + +.ml.cmx: + $(OCAMLOPT) -c $< + +.mll.ml: + $(OCAMLLEX) $< + +############################################################## +cleanbyte: + rm -rf *.{cm?,o} *~ + +clean: cleanbyte + rm -f .depend + rm -f $(PROGRAM) + +.depend depend: *.ml *.mli + rm -f .depend + $(OCAMLDEP) *.ml *.mli > .depend + +include .depend diff --git a/.pc/disable_spf.patch/policy.ml b/.pc/disable_spf.patch/policy.ml new file mode 100644 index 0000000..4b5f004 --- /dev/null +++ b/.pc/disable_spf.patch/policy.ml @@ -0,0 +1,117 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +exception ParseError +exception Unknown +exception DSN + +type t = (string, string) Hashtbl.t + +(* Private : access to hashtbl *) + +let get h k = + try Hashtbl.find h k + with Not_found -> raise Unknown + +let getu h k = + try Hashtbl.find h k + with Not_found -> "unknown" + +let check_policy pcy = + if getu pcy "request" = "smtpd_access_policy" then + pcy + else + raise ParseError + +let domain s = + try + Str.string_after s ((String.index s '@')+1) + with Not_found -> raise Unknown (* case of rpcts / sender that are not email addresses *) + + +(* Private : log formats *) + +let log_end pcy = + Printf.sprintf + "from=<%s> to=<%s> proto=%s helo=<%s>" + ( getu pcy "sender" ) + ( getu pcy "recipient" ) + ( getu pcy "protocol_name" ) + ( getu pcy "helo_name" ) + +let log_start : t -> string = fun pcy -> + Printf.sprintf + "%s from %s[%s][%s]" + ( getu pcy "protocol_state" ) + ( getu pcy "client_name" ) + ( getu pcy "client_address" ) + ( getu pcy "reverse_client_name" ) + + +(* public *) + +let read ic = + let res = Hashtbl.create 13 in + try + while true do + try + let line = input_line ic in + if String.length line = 0 then raise End_of_file; + let i = String.index line '=' in + Hashtbl.add res (Str.string_before line i) (Str.string_after line (i+1)) + with Not_found -> raise ParseError + done; + assert false + with + | End_of_file -> check_policy res + +let clear = Hashtbl.clear + +let client_address h = get h "client_address" +let client_name h = get h "client_name" +let reverse_client_name h = get h "reverse_client_name" +let helo_name h = get h "helo_name" +let sender h = + try + ( + match Hashtbl.find h "sender" with + | "" -> raise DSN + | s -> s + ) + with Not_found -> raise DSN + +let rcpt_domain h = domain (get h "recipient") +let sender_domain h = domain (sender h) + + +let log_format level answer pcy = + Printf.sprintf "%s: %s: %s; %s" + level (log_start pcy) answer (log_end pcy) + +let spf_explain pcy = + let sender = getu pcy "sender" in + let ip = getu pcy "client_address" in + Printf.sprintf " - Please see http://spf.pobox.com/why.html?sender=%s&ip=%s" sender ip diff --git a/.pc/disable_spf.patch/policy.mli b/.pc/disable_spf.patch/policy.mli new file mode 100644 index 0000000..5c398c9 --- /dev/null +++ b/.pc/disable_spf.patch/policy.mli @@ -0,0 +1,45 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +type t + +exception ParseError +exception Unknown +exception DSN + +val read : in_channel -> t +val clear : t -> unit + +val client_address : t -> string +val client_name : t -> string +val reverse_client_name : t -> string +val sender : t -> string +val helo_name : t -> string +val rcpt_domain: t -> string +val sender_domain : t -> string + +val log_format : string -> string -> t -> string +val spf_explain : t -> string diff --git a/.pc/disable_spf.patch/rules.ml b/.pc/disable_spf.patch/rules.ml new file mode 100644 index 0000000..6a4d561 --- /dev/null +++ b/.pc/disable_spf.patch/rules.ml @@ -0,0 +1,116 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +open Policy + +exception Dirty of string +exception Reject of string + +type rhbl_type = Helo | Rcpt | Sender | Client + +(* HELPERS *) + +(** [rev_ip "ip1.ip2.ip3.ip4"] returns ["ip4.ip3.ip2.ip1"] *) +let rev_ip ip = + let l = Str.split (Str.regexp_string ".") ip in + String.concat "." (List.rev l) + +(** [dns_check r i h] returns : + * [i+1] if ["$r.$h"] domain exists, + * [i] if domain does not exists, + * [i+1] if any error occur. + * This is meant to be use with List.fold_left + *) +let dns_check rad host = + try + let _ = Unix.gethostbyname (rad ^ "." ^ host) in + raise (Dirty ("blacklisted by " ^ host)) + with + | Not_found -> () + | Dirty _ as e -> raise e + | _ -> raise (Dirty "rbl failure") + + +let rhbl_extract_domain = + function + | Helo -> helo_name + | Rcpt -> helo_name + | Sender -> sender_domain + | Client -> client_name + +(* PUBLIC INTERFACE *) + +let check_rbl rbl_list pcy = + try + let revip = rev_ip (client_address pcy) in + List.iter (dns_check revip) rbl_list + with + | Policy.Unknown -> raise (Dirty "no `client_address' found") + | Policy.DSN -> () + +let check_rhbl kind rhbl_list pcy = + try + let host = (rhbl_extract_domain kind) pcy in + List.iter (dns_check host) rhbl_list + with + | Policy.Unknown -> + ( + match kind with + | Helo | Rcpt -> raise (Dirty "no `helo_name' found") + | Sender -> raise (Dirty "no `sender_name' found") + | Client -> raise (Dirty "no `client_name' found") + ) + | Policy.DSN -> () + +open Spf + +type spf_mode = Spf_off | Spf_normal | Spf_strict | Spf_paranoid + +let check_spf mode dorej pcy = + if mode != Spf_off then + let fail s = + if dorej then raise (Reject (s ^ Policy.spf_explain pcy)) else raise (Dirty s) + in try + match spf_query (sender pcy) (client_address pcy) (helo_name pcy) with + | SPF_pass -> () + | SPF_softerr -> fail "SPF soft error" + | SPF_harderr -> fail "SPF hard error" + | SPF_none -> if mode != Spf_normal then raise (Dirty "no SPF record found") + | SPF_neutral -> if mode = Spf_paranoid then raise (Dirty "SPF neutral") + with + | Spf.Error -> raise (Dirty "SPF Internal error") + | Policy.DSN -> () + +let check_dns_client dorej pcy = + if dorej then + if (client_name pcy) = "unknown" then raise (Dirty "no client_name (reject_unknown_client)") + else () + +let check_dns_rev_client dorej pcy = + if dorej then + if (reverse_client_name pcy) = "unknown" then raise (Dirty "no reverse_client_name (reject_unknown_reverse_client)") + else () + diff --git a/.pc/disable_spf.patch/spf.ml b/.pc/disable_spf.patch/spf.ml new file mode 100644 index 0000000..6805450 --- /dev/null +++ b/.pc/disable_spf.patch/spf.ml @@ -0,0 +1,41 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +exception Error +type spf_result = + SPF_pass | SPF_neutral | SPF_none | SPF_softerr | SPF_harderr + +external _spf_query : string -> string -> string -> int = "spf_query" + +let spf_query host ip helo = + match _spf_query host ip helo with + | 0 -> SPF_pass + | 1 -> SPF_neutral + | 2 -> SPF_none + | 3 -> SPF_softerr + | 4 -> SPF_harderr + | _ -> raise Error + diff --git a/.pc/disable_spf.patch/spf.mli b/.pc/disable_spf.patch/spf.mli new file mode 100644 index 0000000..e16915c --- /dev/null +++ b/.pc/disable_spf.patch/spf.mli @@ -0,0 +1,33 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +exception Error + +type spf_result = + SPF_pass | SPF_neutral | SPF_none | SPF_softerr | SPF_harderr + +val spf_query : string -> string -> string -> spf_result + diff --git a/.pc/disable_spf.patch/spfstubs.c b/.pc/disable_spf.patch/spfstubs.c new file mode 100644 index 0000000..2b0331f --- /dev/null +++ b/.pc/disable_spf.patch/spfstubs.c @@ -0,0 +1,42 @@ +#include +#include + +#include + +value spf_query(value from, value ip, value helo) +{ + int res; + peer_info_t* peer_info; + + CAMLparam3(from, ip, helo); + + peer_info = SPF_init("whitelister", String_val(ip), NULL, NULL, NULL, 0, 0); + SPF_smtp_helo(peer_info, String_val(helo)); + SPF_smtp_from(peer_info, String_val(from)); + res = peer_info->RES = SPF_policy_main(peer_info); + + SPF_close(peer_info); + + switch(res) + { + case SPF_PASS: + CAMLreturn(Val_int(0)); + + case SPF_NEUTRAL: + CAMLreturn(Val_int(1)); + + case SPF_NONE: + CAMLreturn(Val_int(2)); + + case SPF_S_FAIL: + CAMLreturn(Val_int(3)); + + case SPF_H_FAIL: + CAMLreturn(Val_int(4)); + + default: /* SPF_ERROR, SPF_UNKNOWN, SPF_UNMECH */ + CAMLreturn(Val_int(5)); + + } +} + diff --git a/.pc/disable_spf.patch/whitelister.ml b/.pc/disable_spf.patch/whitelister.ml new file mode 100644 index 0000000..c6bf325 --- /dev/null +++ b/.pc/disable_spf.patch/whitelister.ml @@ -0,0 +1,184 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +(* types *) + +type config = { + mutable pidf: string; + mutable sock: Unix.sockaddr; + mutable user: string; + mutable group: string; + mutable verb: int; + + mutable spf: Rules.spf_mode; + mutable spfrej: bool; + + mutable rbl: string list; + mutable rhbl_client: string list; + mutable rhbl_rcpt: string list; + mutable rhbl_sender: string list; + mutable rhbl_helo: string list; + + mutable dns_client: bool; + mutable dns_rev_client: bool; +} + +(* Checker *) + +let log msg = + let lg = Syslog.openlog ~facility:`LOG_MAIL ~flags:[`LOG_PID] "whitelister" in + Syslog.syslog lg `LOG_INFO msg; + Syslog.closelog lg + +let log_event level answer pcy = + log (Policy.log_format level answer pcy) + +let checker cfg s = + let oc = Unix.out_channel_of_descr s in + try + while true do + let pcy = Policy.read (Unix.in_channel_of_descr s) in + let ans = ( + try + Rules.check_rbl cfg.rbl pcy; + Rules.check_rhbl Rules.Helo cfg.rhbl_helo pcy; + Rules.check_rhbl Rules.Sender cfg.rhbl_sender pcy; + Rules.check_rhbl Rules.Rcpt cfg.rhbl_rcpt pcy; + Rules.check_rhbl Rules.Client cfg.rhbl_client pcy; + Rules.check_spf cfg.spf cfg.spfrej pcy; + Rules.check_dns_client cfg.dns_client pcy; + Rules.check_dns_rev_client cfg.dns_rev_client pcy; + if cfg.verb > 0 then log_event "Clean" "OK" pcy; + "OK" + with + | Rules.Dirty s -> + log_event "Dirty" (Printf.sprintf "DUNNO (%s)" s) pcy; + "DUNNO" + | Rules.Reject s -> + let s' = "REJECT "^s in + log_event "Reject" s' pcy; + s' + ) in + Printf.fprintf oc "action=%s\n\n" ans; + flush oc; + Policy.clear pcy (* not needed, but can help the GC *) + done + with _ -> Unix.shutdown s Unix.SHUTDOWN_ALL + +(* Configuration *) + +let empty_config () = { + pidf = "/var/run/whitelister.pid" ; + sock = Unix.ADDR_INET (Unix.inet_addr_of_string "127.0.0.1", 10000) ; + user = "nobody" ; + group = "nogroup" ; + verb = 0 ; + + spf = Rules.Spf_off ; + spfrej = false; + + rbl = [] ; + rhbl_client = [] ; + rhbl_rcpt = [] ; + rhbl_sender = [] ; + rhbl_helo = [] ; + + dns_client = false; + dns_rev_client = false; +} + +let to_bool s = + function + | "0" | "off" | "no" -> false + | "1" | "yes" | "on" -> true + | _ -> prerr_endline ("cannot read a boolean value for `"^s^"', possible values are on/off, yes/no, 1/0"); exit 1 + +let update_config cfg file = + let ic = open_in file in + try + while true do + let line = input_line ic in + if String.length line > 0 && line.[0] <> '#' then + match Str.split (Str.regexp "[ \t:]+") line with + | [] -> () + + | ["pidf"; f] -> cfg.pidf <- f + | ["sock"; s] -> cfg.sock <- Unix.ADDR_UNIX s + | ["sock"; ip; p] -> cfg.sock <- Unix.ADDR_INET (Unix.inet_addr_of_string ip, int_of_string p) + | ["user"; u] -> cfg.user <- u + | ["group"; g] -> cfg.group <- g + | ["verb"; "0"] -> cfg.verb <- 0 + | ["verb"; "1"] -> cfg.verb <- 1 + + | ["spf"; "0"] -> cfg.spf <- Rules.Spf_off + | ["spf"; "1"] -> cfg.spf <- Rules.Spf_normal + | ["spf"; "2"] -> cfg.spf <- Rules.Spf_strict + | ["spf"; "3"] -> cfg.spf <- Rules.Spf_paranoid + | ["spfrej"; b] -> cfg.spfrej <- to_bool "spfrej" b + + | ["rbl"; h] -> cfg.rbl <- h::cfg.rbl + | ["rhbl_client"; h] -> cfg.rhbl_client <- h::cfg.rhbl_client + | ["rhbl_helo"; h] -> cfg.rhbl_helo <- h::cfg.rhbl_helo + | ["rhbl_rcpt"; h] -> cfg.rhbl_rcpt <- h::cfg.rhbl_rcpt + | ["rhbl_sender"; h] -> cfg.rhbl_sender <- h::cfg.rhbl_sender + + | ["dns_client"; d] -> cfg.dns_client <- to_bool "dns_client" d + | ["dns_rev_client"; e] -> cfg.dns_rev_client <- to_bool "dns_rev_client" e + + (* deprecated settings *) + | ["rhbl"; h] -> prerr_endline "rhbl is deprecated, it defaults to rhbl_client which may not be what you want."; + cfg.rhbl_client <- h::cfg.rhbl_client + | _ -> prerr_string (Printf.sprintf "`%s' is not a valid config line\n" line) + done; + assert false + with End_of_file -> close_in ic; cfg + +let read_config () = + let found_files = List.filter Sys.file_exists ["/etc/whitelister.conf" ; "whitelister.conf"] in + List.fold_left update_config (empty_config ()) found_files + +(* Server thingies *) + +open Server + +let my_server_loop cfg sock = + pidfile_write (); + pidfile_close (); + log ("started, listening to " ^ (string_of_sockaddr cfg.sock)); + while true do + let s = fst (unix_do Unix.accept sock) in + double_fork (fun s -> Unix.close sock; checker cfg s ; exit 0) s; + Unix.close s + done + +(* Main LOOP *) + +let _ = + let cfg = read_config () in + pidfile_open cfg.pidf; + pidfile_write (); + daemonize (cfg.user,cfg.group) (my_server_loop cfg) (bind_to_sock cfg.sock) + diff --git a/.pc/reject_unknown_client.patch/Makefile b/.pc/reject_unknown_client.patch/Makefile new file mode 100644 index 0000000..27a528b --- /dev/null +++ b/.pc/reject_unknown_client.patch/Makefile @@ -0,0 +1,95 @@ +########################################################################## +# # +# whitelister : a Whitelister Policy Daemon for Postfix # +# ~~~~~~~~~~~ # +# # +# Copyright (C) 2005 AAEGE.org # +# Author : Pierre Habouzit # +# ____________________________________________________________________ # +# # +# This program is free software; you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation; either version 2 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program; if not, write to the Free Software # +# Foundation, Inc., # +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # +# # +########################################################################## + +VERSION = 0.8 +PROGRAM = whitelister +PKG_DIST = $(PROGRAM)-$(VERSION) +REPO = $(shell svn info | grep URL | sed -e 's,URL *: *,,') + +PKGS = -package syslog +CFLAGS = -O2 -Wall -fPIC +CCLIB = -cclib -lspf +CCOPT = $(foreach opt,$(CFLAGS),-ccopt $(opt)) + +OCAMLC = ocamlfind ocamlc $(PKGS) $(CCOPT) $(CCLIB) +OCAMLOPT= ocamlfind ocamlopt $(PKGS) $(CCOPT) $(CCLIB) +OCAMLDEP= ocamlfind ocamldep $(PKGS) + +BIB = str.cmxa unix.cmxa syslog.cmxa +CMX = spf.cmx policy.cmx rules.cmx server.cmx whitelister.cmx +COB = spfstubs.o + +############################################################## + +all: $(PROGRAM) + +whitelister: $(COB) $(CMX) + $(OCAMLOPT) -o $@ $(BIB) $^ + +headers: Makefile *.ml *.mli + headache -h tpl/header $^ + +dist: + @rm -rf $(PKG_DIST) $(PKG_DIST).tar.gz + @svn export $(REPO) $(PKG_DIST) + @rm -rf $(PKG_DIST)/debian + @tar czf $(PKG_DIST).tar.gz $(PKG_DIST) + @rm -rf $(PKG_DIST) + @echo -e "\ndistribution built in $(PKG_DIST).tar.gz\n" + + +############################################################## + +.SUFFIXES: .mli .ml .cmi .cmo .cmx .mll .mly .c .o + +.c.o: + $(OCAMLC) $(CCOPT) -c $< + +.mli.cmi: + $(OCAMLC) -c $< + +.ml.cmo: + $(OCAMLC) -c $< + +.ml.cmx: + $(OCAMLOPT) -c $< + +.mll.ml: + $(OCAMLLEX) $< + +############################################################## +cleanbyte: + rm -rf *.{cm?,o} *~ + +clean: cleanbyte + rm -f $(PROGRAM) + +.depend depend: *.ml *.mli + rm -f .depend + $(OCAMLDEP) *.ml *.mli > .depend + +include .depend + diff --git a/.pc/reject_unknown_client.patch/policy.ml b/.pc/reject_unknown_client.patch/policy.ml new file mode 100644 index 0000000..6fc9fa1 --- /dev/null +++ b/.pc/reject_unknown_client.patch/policy.ml @@ -0,0 +1,115 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +exception ParseError +exception Unknown +exception DSN + +type t = (string, string) Hashtbl.t + +(* Private : access to hashtbl *) + +let get h k = + try Hashtbl.find h k + with Not_found -> raise Unknown + +let getu h k = + try Hashtbl.find h k + with Not_found -> "unknown" + +let check_policy pcy = + if getu pcy "request" = "smtpd_access_policy" then + pcy + else + raise ParseError + +let domain s = + try + Str.string_after s ((String.index s '@')+1) + with Not_found -> raise Unknown (* case of rpcts / sender that are not email addresses *) + + +(* Private : log formats *) + +let log_end pcy = + Printf.sprintf + "from=<%s> to=<%s> proto=%s helo=<%s>" + ( getu pcy "sender" ) + ( getu pcy "recipient" ) + ( getu pcy "protocol_name" ) + ( getu pcy "helo_name" ) + +let log_start : t -> string = fun pcy -> + Printf.sprintf + "%s from %s[%s]" + ( getu pcy "protocol_state" ) + ( getu pcy "client_name" ) + ( getu pcy "client_address" ) + + +(* public *) + +let read ic = + let res = Hashtbl.create 13 in + try + while true do + try + let line = input_line ic in + if String.length line = 0 then raise End_of_file; + let i = String.index line '=' in + Hashtbl.add res (Str.string_before line i) (Str.string_after line (i+1)) + with Not_found -> raise ParseError + done; + assert false + with + | End_of_file -> check_policy res + +let clear = Hashtbl.clear + +let client_address h = get h "client_address" +let client_name h = get h "client_name" +let helo_name h = get h "helo_name" +let sender h = + try + ( + match Hashtbl.find h "sender" with + | "" -> raise DSN + | s -> s + ) + with Not_found -> raise DSN + +let rcpt_domain h = domain (get h "recipient") +let sender_domain h = domain (sender h) + + +let log_format level answer pcy = + Printf.sprintf "%s: %s: %s; %s" + level (log_start pcy) answer (log_end pcy) + +let spf_explain pcy = + let sender = getu pcy "sender" in + let ip = getu pcy "client_address" in + Printf.sprintf " - Please see http://spf.pobox.com/why.html?sender=%s&ip=%s" sender ip diff --git a/.pc/reject_unknown_client.patch/policy.mli b/.pc/reject_unknown_client.patch/policy.mli new file mode 100644 index 0000000..d6b8eaa --- /dev/null +++ b/.pc/reject_unknown_client.patch/policy.mli @@ -0,0 +1,44 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +type t + +exception ParseError +exception Unknown +exception DSN + +val read : in_channel -> t +val clear : t -> unit + +val client_address : t -> string +val client_name : t -> string +val sender : t -> string +val helo_name : t -> string +val rcpt_domain: t -> string +val sender_domain : t -> string + +val log_format : string -> string -> t -> string +val spf_explain : t -> string diff --git a/.pc/reject_unknown_client.patch/rules.ml b/.pc/reject_unknown_client.patch/rules.ml new file mode 100644 index 0000000..d224124 --- /dev/null +++ b/.pc/reject_unknown_client.patch/rules.ml @@ -0,0 +1,106 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +open Policy + +exception Dirty of string +exception Reject of string + +type rhbl_type = Helo | Rcpt | Sender | Client + +(* HELPERS *) + +(** [rev_ip "ip1.ip2.ip3.ip4"] returns ["ip4.ip3.ip2.ip1"] *) +let rev_ip ip = + let l = Str.split (Str.regexp_string ".") ip in + String.concat "." (List.rev l) + +(** [dns_check r i h] returns : + * [i+1] if ["$r.$h"] domain exists, + * [i] if domain does not exists, + * [i+1] if any error occur. + * This is meant to be use with List.fold_left + *) +let dns_check rad host = + try + let _ = Unix.gethostbyname (rad ^ "." ^ host) in + raise (Dirty ("blacklisted by " ^ host)) + with + | Not_found -> () + | Dirty _ as e -> raise e + | _ -> raise (Dirty "rbl failure") + + +let rhbl_extract_domain = + function + | Helo -> helo_name + | Rcpt -> helo_name + | Sender -> sender_domain + | Client -> client_name + +(* PUBLIC INTERFACE *) + +let check_rbl rbl_list pcy = + try + let revip = rev_ip (client_address pcy) in + List.iter (dns_check revip) rbl_list + with + | Policy.Unknown -> raise (Dirty "no `client_address' found") + | Policy.DSN -> () + +let check_rhbl kind rhbl_list pcy = + try + let host = (rhbl_extract_domain kind) pcy in + List.iter (dns_check host) rhbl_list + with + | Policy.Unknown -> + ( + match kind with + | Helo | Rcpt -> raise (Dirty "no `helo_name' found") + | Sender -> raise (Dirty "no `sender_name' found") + | Client -> raise (Dirty "no `client_name' found") + ) + | Policy.DSN -> () + +open Spf + +type spf_mode = Spf_off | Spf_normal | Spf_strict | Spf_paranoid + +let check_spf mode dorej pcy = + if mode != Spf_off then + let fail s = + if dorej then raise (Reject (s ^ Policy.spf_explain pcy)) else raise (Dirty s) + in try + match spf_query (sender pcy) (client_address pcy) (helo_name pcy) with + | SPF_pass -> () + | SPF_softerr -> fail "SPF soft error" + | SPF_harderr -> fail "SPF hard error" + | SPF_none -> if mode != Spf_normal then raise (Dirty "no SPF record found") + | SPF_neutral -> if mode = Spf_paranoid then raise (Dirty "SPF neutral") + with + | Spf.Error -> raise (Dirty "SPF Internal error") + | Policy.DSN -> () + diff --git a/.pc/reject_unknown_client.patch/rules.mli b/.pc/reject_unknown_client.patch/rules.mli new file mode 100644 index 0000000..0dd035e --- /dev/null +++ b/.pc/reject_unknown_client.patch/rules.mli @@ -0,0 +1,50 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +exception Dirty of string +exception Reject of string +type spf_mode = Spf_off | Spf_normal | Spf_strict | Spf_paranoid +type rhbl_type = Helo | Rcpt | Sender | Client + +(** + * this module defines some rules that a Policy should verify to be whitelisted. + * + * quoting POSTFIX POLICY README [1] : + * In case of trouble the policy server must not send a reply. + * Instead the server must log a warning and disconnect. + * Postfix will retry the request at some later time. + * + * We DON'T follow that policy since we write a WHITELIST server. + * So our choice is that any problem is similar to a invalid Check. + * + * [1] http://www.postfix.org/SMTPD_POLICY_README.html + *) + +val check_rbl : string list -> Policy.t -> unit + +val check_rhbl : rhbl_type -> string list -> Policy.t -> unit + +val check_spf : spf_mode -> bool -> Policy.t -> unit diff --git a/.pc/reject_unknown_client.patch/whitelister-example.conf b/.pc/reject_unknown_client.patch/whitelister-example.conf new file mode 100644 index 0000000..464d63e --- /dev/null +++ b/.pc/reject_unknown_client.patch/whitelister-example.conf @@ -0,0 +1,63 @@ +## +## Example config for whitelister. +## put this file in /etc/whitelister.conf +## + +################################################################################ +# +# 1. DAEMON CONFIGURATION +# +################################################################################ + +# verb +# verbosity of the logs +# 0: `Clean' notifications are off +# 1: enable `Clean' notifications + +# pidf +# path to the pidfile whitelister has to use +# default is /var/run/whitelister.pid +# examples: +# pidf: /dev/null + +# sock +# socket the server has to listen to +# either unix or tcp socket are possible. +# syntax is ip:port (the :port is required) for tcp sockets +# default is 127.0.0.1:10000 +# examples : +# sock: /var/spool/postfix/private/whitelister.ctl +# sock: 127.0.0.1:100 + +# user +# name of the user that whitelister will run under if launched from root +# default is nobody +user: nobody + +# group +# name of the group that whitelister will run under if launched from root +# default is nogroup +group: nogroup + +################################################################################ +# +# 2. RULES CONFIGURATION +# +################################################################################ + +# rbl +# put one rbl per line +rbl: dynablock.njabl.org +rbl: dul.dnsbl.sorbs.net + +# rhbl_* : rhbl_client / rhbl_sender / rhbl_rcpt / rhbl_helo +# put one rhbl per line +rhbl_sender: bogusmx.rfc-ignorant.org + +# spf +# use spf diagnostics (default is 0) +# spf: 1 + +# spfrej +# what to do with spf rejects, default is nothing. ignored if spf is off +# spfrej: off diff --git a/.pc/reject_unknown_client.patch/whitelister.ml b/.pc/reject_unknown_client.patch/whitelister.ml new file mode 100644 index 0000000..edb7d15 --- /dev/null +++ b/.pc/reject_unknown_client.patch/whitelister.ml @@ -0,0 +1,173 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +(* types *) + +type config = { + mutable pidf: string; + mutable sock: Unix.sockaddr; + mutable user: string; + mutable group: string; + mutable verb: int; + + mutable spf: Rules.spf_mode; + mutable spfrej: bool; + + mutable rbl: string list; + mutable rhbl_client: string list; + mutable rhbl_rcpt: string list; + mutable rhbl_sender: string list; + mutable rhbl_helo: string list; +} + +(* Checker *) + +let log msg = + let lg = Syslog.openlog ~facility:`LOG_MAIL ~flags:[`LOG_PID] "whitelister" in + Syslog.syslog lg `LOG_INFO msg; + Syslog.closelog lg + +let log_event level answer pcy = + log (Policy.log_format level answer pcy) + +let checker cfg s = + let oc = Unix.out_channel_of_descr s in + try + while true do + let pcy = Policy.read (Unix.in_channel_of_descr s) in + let ans = ( + try + Rules.check_rbl cfg.rbl pcy; + Rules.check_rhbl Rules.Helo cfg.rhbl_helo pcy; + Rules.check_rhbl Rules.Sender cfg.rhbl_sender pcy; + Rules.check_rhbl Rules.Rcpt cfg.rhbl_rcpt pcy; + Rules.check_rhbl Rules.Client cfg.rhbl_client pcy; + Rules.check_spf cfg.spf cfg.spfrej pcy; + if cfg.verb > 0 then log_event "Clean" "OK" pcy; + "OK" + with + | Rules.Dirty s -> + log_event "Dirty" (Printf.sprintf "DUNNO (%s)" s) pcy; + "DUNNO" + | Rules.Reject s -> + let s' = "REJECT "^s in + log_event "Reject" s' pcy; + s' + ) in + Printf.fprintf oc "action=%s\n\n" ans; + flush oc; + Policy.clear pcy (* not needed, but can help the GC *) + done + with _ -> Unix.shutdown s Unix.SHUTDOWN_ALL + +(* Configuration *) + +let empty_config () = { + pidf = "/var/run/whitelister.pid" ; + sock = Unix.ADDR_INET (Unix.inet_addr_of_string "127.0.0.1", 10000) ; + user = "nobody" ; + group = "nogroup" ; + verb = 0 ; + + spf = Rules.Spf_off ; + spfrej = false; + + rbl = [] ; + rhbl_client = [] ; + rhbl_rcpt = [] ; + rhbl_sender = [] ; + rhbl_helo = [] ; +} + +let to_bool s = + function + | "0" | "off" | "no" -> false + | "1" | "yes" | "on" -> true + | _ -> prerr_endline ("cannot read a boolean value for `"^s^"', possible values are on/off, yes/no, 1/0"); exit 1 + +let update_config cfg file = + let ic = open_in file in + try + while true do + let line = input_line ic in + if String.length line > 0 && line.[0] <> '#' then + match Str.split (Str.regexp "[ \t:]+") line with + | [] -> () + + | ["pidf"; f] -> cfg.pidf <- f + | ["sock"; s] -> cfg.sock <- Unix.ADDR_UNIX s + | ["sock"; ip; p] -> cfg.sock <- Unix.ADDR_INET (Unix.inet_addr_of_string ip, int_of_string p) + | ["user"; u] -> cfg.user <- u + | ["group"; g] -> cfg.group <- g + | ["verb"; "0"] -> cfg.verb <- 0 + | ["verb"; "1"] -> cfg.verb <- 1 + + | ["spf"; "0"] -> cfg.spf <- Rules.Spf_off + | ["spf"; "1"] -> cfg.spf <- Rules.Spf_normal + | ["spf"; "2"] -> cfg.spf <- Rules.Spf_strict + | ["spf"; "3"] -> cfg.spf <- Rules.Spf_paranoid + | ["spfrej"; b] -> cfg.spfrej <- to_bool "spfrej" b + + | ["rbl"; h] -> cfg.rbl <- h::cfg.rbl + | ["rhbl_client"; h] -> cfg.rhbl_client <- h::cfg.rhbl_client + | ["rhbl_helo"; h] -> cfg.rhbl_helo <- h::cfg.rhbl_helo + | ["rhbl_rcpt"; h] -> cfg.rhbl_rcpt <- h::cfg.rhbl_rcpt + | ["rhbl_sender"; h] -> cfg.rhbl_sender <- h::cfg.rhbl_sender + + (* deprecated settings *) + | ["rhbl"; h] -> prerr_endline "rhbl is deprecated, it defaults to rhbl_client which may not be what you want."; + cfg.rhbl_client <- h::cfg.rhbl_client + | _ -> prerr_string (Printf.sprintf "`%s' is not a valid config line\n" line) + done; + assert false + with End_of_file -> close_in ic; cfg + +let read_config () = + let found_files = List.filter Sys.file_exists ["/etc/whitelister.conf" ; "whitelister.conf"] in + List.fold_left update_config (empty_config ()) found_files + +(* Server thingies *) + +open Server + +let my_server_loop cfg sock = + pidfile_write (); + pidfile_close (); + log ("started, listening to " ^ (string_of_sockaddr cfg.sock)); + while true do + let s = fst (unix_do Unix.accept sock) in + double_fork (fun s -> Unix.close sock; checker cfg s ; exit 0) s; + Unix.close s + done + +(* Main LOOP *) + +let _ = + let cfg = read_config () in + pidfile_open cfg.pidf; + pidfile_write (); + daemonize (cfg.user,cfg.group) (my_server_loop cfg) (bind_to_sock cfg.sock) + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..c7a0aa4 --- /dev/null +++ b/COPYING @@ -0,0 +1,26 @@ +Copyright (c) The Regents of the University of California. +All rights reserved. + +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. Neither the name of the University nor the names of its contributors + may 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. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e7bd412 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,69 @@ +whitelister 0.8 + + * relicense under BSD license. GPLv3 seems to be delirious wrt DRM, and I + mean whitelister to be as free as possible. + * rework DSN code fully, so that it won't allow DSN's to skip some tests. + * rework rhbl logging: on DSN, whitelister claimed client_name did not + exist, where it was in fact the sender that has no domain. + +whitelister 0.7 + + * reorganize code so that privilege drop is done before exec. + (needed kludgy organization for the pidfile) + * put daemon independante code in server.ml + * close /dev/null after dup2 calls. + * DSN (mail from <> are mails we MUST accept). + +whitelister 0.6 + + * reorganize logs a bit, copy postfix logs formats. + * SPF REJECT now build a message pointing to spf.pobox.com. + * Add setting `verb' in order to hide `Clean' log entries. + * Finer grained rhbl checkings : + rhbl_helo / rhbl_rcpt / rhbl_sender / rhbl_client. + * whitelister can answer to multiple requests: + dont close the socket ourselves, so that postfix can talk to us on the + same socket. postfix is now the one that will close the socket. + + Bugfixes: + + * Fix fd starvation introduced in 0.5. + +whitelister 0.5.2 (the `never release too quickly' release) + + * umask set to 0111 during socket bind, so that anybody can talk to it. + * shutdown the socket ... else it remains open, and makes postfix timeout. + +whitelister 0.5 + + * rewrite the main server loop so that the privileges are drop just after + the bind/listen calls. + * better logging format : now use "$level ($hostname[$ip] $reason)", eg + `Dirty (foo.bar.org[1.2.3.4] blacklisted by rbl.quux.com)'. + +whitelister 0.4 + + * create module Spf (against libspf). + * enable an spf check. + * allow whitelister to reject mails on unvalid SPF. + +whitelister 0.3 + + * now use a pidfile. + * now daemonize itself in the background. + +whitelister 0.2.1 (bugfix release) + + * bug in logging: in case of an rbl failure, the error was masked. + +whitelister 0.2 + + * add support for privilege drop: + - create user and group configs (defaults to nobody:nogroup). + * Logging facilities. + +whitelister 0.1 + + * initial release. + +vim: set ts=2 sts=2 sw=2 noet: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8ddce73 --- /dev/null +++ b/Makefile @@ -0,0 +1,94 @@ +########################################################################## +# # +# whitelister : a Whitelister Policy Daemon for Postfix # +# ~~~~~~~~~~~ # +# # +# Copyright (C) 2005 AAEGE.org # +# Author : Pierre Habouzit # +# ____________________________________________________________________ # +# # +# This program is free software; you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation; either version 2 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program; if not, write to the Free Software # +# Foundation, Inc., # +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # +# # +########################################################################## + +VERSION = 0.8 +PROGRAM = whitelister +PKG_DIST = $(PROGRAM)-$(VERSION) +REPO = $(shell svn info | grep URL | sed -e 's,URL *: *,,') + +PKGS = -package syslog +CFLAGS = -O2 -Wall -fPIC +CCLIB = -cclib +CCOPT = $(foreach opt,$(CFLAGS),-ccopt $(opt)) + +OCAMLC = ocamlfind ocamlc $(PKGS) $(CCOPT) $(CCLIB) +OCAMLOPT= ocamlfind ocamlopt -linkpkg $(PKGS) $(CCOPT) $(CCLIB) +OCAMLDEP= ocamlfind ocamldep $(PKGS) + +BIB = str.cmxa unix.cmxa syslog.cmxa +CMX = policy.cmx rules.cmx server.cmx whitelister.cmx + +############################################################## + +all: $(PROGRAM) + +whitelister: $(CMX) + ocamlfind ocamlopt $(PKGS) $(CCOPT) -o $@ $(BIB) $^ + +headers: Makefile *.ml *.mli + headache -h tpl/header $^ + +dist: + @rm -rf $(PKG_DIST) $(PKG_DIST).tar.gz + @svn export $(REPO) $(PKG_DIST) + @rm -rf $(PKG_DIST)/debian + @tar czf $(PKG_DIST).tar.gz $(PKG_DIST) + @rm -rf $(PKG_DIST) + @echo -e "\ndistribution built in $(PKG_DIST).tar.gz\n" + + +############################################################## + +.SUFFIXES: .mli .ml .cmi .cmo .cmx .mll .mly .c .o + +.c.o: + $(OCAMLC) $(CCOPT) -c $< + +.mli.cmi: + $(OCAMLC) -c $< + +.ml.cmo: + $(OCAMLC) -c $< + +.ml.cmx: + $(OCAMLOPT) -a str.cmxa -c $< + +.mll.ml: + $(OCAMLLEX) $< + +############################################################## +cleanbyte: + rm -rf *.{cm?,o} *~ + +clean: cleanbyte + rm -f .depend + rm -f $(PROGRAM) + +.depend depend: *.ml *.mli + rm -f .depend + $(OCAMLDEP) *.ml *.mli > .depend + +include .depend diff --git a/README b/README new file mode 100644 index 0000000..2afe8f8 --- /dev/null +++ b/README @@ -0,0 +1,159 @@ +whitelister is a Postfix Policy Server (see [1]). + + +the aim is to accept every really clean mail immediately, and to reserve the +evil treatments (like greylisting) to suspicious mails. + + +BUILDING WHITELISTER +~~~~~~~~~~~~~~~~~~~~ + + you need : + * a recent ocaml distribution (like 3.08.3), supporting Unix module and + native compilation. + * ocamlfind + * the package syslog + + then you only have to run `make' and it should produce a `whitelister' + executable. + + Note for Debian : if you have a deb-src line in your /etc/apt/source.list, + you can get the Build Dependecies of whitelister by simply running : + `apt-get build-dep whitelister' + + +POSTFIX CONFIGURATION +~~~~~~~~~~~~~~~~~~~~~ + + It is intended to be used like that : + + (1) Default inet socket : + + in your main.cf : + + smtpd_recipient_restrictions = + ... + reject_unauth_destination + check_policy_service inet:127.0.0.1:10000 + ... here your nasty treatments , like postgrey ... + + in your whitelister.conf, don't specify sock as its default value is OK. + + + (2) Unix socket + + if you use a unix socket (more efficient on most systems) : + + smtpd_recipient_restrictions = + ... + reject_unauth_destination + check_policy_service unix:private/whitelister.ctl + ... here your nasty treatments , like postgrey ... + + in your whitelister.conf, set : + + sock: /var/spool/postfix/private/whitelister.ctl + + + Some Notes : + ~~~~~~~~~~ + + Postfix DOC states : + + In case of trouble the policy server must not send a reply. Instead the + server must log a warning and disconnect. Postfix will retry the + request at some later time. + + But whitelister does not work that way. Either the mail seems to be clean + (wrt whitelister checks) and whitelister returns 'OK' (and the mail is also + accepted), or one check fails and it returns 'DUNNO' in order to let the + mail go through the nexts checks (like greylister). + + That's why, you *have* to use whitelister as one of the *last* rules in your + smtpd_recipient_restrictions. Else, it can transform your smtp server into + an open-relay. + + Any failure (in DNS e.g.) is also interpreted as a suspicious mail, and it + will also go through the next restrictions checks too. + + +WHITELISTER CONFIGURATION +~~~~~~~~~~~~~~~~~~~~~~~~~ + + whitelister search for two config files, named whitelister.conf in : + o /etc/whitelister.conf + o the same directory as itself + and it aggregates the configuration of both files. + + + the syntax is : « key: value » where key is the setting key, and value its + value without quotes or anything. Comments begin with a # at the first char + of the line see whitelister-example.conf for an example config file. + + + current settings are : + + + (1) Daemon Options + -------------- + + verb verbosity of the logs. + `0' means don't log `Clean' mails + `1' means log everything + + pidf path to the pidfile whitelister will write its pid into. + default is /var/run/whitelister.pid , + use /dev/null if you don't want to use the feature + + sock the socket whitelister will listen to. + the value is the path for a unix socket, + or ip:port for a TCP one. + + last setting is used (/etc/whitelister.conf overrides local configs) + + user name of the user used to run whitelister if launched as root + default is nobody + + group name of the group used to run whitelister if launched as root + default is nogroup + + + (2) Rules Configuration + ------------------- + + rbl hostname of a rbl service + + rhbl_rcpt / rhbl_helo / rhbl_client / rhbl_sender + + hostname of a rhbl service. + + rcpt/helo/client/sender refers to the classical postfix + terminology related to the settings : + smtpd_(recipient, helo, client, sender)_restriction + + it means that an rhbl check is performed on (resp.) : + - the domain of the recipient + - the domain from the HELO/EHLO command + - the domain of the client connection + - the domain part of the sender address + + spf spf checkings : + 0 means no check. + + for the rest, see the table : + -------+-------+----------+----------------+------- + level | pass | neutal | empty record | other + -------+-------+----------+----------------+------- + 1 | ok | ok | ok | !!!! + 2 | ok | ok | !!!! | !!!! + 3 | ok | !!!! | !!!! | !!!! + -------+-------+----------+----------------+------- + + spfrej reject mail on invalid SPF (default is yes), possible values are + on/off/yes/no/0/1 + + +APPENDIX + + [1] http://www.postfix.org/SMTPD_POLICY_README.html + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..443fe4e --- /dev/null +++ b/debian/changelog @@ -0,0 +1,159 @@ +whitelister (0.8-5.evolix1) unstable; urgency=low + + * Disable SPF. + * Test build with Wheezy. + * Switch to dpkg-source 3.0 (quilt) format + + -- Gregory Colpart Mon, 22 Sep 2014 14:05:25 +0200 + +whitelister (0.8-5.evolix) unstable; urgency=low + + * Applied patch for supporting DNS verifications (Closes: #421501) + + -- Gregory Colpart Thu, 27 May 2010 19:34:34 +0200 + +whitelister (0.8-5) unstable; urgency=low + + * Make the logcheck rules less leaky, thanks to Tim Small (Closes: #499499). + + -- Pierre Habouzit Thu, 25 Sep 2008 10:53:04 +0200 + +whitelister (0.8-4.1) unstable; urgency=low + + * Non-maintainer upload to solve release goal. + * Add LSB dependency header to init.d scripts (Closes: #466659). + + -- Petter Reinholdtsen Sun, 30 Mar 2008 11:46:06 +0200 + +whitelister (0.8-4) unstable; urgency=low + + * Update ignore.d/server rules to ignore normal traffic. We especially don't + wan't to monitor `Dirty' from logcheck as there is too many spam around + nowadays. (Closes: #376929). + * Bump Standards-Version to 3.7.2. + + -- Pierre Habouzit Sun, 7 Jan 2007 17:25:39 +0100 + +whitelister (0.8-3) unstable; urgency=high + + * Prevent ftbfs by forcing make depend, urgency set to high as it prevents + ftbfs'es. + + -- Pierre Habouzit Sun, 5 Nov 2006 23:38:09 +0100 + +whitelister (0.8-2) unstable; urgency=low + + * Add kfreebsd-amd64 to the architecture list (closes: #361631). + + -- Pierre Habouzit Sun, 5 Nov 2006 23:37:45 +0100 + +whitelister (0.8-1) unstable; urgency=low + + * New upstream release (Note that whitelister is now under a BSD license). + + -- Pierre Habouzit Mon, 13 Feb 2006 23:16:17 +0100 + +whitelister (0.7-2) unstable; urgency=low + + * Fix upstream bug : group/user inversion in server.ml. + + -- Pierre Habouzit Sat, 14 Jan 2006 16:10:59 +0100 + +whitelister (0.7-1) unstable; urgency=low + + * New upstream release. + + -- Pierre Habouzit Sat, 14 Jan 2006 15:53:42 +0100 + +whitelister (0.6-2) unstable; urgency=low + + * Update archs that have ocamlopt (closes: #347893). + + -- Pierre Habouzit Sat, 14 Jan 2006 13:06:19 +0100 + +whitelister (0.6-1) unstable; urgency=low + + * New upstream release. + * Fixes FD starvation problem (closes: #325282). + + -- Pierre Habouzit Sun, 28 Aug 2005 11:15:58 +0200 + +whitelister (0.5.2-1) unstable; urgency=low + + * New upstream release. + + -- Pierre Habouzit Wed, 24 Aug 2005 02:02:36 +0200 + +whitelister (0.5-1) unstable; urgency=low + + * New upstream release. + * Server loop has been rewritten, so that privileges drop comme after socket + bind/listen (closes: #322582). + * Some improvements in the init script. + + -- Pierre Habouzit Tue, 23 Aug 2005 21:44:16 +0200 + +whitelister (0.4-4) unstable; urgency=low + + * Fix a problem related to set -e in the init script that make the init + script misfunction in case of whitelister failure. + + -- Pierre Habouzit Tue, 23 Aug 2005 19:42:10 +0200 + +whitelister (0.4-3) unstable; urgency=low + + * Add kfreebsd-i386 to the Arch list (closes: #315938). + * Bump Standards Version (no change required). + + -- Pierre Habouzit Tue, 19 Jul 2005 12:56:36 +0200 + +whitelister (0.4-2) unstable; urgency=low + + * Only build the package where ocaml native compiler are supported. + + -- Pierre Habouzit Thu, 23 Jun 2005 14:01:47 +0200 + +whitelister (0.4-1) unstable; urgency=low + + * First upload to debian (closes: #312309). + + -- Pierre Habouzit Tue, 7 Jun 2005 12:21:08 +0200 + +whitelister (0.3-2) UNRELEASED; urgency=low + + * Add a logcheck rule to ignore Clean messages. + + -- Pierre Habouzit Fri, 20 May 2005 15:30:36 +0200 + +whitelister (0.3-1) UNRELEASED; urgency=low + + * New upstram release. + * Use start-stop-daemon with pidfile since whitelister supports it now. + + -- Pierre Habouzit Fri, 20 May 2005 14:22:29 +0200 + +whitelister (0.2.1-1) UNRELEASED; urgency=low + + * New upstream release. + + -- Pierre Habouzit Thu, 19 May 2005 08:28:07 +0200 + +whitelister (0.2-1) UNRELEASED; urgency=low + + * New upstream release. + + -- Pierre Habouzit Wed, 18 May 2005 14:10:34 +0200 + +whitelister (0.1-2) UNRELEASED; urgency=low + + * Add a default empty config. + * Make whitelister a real daemon (live in sbin + init script). + + -- Pierre Habouzit Tue, 17 May 2005 16:15:12 +0200 + +whitelister (0.1-1) UNRELEASED; urgency=low + + * Initial Release. + + -- Pierre Habouzit Tue, 17 May 2005 14:34:39 +0200 + diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7ed6ff8 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +5 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..52b64a6 --- /dev/null +++ b/debian/control @@ -0,0 +1,16 @@ +Source: whitelister +Section: mail +Priority: optional +Maintainer: Pierre Habouzit +Build-Depends: debhelper (>= 4.2.1), cdbs (>=0.4.26), ocaml-base-nox, ocaml-findlib, libsyslog-ocaml-dev +Standards-Version: 3.7.2 + +Package: whitelister +Architecture: alpha amd64 arm hurd-i386 i386 ia64 kfreebsd-i386 kfreebsd-amd64 powerpc sparc +Depends: ${shlibs:Depends}, ${misc:Depends} +Suggests: postgrey +Description: a Postfix Whitelister daemon + whitelister is a Postfix whitelister Policy Daemon aimed to whitelist mails + that come from clean hosts wrt rbl/rhbl in order to let suspicious mails go + through evil treatments like greylisting. + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..5c3c8bc --- /dev/null +++ b/debian/copyright @@ -0,0 +1,13 @@ +This package was debianized by Pierre Habouzit on +Fri, 15 Apr 2005 12:52:37 +0200. + +Copyright Holder: Pierre Habouzit + +License: BSD License + +The original source code for this package was downloaded from +https://projects.aaege.net/mailtools/wiki/Project.Whitelister + +On Debian GNU/Linux systems the complete text of the BSD can be found +in /usr/share/common-licenses/BSD + diff --git a/debian/patches/disable_spf.patch b/debian/patches/disable_spf.patch new file mode 100644 index 0000000..95d3221 --- /dev/null +++ b/debian/patches/disable_spf.patch @@ -0,0 +1,270 @@ +Index: whitelister-0.8/Makefile +=================================================================== +--- whitelister-0.8.orig/Makefile 2014-09-22 16:12:20.655776945 +0200 ++++ whitelister-0.8/Makefile 2014-09-22 16:17:09.571315293 +0200 +@@ -31,23 +31,22 @@ + + PKGS = -package syslog + CFLAGS = -O2 -Wall -fPIC +-CCLIB = -cclib -lspf ++CCLIB = -cclib + CCOPT = $(foreach opt,$(CFLAGS),-ccopt $(opt)) + + OCAMLC = ocamlfind ocamlc $(PKGS) $(CCOPT) $(CCLIB) +-OCAMLOPT= ocamlfind ocamlopt $(PKGS) $(CCOPT) $(CCLIB) ++OCAMLOPT= ocamlfind ocamlopt -linkpkg $(PKGS) $(CCOPT) $(CCLIB) + OCAMLDEP= ocamlfind ocamldep $(PKGS) + + BIB = str.cmxa unix.cmxa syslog.cmxa +-CMX = spf.cmx policy.cmx rules.cmx server.cmx whitelister.cmx +-COB = spfstubs.o ++CMX = policy.cmx rules.cmx server.cmx whitelister.cmx + + ############################################################## + + all: $(PROGRAM) + +-whitelister: $(COB) $(CMX) +- $(OCAMLOPT) -o $@ $(BIB) $^ ++whitelister: $(CMX) ++ ocamlfind ocamlopt $(PKGS) $(CCOPT) -o $@ $(BIB) $^ + + headers: Makefile *.ml *.mli + headache -h tpl/header $^ +@@ -75,7 +74,7 @@ + $(OCAMLC) -c $< + + .ml.cmx: +- $(OCAMLOPT) -c $< ++ $(OCAMLOPT) -a str.cmxa -c $< + + .mll.ml: + $(OCAMLLEX) $< +Index: whitelister-0.8/spf.ml +=================================================================== +--- whitelister-0.8.orig/spf.ml 2014-09-22 16:12:20.655776945 +0200 ++++ /dev/null 1970-01-01 00:00:00.000000000 +0000 +@@ -1,41 +0,0 @@ +-(**************************************************************************) +-(* *) +-(* whitelister : a Whitelister Policy Daemon for Postfix *) +-(* ~~~~~~~~~~~ *) +-(* *) +-(* Copyright (C) 2005 AAEGE.org *) +-(* Author : Pierre Habouzit *) +-(* ____________________________________________________________________ *) +-(* *) +-(* This program is free software; you can redistribute it and/or modify *) +-(* it under the terms of the GNU General Public License as published by *) +-(* the Free Software Foundation; either version 2 of the License, or *) +-(* (at your option) any later version. *) +-(* *) +-(* This program is distributed in the hope that it will be useful, *) +-(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +-(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +-(* GNU General Public License for more details. *) +-(* *) +-(* You should have received a copy of the GNU General Public License *) +-(* along with this program; if not, write to the Free Software *) +-(* Foundation, Inc., *) +-(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +-(* *) +-(**************************************************************************) +- +-exception Error +-type spf_result = +- SPF_pass | SPF_neutral | SPF_none | SPF_softerr | SPF_harderr +- +-external _spf_query : string -> string -> string -> int = "spf_query" +- +-let spf_query host ip helo = +- match _spf_query host ip helo with +- | 0 -> SPF_pass +- | 1 -> SPF_neutral +- | 2 -> SPF_none +- | 3 -> SPF_softerr +- | 4 -> SPF_harderr +- | _ -> raise Error +- +Index: whitelister-0.8/spf.mli +=================================================================== +--- whitelister-0.8.orig/spf.mli 2014-09-22 16:12:20.655776945 +0200 ++++ /dev/null 1970-01-01 00:00:00.000000000 +0000 +@@ -1,33 +0,0 @@ +-(**************************************************************************) +-(* *) +-(* whitelister : a Whitelister Policy Daemon for Postfix *) +-(* ~~~~~~~~~~~ *) +-(* *) +-(* Copyright (C) 2005 AAEGE.org *) +-(* Author : Pierre Habouzit *) +-(* ____________________________________________________________________ *) +-(* *) +-(* This program is free software; you can redistribute it and/or modify *) +-(* it under the terms of the GNU General Public License as published by *) +-(* the Free Software Foundation; either version 2 of the License, or *) +-(* (at your option) any later version. *) +-(* *) +-(* This program is distributed in the hope that it will be useful, *) +-(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +-(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +-(* GNU General Public License for more details. *) +-(* *) +-(* You should have received a copy of the GNU General Public License *) +-(* along with this program; if not, write to the Free Software *) +-(* Foundation, Inc., *) +-(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +-(* *) +-(**************************************************************************) +- +-exception Error +- +-type spf_result = +- SPF_pass | SPF_neutral | SPF_none | SPF_softerr | SPF_harderr +- +-val spf_query : string -> string -> string -> spf_result +- +Index: whitelister-0.8/policy.ml +=================================================================== +--- whitelister-0.8.orig/policy.ml 2014-09-22 16:12:20.655776945 +0200 ++++ whitelister-0.8/policy.ml 2014-09-22 16:12:25.000000000 +0200 +@@ -110,8 +110,3 @@ + let log_format level answer pcy = + Printf.sprintf "%s: %s: %s; %s" + level (log_start pcy) answer (log_end pcy) +- +-let spf_explain pcy = +- let sender = getu pcy "sender" in +- let ip = getu pcy "client_address" in +- Printf.sprintf " - Please see http://spf.pobox.com/why.html?sender=%s&ip=%s" sender ip +Index: whitelister-0.8/policy.mli +=================================================================== +--- whitelister-0.8.orig/policy.mli 2014-09-22 16:12:20.655776945 +0200 ++++ whitelister-0.8/policy.mli 2014-09-22 16:12:25.000000000 +0200 +@@ -42,4 +42,3 @@ + val sender_domain : t -> string + + val log_format : string -> string -> t -> string +-val spf_explain : t -> string +Index: whitelister-0.8/rules.ml +=================================================================== +--- whitelister-0.8.orig/rules.ml 2014-09-22 16:12:20.655776945 +0200 ++++ whitelister-0.8/rules.ml 2014-09-22 16:12:25.000000000 +0200 +@@ -85,25 +85,6 @@ + ) + | Policy.DSN -> () + +-open Spf +- +-type spf_mode = Spf_off | Spf_normal | Spf_strict | Spf_paranoid +- +-let check_spf mode dorej pcy = +- if mode != Spf_off then +- let fail s = +- if dorej then raise (Reject (s ^ Policy.spf_explain pcy)) else raise (Dirty s) +- in try +- match spf_query (sender pcy) (client_address pcy) (helo_name pcy) with +- | SPF_pass -> () +- | SPF_softerr -> fail "SPF soft error" +- | SPF_harderr -> fail "SPF hard error" +- | SPF_none -> if mode != Spf_normal then raise (Dirty "no SPF record found") +- | SPF_neutral -> if mode = Spf_paranoid then raise (Dirty "SPF neutral") +- with +- | Spf.Error -> raise (Dirty "SPF Internal error") +- | Policy.DSN -> () +- + let check_dns_client dorej pcy = + if dorej then + if (client_name pcy) = "unknown" then raise (Dirty "no client_name (reject_unknown_client)") +Index: whitelister-0.8/spfstubs.c +=================================================================== +--- whitelister-0.8.orig/spfstubs.c 2014-09-22 16:12:20.655776945 +0200 ++++ /dev/null 1970-01-01 00:00:00.000000000 +0000 +@@ -1,42 +0,0 @@ +-#include +-#include +- +-#include +- +-value spf_query(value from, value ip, value helo) +-{ +- int res; +- peer_info_t* peer_info; +- +- CAMLparam3(from, ip, helo); +- +- peer_info = SPF_init("whitelister", String_val(ip), NULL, NULL, NULL, 0, 0); +- SPF_smtp_helo(peer_info, String_val(helo)); +- SPF_smtp_from(peer_info, String_val(from)); +- res = peer_info->RES = SPF_policy_main(peer_info); +- +- SPF_close(peer_info); +- +- switch(res) +- { +- case SPF_PASS: +- CAMLreturn(Val_int(0)); +- +- case SPF_NEUTRAL: +- CAMLreturn(Val_int(1)); +- +- case SPF_NONE: +- CAMLreturn(Val_int(2)); +- +- case SPF_S_FAIL: +- CAMLreturn(Val_int(3)); +- +- case SPF_H_FAIL: +- CAMLreturn(Val_int(4)); +- +- default: /* SPF_ERROR, SPF_UNKNOWN, SPF_UNMECH */ +- CAMLreturn(Val_int(5)); +- +- } +-} +- +Index: whitelister-0.8/whitelister.ml +=================================================================== +--- whitelister-0.8.orig/whitelister.ml 2014-09-22 16:12:20.655776945 +0200 ++++ whitelister-0.8/whitelister.ml 2014-09-22 16:12:25.000000000 +0200 +@@ -33,9 +33,6 @@ + mutable group: string; + mutable verb: int; + +- mutable spf: Rules.spf_mode; +- mutable spfrej: bool; +- + mutable rbl: string list; + mutable rhbl_client: string list; + mutable rhbl_rcpt: string list; +@@ -68,7 +65,6 @@ + Rules.check_rhbl Rules.Sender cfg.rhbl_sender pcy; + Rules.check_rhbl Rules.Rcpt cfg.rhbl_rcpt pcy; + Rules.check_rhbl Rules.Client cfg.rhbl_client pcy; +- Rules.check_spf cfg.spf cfg.spfrej pcy; + Rules.check_dns_client cfg.dns_client pcy; + Rules.check_dns_rev_client cfg.dns_rev_client pcy; + if cfg.verb > 0 then log_event "Clean" "OK" pcy; +@@ -97,9 +93,6 @@ + group = "nogroup" ; + verb = 0 ; + +- spf = Rules.Spf_off ; +- spfrej = false; +- + rbl = [] ; + rhbl_client = [] ; + rhbl_rcpt = [] ; +@@ -133,12 +126,6 @@ + | ["verb"; "0"] -> cfg.verb <- 0 + | ["verb"; "1"] -> cfg.verb <- 1 + +- | ["spf"; "0"] -> cfg.spf <- Rules.Spf_off +- | ["spf"; "1"] -> cfg.spf <- Rules.Spf_normal +- | ["spf"; "2"] -> cfg.spf <- Rules.Spf_strict +- | ["spf"; "3"] -> cfg.spf <- Rules.Spf_paranoid +- | ["spfrej"; b] -> cfg.spfrej <- to_bool "spfrej" b +- + | ["rbl"; h] -> cfg.rbl <- h::cfg.rbl + | ["rhbl_client"; h] -> cfg.rhbl_client <- h::cfg.rhbl_client + | ["rhbl_helo"; h] -> cfg.rhbl_helo <- h::cfg.rhbl_helo diff --git a/debian/patches/reject_unknown_client.patch b/debian/patches/reject_unknown_client.patch new file mode 100644 index 0000000..a4b319c --- /dev/null +++ b/debian/patches/reject_unknown_client.patch @@ -0,0 +1,131 @@ +--- whitelister-0.8.orig/whitelister-example.conf ++++ whitelister-0.8/whitelister-example.conf +@@ -61,3 +61,8 @@ + # spfrej + # what to do with spf rejects, default is nothing. ignored if spf is off + # spfrej: off ++ ++# dns ++# support dns verifications (default is 0) ++# dns_client: 1 ++# dns_rev_client: 1 +--- whitelister-0.8.orig/rules.mli ++++ whitelister-0.8/rules.mli +@@ -48,3 +48,7 @@ + val check_rhbl : rhbl_type -> string list -> Policy.t -> unit + + val check_spf : spf_mode -> bool -> Policy.t -> unit ++ ++val check_dns_client : bool -> Policy.t -> unit ++ ++val check_dns_rev_client : bool -> Policy.t -> unit +--- whitelister-0.8.orig/rules.ml ++++ whitelister-0.8/rules.ml +@@ -104,3 +104,13 @@ + | Spf.Error -> raise (Dirty "SPF Internal error") + | Policy.DSN -> () + ++let check_dns_client dorej pcy = ++ if dorej then ++ if (client_name pcy) = "unknown" then raise (Dirty "no client_name (reject_unknown_client)") ++ else () ++ ++let check_dns_rev_client dorej pcy = ++ if dorej then ++ if (reverse_client_name pcy) = "unknown" then raise (Dirty "no reverse_client_name (reject_unknown_reverse_client)") ++ else () ++ +--- whitelister-0.8.orig/Makefile ++++ whitelister-0.8/Makefile +@@ -85,6 +85,7 @@ + rm -rf *.{cm?,o} *~ + + clean: cleanbyte ++ rm -f .depend + rm -f $(PROGRAM) + + .depend depend: *.ml *.mli +@@ -92,4 +93,3 @@ + $(OCAMLDEP) *.ml *.mli > .depend + + include .depend +- +--- whitelister-0.8.orig/whitelister.ml ++++ whitelister-0.8/whitelister.ml +@@ -41,6 +41,9 @@ + mutable rhbl_rcpt: string list; + mutable rhbl_sender: string list; + mutable rhbl_helo: string list; ++ ++ mutable dns_client: bool; ++ mutable dns_rev_client: bool; + } + + (* Checker *) +@@ -65,7 +68,9 @@ + Rules.check_rhbl Rules.Sender cfg.rhbl_sender pcy; + Rules.check_rhbl Rules.Rcpt cfg.rhbl_rcpt pcy; + Rules.check_rhbl Rules.Client cfg.rhbl_client pcy; +- Rules.check_spf cfg.spf cfg.spfrej pcy; ++ Rules.check_spf cfg.spf cfg.spfrej pcy; ++ Rules.check_dns_client cfg.dns_client pcy; ++ Rules.check_dns_rev_client cfg.dns_rev_client pcy; + if cfg.verb > 0 then log_event "Clean" "OK" pcy; + "OK" + with +@@ -100,6 +105,9 @@ + rhbl_rcpt = [] ; + rhbl_sender = [] ; + rhbl_helo = [] ; ++ ++ dns_client = false; ++ dns_rev_client = false; + } + + let to_bool s = +@@ -137,6 +145,9 @@ + | ["rhbl_rcpt"; h] -> cfg.rhbl_rcpt <- h::cfg.rhbl_rcpt + | ["rhbl_sender"; h] -> cfg.rhbl_sender <- h::cfg.rhbl_sender + ++ | ["dns_client"; d] -> cfg.dns_client <- to_bool "dns_client" d ++ | ["dns_rev_client"; e] -> cfg.dns_rev_client <- to_bool "dns_rev_client" e ++ + (* deprecated settings *) + | ["rhbl"; h] -> prerr_endline "rhbl is deprecated, it defaults to rhbl_client which may not be what you want."; + cfg.rhbl_client <- h::cfg.rhbl_client +--- whitelister-0.8.orig/policy.ml ++++ whitelister-0.8/policy.ml +@@ -64,10 +64,11 @@ + + let log_start : t -> string = fun pcy -> + Printf.sprintf +- "%s from %s[%s]" +- ( getu pcy "protocol_state" ) +- ( getu pcy "client_name" ) +- ( getu pcy "client_address" ) ++ "%s from %s[%s][%s]" ++ ( getu pcy "protocol_state" ) ++ ( getu pcy "client_name" ) ++ ( getu pcy "client_address" ) ++ ( getu pcy "reverse_client_name" ) + + + (* public *) +@@ -91,6 +92,7 @@ + + let client_address h = get h "client_address" + let client_name h = get h "client_name" ++let reverse_client_name h = get h "reverse_client_name" + let helo_name h = get h "helo_name" + let sender h = + try +--- whitelister-0.8.orig/policy.mli ++++ whitelister-0.8/policy.mli +@@ -35,6 +35,7 @@ + + val client_address : t -> string + val client_name : t -> string ++val reverse_client_name : t -> string + val sender : t -> string + val helo_name : t -> string + val rcpt_domain: t -> string diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 0000000..d12d092 --- /dev/null +++ b/debian/patches/series @@ -0,0 +1,2 @@ +reject_unknown_client.patch +disable_spf.patch diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..29b30b6 --- /dev/null +++ b/debian/rules @@ -0,0 +1,17 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +DEB_DH_INSTALLINIT_ARGS := -- defaults 19 + +include /usr/share/cdbs/1/class/makefile.mk +include /usr/share/cdbs/1/rules/debhelper.mk + +DEB_MAKE_CLEAN_TARGET := clean +DEB_MAKE_BUILD_TARGET := all +DEB_MAKE_INSTALL_TARGET := + +DEB_INSTALL_DOCS_ALL := README +DEB_INSTALL_CHANGELOG_ALL := ChangeLog + +common-build-arch common-build-indep:: + $(MAKE) depend diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/whitelister.conf b/debian/whitelister.conf new file mode 100644 index 0000000..5789e63 --- /dev/null +++ b/debian/whitelister.conf @@ -0,0 +1,23 @@ +## +## configuration for whitelister +## + +## sock +## socket the server has to listen to +## either unix or tcp socket are possible. +## syntax is ip:port (the :port is required) for tcp sockets +## defaults : 127.0.0.1:10000 + +#sock: 127.0.0.1:10000 + +## rbl +## put one rbl per line + +#rbl: dynablock.njabl.org +#rbl: dul.dnsbl.sorbs.net + +## rhbls +## put one rhbl per line + +#rhbl: bogusmx.rfc-ignorant.org + diff --git a/debian/whitelister.init b/debian/whitelister.init new file mode 100644 index 0000000..b8a6acb --- /dev/null +++ b/debian/whitelister.init @@ -0,0 +1,73 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: whitelister +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# X-Start-Before: postfix +# X-Stop-After: postfix +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +### END INIT INFO +# +# whitelister start/stop the whitelister deamon for postfix +# (priority should be smaller than that of postfix) +# +# Author: (c)2005 Pierre Habouzit +# Based on Debian sarge's 'skeleton' example +# Distribute and/or modify at will. +# +# Version: $Id: whitelister.init,v 1.3 2005/05/20 13:13:14 x2000habouzit Exp $ +# + +set -e + +PATH=/sbin:/bin:/usr/sbin:/usr/bin +DESC="postfix whitelister daemon" +NAME=whitelister +DAEMON=/usr/sbin/$NAME +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +# Gracefully exit if the package has been removed. +test -x $DAEMON || exit 0 + +d_start() { + start-stop-daemon --start --quiet --pidfile $PIDFILE \ + --exec $DAEMON || echo -n " (already running)" +} + +d_stop() { + start-stop-daemon --stop --oknodo --quiet --pidfile $PIDFILE \ + --name $NAME + rm -f $PIDFILE +} + +d_restart() { + d_stop + sleep 1 + d_start +} + +case "$1" in + start) + echo -n "Starting $DESC: $NAME" + d_start + echo "." + ;; + stop) + echo -n "Stopping $DESC: $NAME" + d_stop + echo "." + ;; + force-reload|restart) + echo -n "Restarting $DESC: $NAME" + d_restart + echo "." + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/debian/whitelister.install b/debian/whitelister.install new file mode 100644 index 0000000..ba2a639 --- /dev/null +++ b/debian/whitelister.install @@ -0,0 +1,3 @@ +whitelister-example.conf /usr/share/doc/whitelister/examples/ +whitelister /usr/sbin/ +debian/whitelister.conf /etc/ diff --git a/debian/whitelister.logcheck.ignore.server b/debian/whitelister.logcheck.ignore.server new file mode 100644 index 0000000..8788a0d --- /dev/null +++ b/debian/whitelister.logcheck.ignore.server @@ -0,0 +1,4 @@ +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ whitelister\[[0-9]+\]: Clean$ +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ whitelister\[[0-9]+\]: listening to inet:[:.[:xdigit:]]+:[[:digit:]]$ +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ whitelister\[[0-9]+\]: listening to unix:[-/._[:alnum:]]+$ +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ whitelister\[[0-9]+\]: (Dirty|Reject): RCPT from [._[:alnum:]-]+\[([0-9.]{7,15}|[0-9a-fA-F:.]{4,39})\]: (DUNNO \(.*\)|REJECT SPF.*); from=<.*> to=<.*> proto=E?SMTP helo=<.*>$ diff --git a/policy.ml b/policy.ml new file mode 100644 index 0000000..b77287d --- /dev/null +++ b/policy.ml @@ -0,0 +1,112 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +exception ParseError +exception Unknown +exception DSN + +type t = (string, string) Hashtbl.t + +(* Private : access to hashtbl *) + +let get h k = + try Hashtbl.find h k + with Not_found -> raise Unknown + +let getu h k = + try Hashtbl.find h k + with Not_found -> "unknown" + +let check_policy pcy = + if getu pcy "request" = "smtpd_access_policy" then + pcy + else + raise ParseError + +let domain s = + try + Str.string_after s ((String.index s '@')+1) + with Not_found -> raise Unknown (* case of rpcts / sender that are not email addresses *) + + +(* Private : log formats *) + +let log_end pcy = + Printf.sprintf + "from=<%s> to=<%s> proto=%s helo=<%s>" + ( getu pcy "sender" ) + ( getu pcy "recipient" ) + ( getu pcy "protocol_name" ) + ( getu pcy "helo_name" ) + +let log_start : t -> string = fun pcy -> + Printf.sprintf + "%s from %s[%s][%s]" + ( getu pcy "protocol_state" ) + ( getu pcy "client_name" ) + ( getu pcy "client_address" ) + ( getu pcy "reverse_client_name" ) + + +(* public *) + +let read ic = + let res = Hashtbl.create 13 in + try + while true do + try + let line = input_line ic in + if String.length line = 0 then raise End_of_file; + let i = String.index line '=' in + Hashtbl.add res (Str.string_before line i) (Str.string_after line (i+1)) + with Not_found -> raise ParseError + done; + assert false + with + | End_of_file -> check_policy res + +let clear = Hashtbl.clear + +let client_address h = get h "client_address" +let client_name h = get h "client_name" +let reverse_client_name h = get h "reverse_client_name" +let helo_name h = get h "helo_name" +let sender h = + try + ( + match Hashtbl.find h "sender" with + | "" -> raise DSN + | s -> s + ) + with Not_found -> raise DSN + +let rcpt_domain h = domain (get h "recipient") +let sender_domain h = domain (sender h) + + +let log_format level answer pcy = + Printf.sprintf "%s: %s: %s; %s" + level (log_start pcy) answer (log_end pcy) diff --git a/policy.mli b/policy.mli new file mode 100644 index 0000000..3fd8b7f --- /dev/null +++ b/policy.mli @@ -0,0 +1,44 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +type t + +exception ParseError +exception Unknown +exception DSN + +val read : in_channel -> t +val clear : t -> unit + +val client_address : t -> string +val client_name : t -> string +val reverse_client_name : t -> string +val sender : t -> string +val helo_name : t -> string +val rcpt_domain: t -> string +val sender_domain : t -> string + +val log_format : string -> string -> t -> string diff --git a/rules.ml b/rules.ml new file mode 100644 index 0000000..9cb3655 --- /dev/null +++ b/rules.ml @@ -0,0 +1,97 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +open Policy + +exception Dirty of string +exception Reject of string + +type rhbl_type = Helo | Rcpt | Sender | Client + +(* HELPERS *) + +(** [rev_ip "ip1.ip2.ip3.ip4"] returns ["ip4.ip3.ip2.ip1"] *) +let rev_ip ip = + let l = Str.split (Str.regexp_string ".") ip in + String.concat "." (List.rev l) + +(** [dns_check r i h] returns : + * [i+1] if ["$r.$h"] domain exists, + * [i] if domain does not exists, + * [i+1] if any error occur. + * This is meant to be use with List.fold_left + *) +let dns_check rad host = + try + let _ = Unix.gethostbyname (rad ^ "." ^ host) in + raise (Dirty ("blacklisted by " ^ host)) + with + | Not_found -> () + | Dirty _ as e -> raise e + | _ -> raise (Dirty "rbl failure") + + +let rhbl_extract_domain = + function + | Helo -> helo_name + | Rcpt -> helo_name + | Sender -> sender_domain + | Client -> client_name + +(* PUBLIC INTERFACE *) + +let check_rbl rbl_list pcy = + try + let revip = rev_ip (client_address pcy) in + List.iter (dns_check revip) rbl_list + with + | Policy.Unknown -> raise (Dirty "no `client_address' found") + | Policy.DSN -> () + +let check_rhbl kind rhbl_list pcy = + try + let host = (rhbl_extract_domain kind) pcy in + List.iter (dns_check host) rhbl_list + with + | Policy.Unknown -> + ( + match kind with + | Helo | Rcpt -> raise (Dirty "no `helo_name' found") + | Sender -> raise (Dirty "no `sender_name' found") + | Client -> raise (Dirty "no `client_name' found") + ) + | Policy.DSN -> () + +let check_dns_client dorej pcy = + if dorej then + if (client_name pcy) = "unknown" then raise (Dirty "no client_name (reject_unknown_client)") + else () + +let check_dns_rev_client dorej pcy = + if dorej then + if (reverse_client_name pcy) = "unknown" then raise (Dirty "no reverse_client_name (reject_unknown_reverse_client)") + else () + diff --git a/rules.mli b/rules.mli new file mode 100644 index 0000000..dc10e94 --- /dev/null +++ b/rules.mli @@ -0,0 +1,54 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +exception Dirty of string +exception Reject of string +type spf_mode = Spf_off | Spf_normal | Spf_strict | Spf_paranoid +type rhbl_type = Helo | Rcpt | Sender | Client + +(** + * this module defines some rules that a Policy should verify to be whitelisted. + * + * quoting POSTFIX POLICY README [1] : + * In case of trouble the policy server must not send a reply. + * Instead the server must log a warning and disconnect. + * Postfix will retry the request at some later time. + * + * We DON'T follow that policy since we write a WHITELIST server. + * So our choice is that any problem is similar to a invalid Check. + * + * [1] http://www.postfix.org/SMTPD_POLICY_README.html + *) + +val check_rbl : string list -> Policy.t -> unit + +val check_rhbl : rhbl_type -> string list -> Policy.t -> unit + +val check_spf : spf_mode -> bool -> Policy.t -> unit + +val check_dns_client : bool -> Policy.t -> unit + +val check_dns_rev_client : bool -> Policy.t -> unit diff --git a/server.ml b/server.ml new file mode 100644 index 0000000..0f6e8e8 --- /dev/null +++ b/server.ml @@ -0,0 +1,96 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +open Unix + +let string_of_sockaddr = function + | ADDR_UNIX u -> "unix:"^u + | ADDR_INET (a,p) -> Printf.sprintf "inet:%s:%i" (string_of_inet_addr a) p + +let rec unix_do f x = + try f x + with Unix_error(EINTR, _, _) -> unix_do f x + +(* pid file stuff *) + +let pidfile_fd = ref None + +let pidfile_close () = + match !pidfile_fd with + | Some f -> close f; pidfile_fd := None + | None -> () + +let pidfile_open file = + try + pidfile_close (); + pidfile_fd := Some (openfile file [O_WRONLY;O_CREAT;O_TRUNC] 0o644) + with Sys_error _ -> + prerr_endline ("Cannot write my pid in the pidfile "^file); + exit 1 + +let pidfile_write () = + match !pidfile_fd with + | None -> + prerr_endline ("pidfile is not open, call pidfile_open before pidfile_write"); + exit 1 + | Some fd -> + ignore (lseek fd 0 SEEK_SET); + ftruncate fd 0; + let pid = Printf.sprintf "%i\n" (getpid()) in + ignore (unix_do (single_write fd pid 0) (String.length pid)) + +(* server stuff *) + +let drop_privs ( user, group ) = + if geteuid () = 0 then ( + setgid (getgrnam group).gr_gid; + setuid (getpwnam user).pw_uid + ) + +let bind_to_sock sockaddr = + (match sockaddr with ADDR_UNIX u -> (try unlink u with _ -> ()) | _ -> ()); + let mask = umask 0o111 in + let sock = socket (domain_of_sockaddr sockaddr) SOCK_STREAM 0 in + setsockopt sock SO_REUSEADDR true; + bind sock sockaddr; + listen sock 0; + let _ = umask mask in + sock + +let double_fork f x = + match fork () with + | 0 -> if fork () <> 0 then exit 0; f x + | id -> ignore (waitpid [] id) + +let daemonize runas server_loop arg = + drop_privs runas; + let dev_null = openfile "/dev/null" [O_WRONLY] 0o666 in + close stdin; + dup2 dev_null stdout; + dup2 dev_null stderr; + close dev_null; + double_fork server_loop arg + diff --git a/server.mli b/server.mli new file mode 100644 index 0000000..333236a --- /dev/null +++ b/server.mli @@ -0,0 +1,39 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +val string_of_sockaddr : Unix.sockaddr -> string +val unix_do : ('a -> 'b) -> 'a -> 'b + +val pidfile_open : string -> unit +val pidfile_write : unit -> unit +val pidfile_close : unit -> unit + +val drop_privs : string * string -> unit + +val bind_to_sock : Unix.sockaddr -> Unix.file_descr +val double_fork : ('a -> unit) -> 'a -> unit +val daemonize : string * string -> ('a -> unit) -> 'a -> unit + diff --git a/tpl/header b/tpl/header new file mode 100644 index 0000000..7eff27d --- /dev/null +++ b/tpl/header @@ -0,0 +1,23 @@ + + whitelister : a Whitelister Policy Daemon for Postfix + ~~~~~~~~~~~ + +Copyright (C) 2005 AAEGE.org +Author : Pierre Habouzit +____________________________________________________________________ + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., +59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + diff --git a/whitelister-example.conf b/whitelister-example.conf new file mode 100644 index 0000000..3920d65 --- /dev/null +++ b/whitelister-example.conf @@ -0,0 +1,68 @@ +## +## Example config for whitelister. +## put this file in /etc/whitelister.conf +## + +################################################################################ +# +# 1. DAEMON CONFIGURATION +# +################################################################################ + +# verb +# verbosity of the logs +# 0: `Clean' notifications are off +# 1: enable `Clean' notifications + +# pidf +# path to the pidfile whitelister has to use +# default is /var/run/whitelister.pid +# examples: +# pidf: /dev/null + +# sock +# socket the server has to listen to +# either unix or tcp socket are possible. +# syntax is ip:port (the :port is required) for tcp sockets +# default is 127.0.0.1:10000 +# examples : +# sock: /var/spool/postfix/private/whitelister.ctl +# sock: 127.0.0.1:100 + +# user +# name of the user that whitelister will run under if launched from root +# default is nobody +user: nobody + +# group +# name of the group that whitelister will run under if launched from root +# default is nogroup +group: nogroup + +################################################################################ +# +# 2. RULES CONFIGURATION +# +################################################################################ + +# rbl +# put one rbl per line +rbl: dynablock.njabl.org +rbl: dul.dnsbl.sorbs.net + +# rhbl_* : rhbl_client / rhbl_sender / rhbl_rcpt / rhbl_helo +# put one rhbl per line +rhbl_sender: bogusmx.rfc-ignorant.org + +# spf +# use spf diagnostics (default is 0) +# spf: 1 + +# spfrej +# what to do with spf rejects, default is nothing. ignored if spf is off +# spfrej: off + +# dns +# support dns verifications (default is 0) +# dns_client: 1 +# dns_rev_client: 1 diff --git a/whitelister.ml b/whitelister.ml new file mode 100644 index 0000000..05b133a --- /dev/null +++ b/whitelister.ml @@ -0,0 +1,171 @@ +(**************************************************************************) +(* *) +(* whitelister : a Whitelister Policy Daemon for Postfix *) +(* ~~~~~~~~~~~ *) +(* *) +(* Copyright (C) 2005 AAEGE.org *) +(* Author : Pierre Habouzit *) +(* ____________________________________________________________________ *) +(* *) +(* This program is free software; you can redistribute it and/or modify *) +(* it under the terms of the GNU General Public License as published by *) +(* the Free Software Foundation; either version 2 of the License, or *) +(* (at your option) any later version. *) +(* *) +(* This program is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU General Public License for more details. *) +(* *) +(* You should have received a copy of the GNU General Public License *) +(* along with this program; if not, write to the Free Software *) +(* Foundation, Inc., *) +(* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *) +(* *) +(**************************************************************************) + +(* types *) + +type config = { + mutable pidf: string; + mutable sock: Unix.sockaddr; + mutable user: string; + mutable group: string; + mutable verb: int; + + mutable rbl: string list; + mutable rhbl_client: string list; + mutable rhbl_rcpt: string list; + mutable rhbl_sender: string list; + mutable rhbl_helo: string list; + + mutable dns_client: bool; + mutable dns_rev_client: bool; +} + +(* Checker *) + +let log msg = + let lg = Syslog.openlog ~facility:`LOG_MAIL ~flags:[`LOG_PID] "whitelister" in + Syslog.syslog lg `LOG_INFO msg; + Syslog.closelog lg + +let log_event level answer pcy = + log (Policy.log_format level answer pcy) + +let checker cfg s = + let oc = Unix.out_channel_of_descr s in + try + while true do + let pcy = Policy.read (Unix.in_channel_of_descr s) in + let ans = ( + try + Rules.check_rbl cfg.rbl pcy; + Rules.check_rhbl Rules.Helo cfg.rhbl_helo pcy; + Rules.check_rhbl Rules.Sender cfg.rhbl_sender pcy; + Rules.check_rhbl Rules.Rcpt cfg.rhbl_rcpt pcy; + Rules.check_rhbl Rules.Client cfg.rhbl_client pcy; + Rules.check_dns_client cfg.dns_client pcy; + Rules.check_dns_rev_client cfg.dns_rev_client pcy; + if cfg.verb > 0 then log_event "Clean" "OK" pcy; + "OK" + with + | Rules.Dirty s -> + log_event "Dirty" (Printf.sprintf "DUNNO (%s)" s) pcy; + "DUNNO" + | Rules.Reject s -> + let s' = "REJECT "^s in + log_event "Reject" s' pcy; + s' + ) in + Printf.fprintf oc "action=%s\n\n" ans; + flush oc; + Policy.clear pcy (* not needed, but can help the GC *) + done + with _ -> Unix.shutdown s Unix.SHUTDOWN_ALL + +(* Configuration *) + +let empty_config () = { + pidf = "/var/run/whitelister.pid" ; + sock = Unix.ADDR_INET (Unix.inet_addr_of_string "127.0.0.1", 10000) ; + user = "nobody" ; + group = "nogroup" ; + verb = 0 ; + + rbl = [] ; + rhbl_client = [] ; + rhbl_rcpt = [] ; + rhbl_sender = [] ; + rhbl_helo = [] ; + + dns_client = false; + dns_rev_client = false; +} + +let to_bool s = + function + | "0" | "off" | "no" -> false + | "1" | "yes" | "on" -> true + | _ -> prerr_endline ("cannot read a boolean value for `"^s^"', possible values are on/off, yes/no, 1/0"); exit 1 + +let update_config cfg file = + let ic = open_in file in + try + while true do + let line = input_line ic in + if String.length line > 0 && line.[0] <> '#' then + match Str.split (Str.regexp "[ \t:]+") line with + | [] -> () + + | ["pidf"; f] -> cfg.pidf <- f + | ["sock"; s] -> cfg.sock <- Unix.ADDR_UNIX s + | ["sock"; ip; p] -> cfg.sock <- Unix.ADDR_INET (Unix.inet_addr_of_string ip, int_of_string p) + | ["user"; u] -> cfg.user <- u + | ["group"; g] -> cfg.group <- g + | ["verb"; "0"] -> cfg.verb <- 0 + | ["verb"; "1"] -> cfg.verb <- 1 + + | ["rbl"; h] -> cfg.rbl <- h::cfg.rbl + | ["rhbl_client"; h] -> cfg.rhbl_client <- h::cfg.rhbl_client + | ["rhbl_helo"; h] -> cfg.rhbl_helo <- h::cfg.rhbl_helo + | ["rhbl_rcpt"; h] -> cfg.rhbl_rcpt <- h::cfg.rhbl_rcpt + | ["rhbl_sender"; h] -> cfg.rhbl_sender <- h::cfg.rhbl_sender + + | ["dns_client"; d] -> cfg.dns_client <- to_bool "dns_client" d + | ["dns_rev_client"; e] -> cfg.dns_rev_client <- to_bool "dns_rev_client" e + + (* deprecated settings *) + | ["rhbl"; h] -> prerr_endline "rhbl is deprecated, it defaults to rhbl_client which may not be what you want."; + cfg.rhbl_client <- h::cfg.rhbl_client + | _ -> prerr_string (Printf.sprintf "`%s' is not a valid config line\n" line) + done; + assert false + with End_of_file -> close_in ic; cfg + +let read_config () = + let found_files = List.filter Sys.file_exists ["/etc/whitelister.conf" ; "whitelister.conf"] in + List.fold_left update_config (empty_config ()) found_files + +(* Server thingies *) + +open Server + +let my_server_loop cfg sock = + pidfile_write (); + pidfile_close (); + log ("started, listening to " ^ (string_of_sockaddr cfg.sock)); + while true do + let s = fst (unix_do Unix.accept sock) in + double_fork (fun s -> Unix.close sock; checker cfg s ; exit 0) s; + Unix.close s + done + +(* Main LOOP *) + +let _ = + let cfg = read_config () in + pidfile_open cfg.pidf; + pidfile_write (); + daemonize (cfg.user,cfg.group) (my_server_loop cfg) (bind_to_sock cfg.sock) +