Using a different theme for Mediawiki’s SyntaxHighlight extension

Probably the best syntax highlighting plugin for Mediawiki at the moment is the one simply called SyntaxHighlight. It uses Pygments to do the heavy lifting. What sets it apart from the other extensions is that it supports line numbers and picking out highlighted lines.

Unfortunately the default style (theme) is dark-on-light whereas for most of my syntax highlighting I am giving examples of either shell sessions or code. All my shell sessions and code are viewed as light-on-dark, so I would prefer that the wiki’s syntax highlighting followed suit.

I spent quite a while messing about with editing the extension itself but to little effect, until Robert pointed out that I just needed to edit the Common.css file inside the wiki itself. Then you get some decent results.

I used something like this to generate the correct CSS for the “native” style:

$ ./extensions/SyntaxHighlight_GeSHi/pygments/pygmentize -S native -f html|sed -e 's/^/.mw-highlight > pre /'
.mw-highlight > pre .hll { background-color: #404040 }
.mw-highlight > pre .c { color: #999999; font-style: italic } /* Comment */
.mw-highlight > pre .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.mw-highlight > pre .esc { color: #d0d0d0 } /* Escape */
.mw-highlight > pre .g { color: #d0d0d0 } /* Generic */
.mw-highlight > pre .k { color: #6ab825; font-weight: bold } /* Keyword */
.mw-highlight > pre .l { color: #d0d0d0 } /* Literal */
.mw-highlight > pre .n { color: #d0d0d0 } /* Name */
.mw-highlight > pre .o { color: #d0d0d0 } /* Operator */
.mw-highlight > pre .x { color: #d0d0d0 } /* Other */
.mw-highlight > pre .p { color: #d0d0d0 } /* Punctuation */
.mw-highlight > pre .ch { color: #999999; font-style: italic } /* Comment.Hashbang */
.mw-highlight > pre .cm { color: #999999; font-style: italic } /* Comment.Multiline */
.mw-highlight > pre .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
.mw-highlight > pre .cpf { color: #999999; font-style: italic } /* Comment.PreprocFile */
.mw-highlight > pre .c1 { color: #999999; font-style: italic } /* Comment.Single */
.mw-highlight > pre .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
.mw-highlight > pre .gd { color: #d22323 } /* Generic.Deleted */
.mw-highlight > pre .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
.mw-highlight > pre .gr { color: #d22323 } /* Generic.Error */
.mw-highlight > pre .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
.mw-highlight > pre .gi { color: #589819 } /* Generic.Inserted */
.mw-highlight > pre .go { color: #cccccc } /* Generic.Output */
.mw-highlight > pre .gp { color: #aaaaaa } /* Generic.Prompt */
.mw-highlight > pre .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
.mw-highlight > pre .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
.mw-highlight > pre .gt { color: #d22323 } /* Generic.Traceback */
.mw-highlight > pre .kc { color: #6ab825; font-weight: bold } /* Keyword.Constant */
.mw-highlight > pre .kd { color: #6ab825; font-weight: bold } /* Keyword.Declaration */
.mw-highlight > pre .kn { color: #6ab825; font-weight: bold } /* Keyword.Namespace */
.mw-highlight > pre .kp { color: #6ab825 } /* Keyword.Pseudo */
.mw-highlight > pre .kr { color: #6ab825; font-weight: bold } /* Keyword.Reserved */
.mw-highlight > pre .kt { color: #6ab825; font-weight: bold } /* Keyword.Type */
.mw-highlight > pre .ld { color: #d0d0d0 } /* Literal.Date */
.mw-highlight > pre .m { color: #3677a9 } /* Literal.Number */
.mw-highlight > pre .s { color: #ed9d13 } /* Literal.String */
.mw-highlight > pre .na { color: #bbbbbb } /* Name.Attribute */
.mw-highlight > pre .nb { color: #24909d } /* Name.Builtin */
.mw-highlight > pre .nc { color: #447fcf; text-decoration: underline } /* Name.Class */
.mw-highlight > pre .no { color: #40ffff } /* Name.Constant */
.mw-highlight > pre .nd { color: #ffa500 } /* Name.Decorator */
.mw-highlight > pre .ni { color: #d0d0d0 } /* Name.Entity */
.mw-highlight > pre .ne { color: #bbbbbb } /* Name.Exception */
.mw-highlight > pre .nf { color: #447fcf } /* Name.Function */
.mw-highlight > pre .nl { color: #d0d0d0 } /* Name.Label */
.mw-highlight > pre .nn { color: #447fcf; text-decoration: underline } /* Name.Namespace */
.mw-highlight > pre .nx { color: #d0d0d0 } /* Name.Other */
.mw-highlight > pre .py { color: #d0d0d0 } /* Name.Property */
.mw-highlight > pre .nt { color: #6ab825; font-weight: bold } /* Name.Tag */
.mw-highlight > pre .nv { color: #40ffff } /* Name.Variable */
.mw-highlight > pre .ow { color: #6ab825; font-weight: bold } /* Operator.Word */
.mw-highlight > pre .w { color: #666666 } /* Text.Whitespace */
.mw-highlight > pre .mb { color: #3677a9 } /* Literal.Number.Bin */
.mw-highlight > pre .mf { color: #3677a9 } /* Literal.Number.Float */
.mw-highlight > pre .mh { color: #3677a9 } /* Literal.Number.Hex */
.mw-highlight > pre .mi { color: #3677a9 } /* Literal.Number.Integer */
.mw-highlight > pre .mo { color: #3677a9 } /* Literal.Number.Oct */
.mw-highlight > pre .sa { color: #ed9d13 } /* Literal.String.Affix */
.mw-highlight > pre .sb { color: #ed9d13 } /* Literal.String.Backtick */
.mw-highlight > pre .sc { color: #ed9d13 } /* Literal.String.Char */
.mw-highlight > pre .dl { color: #ed9d13 } /* Literal.String.Delimiter */
.mw-highlight > pre .sd { color: #ed9d13 } /* Literal.String.Doc */
.mw-highlight > pre .s2 { color: #ed9d13 } /* Literal.String.Double */
.mw-highlight > pre .se { color: #ed9d13 } /* Literal.String.Escape */
.mw-highlight > pre .sh { color: #ed9d13 } /* Literal.String.Heredoc */
.mw-highlight > pre .si { color: #ed9d13 } /* Literal.String.Interpol */
.mw-highlight > pre .sx { color: #ffa500 } /* Literal.String.Other */
.mw-highlight > pre .sr { color: #ed9d13 } /* Literal.String.Regex */
.mw-highlight > pre .s1 { color: #ed9d13 } /* Literal.String.Single */
.mw-highlight > pre .ss { color: #ed9d13 } /* Literal.String.Symbol */
.mw-highlight > pre .bp { color: #24909d } /* Name.Builtin.Pseudo */
.mw-highlight > pre .fm { color: #447fcf } /* Name.Function.Magic */
.mw-highlight > pre .vc { color: #40ffff } /* Name.Variable.Class */
.mw-highlight > pre .vg { color: #40ffff } /* Name.Variable.Global */
.mw-highlight > pre .vi { color: #40ffff } /* Name.Variable.Instance */
.mw-highlight > pre .vm { color: #40ffff } /* Name.Variable.Magic */
.mw-highlight > pre .il { color: #3677a9 } /* Literal.Number.Integer.Long */

(Yes, I also need to do the light-on-dark thing here in this blog)

To get a list of available styles:

$ ./extensions/SyntaxHighlight_GeSHi/pygments/pygmentize -L styles
Pygments version 2.2.0, (c) 2006-2017 by Georg Brandl.
 
Styles:
~~~~~~~
* manni:
    A colorful style, inspired by the terminal highlighting style.
* igor:
    Pygments version of the official colors for Igor Pro procedures.
* lovelace:
    The style used in Lovelace interactive learning environment. Tries to avoid the "angry fruit salad" effect with desaturated and dim colours.
* xcode:
    Style similar to the Xcode default colouring theme.
* vim:
    Styles somewhat like vim 7.0
* autumn:
    A colorful style, inspired by the terminal highlighting style.
* abap:
 
* vs:
 
* rrt:
    Minimalistic "rrt" theme, based on Zap and Emacs defaults.
* native:
    Pygments version of the "native" vim theme.
* perldoc:
    Style similar to the style used in the perldoc code blocks.
* borland:
    Style similar to the style used in the borland IDEs.
* arduino:
    The Arduino® language style. This style is designed to highlight the Arduino source code, so exepect the best results with it.
* tango:
    The Crunchy default Style inspired from the color palette from the Tango Icon Theme Guidelines.
* emacs:
    The default style (inspired by Emacs 22).
* friendly:
    A modern style based on the VIM pyte theme.
* monokai:
    This style mimics the Monokai color scheme.
* paraiso-dark:
 
* colorful:
    A colorful style, inspired by CodeRay.
* murphy:
    Murphy's style from CodeRay.
* bw:
 
* pastie:
    Style similar to the pastie default style.
* rainbow_dash:
    A bright and colorful syntax highlighting theme.
* algol_nu:
 
* paraiso-light:
 
* trac:
    Port of the default trac highlighter design.
* default:
    The default style (inspired by Emacs 22).
* algol:
 
* fruity:
    Pygments version of the "native" vim theme.

Although you may find it easier looking at the Pygments style gallery.

Let’s Encrypt wildcard certificates, acme.sh and automated DNS verification

Let’s Encrypt’s wildcard certificates ^

Now that Let’s Encrypt can issue wildcard TLS certificates I found some time to look into that.

I already use a Lua script with haproxy which takes care of automatically answering http-01 ACME challenges, but to issue/renew a wildcard certificate you need to answer a dns-01 challenge. A different client/setup would be needed.

dns-01 ACME challenges ^

Most of the clients that support ACME v2 offer a range of integrations for DNS providers, plus a manual mode that prints out the DNS record that you need to add and then waits for you to indicate that you’ve done it. I run my own DNS infrastructure so the thing to do would be RFC2136 dynamic DNS updates.

One wrinkle here is that currently none of my DNS zones have dynamic updates enabled. At the moment I manage them as zone files (some are automatically generated by scripts though). After looking at a few of the client options I found that acme.sh supports an “alias zone”.

Basically, in your main zone you create a CNAME for the challenge record that points at another zone, and then enable dynamic updates in that other zone. The other zone is dedicated for this purpose, so the only updates which will be happening will be for the purpose of answering dns-01 ACME challenges. I made my dynamic zone a sub-zone of my main one:

strugglers.net zone file content ^

These records need to be added to the main zone for this to work.

.
.
.
; sub-zone purely used for dns-01 ACME challenges.
acmesh          NS a.authns.bitfolk.co.uk.
                NS b.authns.bitfolk.com.
                NS c.authns.bitfolk.com.
 
; Alias the dns-01 challenge record into the dedicated zone.
_acme-challenge CNAME _acme-challenge.acmesh.strugglers.net.
.
.
.

acmesh.strugglers.net zone file content ^

Initially this just needs to be an empty zone with only SOA and NS records, so this is the entire content of the file.

$ORIGIN .
$TTL 86400      ; 1 day
acmesh.strugglers.net   IN SOA  a.authns.bitfolk.co.uk. hostmaster.bitfolk.com. (
                                2018031905 ; serial
                                14400      ; refresh (4 hours)
                                7200       ; retry (2 hours)
                                1209600    ; expire (2 weeks)
                                43200      ; minimum (12 hours)
                                )
                        NS      a.authns.bitfolk.co.uk.
                        NS      b.authns.bitfolk.com.
                        NS      c.authns.bitfolk.com.

DNS server configuration ^

The DNS server needs to know a key by which it will authenticate acme.sh‘s updates, and also needs to be told that the new zone is a dynamic zone. I use BIND, so it goes as follows.

Generate a key for dynamic DNS updates ^

Use the dnssec-keygen command to generate a key suitable for authenticating DNS updates.

$ dnssec-keygen -r /dev/urandom -a HMAC-SHA512 -b 512 -n HOST DDNS_UPDATE

This creates two files named like Kddns_update.+165+14059.key and Kddns_update.+165+14059.private.

Put the key in the BIND config ^

Look in the private file and take the key from the line that starts “Key:”. Put that in some config file that you will load into your BIND like this:

key "strugglers" {
    algorithm hmac-sha512;
    secret "Sb8nvwpO8bhiU4haPB+NiJKoMO6vVJumrr29Bj3daSuB8hBoTKoqPKMBKTYLRUv12pbKPwJATgdsU6BtL4Hmcw==";
};

The thing in quotes after “key” is a symbolic name for this key and can be anything that makes sense to you. The “secret” is the key from the private file. You can delete the two Kddns_update.+165+14059.* files now.

Put the new zone into the BIND config ^

The config for the zone itself looks something like this:

zone "acmesh.strugglers.net" {
    type master;
    file "/path/to/acmesh.strugglers.net";
    allow-update {
        key "strugglers";
    };
};

Reload the DNS server ^

Once BIND has been reloaded the log file should indicate that the acemsh.strugglers.net zone was loaded correctly, and in my case that triggers DNS NOTIFY to my secondary servers which automatically begin zone transfers.

Check things out with nsupdate ^

At this point it might be worth using the nsupdate command to check that you can do dynamic DNS updates.

Just type the nsupdate line in the shell, the > is a prompt at which you will type the updates you wish to send. We’ll add a trivial TXT record. The -k argument is the path to the file containing the key.

$ nsupdate -k /path/to/strugglers.key -v
> server a.authns.bitfolk.co.uk
> debug yes
> zone acmesh.strugglers.net.
> update add foo.acmesh.strugglers.net. 86400 TXT "bar"
> show
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:      0
;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
;; ZONE SECTION:
;acmesh.strugglers.net.         IN      SOA
 
;; UPDATE SECTION:
foo.acmesh.strugglers.net. 86400 IN     TXT     "bar"
 
> send
Sending update to 85.119.80.222#53
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:  19987
;; flags:; ZONE: 1, PREREQ: 0, UPDATE: 1, ADDITIONAL: 1
;; ZONE SECTION:
;acmesh.strugglers.net.         IN      SOA
 
;; UPDATE SECTION:
foo.acmesh.strugglers.net. 86400 IN     TXT     "bar"
 
;; TSIG PSEUDOSECTION:
strugglers.             0       ANY     TSIG    hmac-sha512. 1521454639 300 64 dPndp1/ZyqzmSEn0AKIsGR62HrsplJBhntWioM4oBdPlNXUIAwg7Jwpg DGSM2S3kY+5hfGTleNqwXZrMvnBhUQ== 19987 NOERROR 0 
 
 
Reply from update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:  19987
;; flags: qr; ZONE: 1, PREREQ: 0, UPDATE: 0, ADDITIONAL: 1
;; ZONE SECTION:
;acmesh.strugglers.net.         IN      SOA
 
;; TSIG PSEUDOSECTION:
strugglers.             0       ANY     TSIG    hmac-sha512. 1521454639 300 64 NfH/78kvq6f+59RXnyJwC6kfFRLGjG6Rh9jdYRId7UjH0jwIbtRVpqCu xx4HToGmlJrDTUqpgbYZq2orUOZlkQ== 19987 NOERROR 0
 
> [Ctrl-D]

And to verify it really got added (though the status of NOERROR should be confirmation enough):

$ dig +short -t txt foo.acmesh.strugglers.net
"bar"

That it; you can do dynamic DNS updates.

acme.sh ^

I’m going to assume you’ve installed acme.sh according to one of its supported installation methods. Personally I am not into curl | sh so I:

  • Create a system user that can’t log in.
  • git clone the source.
  • acme.sh --install it as that user.

acme.sh doesn’t have to be run on the primary DNS server, because it’s going to use a dynamic DNS update to do all the DNS things. It just needs access to the dynamic DNS update key file. Either you can install acme.sh on each host that will need to generate/renew certificates and copy the DNS key there, or else do all the certificate generation/renewal in one place and copy the certificate files around.

However you manage it, make sure that the user you’re going to run acme.sh as can read the dynamic DNS update key file.

Issuing the first wildcard certificate ^

The first time you issue the certificate you need to set NSUPDATE_KEY and NSUPDATE_SERVER in your environment. After the first successful issuance acme.sh will store these variables in its configuration for use in the automated renewals.

$ NSUPDATE_SERVER=a.authns.bitfolk.co.uk NSUPDATE_KEY=/path/to/strugglers.key ./acme.sh --issue -d strugglers.net -d '*.strugglers.net' --challenge-alias acmesh.strugglers.net --dns dns_nsupdate
[Mon 19 Mar 09:19:00 UTC 2018] Multi domain='DNS:strugglers.net,DNS:*.strugglers.net'
[Mon 19 Mar 09:19:00 UTC 2018] Getting domain auth token for each domain
[Mon 19 Mar 09:19:03 UTC 2018] Getting webroot for domain='strugglers.net'
[Mon 19 Mar 09:19:03 UTC 2018] Getting webroot for domain='*.strugglers.net'
[Mon 19 Mar 09:19:04 UTC 2018] Found domain api file: /path/to/acmesh/dnsapi/dns_nsupdate.sh
[Mon 19 Mar 09:19:04 UTC 2018] adding _acme-challenge.acmesh.strugglers.net. 60 in txt "WmenhbXRtenhpNLYLOBjznyHcVvFk-jjxurCVTrhWc8"
[Mon 19 Mar 09:19:04 UTC 2018] Found domain api file: /path/to/acmesh/dnsapi/dns_nsupdate.sh
[Mon 19 Mar 09:19:04 UTC 2018] adding _acme-challenge.acmesh.strugglers.net. 60 in txt "fwZPUBHijOQkJJaoOF_nIn3Z_FtuVU9R635NDVz_hPA"
[Mon 19 Mar 09:19:04 UTC 2018] Sleep 120 seconds for the txt records to take effect

At this point a DNS update has been crafted and sent so you should see your zone update and zone transfer happen to any secondary servers. If that doesn’t happen within 120 seconds then when Let’s Encrypt tries to verify the challenge it might query a DNS server that doesn’t yet have the record. Your zone transfers need to be reliable.

[Mon 19 Mar 09:21:08 UTC 2018] Verifying:strugglers.net
[Mon 19 Mar 09:21:12 UTC 2018] Success
[Mon 19 Mar 09:21:12 UTC 2018] Verifying:*.strugglers.net
[Mon 19 Mar 09:21:15 UTC 2018] Success
[Mon 19 Mar 09:21:15 UTC 2018] Removing DNS records.
[Mon 19 Mar 09:21:15 UTC 2018] removing _acme-challenge.acmesh.strugglers.net. txt
[Mon 19 Mar 09:21:16 UTC 2018] removing _acme-challenge.acmesh.strugglers.net. txt
[Mon 19 Mar 09:21:16 UTC 2018] Verify finished, start to sign.
[Mon 19 Mar 09:21:18 UTC 2018] Cert success.
-----BEGIN CERTIFICATE-----
MIIFETCCA/mgAwIBAgISAz4ZQV27n1FgemVAEhIqiUZnMA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
.
.
.
NeAmr5I=
-----END CERTIFICATE-----
[Mon 19 Mar 09:21:18 UTC 2018] Your cert is in  /path/to/acmesh/.acme.sh/strugglers.net/strugglers.net.cer 
[Mon 19 Mar 09:21:18 UTC 2018] Your cert key is in  /path/to/acmesh/.acme.sh/strugglers.net/strugglers.net.key 
[Mon 19 Mar 09:21:18 UTC 2018] The intermediate CA cert is in  /path/to/acmesh/.acme.sh/strugglers.net/ca.cer 
[Mon 19 Mar 09:21:18 UTC 2018] And the full chain certs is there:  /path/to/acmesh/.acme.sh/strugglers.net/fullchain.cer

Examining a certificate ^

Just for peace of mind…

$ openssl x509 -text -noout -certopt no_subject,no_header,no_version,no_serial,no_signame,no_subject,no_issuer,no_pubkey,no_sigdump,no_aux -in /path/to/acmesh/.acme.sh/strugglers.net/strugglers.net.cer
        Validity
            Not Before: Mar 19 08:21:17 2018 GMT
            Not After : Jun 17 08:21:17 2018 GMT
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Key Identifier: 
                BF:C7:8E:F5:87:05:D0:6E:15:AC:7B:37:9F:82:05:C3:E3:11:B7:32
            X509v3 Authority Key Identifier: 
                keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1
 
            Authority Information Access: 
                OCSP - URI:http://ocsp.int-x3.letsencrypt.org
                CA Issuers - URI:http://cert.int-x3.letsencrypt.org/
 
            X509v3 Subject Alternative Name: 
                DNS:*.strugglers.net, DNS:strugglers.net
            X509v3 Certificate Policies: 
                Policy: 2.23.140.1.2.1
                Policy: 1.3.6.1.4.1.44947.1.1.1
                  CPS: http://cps.letsencrypt.org
                  User Notice:
                    Explicit Text: This Certificate may only be relied upon by Relying Parties and only in accordance with the Certificate Policy found at https://letsencrypt.org/repository/

From the Subject Alternative Name we can see it is a wildcard certificate.

Scrobbling to Last.fm from D-Bus

Yesterday afternoon I noticed that my music player, Banshee, had not been scrobbling to my Last.fm for a few weeks. Last.fm seem to be in the middle of reorganising their site but that shouldn’t affect their API (at least not for scrobbling). However, it seems that it has upset Banshee so no more scrobbling for me.

Banshee has a number of deficiencies but there’s a few things about it that I really do like, so I wasn’t relishing changing to a different player. It’s also written in Mono which doesn’t look like something I could learn very quickly.

I then noticed that Banshee has some sort of D-Bus interface where it writes things about what it it doing, such as the metadata for the currently-playing track… and so a hackish idea was formed.

Here’s a thing that listens to what Banshee is saying over D-Bus and submits the relevant “now playing” and scrobble to Last.fm. The first time you run it it asks you to authorise it and then it remembers that forever.

https://github.com/grifferz/dbus-scrobbler

I’ve never looked at D-Bus before so I’m probably doing it all very wrong, but it appears to work. Look, I have scrobbles again! And after all it would not be Linux on the desktop if it didn’t require some sort of lash-up that would make Heath Robinson cry his way to the nearest Apple store to beg a Genius to install iTunes, right?

Anyway it turns out that there is a standard for this remote control and introspection of media players, called MPRIS, and quite a few of them support it. Even Spotify, apparently. So it probably wouldn’t be hard to adapt this script to scrobble from loads of different things even if they don’t have scrobbling extensions themselves.

“My IP is blocked by a repressive regime, can I have a different one?”

I asked this question on Twitter yesterday and got a wider range of responses than I expected, although from a limited number of people. So I wondered what others would think.

Say you sell virtual machines and a customer says:

My service allows journalists and others inside repressive regimes to get their stories out. My IP address is being blocked by one of these repressive regimes. Can you switch it for another one?

Would you grant that request?

Assume you have never heard of their service or anyone that uses it, have no independent verification of what whether they are saying is true, and haven’t yet looked for any.

Responses so far could roughly be grouped as:

  • 2x “Yes; it’s a reasonable request and other networks’ policies are their own business”
  • 2x “Yes; once, but check it’s not some global spam blacklisting issue”
  • 3x “Yes; but charge them for your time each time they ask for this”
  • 2x “No; you’ll end up with all your IPs blocked, which may affect other customers”
  • 1x “No; tell them to use a cloud with a constantly-changing IP address” (involves me losing the customer)

What would you do?

Scanning for open recursive DNS resolvers

A few days ago we unfortunately had some abuse reports regarding customers with DNS resolvers being abused in order to participate in a distributed denial of service attack.

Amongst other issues, DNS servers which are misconfigured to allow arbitrary hosts to do recursive queries through them can be used by attackers to launch an amplified attack on a forged source address.

I try to scan our address space reasonably often but I must admit I hadn’t done so for some time. I kicked off another scan and found one more customer with a misconfigured resolver, which has since been fixed.

After mentioning that I would do a scan I was asked how I do that.

I use a Perl script I’ve hacked together over the last couple of years. I took a few minutes to tidy it up and add a small amount of documentation (run it with --man to read that), so here it is in case anyone finds it useful:

Update: This code has now been moved to GitHub. If you have any comments, problems or improvements please do submit them as an issue and I will get to it much quicker. The gist below is now out of date so please don’t use it.

Using the default 100 concurrent queries it scans a /21 in about 80 seconds (YMMV depending upon how many hosts you have that firewall 53/UDP). That scales sort of linearly with how many you do, so using -q 200 for example will cut that down to about 40 seconds. It’s only a select loop though so it’ll use more CPU if you do that.

Two things I’ve noticed since:

  • It doesn’t handle failing to create a socket with bgsend so for example if you run up against your limit of file descriptors (commonly ~1024 on Linux) the whole thing will get stuck at 100% CPU.
  • One person reporting a similar situation (bgsend fails, stuck at 100% CPU) when they allowed it to try to send to a broadcast address. I haven’t been ale to replicate that one yet.

Converting an IPv6 address to its reverse zone in Perl

I’m needing to work out the IPv6 reverse zone for a given IPv6 CIDR prefix, that is a prefix with number of bits in the network on the end after a forward slash. e.g.:

  • 2001:ba8:1f1:f004::/64 → 4.0.0.f.1.f.1.0.8.a.b.0.1.0.0.2.ip6.arpa
  • 4:2::/32 → 2.0.0.0.4.0.0.0.ip6.arpa
  • 2001:ba8:1f1:400::/56 → 0.0.4.0.1.f.1.0.8.a.b.0.1.0.0.2.ip6.arpa

I had a quick look for a module that does it, but couldn’t find one, so I hacked this subroutine together:

Is there a more elegant way? Is there a module I can replace this with?

Must support:

  • Arbitrary prefix length
  • Use of ‘::’ anywhere legal in the address

Linux, IPv6, router advertisements and forwarding

By default, a Linux host on an IPv6 network will listen for and solicit router advertisements in order to choose an IPv6 address for itself and to set up its default route. This is referred to as stateless address autoconfiguration (SLAAC).

If you don’t want a host to automatically configure an address and route then you could disable this behaviour by writing “0” to /proc/sys/net/ipv6/conf/*/accept_ra.

Additionally, if the Linux host considers itself to be a router then it will ignore all router advertisements.

In this context, what makes the difference between router or not are the settings of the /proc/sys/net/ipv6/conf/*/forwarding files (or the net.ipv6.conf.*.forwarding sysctl). If you turn your host into a router by setting one of those to “1”, you may find that your host removes any IPv6 address and default route it learnt via SLAAC.

There is a valid argument that a router should not be autoconfiguring itself, and should have its addresses and routes configured statically. Linux has IP forwarding features for a reason though, and sometimes you want to forward packets with a Linux box while still enjoying autoconfiguration. In my case I have some hosts running virtual machines, with IPv6 prefixes routed to the virtual machines. I’d still like the hosts to learn their default route via SLAAC.

It’s taken me a long time to work out how to do this. It isn’t well-documented.

Firstly, if you have a kernel version of 2.6.37 or higher then your answer is to set accept_ra to “2”. From ip-sysctl.txt:

accept_ra – BOOLEAN

Accept Router Advertisements; autoconfigure using them.

Possible values are:

  • 0 Do not accept Router Advertisements.
  • 1 Accept Router Advertisements if forwarding is disabled.
  • 2 Overrule forwarding behaviour. Accept Router Advertisements even if forwarding is enabled.

Functional default:

  • enabled if local forwarding is disabled.
  • disabled if local forwarding is enabled.

This appears to be a type of boolean that I wasn’t previously familiar with – one that has three different values.

If you don’t have kernel version 2.6.37 though, like say, everyone running the current Debian stable (2.6.32), this will not work. Helpfully, it also doesn’t give you any sort of error when you set accept_ra to “2”. It just sets it and continues silently ignoring router advertisements.

fuuuuuuuuuuuuuuuuuuuuuu

Fortunately Bjørn Mork posted about a workaround for earlier kernels which I would likely have never discovered otherwise. You just have to disable forwarding for the interface that your router advertisements will come in on, e.g.:

# echo 0 > /proc/sys/net/ipv6/conf/eth0/forwarding

Apparently as long as /proc/sys/net/ipv6/conf/all/forwarding is still set to “1” then forwarding will still be enabled. Obviously.

Additionally there are some extremely unintuitive interactions between “default” and “all” settings you may set in /etc/sysctl.conf and pre-existing interfaces. So there is a race condition on boot between IPv6 interfaces coming up and sysctl configuration being parsed. martin f krafft posted about this, and on Debian recommends setting desired sysctls in pre-up headers of the relevant iface stanza in /etc/network/interfaces, e.g.:

iface eth0 inet6 static
    address 2001:0db8:10c0:d0c5::1
    netmask 64
# Enable forwarding
    pre-up echo 1 > /proc/sys/net/ipv6/conf/default/forwarding
    pre-up echo 1 > /proc/sys/net/ipv6/conf/all/forwarding
# But disable forwarding on THIS interface so we still get RAs
    pre-up echo 0 > /proc/sys/net/ipv6/conf/$IFACE/forwarding
    pre-up echo 1 > /proc/sys/net/ipv6/conf/$IFACE/accept_ra
    pre-up echo 1 > /proc/sys/net/ipv6/conf/all/accept_ra
    pre-up echo 1 > /proc/sys/net/ipv6/conf/default/accept_ra

You will now have forwarding and SLAAC.

everything went better than expected

My email marketing adventure with British Telecom

The saga so far ^

I have a phone line from BT. I only use it for ADSL (which I get from Zen Internet). I gave my email address to BT because they offered to tell me useful things about my account via email. I now wish I had never done this.

I use extension addresses to identify what the email addresses are being used for. This is not a new idea and I didn’t invent it. For those who don’t know what an extension address is, it’s an email address like andy+foo@example.com. It ends up at the same place as andy@example.com. The point is that if I receive an email to andy+foo@example.com then I know that it’s either from whoever I gave that address to, or it’s from someone they gave/lost my address to. It’s handy for working out who’s sold their database to spammers, or had it stolen.

I used to prefer using “+” in the extension address just because it looks nicer to me than other popular alternatives like “-“. Unfortunately, some web developers are idiots and don’t believe that “+” is valid in an email address, so they try to help by refusing to accept the address. For that reason my email servers accept both “+” and “-” and I used to use “-” when “+” wasn’t accepted.

After I started doing that, I began to experience an even more annoying failure: web sites that accepted “+” in my email address when I signed up, but later got redeveloped by idiots who think that “+” is no longer valid. That means that I can no longer log in to those sites, and predictably customer service is not trained to deal with situations like that.

It seems that BT is an example of such a company, and I am having unbelievable difficulty finding anyone there that can understand this.

When I signed up with BT, the email address I gave them had a “+” in it. They accepted it at the time.

March 2011 ^

I start to receive marketing emails from BT for extra BT services, as well as BT group companies such as Dabs and Plusnet.

29th March 2011 ^

I receive another marketing email from BT, decide I don’t want to receive them any more, and follow the unsubscribe link. The unsubscribe page at http://bt.custhelp.com/app/contact/c/769,978 tells me that the email address (which BT is emailing me on) is invalid.

I contact BTCare on Twitter to ask them how to opt out and to opt me out on my behalf. Also sent a request via BT’s site for someone to call me back about it.

Am called back by a polite BT chap who totally failed to understand the problem, told me I was opted out (funny, I never opted in…) and advised that I sign up to a no commercial email scheme.

18th April 2011 ^

Receive more marketing email from BT. Ask BTCare on Twitter why that is. Am told that it can take a month to take effect.

18th May 2011 ^

Receive more marketing email from BT. Ask BTCare on Twitter why that is.

29th May 2011 ^

BTCare tells me on twitter that they opted out the wrong address last time. Apologises and says it may take a further month.

25th July 2011 ^

BT sends me a marketing email on behalf of Plusnet.

2nd August 2011 ^

I (somewhat exasperatedly) ask BTCare if, since they can’t opt me out of the emails, we can come to a more formal arrangement for my proofreading services of £50 per future email.

BTCare replies that “We can’t opt you out of emails for other companies” and that “no compensation is available sorry.”

I point out that Plusnet is a BT company, that the emails are sent by BT on an email address given only to BT, and contain a BT unsubscribe link which does not work.

3rd August 2011 ^

BTCare asks if the email was from BT, and advises the use of a US-based commercial email opt-out site.

4th August 2011 ^

BTCare tells me that their unsubscribe link works now and that I should try it again. I try it again. It fails the same way. I tell BTCare.

5th August 2011 ^

BTCare tells me that I need to contact Plusnet directly: “the link may be BT related but its seperate to us and we have no control over them

PlusNet (on twitter and identica) disagrees with BTCare and says BT sends those emails and operates the unsubscribe facility. They give me an email address at Plusnet to forward the marketing to anyway.

I have forwarded the email there and have so far got nothing back except an out of office email bounce. Oh well, it’s not really their problem anyway.

What to do now? ^

I would quite like to send a snail mail letter to BT to complain about this cluelessness. Does anyone know the best postal address and entity within BT for that to be directed to? If nothing else perhaps I can start sending the £50 a time invoices there?

I’d also quite like to not be a BT customer after this. I’m not too aware of my choices on that front though. My DSL is currently through Zen Internet, who I’m fairly happy with. I’d like a bit faster but don’t want to become a Sky or Virgin Media customer.

I’m told I can get Zen to “take over the copper”. What does this mean? Would it cause me difficulty in switching to another ISP in future?

Finally I have a feeling that there’s some DPA consequences for failing to opt me out of marketing in 4 months of asking, and then saying that I can’t get them to opt me out of marketing from companies they have given my email address to. Worth dropping a line to ICO?

Just hit delete / block all email from BT ^

Yeah it’s not that annoying but hopefully you can agree that this run-around is ridiculous. While I remain a BT customer I would prefer not to bitbucket all email from them as they do sometimes send stuff related to the operation of my account.

On extension addresses ^

It’s a shame, but I now consider “+” as unusable in an extension address because of idiot web developers who turn sites that used to accept these completely valid addresses into sites that reject them.

Just use “-” instead. It doesn’t look as pretty but at least not even the most ill-informed developer can think that “-” is invalid. If your email address already contains “-” (perhaps because your name does?), shit, sucks to be you.

Domain name as hostname not recommended

I had an interesting support ticket yesterday.

Someone was trying to do an apt-get update via BitFolk‘s apt cache and was ending up connecting to 2607:f0d0:1003:85::c40a:2942, where it was failing to update. This is not a BitFolk IPv6 address, nor is it the IPv6 address of a Debian mirror. Where was it coming from?

I’d asked the customer for the contents of a bunch of config files and output of the dig command, and while I was waiting for that I mentioned the problem on IRC, where Graham said:

<gdb> $ dig -t aaaa +short apt-cacher.com.net
<gdb> 2a00:1c10:3:634::3486:75a0
<gdb> 2607:f0d0:1003:85::c40a:2942
<grifferz> interesting
<gdb> Same for apt-cacher.bitfolk.com.net
<grifferz> so he's probably got some  search line in
           his resolv.conf
<gdb> I would ask what the search line is
<grifferz> r
<grifferz> search lines always good entertainment for
           those times when wtf moments are scarce
<gdb> Actually it's possible that the hostname is
      foo.net and there's no search line.

It seems that the enterprising folks at com.net have put in wildcard A and AAAA records which basically means that if you try to resolve *.com.net you end up at their “search portal”. That’s all web-based of course.

The customer didn’t have a search line, but the issue was that their host had a fully-qualified domain name (FQDN) along the lines of example.net.

This meant that according to default resolver settings it considered itself to be inside the domain net, and when searching for hosts (like apt-cacher.bitfolk.com) it would try to find them with .net appended first.

Massively confusing.

It can be fixed by giving the resolver libraries a hint as to which domain you are actually in, in the /etc/resolv.conf:

domain example.net
nameserver 192.168.1.2
nameserver 192.168.1.3

Having said that, it’s better not to pick your domain as the FQDN for any host and this is just one of the weird issues I have seen.

Sometimes customers order a VPS with a FQDN set to something like this, and I’ve yearned for an authoritative bit of documentation that says it’s not recommended. I asked about it on HantsLUG a while back also, and while it seems there was some agreement, it still seems to be down to preference.

I’ve never really tried to tell a prospective customer that they should pick a host within their domain (e.g. foo.example.net) instead of the domain name as the FQDN, because it always seemed like too complicated a subject to explain. Maybe I should try to find a way in future.

Mass-setting the default view mode for cacti

Recently it came to my attention that many of BitFolk‘s customers were finding our Cacti install confusing. The main problem was that upon logging in they were confronted with the default graph view – the “Tree View” – and they didn’t understand where they might find the relevant graphs within this tree.

Experienced Cacti users will know that you can also click on the “List View” or “Preview View” to get a list or grid respectively of all graphs that they’re permitted to view, but most customers are not experienced Cacti users. For me personally, having permission to view some 1400 graphs I appreciate the tree view to enforce some order, but it’s not about me. Customers generally have 2-5 graphs to view.

I decided that I would set the default view for all users to be “Preview View”. Now I wasn’t going to click on every one of the hundreds of them in the web interface to set this, and I wasn’t going to send instructions to people on how to do it for themselves either. I decided to fiddle with the database directly. This turned out to be very simple, once you know how. Here’s how.

Danger, Will Robinson! ^

Firstly, don’t do this lightly. This worked with Cacti as present in Debian squeeze and I don’t believe the database schema has changed in ages, but maybe it has or maybe it will, so before you try this:

  • Take a backup of your cacti database. Just mysqldump it or whatever.
  • Check that the queries make sense. You might like to practice on just one user account before doing it on all.

Basically if you don’t understand what these queries do, don’t do them.

There are probably more elegant ways to do this, but it’s only going to be done once so I’m not going to try to optimise it.

Set “What to do when this user logs in” to “Show the default graph screen” ^

mysql> UPDATE user_auth SET login_opts=3;

Set “Which mode you want displayed when you visit ‘graph_view.php'” to “Preview View” for users who have graph settings already ^

If a user has changed their graph settings then they’ll have a row in the settings_graphs table for this particular setting already.

mysql> UPDATE settings_graphs SET value=3 WHERE name="default_view_mode";

Set “Which mode you want displayed when you visit ‘graph_view.php'” to “Preview View” for users with no graph settings ^

Most users won’t have changed their graph settings and so won’t have a row for this. We’ll need to insert one.

mysql> INSERT IGNORE INTO settings_graphs (user_id, name, value)
  SELECT id, "default_view_mode", 3 FROM user_auth;

The IGNORE is required because there will probably be some pre-existing rows from users who did have some graph settings.

You could probably combine the UPDATE and INSERT steps by using REPLACE instead.

That’s it ^

On next login, the user should be put directly at the “Preview View”. They can still change their settings to something different if they end up not liking that.