—#!/usr/bin/env perl
use
strict;
use
warnings;
use
Getopt::Long;
use
JSON;
use
Module::Load;
use
Pod::Usage;
# ABSTRACT: A Perl Mail Authentication Milter
our
$VERSION
=
'2.20200202'
;
# VERSION
# PODNAME: authentication_milter
# CONFIG
my
$pid_file
=
'/run/authentication_milter.pid'
;
my
$daemon
= 0;
my
$help
= 0;
my
$prefix
;
my
$control
;
my
$ident
;
GetOptions (
"daemon"
=> \
$daemon
,
"control=s"
=> \
$control
,
"pidfile=s"
=> \
$pid_file
,
"help:s"
=> \
$help
,
"prefix=s"
=> \
$prefix
,
"ident=s"
=> \
$ident
,
) or
die
"Error in command line arguments\n"
;
# Reconstruct relevant ARGV line for restart
push
@ARGV
,
'--daemon'
if
$daemon
;
push
@ARGV
,
'--pidfile='
.
$pid_file
if
$pid_file
;
push
@ARGV
,
'--prefix='
.
$prefix
if
$prefix
;
push
@ARGV
,
'--ident='
.
$ident
if
$ident
;
if
(
$help
eq
q{}
) {
usage();
exit
0;
}
elsif
(
$help
) {
if
(
$help
eq
'installed'
) {
modules_installed();
}
elsif
(
$help
eq
'default_config'
) {
show_default_config();
}
else
{
module_usage(
$help
);
}
exit
0;
}
if
(
$prefix
) {
$Mail::Milter::Authentication::Config::PREFIX
=
$prefix
;
}
if
(
$ident
) {
$Mail::Milter::Authentication::Config::IDENT
.=
'_'
.
$ident
;
}
if
(
$control
) {
Mail::Milter::Authentication::control({
'command'
=>
$control
,
'pid_file'
=>
$pid_file
,
});
exit
0;
}
Mail::Milter::Authentication::start({
'pid_file'
=>
$pid_file
,
'daemon'
=>
$daemon
,
});
sub
usage {
pod2usage(
-verbose
=> 2 );
return
;
}
sub
module_usage {
my
(
$module
) =
@_
;
$module
=~ s/[^a-zA-Z0-9]//g;
my
$full_module
=
'Mail::Milter::Authentication::Handler::'
.
$module
;
eval
{
load
$full_module
;
};
if
(
my
$error
= $@ ) {
die
"Could not find help for $module"
;
}
my
$part_path
=
$full_module
;
$part_path
=~ s/::/\//g;
$part_path
.=
'.pm'
;
my
$module_file
=
$INC
{
$part_path
};
if
( !
$module_file
) {
die
"Could not find help for $module"
;
}
pod2usage(
-input
=>
$module_file
,
-verbose
=> 2 );
return
;
}
sub
modules_installed {
my
$installed_handlers
= Mail::Milter::Authentication::get_installed_handlers();
"Authentication Milter\n\n"
;
join
(
"\n "
,
'Installed handlers:'
,
uniq
sort
@$installed_handlers
,
);
"\n\n"
;
return
;
}
sub
show_default_config {
my
$default_config
= default_config();
my
$json
= JSON->new();
$json
->pretty();
$json
->canonical( 1 );
$json
->encode(
$default_config
);
exit
0;
}
__END__
=pod
=encoding UTF-8
=head1 NAME
authentication_milter - A Perl Mail Authentication Milter
=head1 VERSION
version 2.20200202
=head1 USAGE
authentication_milter [-c|--control <command>] [-d|--daemon] [--pidfile <file>] [-h|--help] [--prefix <dir>] [-i|--ident <ident>]
=head1 OPTIONS
=over
=item -c|--control <command>
Control a running daemon process or start a new one.
start|restart
Start a new daemon process, or restart an existing process.
stop
Stop an already running daemon process.
status
Show the status of a running daemon process.
Control implies --daemon, and takes account of --pidfile
If no valid data is found in the pidfile then control will
search the process list for a master process.
If you are running multiple distinct instances of authentication
milter on a single host, each with differing configurations then you
should use the --ident identifier to differentiate between them.
=item -h|--help
Show this help.
=item -h|--help default_config
Output an example default configuration, including
config for all installed handler modules.
=item -h|--help installed
Show a list of installed handler modules.
=item -h|--help <ModuleName>
Show help for a particular handler module.
Modules installed by default include the following.
AddID Auth DKIM DMARC IPRev LocalIP PTR ReturnOK
Sanitize SenderID SpamAssassin SPF TrustedIP TLS
=item -d|--daemon
detach from shell and run as a daemon
=item --pidfile <file>
Write the process PID to the given file.
defaults to /run/authentication_milter.pid
=item --prefix <dir>
Read configuration from dir rather than /etc/
=item -i|--ident <ident>
A string identifier for use in process management and logging
This should be used when you are running multiple instances of
authentication_milter on a single host, each with different configurations.
=back
=head1 CONFIGURATION
The milter reads configuration from /etc/authentication_milter.json
The configuration file format is as follows...
{
"external_callback_processor" : "My::Module", | Name of module containing external callback methods
"debug" : 0, | Verbose debugging output
"dryrun" : 0, | Dryrun (do not alter or reject mail)
"logtoerr" : 0, | Also write logs to STDERR
"error_log" : "/var/log/authentication_milter.err", | Capture STDERR to logfile
"log_dispatchouli" : {}, | Optional args to pass directly into Log::Dispatchouli->new()
"connection" : "inet:12345@localhost", | The connection to use
"umask" : "0000", | Set umask (for unix socket)
"connections" : { | Other than the default connection, also bind to
| these connections.
"name_two" : { | Name of connection
"connection" : "unix:/var/sock/a.sock", | The connection to use
"umask" : "0000", | Set umask
}
"name_one" : { | Name of connection
"connection" : "inet:12346@localhost", | The connection to use
}
},
"runas" : "nobody", | Drop privs and run as this user (root only)
"rungroup" : "nogroup", | Drop privs and run as this group (root daemon only)
"chroot" : "/path/to/chroot" | Set chroot before forking (root only)
| N.B. This path will need to be setup with all required
| files or the server WILL segfault.
"listen_backlog" : 20, | socket listen backlog limit (default 20)
"min_children" : 20, | Max number of children to pre fork
"max_children" : 200, | Max number of children to pre fork
"min_spare_children" : 10, | Min number of spare children to maintain
"max_spare_children" : 20, | Max number of spare children to maintain
"max_requests_per_child" : 200, | Max number of requests per child process (prefork)
"metric_connection" : "inet:12346@localhost", | Optional connection on which to expose metrics interface
"metric_timeout" : 10, | Timeout for metrics IPC (default 5)
"metric_tempfile" : "/tmpfs/authmilter_metrics", | Shared MMap file in which to store metrics, best on a ram disk
# metric_port and metric_host are deprecated.
# please use metric_connection instead
"metric_port" : 8081, | Optional port on which to expose metrics interface
"metric_host" : "127.0.0.1", | Optional host for binding metrics interface
"protocol" : "milter", | The protocol the milter is to use
| can be either milter or smtp
"milter_quarantine" : "0", | Allow Milter protocol quarantine when running in milter mode
| (experimental)
"smtp" : { | Parameters for use when protocol is smtp
"server_name" : "scan.example.com", | The server name to use for the server
"sock_type" : "inet", | Socket type (inet or unix)
"sock_host" : "localhost", | Host to connect to (when inet)
"sock_port" : "2525", | Port to connect to (when inet)
"sock_path" : "/var/run/smtp.sock", | Socket path to connect to (when unix)
"timeout_in" : "10", | Timeout when waiting for inbound SMTP data
"timeout_out" : "10", | Timeout when waiting for outbound SMTP data
"pipeline_limit" : "50", | Limit the number of transactions accepted in an SMTP pipeline
"queue_type" : "before", | SMTP Queue type, either before or after. After queues have
| an upstream queue ID, before queues do not.
"temp_dir" : "/tmp/", | Directory for temporary spool files, defaults to in memory
"chunk_limit" : 1048576, | Process body in chunks of approx this many bytes
"tcp:12346" : { | Outbound SMTP details can be set per inbound port/socket
| This allows outbound SMTP to be routed differently for
| different inbound ports. The key is the inbound port specified
| as unix:<socket path> or inet:<port>
| It is not currently possible to set based on listening host.
| If a specific config set is not found them we use the default
| set as defined above.
"server_name" : "scan.example.com", | The server name to use for the server
"sock_type" : "inet", | Socket type (inet or unix)
"sock_host" : "localhost", | Host to connect to (when inet)
"sock_port" : "2526", | Port to connect to (when inet)
"timeout_in" : "10", | Timeout when waiting for inbound SMTP data
"timeout_out" : "10" | Timeout when waiting for outbound SMTP data
},
"unix:/var/sock/a.sock" : {
"server_name" : "util.example.com",
"sock_type" : "unix",
"sock_path" : "/var/run/smtp.sock",
"timeout_in" : "10",
"timeout_out" : "10"
}
},
| Timeouts for callbacks, should be slightly lower
| than the corresponding timeouts in Postfix
| Timeouts are ignored if missing.
"connect_timeout" : 30, | Timeout for Connect callbacks
"command_timeout" : 30, | Timeout for Helo,Mail,Rcpt,Data and Unknown callbacks
"content_timeout" : 300, | Timeout for Header,Eoh, Body and Eom callbacksa
"dns_resolvers" : [ | Explicit list of DNS resolvers to use
"8.8.8.8",
"127.0.0.1"
],
"dns_timeout" : 10, | Timeout for DNS lookups
"dns_retry" : 2, | Number of times a lookup will retry per call
"header_indent_style" : "entry", | Optional style to indent/fold Authentication-Results header by.
"header_fold_at" : 77, | Optional line length to attempt folding of Authentication-Results header at.
| Header lines over this size will be folded at token boundaries where possible.
"header_indent_by" : 4, | Optional number of spaces to indent/fold Authentication-Results header by.
| The style matches those defined in Mail::AuthenticationResults.
| options are none; no folding
| entry; fold on each entry
| subentry; fold on each subentry
| full; fold on each item
| the default style is entry, and the default by is 4
| NB. This only apply when there are no handlers using the
| legacy string method of adding a section to the header.
"tempfail_on_error" : "1", | Tempfail on errors
"tempfail_on_error_authenticated" : "0", | Tempfail on errors for Authenticated IP Connections
"tempfail_on_error_local" : "0", | Tempfail on errors for Local IP Connections
"tempfail_on_error_trusted" : "0", | Tempfail on errors for Trusted IP Connections
"ip_map" : { | List of IP Addresses or CIDR ranges to remap.
"1.2.3.4" : { | Any incoming IP address which matches a given key
"ip": "5.6.7.8", "helo" : mx.test" | will be remapped to use the value supplied
}, | This is useful, for example, when internal infrastructure
"dead:beef::/32" : { | connects to your MX via a private internal address, but you
"ip" : "5.6.7.8", "helo" : "mx.test" | want to run IP based checks (eg SPF) against its external
|
"helo_map" : { | Additionally match and remap based on a received HELO host
"test.host" : { | from a matching IP address, for milter protocol this is done at the HELO stage, ie
"ip" : "5.6.7.9", | after the connect stage, so handlers which run in the connect stage will not see
"helo" : "mx2.test" | this remapped ip address. SMTP protocol runs both remaps before passing control to
} | the connect handlers, so connect handlers see ip addresses remapped by HELO.
}
} | IP address instead.
},
"handlers" : { | Config for each handlers, can be prefixed with !
| to disable that handler without having to remove
| its config.
"ActiveModule" : {
"foo" : "bar"
},
"!InactiveModule" : {},
| Additionally, config for a module can be placed in a file
| with filename /etc/authentication_milter.d/ModuleName.json
| the contents of which should be the JSON assigned to the
| entry here.
| Please see the help for each handler for its individual
| configuration requirements.
}
}
=head1 DMARC
This milter uses Mail::DMARC as a backend for DMARC checks, this module requires that a configuration file is setup.
You should create and populate /etc/mail-dmarc.ini
For DMARC reporting you are also required to setup a datastore, including creating a basic table structure.
The detauls of this are to be found in the Mail::DMARC documentation.
At this time forensic reports are not supported by Mail::DMARC or this milter. Only aggregate reports will be generated.
To check reports please use the dmarc_view_reports command, to send reports please use the dmarc_send_reports command.
These are included with the Mail::DMARC module.
=head1 AUTHOR
Marc Bradshaw <marc@marcbradshaw.net>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2020 by Marc Bradshaw.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut