SPF-aware greylisting with Exim and memcache

This is a followup of my 2011’s article avoiding Spams with SPF and greylisting within Exim. What changed since then? I actually am not more harrassed by spam that I was earlier on. It works. I am spam free since a decade now. No, but, however, several importants mail providers have a tendancy to send mail through multiples SMTPs, so many it took a while for any of them to do at least two attempt. So some mails takes ages to pass the greylist.

Contemplating the idea to use opensmtpd, I incidentally found an interesting proposal to mix greylisting of IP with SPF-validated domains.

The idea is that you greylist either an SMTP IP or a domain including any SMTP IP approved by SPF.

I updated the memcached-exim.pl script previously used and described. It was simplified because I dont think useful to actually make greylist per sender and recipient, only per IP or domain. Now it either only greylist IP, if not validated by SPF, or the domain and IP on success (to save a few SPF further test).

I dont think it should have any noticeable impact on the server behavior. SPF is anyway checked, so it is meaningless since there is local caching DNS on my mail servers.

The earlier /etc/exim4/memcached.conf is actually no longer required (defaults are enough). You still need exim configuration counterparts:  /etc/exim4/conf.d/main/00_stalag13-config_0greylist and /etc/exim4/conf.d/acl/26_stalag13-config_check_rcpt.

Delisting an Exim4 server from Office365 ban list

Ever tried to get delisted from Office365 ban list, for whatever reason you might try to get (new IP for a server that was abused in the past or else, you won’t know since they wont tell – and it even looks like they probably dont even really know)?

It is a funny process, because it involves receive a mail from their servers, a mail that will probably be flagged as spam, with clues so big that it might be blocked at SMTP time.

With Exim4, you’ll probably get in the log something like:

2019-08-20 22:18:09 1i0Aa1-0004Hu-8h H=mail-eopbgr740042.outbound.protection.outlook.com (NAM01-BN3-obe.outbound.protection.outlook.com) [] X=TLS1.2:ECDHE_RSA_AES_256_CBC_SHA1:256 CV=no F=<no-reply@microsoft.com> rejected after DATA: maximum allowed line length is 998 octets, got 3172

Long story short (this length test is not welcomed by all users), add /etc/exim4/conf.d/main/00_localoptions add


and then restart the server.

Try delisting and check your spam folder. You should get now the relevant mail. Whatever we think about the lenght limit test of Exim4 (based on RCF, isn’t it?), you still end up with a mail sent by Office365 like this:

X-Spam-Flag: YES
X-Spam-Level: *****
X-Spam-Status: Yes, score=5.2 required=3.4 tests=BASE64_LENGTH_79_INF,
	MPART_ALT_DIFF,SPF_HELO_PASS,SPF_PASS autolearn=no autolearn_force=no
	* -0.0 SPF_PASS SPF: sender matches SPF record
	* -0.0 SPF_HELO_PASS SPF: HELO matches SPF record
	*  0.7 MIME_HTML_ONLY BODY: Message only has text/html MIME parts
	*  0.7 MPART_ALT_DIFF BODY: HTML and text parts are different
	*  0.0 HTML_MESSAGE BODY: HTML included in message
	*  1.8 HTML_IMAGE_ONLY_08 BODY: HTML: images with 400-800 bytes of
	*      words
	*  2.0 BASE64_LENGTH_79_INF BODY: base64 encoded email part uses line
	*      length greater than 79 characters
	*  0.0 MIME_HTML_ONLY_MULTI Multipart message only has text/html MIME
	*      parts

Considering the context, it screams incompetence.

Typing SSH passphrase(s) only once per session

Here’s a very simple way to type SSH passphrases only once. This simple function, to be added in your ~/.bashrc, will make sure that ssh-agent will always be called before ssh, once per session, so you do not have to type your ssh passphrase more than once:

function sshwithauthsock {
 if [ ! -S ~/.ssh/ssh_auth_sock ]; then
   eval `ssh-agent`
   ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock
 export SSH_AUTH_SOCK=~/.ssh/ssh_auth_sock
 ssh-add -l > /dev/null || ssh-add

alias ssh='sshwithauthsock ssh'
alias scp='sshwithauthsock scp'

Check for possibly updated version directly in my repository.


Getting nginx’s wildcard-based server names to pass Exim HELO syntax checks

Many PHP-based apps, like webmails, when using SMTP functions, depends on nginx server_name value to set up the HELO sent.

But if your server_name value is wildcard-based, you’ll get “syntactically invalid argument(s)” from the SMTP server. Example with ownCloud.

Assuming that the SMTP running on the same host as your webmail is not accepting mail but from the webmail itself, you can easily work around this. You can addd


in, for example, /etc/exim4/conf.d/main/00_webmail, if your server name is something like ~^mx.


Isn’t SRS breaking SPF itself, at least regarding spam?

Earlier on this blog, I proposed ways to implement SPF (Sender Policy Framework). I recently noticed mails forwarded by one of my servers being tagged as spam by gmail.com due to SPF checks. It means that while SPF works for my domains with near to 0 user base, no real business of forwarding, it is a nuisance for forwarding in general. So you are advised to use SRS (Sender Rewriting Scheme). Strangely enough it is not fully integrated on main servers and some implementation (Exim in Debian) are based on unmaintained library (SRS C library).


Fact is SRS is far from being nice. It makes so your own forwarding server is vouching for fowarded mails. But why would you want that?

SPF test will fail because your forwarding server is not a registered valid source for (forwarded) mails sent from domain X. SRS proposal is that your server will alter header so to forward the mail from X domain X to appear as sent from an address of your own domain for you server is a registered valid source.

I guess the logic is to make forwarders somehow responsible of filtering, not bad in principle.

But it also means that for each spam forwarders fail to identify, they’ll be tagged as spam originator. It is particulary annoying when forwarding is made on public addresses bound to attract spam. So it seems better to get a failed SPF test on every forwarded messages including valid ones than a valid SPF test on every forwarded messages including spam.

SPF without SRS breaks forwarding. But SPF with SRS, the workaround, breaks SPF itself regarding spam and will give you (your IPs, your domains) bad rep, with will make your legit mail at risk of being blacklisted, unless you apply an overly harsh policy on forwarded mails.

Annoying. I am thinking removing SPF completely, instead.  For now, I am updating my SPF records to remove any Fail statement, since there is no way for me to know whether one of my mail can legitimately be forwarded through several servers.  Funny enough, google that promotes SPF usage recommends using SoftFail over Fail. But I might even reset to Neutral.

Interesting link on topic : Mail server setup wih SRS ; Why not SPF?

Alternative: I implemented DKIM on my servers. Seems much smarter to have a server signature.

Using PowerDNS (server and recursor) instead of Bind9, along with domain name spoofing/caching

Recently, I made an article about how to use Bind9 with LXC containers, setup including domain name spoofing/caching. This setup was using Bind9 views so only the caching LXC container would get real IP for cached domains (like ftp.fr.debian.org or packages.devuan.org) so nginx, on this system, could mirror accordingly.

Then Debian 9.0 was released and I found out two views were no longer allowed to share writing rights to a single same zone definition file.

You would then get error like  “writeable file ‘/etc/bind/…’: already in use: /etc/bind/…”.

As sacrifial-spam-address wrote:

At first, I thought this was a bug (how can a config file line conflict
with itself?), then I realized that the conflict was between the
two views.

There does not appear to be any simple workaround. The best solution
appears to be to use the new BIND 9.10 “in-view” feature, which allows a
zone in one view to be a reference to the same zone in a different view.
When this is done, both views may share the same cache file.

The down side is that this violates one of the important principles of
programming: only specify something in one place. Instead, I have to have
a “master” definition and several “in-view” declarations referencing
the master.

I wish BIND would either deal with the problem after noticing it (by
automatically doing the equivalent of the in-view), or provide a way to
import every zone in a view, avoiding the need for a long list of in-view

Then I fixed my setup to work with in-view, updating the article already linked. But the experience was clearly unsatisfying, adding one more layer of complexity to something already quite dense.

Plus I got some error in this new setup: it seemed that in-view, at least the way I configured, cause the different views to behave as if they share a same cache. Say, after Bind9 startup, I pinged ftp.fr.debian.org from any LXC container but the cache one, I would get the IP of the LXC cache container as it should be. But, then, if I pinged the same domain from the LXC cache container, I would still get as answer its own IP, as if there was not two different views setup. Same with the opposited test, if the first ping was from within the LXC cache container, then from any other, I would get the result wanted only for the LXC cache container.

So it lead me to the point that I had to understand better the in-view feature that in first place I did not want to use, in order to get it to behave like view did.

You got it: I found much easier to user PowerDNS instead.

PowerDNS (pdns) is composed of an authoritative DNS server plus a DNS recursor. The first one I only need on this setup for the LAN domain name (quite used: LXC containers + connected devices). The recursor is doing some caching. And can be easily scripted.

apt-get install pdns-server pdns-recursor pdns-backend-sqlite3

Often, when both the name server and the recursor are installed on the same machine, people set up the name server to listen on port 53 on the network and to pass to the recursor, listening on another port, requests it cannot handle (that it is not authoritative for and need a recursor to resolve then).

Ok, why not. Except that I want specific answer to be given depending on the querier’s IP for domains outside of the LAN, so handled by the recursor. That would not work if the recursor get queries sent over loopback device by the authoritative server.

Aside from that, just as general principle, I like better the notion of, by default, soliciting a recursor that, only when necessary, ask the local DNS server  instead of other DNS than the notion of asking a DNS server to handle queries that he is most of the time unlikely to be have authoritative answer for and that he’ll have to pass to a recursor.

So instead of the usual proposed :

  • client ->  local DNS server  -> DNS recursor if non authoritative  -> distant authoritative DNS server

It’ll be:

  • client -> DNS recursor -> authoritative DNS server (local or distant).


authoritative PowerDNS server

First we deal with the DNS server to server YOURDOMAIN.LAN. The sqlite3 backend should be installed and set up (or else of your liking).

By default, the sqlite3  database is in /var/lib/powerdns/pdns.sqlite3

Easiest way is to convert Bind9 zone config to set it up:

zone2sql --named-conf=/etc/bind/named.conf.local --gsqlite | sqlite3 /var/lib/powerdns/pdns.sqlite3

That’s all!

As alternative,  you can also create zone from scratch with pdnsutil:

cd /var/lib/powerdns
sqlite3 pdns.sqlite3 < /usr/share/doc/pdns-backend-sqlite3/schema.sqlite3.sql
chown pdns:pdns pdns.sqlite3
# main zone
pdnsutil create-zone YOURDOMAIN.LAN ns1.YOURDOMAIN.LAN
pdnsutil add-record YOURDOMAIN.LAN main A
pdnsutil add-record YOURDOMAIN.LAN @ MX "10 mx.YOURDOMAIN.LAN"
# first reverse zone 192.168.1
pdnsutil create-zone 1.168.192.in-addr.arpa ns1.YOURDOMAIN.LAN
pdnsutil add-record 1.168.192.in-addr.arpa 1 PTR main.YOURDOMAIN.LAN
# to be continued


In our previous setup, we had DNS update automated by ISC DCPDd: we want any new host on the local network to be given an IP. Nothing changed regarding ISC DHCPd, read the relevant ISC DHCPd setup part. For the record, to generate the relevant update key:

cd /etc/dhcp
dnssec-keygen -a hmac-md5 -b 256 -n USER ddns

The secret will be a string like XXXXX== within the ddns.key generated file.

Obviously, powerdns needs this data. You need to register the key+secret and give right on each zone (YOURDOMAIN.LAN plus the reverse for IP ranges, below for 192.168.1 and 10.0.0)

sqlite3 /var/lib/powerdns/pdns.sqlite3

# XXXXX== = the secret string
insert into tsigkeys (name, algorithm, secret) values ('ddns', 'hmac-md5','XXXXX==');

# find out ids of zones
select id from domains where name='YOURDOMAIN.LAN';
select id from domains where name='1.168.192.in-addr.arpa';
select id from domains where name='0.0.10.in-addr.arpa';

 # authorized the key for each
 insert into domainmetadata (domain_id, kind, content) values (1, 'TSIG-ALLOW-DNSUPDATE', 'ddns');
 insert into domainmetadata (domain_id, kind, content) values (2, 'TSIG-ALLOW-DNSUPDATE', 'ddns');
 insert into domainmetadata (domain_id, kind, content) values (3, 'TSIG-ALLOW-DNSUPDATE', 'ddns');

Finally, you need to configure powerdns itself. You can directly edit /etc/powerdns/pdns.conf but I think easier to create a specific /etc/powerdns/pdns.d/00-pdns.conf so you do not edit the default example:

# base

# dynamic updates

Note it is an IPv4 only setup. It’ll listen only on loopback interface, since no one is supposed to contact him directly beside the recursor sitting on the same loopback.

You can restart the daemon (rc-service pdns restart  with OpenRC, else depending on your init).

PowerDNS recursor :

It is quite straighforward to configure in /etc/powerdns/recursor.conf, this one will listen on LAN addresses (not the loopback):

# restrict netmask allowed to query 


# for local domain, via loopback device, forward queries to the PowerDNS local authoritative server
forward-zones=YOURDOMAIN.LAN=, 1.168.192.in-addr.arpa=, 0.0.10.in-addr.arpa=

# list of IP to listen to

# that is how we will spoof/cache

So all the magic will be done in the /etc/powerdns/redirect.lua script, where the cache LXC container IP is hardcoded (that could be change in future version if necessary):

-- (requires pdns-recursor 4 at least)
-- cached servers
cached = newDS()
cachedest = ""

-- ads kill list
ads = newDS()
adsdest = ""

-- hand maintained black list
blacklisted = newDS()
blacklistdest = ""

function preresolve(dq)
   -- DEBUG
   --pdnslog("Got question for "..dq.qname:toString().." from "..dq.remoteaddr:toString().." to "..dq.localaddr:toString(), pdns.loglevels.Error)
   -- handmade domains blacklist
   if(blacklisted:check(dq.qname)) then
      if(dq.qtype == pdns.A) then
	 dq:addAnswer(dq.qtype, blacklistdest)
	 return true
   -- spam/ads domains
   if(ads:check(dq.qname)) then
      if(dq.qtype == pdns.A) then
	 dq:addAnswer(dq.qtype, adsdest)
	 return true
   -- cached domains
   if(not cached:check(dq.qname)) then
      -- not cached
      return false
      -- cached: variable answer
      dq.variable = true
      -- request coming from the cache itself
      if(dq.remoteaddr:equal(newCA(cachedest))) then
	 return false
      --  redirect to the cache
      if(dq.qtype == pdns.A) then
	 dq:addAnswer(dq.qtype, cachedest)
   return true


This script relies on three files to do its magic.

redirect-blacklisted.lua that is hand made blacklist, the default content is:


redirect-cached.lua is to be generated by redirect-cached-rebuild.sh that you should edit before running, to list which domains you want to cache:



# comment this if you dont cache steam
# (note: nginx cache must also cover this)
DOMAINS="$DOMAINS cs.steampowered.com content1.steampowered.com content2.steampowered.com content3.steampowered.com content4.steampowered.com content5.steampowered.com content6.steampowered.com content7.steampowered.com content8.steampowered.com content9.steampowered.com hsar.steampowered.com.edgesuite.net akamai.steamstatic.com content-origin.steampowered.com client-download.steampowered.com steampipe.steamcontent.com steamcontent.com"
# comment this if you dont cache debian
# (useful read: https://wiki.debian.org/DebianGeoMirror )
DOMAINS="$DOMAINS cdn-fastly.deb.debian.org ftp.fr.debian.org ftp.de.debian.org ftp.debian.org security.debian.org"
# comment this if you dont cache devuan
DOMAINS="$DOMAINS packages.devuan.org amprolla.devuan.org"
# comment this if you dont cache ubuntu
DOMAINS="$DOMAINS fr.archive.ubuntu.com security.ubuntu.com"

echo "-- build by ${0}" > $out
echo "-- re-run it commenting relevant domains if you dont cache them all" >> $out
echo "return{" >> $out
for domain in $DOMAINS; do
    echo \"$domain\", >> $out
echo "}" >> $out


Finally, redirect-ads.lua is to be generated by redirect-ads-rebuild.pl that you put in a weekly cronjob (following by a pdns-recursor restart):

use strict;
use Fcntl ':flock';

# disallow concurrent run
open(LOCK, "< $0") or die "Failed to ask lock. Exiting"; flock(LOCK, LOCK_EX | LOCK_NB) or die "Unable to lock. This daemon is already alive. Exiting"; open(OUT, "> redirect-ads.lua");

# You can choose between wget or curl. Both rock!
# my $snagger = "curl -q";
my $snagger = "wget -q -O - ";

# List of URLs to find ad servers.
my @urls = ("http://pgl.yoyo.org/adservers/serverlist.php?showintro=0;hostformat=one-line;mimetype=plaintext");

print OUT "return{\n";
# Grab the list of domains and add them to the realm file
foreach my $url (@urls) {
    # Open the curl command
    open(CURL, "$snagger \"$url\" |") || die "Cannot execute $snagger: $@\n";

    printf OUT ("--- Added domains on %s --\n", scalar localtime);

    while () {
	next if /^#/;
	next if /^$/;
	foreach my $domain (split(",")) {
	    print OUT "\"$domain\",\n";

print OUT "}\n";

So before starting the recursor, run redirect-ads-rebuild.pl and redirect-cached-rebuild.sh.

Then, after restart, everything should be up and running, with no concerns of inconsistent issues. And, as you can see for yourself, the LUA scripting possibility is as easy as extensible.

Setting up LXC containers to run with ISC DHCPd and BIND instead of dnsmasq, along with domain name spoofing/caching

By default, lxc-net setup containers to work along with dnsmasq, which provides both DNS and dhcpd services, name resolution and IP attribution.

Recommended setup of lxc-net includes /etc/lxc/dnsmasq.conf that only states  “dhcp-hostsfile=…” and the said dhcp-hostfiles as /etc/lxc/dnsmasq-hosts.conf with a line “hostname,IP” per host.

It works fine and there is no real reason to use anything else. Though it is obvious that lxc-net lacks a bit of modularity, since it is clearly tied, hardcoded, to dnsmasq for instance.

Except that on my main server, I already have ISC DHCPd serving IP to local area network and BIND 9 not only doing name resolution caching but also name resolution for said local area network. Not only having both dnsmasq and BIND 9 and ISC DHCPd is a bit overkill, but it requires additional config to bind them to specific interfaces to avoid conflicts.

dnsmasq shutdown

We could simply do a killall dnsmasq and comment the part in /usr/lib/x86_64-linux-gnu/lxc/lxc-net where it get started. For now, we’ll just prevent it from messing with interfaces, setting /etc/lxc/dnsmasq.conf to:


Initial setup

This article assumes you already have BIND and ISC DHCPd set up for local area network (otherwise, as said, in most use cases, dnsmasq will be just fine).

If you do not have a preexisting setup but wants, nonetheless, switch to BIND 9 and ISC DHPCd, you could start with the bind setup provided in my setting up a silent/low energy consumption home server article.

This article includes dynamic clients name update. The only thing to pay attention is that this setup use for local area network whereas, in the following article, will be used for LXC bridge network while will be dedicated to local area network.

DNS setup

I adjusted my preexisting setup (bind9 files part of my -utils-cache-spoof debian package, which I suggest you look at directly to have their current exhaustive content) based on bind9 notion of ACL (access control list) depending on which network clients belongs and, subsequently, bind9 notion of “views” that configure which zones are provided to these clients according to ACL.

The following will seems like a lot but, if you grab my debian -utils-cache-spoof package, it is actually not that much.

Since LXC bridge here is using network, I have in named.conf.acl:


acl lan {
    // the cache host IP should not be part of regular lan ACL
    // private IPv4 address spaces;;;

acl lannocache {
   // counterpart of earlier statement: cache host needs proper unspoofed name resolution;

Note that the .88 container IP is dedicated to caching (apt/steam as in my previous setup with dsniff as spoofer and my another setup using bind9 instead but outside of LXC host/container context) so it needs to be excluded from the general ACL.

These ACL are in turn used in named.conf.views (Update: with latest versions of Bind9, we cannot include twice a file that as allow-update statement within, hence the …local and .local_ref):

// clients are set in named.conf.acl
include "/etc/bind/named.conf.acl";

// loopback view, for the server itself
view "loopback" {
 match-clients { loopback; };
 include "/etc/bind/named.conf.default-zones";
 include "/etc/bind/named.conf.local";
 include "/etc/bind/named.conf.ads";

// otherwise local network area
view "lan" {
 match-clients { lan; };
 include "/etc/bind/named.conf.default-zones";
 include "/etc/bind/named.conf.local_ref";
 include "/etc/bind/named.conf.cache";
 include "/etc/bind/named.conf.ads";

// local network area without cache, for host that will get unspoofed name resolution
// (needs to be set up one by one in named.conf.acl)
view "lannocache" {
 match-clients { lannocache; };
 include "/etc/bind/named.conf.default-zones";
 include "/etc/bind/named.conf.local_ref";
 include "/etc/bind/named.conf.ads";


Obviously, if there was no notion of caching (and name spoofing), the setup would be even more straightforward, a single view would be enough. Nonetheless, this example shows an easy way to treat differently hosts depending whether they are LXC containers or regular LAN clients.

About the zones included (or not) in views (all files being in /etc/bind):

  • named.conf.default-zones is standard ;
  • named.conf.local is almost standard, you need to define here your local domains/network ;
  • Update:  named.conf.*_ref  is required with recent version of Bind9 to be able to use twice content for some named.conf.* in which some zone file is defined and can be updated (allow-update) : you’ll will need to use in-view feature to mimic usage of the view that previously defined it since trying another include would sprout writeable file ‘…’ already in use  ;
  • named.conf.cacheBASEIP contains list of spoofed domains, the one we want to cache, generated by named.conf.cache-rebuild.sh, BASEIP being optional;
  • named.conf.ads contains ads servers blacklist generated by update-bind-ads-block.pl ;

So basically, you need to edit /etc/bind/named.conf.local to something like:

// to store A/CNAME records for DOMAIN.EXT
zone "DOMAIN.EXT" {
 type master;
 notify no;
 file "/etc/bind/db.DOMAIN.EXT";
 allow-update { key ddns; };

// (we use for regular LAN)
// to store PTR records (IP to name) for regular LAN 
zone "1.168.192.in-addr.arpa" {
 type master;
 notify no;
 file "/etc/bind/db.192.168.1";
 allow-update { key ddns; };

// (we use for LXC bridge)
// to store PTR records for LXC bridge)
zone "0.0.10.in-addr.arpa" {
 type master;
 notify no;
 file "/etc/bind/db.10.0.0";
 allow-update { key ddns; };

Update: since recent Bind9 update, to be able to reuse these zones in another view, you’ll need to edit /etc/bind/named.conf.local_ref to something like:

// simple reference to previously defined zones for view loopback in named.conf.local
zone "DOMAIN.EXT" { in-view "loopback"; }; 
zone "1.168.192.in-addr.arpa" { in-view "loopback"; }; 
zone "0.0.10.in-addr.arpa" { in-view "loopback"; };

You also require relevant db. files: for instance db.ads pointing to loopback to filter ads/spam sources, db.cache pointing to the cache container .88 (possibly also db.cacheBASEIP) and local db. files as db.DOMAIN.EXT:

$TTL 86400 ; 1 day
                        2823 ; serial
                        28800 ; refresh (8 hours)
                        7200 ; retry (2 hours)
                        604800 ; expire (1 week)
                        10800 ; minimum (3 hours)
          NS     server.DOMAIN.EXT.
          MX     10 server.DOMAIN.EXT.
server    A
; the rest will be filled by ddns

Likewise, you should have db.192.168.1 and db.10.0.0 (obviously with 1.168.192 replaced by 0.0.10) as:

$TTL 86400 ; 1 day
1.168.192.in-addr.arpa IN SOA server.DOMAIN.EXT. root.DOMAIN.EXT. (
                       2803 ; serial
                       28800 ; refresh (8 hours)
                       7200 ; retry (2 hours)
                       604800 ; expire (1 week)
                       10800 ; minimum (3 hours)
           NS      server.DOMAIN.EXT.
$ORIGIN 1.168.192.in-addr.arpa.
1          PTR     server.DOMAIN.EXT.
; the rest will be filled by ddns too

And then you must run the scripts to generate named.conf.cacheBASEIP and name.conf.ads. You’ll probably need to edit /etc/bind/named.conf.cache-rebuild.sh variables according to what you are actually caching.

BIND gets updates from ISC DHCPd whenever a new clients get a lease, it is configured in name.conf.dhcp (not packaged):

include "/etc/bind/ddns.key";

controls {
 inet allow { localhost; } keys { ddns; };

The ddns key was generated as documented in my setting up a silent/low energy consumption home server article as well as in Debian docs:

# dnssec-keygen -a HMAC-MD5 -b 128 -r /dev/urandom -n USER ddns

Out of the generated Kdhcp_updater.*.private, you get the content of the “Key:” statement and you put it in /etc/bind/ddns.key:

key ddns {
 algorithm HMAC-MD5;

So this setup implies that your named.conf looks like:

include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.dhcp";
include "/etc/bind/named.conf.views";

Besides, my /etc/bind/named.conf.options is generated by /etc/dhcp/dhclient-exit-hooks.d/bind so it include proper forwarders and listen-on exception.

That should cover it for BIND.

ISC DHPCd setup

In my case, I still still want IPs of LXC containers to be fixed. The syntax of /etc/lxc/dnsmasq-hosts.conf was “hostname,IP” per line which is more convenient than ISC DHCPD syntax  “host hostname { hardware ethernet MAC ADDRESS; fixed-address IP; }”.

I decided to use the same /etc/lxc/dnsmasq-hosts.conf symlinked to /etc/lxc/hosts.conf that will be used by the /etc/lxc/dhcpd-hosts.rebuild.sh (not packaged for now) script to generate /etc/dhcp/dhcpd_lxc-hosts.conf:

# /etc/lxc/dhcpd-hosts.rebuild.sh

HOSTS=/etc/lxc/hosts.conf # similar to dnsmasq-hosts.conf: host,IP
LXC_PATH=`lxc-config lxc.lxcpath`

for container in *; do
 if [ ! -d "$container" ]; then continue; fi
 if [ ! -e "$container/config" ]; then continue ; fi
 echo "host lxc-$container {" >> $DESTINATION
 echo " hardware ethernet "`cat "$container/config" | grep lxc.network.hwaddr | cut -f 2 -d "="`";" >> $DESTINATI
 echo " fixed-address "`cat "$HOSTS" | grep "$container" | cut -f 2 -d ","`";" >> $DESTINATION
 echo "}" >> $DESTINATION 

This primitive script will sprout out a proper ISC DHCPd host file. You have to run it each time you create a new container. Once done, we simply edit /etc/dhcp/dhcpd.conf:

# The ddns-updates-style parameter controls whether or not the server will
# attempt to do a DNS update when a lease is confirmed. We default to the
# behavior of the version 2 packages ('none', since DHCP v2 didn't
# have support for DDNS.)
ddns-updates on;
ddns-update-style interim;
ddns-domainname "DOMAIN.EXT";
ddns-rev-domainname "in-addr.arpa.";
ignore client-updates; # no touching the FQDN
include "/etc/dhcp/ddns.key";

# option definitions common to all supported networks...
option domain-name "DOMAIN.EXT";
option domain-search "DOMAIN.EXT", "ANOTHERDOMAIN.EXT";
option domain-name-servers;
option routers;

default-lease-time 600;
max-lease-time 6000;
update-static-leases on;

# If this DHCP server is the official DHCP server for the local
# network, the authoritative directive should be uncommented.

# Use this to send dhcp log messages to a different log file (you also
# have to hack syslog.conf to complete the redirection).
log-facility local7;

# LAN clients
subnet netmask {

 # dynamic IP depends whether the client MAC address is known
 pool {
   deny unknown-clients;
 pool {
   allow unknown-clients; 

 # iPXE / boot on lan
 if exists user-class and option user-class = "iPXE" {
   filename "ipxe-boot";
 } else {
   filename "undionly.kpxe";

# LXC clients
subnet netmask {
 # use the subnet-specific router
 option routers;
 # no pool, all IP are fixed here
 # force lease time to be at least weekly
 min-lease-time 604800;
 max-lease-time 604800;
 # no boot on lan either

# zones
zone DOMAIN.EXT. {
 key ddns;
zone 1.168.192.in-addr.arpa. {
 key ddns;
zone 0.0.10.in-addr.arpa. {
 key ddns;

# LAN known clients 
 host trendnetusb { hardware ethernet 00:50:b6:08:xx:xx; }
 host ugreenusb { hardware ethernet 00:0e:c6:fa:xx:xx; }

# LXC host
include "/etc/dhcp/dhcpd_lxc-hosts.conf";

That’s all. Obviously, if you want your LXC containers to get completely dynamically assigned IP, you do not even need this whole host setup. You just set a pool { } with a range of IP (and remove the specif lease time).

The cache LXC container

I wont get in much details, my my -utils-cache-apt and -utils-cache-steam debian packages should work out of the box on a LXC container, providing both the necessary nginx cache-apt and cache-steam config.

If you use resolvconf and ISC DHCP clients on LXC containers, the resolvconf to nginx resolver config script will set up /etc/nginx/conf.d/resolver.conf accordingly.

If you use udhcpc, this resolvconf script will be ignored  but the default /etc/nginx/conf.d/resolver.conf includes, in comments, proposed changes to /etc/udhcpc/default.script to generate  /etc/nginx/conf.d/resolver.conf accordingly.

Otherwise, you need to hand configure /etc/nginx/conf.d/resolver.conf

## (set resolver to something else if your local interface got
## domain names spoofed, for Google resolver for example.
#resolver ipv6=off; # without lxc
resolver ipv6=off;   # within lxc


I have this setup since a while and noticed the following:

  • with ISC DHCP client within the LXC containers I get the bad udp checksums in N packets issue;  the iptables -A POSTROUTING -t mangle -p udp –dport 67 -j CHECKSUM  –checksum-fill rule set up by lxc-net is helpless; the solution i picked is to use udhcpc within all LXC containers that does not trigger the problem, with the obvious drawback that the cache container must use the edited /etc/udhcpc/default.script option since resolvconf will have no effect;
  • if ISC DHPCd and Bind9, on the LXC host, are started before or at the same time as lxc and lxc-net, they may not listen on the LXC bridge interface, possibly missing at their starting time; as result, while everything could seem properly on, LXC container would fail to get an IP assigned until you restart ISC DHPCd;  this does not occur after adding lxc lxc-net in the Should-Start: part of ISC DHCPd and Bind9 init.d scripts.
  • Update: With Bind9 recent version (notably: since Debian 9.0), if you have twice a zone defined with a file that can be updated, it wont start and logs will state something like writeable file ‘…’ already in use. The workaround, using in-view, is described in the earlier. Granted, it kills a bit the interest of using view and lead to ugly confusing setup.

Avoiding dnsmasq interference

If you are satistified and do not require dnsmasq anymore, I suggest to remove any dnsmasq package and add a symlink so dnsmasq command produces no error (when called by /usr/lib/x86_64-linux-gnu/lxc/lxc-net for instance):

ln -s /bin/true /usr/bin/dnsmasq