. "$BST_LIB" # BST-TIMEOUT: 600 # unbound 1.19.3 -- NLnet Labs' validating, recursive and caching DNS resolver # (meta-networking recipes-support/unbound). What ships is the daemon and its # four tools in /usr/sbin -- unbound, unbound-checkconf, unbound-control, # unbound-host, unbound-anchor -- plus the unbound-control-setup script, # /usr/lib/libunbound.a and unbound.h. # # THE SUBJECT IS A SERVER THAT MUST NOT NEED A SECOND MACHINE. Everything below # is offline by construction: one static local-zone, no forward-zone, no stub, # no root-hints file, so an answer can only have come from this unbound's own # configuration. That is not a convenience for the harness -- it is the only # shape in which the criterion is a statement about unbound rather than about # whatever a network happened to return. # # IT IS RUN WITHOUT THREADS, AND THAT IS THE PORT'S ONE CONFIGURE DECISION. # The recipe passes --without-pthreads (BRINTOS-673), so HAVE_PTHREAD is undef # and util/locks.h maps ub_thread_create onto fork(2) instead. With # `num-threads: 1' the daemon never reaches that path at all: daemon.c runs # worker 0 on the main thread and only workers 1..n-1 are created. The switch is # asserted on target below rather than assumed -- unbound -V prints the whole # configure line, so a recipe that lost the flag shows up here and not as a trap # in some later port. # # THE PORT NUMBER IS 15453 AND IS NOT NEGOTIABLE. 88-dnsmasq leaves a dnsmasq on # 127.0.0.1:53 and 88-c-ares leaves one on :15353, and nothing on this image can # be killed (brintos/linux#21: kill(2) reports success and delivers nothing, # SIGKILL included), so both outlive their suites. Binding either port here would # make this suite's result depend on the run order. For the same reason the # unbound started below is left running when the suite ends, and one run per boot # is the supported shape. # # NOTHING HERE WAITS ON A CHILD, and no case scores kill(2). #21 is already # covered where it was found (88-dnsmasq/sigterm-stops); a second copy of that # red would add noise to the batch's fail count without adding a finding. require /usr/sbin/unbound-checkconf unbound/present UB=/usr/sbin/unbound UBC=/usr/sbin/unbound-checkconf UBH=/usr/sbin/unbound-host PORT=15453 NAME=unbound.bst.test ADDR=10.21.22.23 # 0a151617 on the wire D=${TMPDIR:-/tmp}/bst-unbound.$$ rm -rf "$D"; mkdir -p "$D" || { bst_notok unbound/workdir "cannot create $D"; exit 0; } # db : run and capture, setting $out and $rc. db() { bst_run "$@"; } # lib/bst.sh: a bound that FIRES # (timeout(1) did not, before the # brintos/linux#77 fix) and a file capture # no wedged child can hold open. # has ... : true iff EVERY needle is a substring. has() { _h=$1; shift for _n in "$@"; do case "$_h" in *"$_n"*) ;; *) return 1 ;; esac; done return 0 } # wire : send one DNS datagram to 127.0.0.1:$PORT and set $rhex to # the reply as a flat hex string ("" if nothing came back). socat rather than # netcat, and with -T, for the reason 88-dnsmasq records: a UDP client that # blocks on a read cannot be stopped from the outside on this image, so the # relay has to end from the inside. wire() { rm -f "$D/r.bin" ( bst_bound 20 socat -t 2 -T 2 STDIO "UDP4:127.0.0.1:$PORT" < "$1" > "$D/r.bin" 2>"$D/r.err" ) rhex=$(od -An -tx1 "$D/r.bin" 2>/dev/null | tr -d ' \n') } # ---------------------------------------------------------------- inventory -- # All five programs plus the control-setup script. They are one package: the # recipe adds no PACKAGES split and ${sbindir} falls into ${PN} by the default # rule, so a missing name here means the install ran short, not that an image # list picked a subset. for t in unbound unbound-checkconf unbound-control unbound-host unbound-anchor; do check_eq "unbound/inventory-$t" "yes" "$([ -x "/usr/sbin/$t" ] && echo yes)" done check unbound/inventory-control-setup test -x /usr/sbin/unbound-control-setup # /usr/sbin is on root's PATH and the bare name reaches this binary. check_eq unbound/on-path "$UBC" "$(command -v unbound-checkconf 2>/dev/null)" # The static library and its one public header -- the libunbound half of the # package, which unbound-host below is the shipped consumer of. check unbound/libunbound-archive test -f /usr/lib/libunbound.a check unbound/libunbound-header test -f /usr/include/unbound.h # ----------------------------------------------------------- the binary runs -- db 60 "$UB" -V check_eq unbound/version-rc "0" "$rc" case "$out" in *"Version 1.19.3"*) bst_ok unbound/version-is-1.19.3 ;; *) bst_notok unbound/version-is-1.19.3 "want 'Version 1.19.3', got '$(_bst_1line "$out")'" ;; esac # -V prints CONFCMDLINE, the configure line of the build that produced THIS # binary, so these two cases read the recipe's decisions off the target rather # than off the recipe. --without-pthreads is the ticket's named switch; # --disable-gost is the one the build needed (see the bbappend) and is asserted # alongside it because an OpenSSL-no-engine tree silently regaining the GOST arm # would not compile at all -- so a green here is also the evidence that the # recipe, not luck, is what keeps it out. if has "$out" "--without-pthreads"; then bst_ok unbound/configured-without-pthreads else bst_notok unbound/configured-without-pthreads "configure line does not carry --without-pthreads -- $(_bst_1line "$out")"; fi if has "$out" "--disable-gost"; then bst_ok unbound/configured-disable-gost else bst_notok unbound/configured-disable-gost "configure line does not carry --disable-gost -- $(_bst_1line "$out")"; fi # The crypto backend is OpenSSL, which is what DEPENDS names and what the # DNSSEC half of this package would use. Printed by print_build_options through # OpenSSL_version(), so it is the linked library speaking, not a config value. if has "$out" "Linked libs:" "OpenSSL"; then bst_ok unbound/linked-openssl else bst_notok unbound/linked-openssl "no OpenSSL in the linked-libs line -- $(_bst_1line "$out")"; fi # Both resolver modules are present. `validator iterator' is the compiled-in # default module-conf and unbound-checkconf refuses a config naming a module # that is not in this list, so a build that dropped one would fail every case # below with a message about module conf rather than about DNS. if has "$out" "Linked modules:" "validator" "iterator"; then bst_ok unbound/linked-modules else bst_notok unbound/linked-modules "validator/iterator not both in the linked-modules line -- $(_bst_1line "$out")"; fi # ------------------------------------------------------------ what it ships -- # The recipe installs doc/example.conf as ${sysconfdir}/unbound/unbound.conf and # registers an init script through update-rc.d. Nothing starts it: this image's # inittab runs only the tmpfs mount and run-postinsts, never an rc runlevel, # which is what lets every case below be a statement about a server this suite # started itself. check unbound/conffile-shipped test -f /etc/unbound/unbound.conf check unbound/initscript-shipped test -x /etc/init.d/unbound # The recipe's do_install writes ${systemd_unitdir}/system/unbound.service # UNCONDITIONALLY; systemd.bbclass's rm_systemd_unitdir is what removes it again, # because DISTRO_FEATURES carries no systemd. If that stopped firing the file # would be installed-but-unpackaged and the build would fail QA -- so this is the # on-image half of a build-side invariant. # # THE UNIT, NOT THE DIRECTORY. This asserted an empty /lib/systemd until it went # red on an image where a co-installed package (nuttcp) ships units of its own: # the directory's existence is a statement about that package, not about this # one, and a suite that reds on a neighbour's file cannot say whether the # invariant it names still holds. Both unitdir spellings are checked because # ${systemd_unitdir} follows usrmerge. check_eq unbound/no-systemd-unitdir "" "$(ls -d /lib/systemd/system/unbound.service /usr/lib/systemd/system/unbound.service 2>/dev/null)" # --- THE TICKET'S CRITERION, first half: unbound-checkconf ------------------ # "unbound-checkconf accepts a staged config and rejects a deliberately broken # one with the expected diagnostic." # # EVERY LINE OF THIS CONFIG IS LOAD-BEARING; none of it is decoration. # username: "" -- the compiled-in default is UB_USERNAME "unbound" and this # image's base-passwd defines no such account, so the default makes # unbound-checkconf exit on getpwnam and the daemon exit before it binds. # Asserted separately below rather than papered over. # chroot: "" / directory: "" -- morechecks() stats both when they are set, and # perform_setup() would chroot(2) for real. Empty removes the syscall # rather than removing the check. # pidfile: "" with -p -- the daemon writes RUNFILE before it serves, and a # read-only or absent /run would be OUR wall rather than unbound's. # num-threads: 1 -- with --without-pthreads any higher number makes locks.h # fork(2) a worker per thread. One worker runs on the main thread and forks # nothing, which is the shape this port is judged in. # use-syslog: no, logfile "" -- send the log to stderr, where the readiness # and provenance cases below read it. There is no syslogd running. # do-ip6: no -- the guest has lo and nothing else. # local-zone static + local-data -- the ticket's "local-data and no forwarder". # `static' answers from this data and NXDOMAINs the rest of the zone instead # of falling through to recursion, so the negative control below is a # statement about the zone rather than about a lookup that timed out. cat > "$D/good.conf" <:: error: unknown keyword ''". Line 3 is asserted, not just # the word "error" -- a diagnostic that cannot say WHERE is not the expected one. printf 'server:\n verbosity: 1\n no-such-unbound-option: yes\n' > "$D/bad-keyword.conf" db 60 "$UBC" "$D/bad-keyword.conf" if [ "$rc" -eq 0 ]; then bst_notok unbound/checkconf-rejects-unknown-keyword "accepted an unknown keyword -- $(_bst_1line "$out")" elif [ "$rc" -eq 124 ]; then bst_notok unbound/checkconf-rejects-unknown-keyword "hung on a bad config -- $(_bst_1line "$out")" elif [ "$rc" -eq 1 ] && has "$out" "$D/bad-keyword.conf:3: error:" "unknown keyword"; then bst_ok unbound/checkconf-rejects-unknown-keyword else bst_notok unbound/checkconf-rejects-unknown-keyword "want exit 1 naming line 3, got rc=$rc -- $(_bst_1line "$out")" fi # (2) THE SEMANTIC PASS. `chroot:' parses fine and is then stat'd by # morechecks(): a directory that does not exist is fatal with "bad chroot # directory". This is the half a keyword table cannot catch, and the half that # would go missing first if unbound-checkconf were reduced to a parser. sed -e 's,^ chroot: "", chroot: "/no/such/directory",' "$D/good.conf" > "$D/bad-chroot.conf" db 60 "$UBC" "$D/bad-chroot.conf" if [ "$rc" -eq 0 ]; then bst_notok unbound/checkconf-rejects-bad-chroot "accepted a chroot directory that does not exist -- $(_bst_1line "$out")" elif has "$out" "bad chroot directory"; then bst_ok unbound/checkconf-rejects-bad-chroot else bst_notok unbound/checkconf-rejects-bad-chroot "rc=$rc, want the documented 'bad chroot directory' -- $(_bst_1line "$out")" fi # (3) THE ZONE PASS. local-data is parsed as a resource record by # localzonechecks() -> local_zones_apply_cfg, so a malformed RR is refused with # "failed local-zone, local-data configuration" -- and this is the one refusal # that runs the very code path the serving cases below depend on being correct. sed -e "s,^ local-data: .*, local-data: \"$NAME. IN A not-an-address\"," "$D/good.conf" > "$D/bad-data.conf" db 60 "$UBC" "$D/bad-data.conf" if [ "$rc" -eq 0 ]; then bst_notok unbound/checkconf-rejects-bad-local-data "accepted 'not-an-address' as A rdata -- $(_bst_1line "$out")" elif has "$out" "local-zone, local-data"; then bst_ok unbound/checkconf-rejects-bad-local-data else bst_notok unbound/checkconf-rejects-bad-local-data "rc=$rc, want the documented local-zone/local-data refusal -- $(_bst_1line "$out")" fi # -o reads one option back out of the parsed config rather than out of the file, # so it is the cheapest proof that the value the daemon will use is the value # that was written. It also pins num-threads at 1, which is the number this # no-threads build is judged at. db 60 "$UBC" -o port "$D/good.conf" check_eq unbound/checkconf-reads-port "$PORT" "$out" db 60 "$UBC" -o num-threads "$D/good.conf" check_eq unbound/checkconf-reads-num-threads "1" "$out" # WHAT THE PACKAGE'S OWN /etc/unbound/unbound.conf DOES ON THIS IMAGE, asserted # rather than left as folklore. It is doc/example.conf, whose every server: # setting is commented out, so it takes the compiled-in defaults -- including # UB_USERNAME "unbound". morechecks() calls getpwnam on that name, this image's # base-passwd has no unbound account, and the answer is a refusal naming the # user. The port does not add the account: BRINTOS-673 scopes the criterion to a # staged config, creating a system user is a change to what the package installs # that the ticket does not ask for, and it would not make the shipped default # START anyway -- the credential setters are ENOSYS here (brintos/linux#19), so # a real drop to an unbound uid cannot succeed whether or not the account exists. # The case is therefore named for what it asserts: the refusal, and its reason. db 60 "$UBC" /etc/unbound/unbound.conf if [ "$rc" -eq 0 ]; then bst_notok unbound/shipped-conf-refused-no-user "the shipped config validated -- this image gained an 'unbound' account, so the note in this suite and in the BRINTOS-673 design doc is now stale -- $(_bst_1line "$out")" elif has "$out" "user 'unbound' does not exist"; then bst_ok unbound/shipped-conf-refused-no-user else bst_notok unbound/shipped-conf-refused-no-user "rc=$rc, want the getpwnam refusal naming user 'unbound' -- $(_bst_1line "$out")" fi # --- THE TICKET'S CRITERION, second half: serve and answer ------------------ # Readiness comes from unbound's own "start of service" line, not from a fixed # sleep. daemon_fork() binds and listens in daemon_open_shared_ports() before # daemon.c reaches that log_info, so the appearance of the line IS the listening # socket and the race closes structurally. log_info is unconditional, so # verbosity cannot suppress it. # # NOT wrapped in `timeout': a wrapper would only add a second process that # equally cannot be stopped (brintos/linux#21). "$UB" -d -p -c "$D/good.conf" > "$D/unbound.log" 2>&1 & upid=$! i=0; up=0 while [ $i -lt 60 ]; do if grep -q "start of service" "$D/unbound.log" 2>/dev/null; then up=1; break; fi kill -0 "$upid" 2>/dev/null || break i=$((i+1)); sleep 1 done if [ "$up" -eq 1 ]; then bst_ok unbound/starts else bst_notok unbound/starts "no 'start of service' after ${i}s -- $(_bst_1line "$(cat "$D/unbound.log" 2>/dev/null)")" fi # It bound loopback and only loopback, read back from the kernel rather than # from the config that asked for it. ss is iproute2's in /sbin, netstat # net-tools' in /bin, and either answers. LST="" # BST-TIMEOUT-OK: ss/netstat, one-shot. The socat carrying the DNS datagram converted. if command -v ss >/dev/null 2>&1; then LST=$(timeout 20 ss -lun 2>/dev/null) elif command -v netstat >/dev/null 2>&1; then LST=$(timeout 20 netstat -lun 2>/dev/null); fi if [ -z "$LST" ]; then bst_notok unbound/listens-loopback-only "neither ss nor netstat listed any UDP listener" elif has "$LST" "127.0.0.1:$PORT"; then case "$LST" in *"0.0.0.0:$PORT"*) bst_notok unbound/listens-loopback-only "bound the wildcard address as well as loopback -- $(_bst_1line "$LST")" ;; *) bst_ok unbound/listens-loopback-only ;; esac else bst_notok unbound/listens-loopback-only "no 127.0.0.1:$PORT UDP listener -- $(_bst_1line "$LST")" fi if ! command -v socat >/dev/null 2>&1; then bst_notok unbound/answers-on-the-wire "socat is not on this image, so the criterion could not be put on the wire" else # THE CRITERION, on the wire. A hand-built query: id 0x2a2b, RD set, QDCOUNT 1, # QNAME 07"unbound" 03"bst" 04"test" 00, QTYPE A, QCLASS IN. Octal escapes, not # \xNN, because \x is a bashism the target's printf need not honour. printf '\052\053\001\000\000\001\000\000\000\000\000\000\007unbound\003bst\004test\000\000\001\000\001' > "$D/q-a.bin" wire "$D/q-a.bin" # 0a151617 is $ADDR as A-record RDATA. Requiring the RDATA rather than a # non-zero ANCOUNT is what makes a reply that merely echoes the question fail. if [ -z "$rhex" ]; then bst_notok unbound/answers-on-the-wire "no reply from 127.0.0.1:$PORT -- $(_bst_1line "$(cat "$D/r.err" 2>/dev/null)")" elif has "$rhex" "0a151617"; then bst_ok unbound/answers-on-the-wire else bst_notok unbound/answers-on-the-wire "want the A record 0a151617 for $NAME, got $(_bst_1line "$rhex")" fi # The reverse direction, which local-data-ptr adds to the same zone and which # goes through a different name in the local-zone tree. The reverse name is # the DECIMAL octets of $ADDR reversed -- 23.22.21.10.in-addr.arpa for # 10.21.22.23 -- and not the hex ones the wire literal 0a151617 shows: the # last label is 23, not 17. # QNAME 02"23" 02"22" 02"21" 02"10" 07"in-addr" 04"arpa" 00, QTYPE PTR (0x000c). printf '\052\054\001\000\000\001\000\000\000\000\000\000\00223\00222\00221\00210\007in-addr\004arpa\000\000\014\000\001' > "$D/q-p.bin" wire "$D/q-p.bin" # 07"unbound" as the first label of the PTR rdata, in hex. Only the LABEL is # required, not the whole name: DNS compression may replace a suffix of an # rdata name with a pointer, and it can never replace the prefix -- so this # matches whether or not unbound compressed the tail, while still being a # string that appears nowhere in the question. if has "$rhex" "07756e626f756e64"; then bst_ok unbound/answers-ptr-on-the-wire else bst_notok unbound/answers-ptr-on-the-wire "want the PTR $NAME for $ADDR, got $(_bst_1line "$rhex")" fi # THE NEGATIVE CONTROL, and what keeps every green above meaningful. A name # inside the static zone that was never written down must not come back as a # configured address. `static' means unbound answers NXDOMAIN from the zone # itself, so this cannot be an unbound query that merely timed out: RCODE 3 is # asserted alongside the absence of the two addresses. Byte 3 of the reply # holds the low nibble RCODE; a hand-built reply header for a refusal from the # local zone is 2a2d 8183 -- QR+AA+RD+RA with RCODE 3. printf '\052\055\001\000\000\001\000\000\000\000\000\000\006absent\003bst\004test\000\000\001\000\001' > "$D/q-n.bin" wire "$D/q-n.bin" if has "$rhex" "0a151617"; then bst_notok unbound/unknown-name-nxdomain "a name this zone never defined got the configured address -- $(_bst_1line "$rhex")" elif [ -z "$rhex" ]; then bst_notok unbound/unknown-name-nxdomain "no reply at all for a name inside a static local-zone -- $(_bst_1line "$(cat "$D/r.err" 2>/dev/null)")" else case "$rhex" in 2a2d8?83*) bst_ok unbound/unknown-name-nxdomain ;; *) bst_notok unbound/unknown-name-nxdomain "want RCODE 3 (NXDOMAIN) from the static zone, got header $(_bst_1line "$rhex")" ;; esac fi fi # PROVENANCE, and the reason log-queries is in the config. An answer alone # cannot say WHO answered; unbound's own log can. worker.c logs the query before # it is resolved, through log_query_in, as " . " # -- the trailing dot is dname_str's and is part of the literal. if has "$(cat "$D/unbound.log" 2>/dev/null)" "127.0.0.1 $NAME. A IN"; then bst_ok unbound/logged-the-query else bst_notok unbound/logged-the-query "log carries no query line for $NAME -- $(_bst_1line "$(grep -i "$NAME" "$D/unbound.log" 2>/dev/null)")" fi # --- libunbound, through the tool the package ships for it ------------------- # unbound-host is the shipped consumer of libunbound.a, and it has no -s flag: # the way to point it at a server is a config with a forward-zone, which is also # what makes this case offline. do-not-query-localhost must be turned off or # libunbound will refuse to use a 127.0.0.1 forwarder -- unbound-checkconf warns # about exactly that combination, which is why the line is here and not omitted. # # `local-zone: "bst.test." transparent' IS LOAD-BEARING, and it is what this case # was red for before: unbound enters a default `local-zone: "test." static' of # its own (the RFC 6761 special-use names, alongside localhost. and the AS112 # reverse zones), so libunbound answers unbound.bst.test NXDOMAIN out of that # built-in zone and never sends a packet to the forwarder at all. The daemon # does not hit this because its own good.conf names bst.test. explicitly, and a # configured zone displaces the default -- which is why the wire cases were green # while this one was not. A transparent zone one label deeper is the documented # way out: closest-enclosing lookup stops at bst.test., finds no local data, and # falls through to the forward-zone below. # # This is the one case that exercises the LIBRARY rather than the daemon, and it # is scored separately from the wire cases for that reason: a red here with # unbound/answers-on-the-wire green means libunbound, not the server. cat > "$D/host.conf" <