- Zig 88.1%
- Nix 11.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The connection was made into a local, copied into the client, and the local released on an error. The match rules and the state query that follow run on the client's copy and grow its buffers, so a failure in any of them freed the stale local and leaked what the copy had allocated. It is now connected straight into the client and released from there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PHfYYuEv1aiLvoPWhPUXsj |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
zig-avahi
Avahi for Zig: publish a service over Multicast DNS, and browse for the services of a type, through the system's Avahi daemon on the system bus. It is written on mdbus, in Zig 0.17, with no C.
The API documentation, generated from the doc comments, is published at https://jeff.jcollie.page/zig-avahi/.
const avahi = @import("avahi");
const client = try avahi.Client.connect(io, gpa, environ);
defer client.deinit();
var running = try io.concurrent(avahi.Client.run, .{client});
defer {
client.stop();
running.await(io) catch {};
}
// Out on the network as `Kitchen._sendspin._tcp.local`, on port 8928.
const group = try client.publish(.{
.name = "Kitchen",
.type = "_sendspin._tcp",
.port = 8928,
.txt = &.{ "name=Kitchen", "path=/sendspin" },
});
// Every `_sendspin-server._tcp` on the network, resolved, as it comes and goes.
const browser = try client.browse("_sendspin-server._tcp", .{ .address_protocol = .inet }, handler);
What it does
Client talks to org.freedesktop.Avahi on the system bus: Server's
EntryGroupNew, ServiceBrowserNew and ServiceResolverNew, and the
objects they make. run reads what the daemon says about them; publish
and browse may be called from any task, before run starts or while it
runs.
- Publishing. Each service is an entry group of its own, added and
committed. The daemon probes for the name and announces it, and the
group's
currentStatebecomesestablished. A name another client of the same daemon holds is changed at once, and one another host holds is changed when the daemon reports the collision --Kitchen #2,#3-- the name the daemon offers throughGetAlternativeServiceName. The group'snamesays what it became. - Refusal. A daemon may not let users publish at all:
disable-user-service-publishing, which NixOS sets unlessservices.avahi.publish.userServicesis on.publishthen fails witherror.NotPermitted, at once, which is a program's cue to answer mDNS itself. Browsing is allowed either way. - Browsing. Each service found is resolved, and the handler's
foundis given its host, address, port and TXT;lostis called when it goes. Both are called once for each interface and protocol a service is seen on, so a program that wants one of each keeps its own map by name.BrowseOptions.address_protocol = .inetasks for IPv4 addresses only, andinclude_own = falseleaves out this host's own services. - Handlers run outside the lock, from
run, so a handler may publish or browse in its turn. - The daemon's own state. A daemon still probing for its host's name will do: what is published before it is running is published once it is. If the daemon restarts, everything is published again and every browser made again, since the objects were the old daemon's.
Avahi addresses every signal about an object -- a group's state, a
browser's items, a resolver's answer -- to the client that made it, so no
match rule has to be installed before the object exists, and a signal that
comes before the reply naming its object waits in mdbus's queue until that
reply has been read. The two signals that are broadcast, the daemon's
StateChanged and the bus's NameOwnerChanged for the daemon's name, are
matched once, on connecting.
wire holds the message bodies -- AddService's, the browsers' and
resolvers', and every signal's -- with no I/O, so they are tested and fuzzed
without a bus.
Trying it
$ nix develop -c zig build
$ ./zig-out/bin/zavahi state
running
$ ./zig-out/bin/zavahi publish Kitchen _sendspin._tcp 8928 name=Kitchen path=/sendspin
published _sendspin._tcp as Kitchen
$ ./zig-out/bin/zavahi browse _sendspin._tcp 5
found Kitchen kitchen.local 192.168.1.20 8928 name=Kitchen path=/sendspin
Each runs until interrupted, or for the seconds given last.
Building and testing
$ nix develop
$ zig build test --summary all
$ zig build check # everything compiles, including zavahi and the docs server
$ zig build docs-serve # the API documentation, at http://localhost:8000/
$ reuse lint
zavahi is tested against real Avahi daemons on three machines in a NixOS
virtual machine test, all as an ordinary user. publisher and browser
let users publish and refusing does not. The test checks that:
- a published service is seen by
avahi-browse, TXT and all; zavahi browseresolves it;- a service
avahi-publishmakes is found; - a name taken on the network becomes
Kitchen #2; - a service withdrawn is reported lost;
- a service is published again after the daemon restarts;
- the refusing daemon refuses with
NotPermitted, while browsing there still works.
$ nix build -L .#checks.x86_64-linux.avahi
Fuzzing
Every signal body the client decodes -- an item found or lost, a resolved
service, a state, the daemon's owner changing -- carries what answered on
the network, so each is fuzzed: whatever arrives, decoding returns, reads
only inside the body, and keeps at most wire.max_txt TXT strings. The
seeds are real bodies, made at build time by tools/fuzz_corpus.zig.
The client is fuzzed whole as well, against a daemon the fuzzer plays
through Client.initDaemon: a script of publishing, browsing, removing and
every signal the daemon sends, with each of the client's calls answered as
the fuzzer chooses -- a collision, a refusal, a timeout, an object path too
long to keep, the daemon going away and coming back. Whatever happens, the
client must not crash, leak or loop, must send only messages the bus would
take, and must keep no resolver whose browser is gone. zig build test runs
both targets on their seeds and a few hundred scripts.
$ zig build fuzz --fuzz # Zig's fuzzer, until stopped
$ zig build fuzz --fuzz=1M # a bounded run, then a report
$ zig build fuzz-run # a minute of the loop
$ zig build fuzz-run -- --target client --seconds 300
$ zig build fuzz-run -Dfuzz-optimize=debug -- --input fuzz-findings/x.bin --target signals
Zig's fuzzer steers by coverage, and gets it because the test binaries are
compiled with LLVM. fuzz-run is a loop of this project's own in
tools/fuzz.zig, reproducible from the seed it prints and watched for hangs.
Zig 0.16 is supported on the zig-0.16 branch, cut at v0.1.0; only
critical fixes go there.
Using it
$ zig fetch --save git+https://git.jcollie.dev/jeff/zig-avahi.git#main
const avahi = b.dependency("avahi", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("avahi", avahi.module("avahi"));
Where this lives
| Where | How to get it |
|---|---|
| Forgejo | git clone https://git.jcollie.dev/jeff/zig-avahi.git |
| Tangled | git clone https://tangled.org/jcollie.dev/zig-avahi |
| Radicle | rad clone rad:zMrjiEVMyuSABNY1wK4McNyfUGgn |
A Radicle repository is findable only by its ID, so that one is written out in
full: rad:zMrjiEVMyuSABNY1wK4McNyfUGgn.
Licensing
MIT, following the REUSE specification; reuse lint checks it.
References cited
Kept in the zig-avahi Zotero collection.
- Avahi project. Avahi: service discovery on Linux using mDNS/DNS-SD,
version 0.8. 2020. https://avahi.org/. Its D-Bus API, from the daemon's
introspection files, and the constants in
avahi-common/defs.handaddress.h, whichwirefollows. - Cheshire, S., and M. Krochmal. Multicast DNS. RFC 6762. Internet Engineering Task Force (IETF), February 2013. https://www.rfc-editor.org/info/rfc6762.
- Cheshire, S., and M. Krochmal. DNS-Based Service Discovery. RFC 6763.
Internet Engineering Task Force (IETF), February 2013.
https://www.rfc-editor.org/info/rfc6763. Service instances, their SRV and
TXT records, and TXT's
key=valuestrings (§6). - Pennington, H., A. Carlsson, A. Larsson, S. Herzberg, S. McVittie, and D. Zeuthen. D-Bus Specification. https://dbus.freedesktop.org/doc/dbus-specification.html. The system bus, match rules, and a signal's destination.