2026-08-12 11:39:46 +02:00
2026-05-22 11:30:29 +02:00
2026-08-12 11:54:54 +02:00
2026-08-12 17:14:11 +02:00
2026-09-16 09:18:12 +02:00
2026-09-22 15:23:45 +02:00
2026-08-12 11:36:19 +02:00
2026-08-12 11:36:19 +02:00
2026-05-20 16:43:54 +02:00
2026-09-22 15:23:45 +02:00
2026-09-22 15:23:45 +02:00
2026-08-12 12:19:22 +02:00
2026-09-16 09:18:12 +02:00
2026-09-22 15:23:45 +02:00

dns

A command line front for the infoblox grid. It adds and removes host records, aliases, txt and mx records, hands out the next free address, writes dhcp options, restarts the grid, stands in as a certbot hook, and answers in one line of json when a script is asking.

One binary and nothing else: no runtime to install, no configuration file to write, no login to keep somewhere. The credentials are sealed into the program and unsealed once, and the program updates itself from the gitea releases.

$ dns -f mail
mail1.fhi.mpg.de     141.14.130.21 (00:1b:21:3c:4d:5e)
mail2.fhi.mpg.de     141.14.130.22
mailgate.fhi.mpg.de  141.14.128.9

Contents

Installing

Every release carries a binary per platform — darwin and linux, amd64 and arm64. Take the one for the machine from the releases, name it dns and put it in the path:

curl -Lo dns https://git.fhi.mpg.de/mike/dns/releases/download/2.5.1/dns-darwin-arm64
chmod 755 dns
mv dns ~/bin/

From there it keeps itself current — see Keeping current.

The first run

Two things have to be right before anything happens at all.

The network. dns runs from 141.14.128.0/20 and nowhere else. From another address it says access violation, permission denied and stops — before the login, before the grid is touched.

The login. It sits sealed inside the binary, under a passphrase shared among the people who use dns. The first run asks for it once, and writes the login to ~/.dnsrc, encrypted and mode 0600; every run after that reads that file and asks nothing.

$ dns -s myhost
? passphrase **********
OK: credentials unsealed to /Users/mike/.dnsrc
{
  "_ref": "record:host/ZG5zLmhvc3Q...",
  "ipv4addrs": [ ... ],
  "name": "myhost.fhi.mpg.de",
  ...
}

A run with nobody sitting there — cron, the certbot hooks — never asks. It says what is missing and stops. So run dns once by hand on every machine that is going to use it, under the account that will be running it.

Names

A name without a dot is completed with .fhi.mpg.de: -s myhost and -s myhost.fhi.mpg.de ask the same question. A name with a dot anywhere in it is taken as it stands, which is how a host, an alias or a mail server outside the default domain is named.

Host records

dns -a myhost                                        # with the next free address
dns -a myhost -i 141.14.130.17                       # with that one
dns -a myhost -i 141.14.130.17 -m 00:1b:21:3c:4d:5e  # and a dhcp reservation
dns -s myhost                                        # the record, as infoblox holds it
dns -f mail                                          # every host whose name contains that
dns -i 141.14.130.17                                 # what sits on that address
dns -l                                               # every free address in the network
dns -d myhost                                        # remove it (asks first)

Without -i the next free address in 141.14.128.0/20 is taken, and the answer says which one it was.

A mac address turns the record into a dhcp reservation, and a reservation only takes effect once the grid has restarted — so that restart happens by itself. The record is in either way: a restart that goes wrong comes back as a warning beside the answer, not as an answer of its own.

Aliases

dns -q myhost                 # the aliases the record carries
dns -q myhost -a www          # add one
dns -q myhost -d www          # take one away (asks)
dns -q myhost -D              # take all of them away (asks)

Aliases belong to the host record, so they go in by rewriting the list on it. The list is kept sorted and without duplicates.

Txt records

dns -t _dmarc.fhi.mpg.de -a "v=DMARC1; p=none"   # add
dns -t _dmarc.fhi.mpg.de                         # show
dns -t _dmarc.fhi.mpg.de -D                      # remove (asks)

One name can carry several txt records. -D removes all of them, and every one is tried before anything is said about it: one that will not go is no reason to leave the rest standing.

Mx records

The name of an mx record is the domain the mail is addressed to, not a host, and the preference decides in which order several of them are tried, lowest first.

dns -M fhi.mpg.de -a mail1 -p 10      # mail for fhi.mpg.de goes to mail1.fhi.mpg.de
dns -M fhi.mpg.de -a mail2 -p 20      # second in line
dns -M fhi.mpg.de -a mail1 -p 30      # the same server again: moved, not doubled
dns -M fhi.mpg.de -a mx.provider.com  # a server outside the domain
dns -M fhi.mpg.de                     # what the domain has, lowest preference first
dns -M fhi.mpg.de -d mail2            # take one out (asks)
dns -M fhi.mpg.de -D                  # take all of them out (asks)

-a both adds and changes, because the mail server is what a single record is addressed by. One that is already on the domain has its preference moved: infoblox would otherwise take the second one — same domain, same server, another preference — and the domain would end up with two records where one was meant.

Without -p nothing on an existing record is touched; the default of 10 is for a record that is being created. A preference runs from 0 to 65535, and 0 is a preference like any other.

$ dns -M fhi.mpg.de
Mx records for 'fhi.mpg.de'
     10  mail1.fhi.mpg.de
     20  mail2.fhi.mpg.de

Dhcp options

dns -o support/xtest.json -i 141.14.130.17

The file goes to the address record as it stands — the two in support/ are the ones in use, for netboot and for opsi. The grid is restarted afterwards.

The grid

dns -r

RESTART_IF_NEEDED, all services, the members one after another. It is the same restart the dhcp operations do by themselves.

Certbot

-c and -x are the two hooks of a dns-01 challenge. They read CERTBOT_DOMAIN and CERTBOT_VALIDATION from the environment, and write and remove the _acme-challenge.<domain> txt record:

certbot certonly --manual --preferred-challenges dns \
  --manual-auth-hook "dns -c" --manual-cleanup-hook "dns -x" \
  -d fhi.mpg.de -d '*.fhi.mpg.de'

The auth hook waits ten seconds after writing, so that the record has spread before letsencrypt looks for it. The cleanup hook removes every txt record of that name, which is what a run that was interrupted earlier leaves behind.

Two things to watch: ~/.dnsrc has to exist for the account certbot runs as — nothing here can ask for a passphrase — and these two hooks, unlike everything else, end the run with 1 when they fail, because certbot has to notice.

Json for scripts

-j puts one json object on stdout and nothing else: no colours, no sentences, no questions. Whatever the operation, the answer has the same shape, and so does everything that can go wrong before it — the network check, the login, the service, infoblox itself.

$ dns -M fhi.mpg.de -j
{"ok":true,"action":"showmx","name":"fhi.mpg.de","mxs":[{"mx":"mail1.fhi.mpg.de","preference":10},{"mx":"mail2.fhi.mpg.de","preference":20}],"count":2}

$ dns -a myhost -j
{"ok":true,"action":"addhost","name":"myhost.fhi.mpg.de","ip":"141.14.130.17"}

$ dns -s nothere -j ; echo "exit $?"
{"ok":false,"action":"showhost","error":"host 'nothere.fhi.mpg.de' not found"}
exit 1

A script cannot answer a question, so -j never asks one: -y stands in for the answer, and an operation that would have asked and did not get it says so rather than going ahead.

$ dns -d myhost -j ; echo "exit $?"
{"ok":false,"action":"delhost","error":"confirmation required, add -y"}
exit 1

Only what an operation has to say is in the answer; the rest stays out. Lists and counts are always written, even when they are empty — "mxs":[],"count":0 is an answer, and a script should not have to tell it from a missing key.

field
ok whether it did what it was asked
action which operation is answering
error, detail what went wrong, and more about it
warning the operation went through, something beside it did not
name, ip, mac, alias, text, file, mx, preference what it worked on
aliases, texts, ips, hosts, mxs lists
record the infoblox record, nested as an object
count how many the list holds
version, build, toolbox from -v

The actions are addhost, delhost, showhost, find, showip, listunused, setoptions, gridrestart, addalias, delalias, delaliases, showaliases, addtxt, deltxt, showtxt, addmx, changemx, delmx, delmxs, showmx, certbotauth, certbotclean, version — and dns, for what goes wrong before any operation is reached.

-a on an mx record answers addmx when the record was created and changemx when one that was already there was used, so that a script can tell the two apart.

--seal, --update and --check-update keep their prose. They are maintenance done by hand, and nobody is parsing them.

Options

  -a <hostname> [-i <ip>] [-m <mac]  add host record
  -o <json file> -i <ip>             write option from json file to ip record
  -s <hostname>                      show host record
  -i <ip>                            show ip record
  -f <hostname>                      search for host names
  -d <hostname>                      delete host record
  -q <hostname> -a <alias>           add alias to host record
  -q <hostname> -d <alias>           remove alias from host record
  -q <hostname> -D                   remove aliases from host record
  -q <hostname>                      show aliases for host record
  -t <record name> -a <text>         add text record
  -t <record name> -D                remove text record
  -t <record name>                   show text record
  -M <domain> -a <server> [-p <n>]   add or change mx record, preference n (default 10)
  -M <domain> -d <server>            remove mx record
  -M <domain> -D                     remove all mx records
  -M <domain>                        show mx records
  -r                                 restart infoblox grid
  -l                                 list unused ip addresses
  -c                                 run as certbot auth hook
  -x                                 run as certbot cleanup hook
  -y                                 supress interactive mode, alwayes answer 'yes'
  -j                                 answer with one line of json, for scripts
                                     (not --seal, --update, --check-update)
  --seal                             encrypt an infoblox login into a block for creds.go
  --check-update                     look for a newer release
  --update                           download and install the newest release
  -v                                 show version
  -h                                 show help

Keeping current

dns --check-update      # look
dns --update            # fetch the newest release and replace this file

Beside that, an ordinary run looks by itself, at most once a day and never in the foreground: it reads a note left in the cache directory, and if that note is stale it starts a background run whose answer the next call finds waiting. When there is a newer version, a line at the end of the run says so.

Nothing of this happens in a pipe, in a cron job or in a json run, and DNS_NO_UPDATE_CHECK=1 turns it off everywhere.

An update is only put in place after the download has been run once with -v and answered: a truncated or wrong-platform file never replaces the one that works.

Building

build.sh owns the build. It steps the patch version, builds every platform with that one version in it, and writes the number back to version.txt, which therefore always says what the binaries in ./bin carry.

./build.sh                          # 2.5.1 -> 2.5.2, all four platforms
PLATFORMS="linux/amd64" ./build.sh  # just the one
VERSION=2.6.0 ./build.sh            # a minor or major step, named outright

The names in ./bin — dns-<goos>-<goarch> — are what --update looks for in a release, so a release has to carry exactly those files, under a tag that is the bare version number. With mgsh that is:

mgsh push 'what changed'    # commit and push to the git server
mgsh pushremote             # mirror to the public server
mgsh release 2.5.2          # tag and publish the release there

bin/dns is a symlink to the build for this machine. build.go carries a build counter that shows up next to the version in -v; nothing in build.sh touches it.

The login

SEALED in creds.go holds user and password under a passphrase — AES-256-GCM, the key derived with argon2id, so that guessing the passphrase from a copy of the binary stays expensive. ~/.dnsrc is encrypted as well, under a key the program carries, which is what keeps the password out of a backup or a synced home directory. The file stays 0600, and dns says so when it is not.

This keeps the credentials out of the repository and out of plain sight on disk. It is not a vault: whoever knows the passphrase has the login, and so has whoever holds ~/.dnsrc together with a copy of dns.

Rotating the infoblox password:

dns --seal                  # asks for user, password and the passphrase

Paste the line it prints into creds.go, rebuild, release, and remove the stale ~/.dnsrc wherever one exists — the next run unseals it afresh.

Files

~/.dnsrc the login, encrypted, 0600. Delete it and the next run asks for the passphrase again
<cache>/dns/update.json when it last looked for a release, and what it found

<cache> is ~/Library/Caches on darwin and ~/.cache on linux.

Exit status

0 when the run did what it was asked. 1 when a json run did not, and 1 in both modes for the things that stop a run before it starts — the network check, a missing login, no service to be found — and for the certbot hooks.

An operation that goes wrong in the ordinary mode says ERROR: and still ends with 0. That is how dns has always behaved and what has been built around it lives off; a script that wants to know should use -j, where a failure is always 1.

The source

dns.go the options and every operation
json.go the json answer, and how a run ends either way
creds.go the sealed login, ~/.dnsrc, --seal
selfupdate.go --update and --check-update, written to be copied into other programs
tools.go the toolbox: printing, colours, prompts, the small helpers
build.go the build counter
build.sh the build, the version, the names a release needs
support/ two dhcp option files that are in use

mwx'2026

S
Description
No description provided
Readme
522 KiB
2.5.1
Latest
2026-09-22 15:24:44 +02:00
Languages
Go 97.7%
Shell 2.3%