Mac OS X Yosemite ignoriert DNS Suchdomains

Der lokale DNS Resolver in Mac OS X 10.10 (Yosemite) berücksichtigt die eingetragenen Suchdomains (mal wieder) nicht, wenn der gesuchte Hostname bereits einen Punkt enthält. Leider hat sich auch der Resolver geändert, so dass die Tipps aus 2011 hier auch nicht mehr helfen.

Mit dem Update auf 10.10.1 gibt es aber wieder eine Option, mit der man diese Funktion wieder herstellen kann:

sudo defaults write \
        /System/Library/LaunchDaemons/com.apple.discoveryd.plist \
        ProgramArguments -array-add "--AlwaysAppendSearchDomains"

Danach einfach noch den discoveryd durchstarten

sudo launchctl unload -w \
     /System/Library/LaunchDaemons/com.apple.discoveryd.plist
sudo launchctl load -w \
     /System/Library/LaunchDaemons/com.apple.discoveryd.plist

und danach funktioniert es wieder wie gewünscht.

DynDNS Alternative selbst gebaut in 10 Minuten

Da die Firma DynDNS aufgrund eines Versäumnisses meinerseits diverse meiner kostenlosen Accounts rückstandsfrei eliminiert hat, habe ich mich gestern spontan entschlossen, den Service einfach selbst nachzubauen. Zumindest mit so viel (bzw. wenig) Funktionalität, wie ich wirklich benötige.

Man braucht folgendes:

  • Sowas wie einen „Root-Server“ mit einer öffentlichen IP-Adresse (z.B. 123.234.231.132) und einer Domain (z.B. meinedomain.de
  • Die Möglichkeit, eigene DNS-Einträge in der Domain vorzunehmen (genauer gesagt NS-Einträge für eigene Nameserver)
  • Auf dem „Root-Server“:
    • MySQL
    • PowerDNS
    • Apache mit mod_cgi

In meinem Fall war auf dem Server bereits Debian 6 (squeeze) mit MySQL und Apache installiert. PowerDNS fehlte noch.

Als erstes habe ich PowerDNS inklusive seinem MySQL-Backend installiert. Bei der Gelegenheit kann man auch direkt die später nötigen Perl-Module mit installieren:

aptitude install pdns-server pdns-backend-mysql libdbd-mysql-perl libcgi-pm-perl

PowerDNS wird die Frage stellen, an welches Interface (IP-Adresse) er sich binden soll. Das muss jeder selbst entscheiden. Es sollte nur ein DNS-Name (A-record) auf diese IP-Adresse zeigen, den müssen wir nämlich später in den SOA-Records verwenden.

Anschließend muss auf dem MySQL-Server eine Datenbank pdns mit ein paar Tabellen erzeugt werden. Das geht mit folgendem SQL-Schnipsel:

CREATE DATABASE pdns;
USE pdns;

CREATE TABLE domains (
  id int(11) NOT NULL AUTO_INCREMENT,
  name varchar(255) NOT NULL,
  master varchar(20) DEFAULT NULL,
  last_check int(11) DEFAULT NULL,
  type varchar(6) NOT NULL,
  notified_serial int(11) DEFAULT NULL,
  account varchar(40) DEFAULT NULL,
  change_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  UNIQUE KEY name_index (name)
) ENGINE=InnoDB;

CREATE TABLE records (
  id int(11) NOT NULL AUTO_INCREMENT,
  domain_id int(11) DEFAULT NULL,
  name varchar(255) DEFAULT NULL,
  type varchar(6) DEFAULT NULL,
  content varchar(255) DEFAULT NULL,
  ttl int(11) DEFAULT NULL,
  prio int(11) DEFAULT NULL,
  username varchar(12) DEFAULT NULL,
  password varchar(64) DEFAULT NULL,
  change_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  KEY rec_name_index (name),
  KEY nametype_index (name,type),
  KEY domain_id (domain_id)
) ENGINE=InnoDB;

CREATE TABLE supermasters (
  ip varchar(25) NOT NULL,
  nameserver varchar(255) NOT NULL,
  account varchar(40) DEFAULT NULL
);

GRANT SELECT ON supermasters TO pdns@localhost IDENTIFIED BY 'SuperGehaimesPasswort';
GRANT ALL ON domains TO pdns@localhost IDENTIFIED BY 'SuperGehaimesPasswort';
GRANT ALL ON records TO pdns@localhost IDENTIFIED BY 'SuperGehaimesPasswort';

Anschließend legen wir eine Domain an, z.B. dyn.meinedomain.de, und darin dann ein paar nötige Records:

INSERT INTO domains (name, type) VALUES ('dyn.meinedomain.de', 'NATIVE');

INSERT INTO records (domain_id, name, type, content) VALUES
(1, 'dyn.meinedomain.de', 'SOA', 'dns.meinedomain.de hostmaster.meinedomain.de 2013121900 1800 300 604800 3600'),
(1, 'dyn.meinedomain.de', 'NS',  'dns.meinedomain.de'),
(1, 'dyn.meinedomain.de', 'A',   '123.234.231.132');

Dabei sollte dns.meinedomain.de durch den DNS-Namen der IP-Adresse ersetzt werden, auf der der vorhin installierte PowerDNS zu erreichen ist. dyn.meinedomain.de ist die Subdomain, unter der später die dynamisch zu erreichenden Hosts angelegt werden.

Der A-Record (letzter Datensatz) ist später für den Apache nötig und sollte daher auch auf die IP-Adresse zeigen, auf der der Apache sitzt.

Nun muss PowerDNS noch konfiguriert werden, so dass er auch das MaSQL-Backend benutzt. Das geht, indem in der Datei /etc/powerdns/pdns.d/pdns.local folgende Zeilen ergänzt werden:

# MySQL Configuration
launch=gmysql
gmysql-host=localhost
gmysql-dbname=pdns
gmysql-user=pdns
gmysql-password=SuperGehaimesPasswort

Nach einem PowerDNS-Neustart mit service pdns restart sollte MySQL mit SHOW PROCESSLIST; ein paar Verbindungen des Users pdns anzeigen:

mysql> show processlist;
+-------+------+-----------+------+---------+------+-------+------------------+
| Id    | User | Host      | db   | Command | Time | State | Info             |
+-------+------+-----------+------+---------+------+-------+------------------+
|  7104 | pdns | localhost | pdns | Sleep   |  160 |       | NULL             |
|  7105 | pdns | localhost | pdns | Sleep   |   64 |       | NULL             |
|  7106 | pdns | localhost | pdns | Sleep   |   37 |       | NULL             |
|  7108 | pdns | localhost | pdns | Sleep   |  129 |       | NULL             |
| 30246 | root | localhost | NULL | Query   |    0 | NULL  | show processlist |
+-------+------+-----------+------+---------+------+-------+------------------+
5 rows in set (0.00 sec)

mysql>

So. Wenn das geklappt hat, sollte man mit dem Kommando dig @123.234.231.132 dyn.meinedomain.de any die in der Datenbank angelegten Einträge zu sehen bekommen.

# dig @123.234.231.132 dyn.meinedomain.de any

; <<>> DiG 9.7.3 <<>> @123.234.231.132 dyn.meinedomain.de any
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6595
;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;dyn.meinedomain.de.		IN	ANY

;; ANSWER SECTION:
dyn.meinedomain.de.	3600	IN	NS	dns.meinedomain.de.
dyn.meinedomain.de.	3600	IN	SOA	dns.meinedomain.de. hostmaster.meinedomain.de. 2013121900 1800 300 604800 3600
dyn.meinedomain.de.	3600	IN	A	123.234.231.132

;; Query time: 8 msec
;; SERVER: 123.234.231.132#53(123.234.231.132)
;; WHEN: Thu Dec 19 14:27:43 2013
;; MSG SIZE  rcvd: 140

Wenn das so weit klappt, können wir in die DNS-Konfiguration des Providers gehen und die Subdomain an unseren Server delegieren. Das geht ganz einfach, indem man im DNS-Zonefile der Zone meinedomain.de folgenden Record hinzufügt:

dyn                      IN NS      dns.meinedomain.de.

Jetzt (bzw. nachdem die TTL beim angefragten Cache abgelaufen ist) werden DNS-Anfragen für [irgendwas].dyn.meinedomain.de an unseren PowerDNS-Server dns.meinedomain.de weiter geleitet.

Wir gehen nochmal in die Datenbank und legen dort einen A-Record für den ersten DynDNS-Host an, z.B. so:

INSERT INTO records (domain_id, name, type, content, username, password) VALUES
(1, 'test.dyn.meinedomain.de', 'A', '127.0.0.1', 'benutzer', 'Gehaim');

Benutzername und Passwort werden später im DynDNS-Client angegeben. Die IP-Adresse ist hier erstmal 127.0.0.1. Man sollte den Record dann auch mit dig test.dyn.meinedomain.de abfragen können.

Jetzt brauchen wir ein Update-Script. Ich habe das Verzeichnis /opt/dyn erzeugt und darin die Datei update, ein Perl-Script mit folgendem Inhalt:

#!/usr/bin/perl

use strict;
use warnings;
use CGI;
use DBI;
use utf8;

binmode STDOUT, ':encoding(UTF-8)';

my $q = new CGI;
my $ip = $q->param('ip');
my $user = $q->param('user');
my $pass = $q->param('pass');
my $host = $q->param('host');

unless (
    $ip   =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ and
    $user =~ /^[a-zA-Z0-9\-\._]+$/ and
    $pass !~ /['"\t]/ and
    $host =~ /^\S+\./
) {
    print $q->header( -status => '400 Bad Request' );
    exit;
}

my $dbh = DBI->connect("DBI:mysql:database=pdns;host=localhost", 'pdns', 'SuperGehaimesPasswort', {'RaiseError' => 1, 'mysql_enable_utf8' => 1});
my $db_update = $dbh->prepare('UPDATE records SET content = ? WHERE name = ? AND TYPE = ? AND username = ? AND password = ?');

eval {
    $db_update->execute($ip, $host, 'A', $user, $pass);
};
if ($@) {
    warn 'SQL Error: ' . $@;
    print $q->header( -status => '500 Internal Server Error' );
    $dbh->disconnect;
    exit;
}

$dbh->disconnect;

print $q->header( -status => '200 OK' );

Die Datei muss ausführbar sein, also sollte man nach dem Anlegen chmod +x update machen!

Anschließend wird der Apache konfiguriert, indem man die Datei /etc/apache2/conf.d/dyn mit folgendem Inhalt anlegt:

Alias /dyn /opt/dyn

<Directory /opt/dyn>
    <FilesMatch ^(update)$>
        SetHandler cgi-script
        Options +ExecCGI
    </FilesMatch>
</Directory>

Das sorgt dafür, dass das Update-Script unter der URL http://dyn.meinedomain.de/dyn/update erreichbar ist. Es erwartet vier Parameter:

  • IP-Adresse
  • Benutzername
  • Passwort
  • Hostname

Folgendermaßen sieht also ein Aufruf aus:

http://dyn.meinedomain.de/dyn/update?ip=127.0.0.2&user=benutzer&pass=Gehaim&host=test.dyn.meinedomain.de

Ein Aufruf dieser URL sollte dafür sorgen, dass der A-Record für test.dyn.meinedomain.de aktualisiert wird und ab sofort auf 127.0.0.2 zeigt.

In meiner Fritz!Box 7390 kann ich als DynDNS-Provider "Benutzerdefiniert" wählen und dann die entsprechende URL als Update-URL mit Platzhaltern eintragen. Das sieht dann so aus:

http://dyn.meinedomain.de/dyn/update?ip=<ipaddr>&user=<username>&pass=<pass>&host=<domain>

Das Feld "Domainname" wird mit test.dyn.meinedomain.de gefüllt, Benutzername und Passwort mit benutzer und Gehaim.

Ein anderer Router im Dunstkreis meiner Administration hat ein altes FreeWRT. Dort habe ich den veralteten ez-ipupdate einfach abgeschaltet und durch das Skript /etc/ppp/ip-up.d/zzz_dyn_dns ersetzt, was folgenden Inhalt hat:

#!/bin/sh
# pppd provides $IFNAME and $IPREMOTE amd $IPLOCAL
wget -O /dev/null -q http://dyn.meinedomain.de/dyn/update?ip=$IPLOCAL\&user=benutzer\&pass=Gehaim\&host=test.dyn.meinedomain.de

Fertig.

Man kann in der Datenbank im Feld change_date ablesen, wann der Client das letzte Mal aktualisiert hat. Die Fritz!Box 7390 kann auch per HTTPS aktualisieren, der wget Client im FreeWRT will das nicht. Damit kann ich aber leben. Benutzername und Passwort werden ohnehin als GET-Parameter in der URL übertragen. Wobei es dem Perl-Script update eigentlich egal sein dürfte, ob die Parameter als GET oder POST kommen.

Was noch fehlt ist ein schickes kleines Web-Interface. Vielleicht kommt ja eines über die Feiertage... 😉

Lokaler DNS Resolver ignoriert Suchdomains in Mac OS X Lion

Der lokale DNS Resolver in Mac OS X 10.7 (Lion) berücksichtigt die eingetragenen Suchdomains nicht, wenn der gesuchte Hostname bereits einen Punkt enthält.

Enthält der Suchdomaineintrag „example.com“, war es bisher möglich mit ping www.subdomain den Host www.subdomain.example.com zu erreichen. Mit MacOS X Lion ist dies jetzt nicht mehr möglich. Mit einer Änderung der Optionen für den lokalen DNS Resolver ist es möglich, den alten Zustand wieder herzustellen. Hierzu muss die Option -AlwaysAppendSearchDomains aktiviert werden.

Da in der Regel keine Optionen gesetzt sind, kann dies durch den Befehl

sudo defaults write /System/Library/LaunchDaemons/com.apple.mDNSResponder \
        ProgramArguments -array-add "-AlwaysAppendSearchDomains"

erreicht werden. Danach muss der mDNSResponder neu gestartet werden.

sudo launchctl unload -w \
           /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist
sudo launchctl load -w \
           /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist

Ein weiterer Parameter für den mDNSResponder ist -NoMulticastAdvertisements. Damit kann man verhindern, dass diverse Dienste nicht mehr per Multicast verkündet werden. Allen voran ist hier Bonjour zu nennen.

Ähnlich wie bei -AlwaysAppendSearchDomains kann diese Option mittels

sudo defaults write /System/Library/LaunchDaemons/com.apple.mDNSResponder \
        ProgramArguments -array-add "-NoMulticastAdvertisements"

eingestellt werden.

Für den Fall, dass man das Ganze wieder Rückgängig machen will/muss, kann dies durch den Befehl

sudo defaults write /System/Library/LaunchDaemons/com.apple.mDNSResponder \
        ProgramArguments -array /usr/sbin/mDNSResponder -launchd

erreicht werden.