# HashiCorp mdns: publishing and discovering local network services in Go

> HashiCorp's mdns is a small Go library for multicast DNS service publication and lookup, giving peer-to-peer discovery on a local network without an authoritative DNS server. It suits a tool that needs to be found by its neighbours, and the documentation is blunt about the environment it cannot work in, anything shared or cloud based.

**hashicorp/mdns** — Simple mDNS client/server library in Golang

- Repository: https://github.com/hashicorp/mdns
- Stars: 1,394 · Forks: 227
- Language: Go
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/hashicorp-mdns

## Discovery without a DNS server, and the one place it fails

mDNS, or Multicast DNS, can be used to discover services on the local network without the use of an authoritative DNS server. That is the whole idea, and it enables peer-to-peer discovery.

The mechanism is that a service publishes its records to a multicast address, and any listener on the segment receives them. There is no registry to register with and no address to configure on either side, which is what makes it useful for tools that move between networks, for a phone discovering a printer, or for a cluster of machines on a desk finding each other.

The documentation is unusually direct about where this does not work, and that paragraph is the most important thing to read before you design around it. It is important to note that many networks restrict the use of multicasting, which prevents mDNS from functioning. Notably, multicast cannot be used in any sort of cloud, or shared infrastructure environment. However it works well in most office, home, or private infrastructure environments.

That is a hard environmental constraint, not a configuration knob. If your service runs in a container on a cloud instance, the multicast packets either never leave the host or never arrive, and the symptom is that a lookup never returns rather than that the program reports an error. The failure is silent, which is the reason to test it on a real segment before building a feature on top of it.

What is inside the repository is correspondingly small. There is a `client.go` for the lookup side, a `server.go` for the publication side, and a `zone.go` for the record set, along with `server_test.go` and `zone_test.go` and a `go.mod` naming the module as `github.com/hashicorp/mdns`.

## Publishing a service takes four statements

The server side is the more interesting of the two, because it is the part that involves a type you have to construct.

The example in the documentation sets up a service export and then starts a server:

```go
// Setup our service export
host, _ := os.Hostname()
info := []string{"My awesome service"}
service, _ := mdns.NewMDNSService(host, "_foobar._tcp", "", "", 8000, nil, info)

// Create the mDNS server, defer shutdown
server, _ := mdns.NewServer(&mdns.Config{Zone: service})
defer server.Shutdown()
```

Two things to read here. The service type is a DNS style service name, `_foobar._tcp`, and the naming convention is the standard one: an underscore, the service name, an underscore, and the protocol. The port is the 8000 argument, and the `info` slice is the TXT record, which is the free-form key-value area a client reads to learn about the service. Here it is a single string, which the documentation describes as the service info.

The server takes a `Config` with a `Zone` field, and a zone is the set of records being served. That indirection is what lets one server publish more than one service, or serve a zone that is built up from several sources, rather than a single hardcoded record.

The `defer server.Shutdown()` is not decoration. A server that is not shut down cleanly leaves its multicast membership and sockets open, and in a program that restarts, that is how you end up with a port already in use or a second instance answering on the same service name. Deferring the shutdown immediately after the constructor is the pattern to copy.

The empty strings in the `NewMDNSService` call are the domain and the interface fields, left empty in the example so the library picks sensible defaults, and the trailing `nil` is the optional additional host or addresses argument.

## The lookup side is a channel and a goroutine

Looking up service providers is, in the documentation's word, also very simple. You make a channel for results, start listening on it, and then start the lookup:

```go
// Make a channel for results and start listening
entriesCh := make(chan *mdns.ServiceEntry, 4)
go func() {
    for entry := range entriesCh {
        fmt.Printf("Got new entry: %v\n", entry)
    }
}()

// Start the lookup
mdns.Lookup("_foobar._tcp", entriesCh)
close(entriesCh)
```

The shape here is a Go idiom, and it is worth understanding why the lookup is a channel rather than a return value.

A discovery lookup is open ended. There is no moment at which you can say the list of services is complete, because a service may appear a second after you started looking, and one may disappear a second after that. A function that returned a slice would have to decide on an arbitrary timeout, and a long timeout would make the caller wait for something that may never arrive.

So the lookup hands you a channel and keeps feeding it. The consumer goroutine ranges over the channel, which is why the example closes it at the end: the range loop terminates when the channel is closed and drained. In a longer lived program you would keep the channel open and handle entries as they arrive, which is the whole point of the design.

The buffered channel of size 4 in the example is worth noting too. It is a small buffer so that a consumer that is briefly busy does not immediately block the producer. If your consumer is slow, a larger buffer absorbs bursts, and if you want to know you are falling behind, an unbuffered channel will make the producer block, which is a crude but effective signal.

The service type argument is the same `_foobar._tcp` form used on the publishing side, which is the whole contract between the two: a publisher advertises a name and a client asks for a name. Nothing else needs to be agreed in advance.

## Two dependencies and a zone abstraction

The go.mod is short, and the two direct requirements explain most of the design.

The module is `github.com/hashicorp/mdns` and it requires `github.com/miekg/dns` at v1.1.73 along with `golang.org/x/net` at v0.59.0, with `golang.org/x/sys` as an indirect dependency. The go directive is `go 1.26.0`, which is a recent requirement worth noting if your build environment is not current.

`github.com/miekg/dns` is a DNS library that can both parse and construct DNS messages, and it is the right dependency for this because mDNS is DNS over UDP sent to a multicast address, using the same record formats. Rather than hand-rolling the message encoding, the library does it, and the zone type in `zone.go` is where the records are assembled and serialised.

`golang.org/x/net` is there for the multicast socket handling, specifically the IPv4 and IPv6 multicast group joins, which is the part that has to be right for the packets to be received at all.

That is a very small dependency surface for a networking library, and it is the main reason this is an easy thing to adopt. There is no cgo, no external resolver, no daemon and no configuration file.

The repository layout reinforces that this is a focused library rather than a framework. `client.go`, `server.go` and `zone.go` are the three source files, with tests for the server and the zone, plus a `Makefile`, a `CHANGELOG.md`, a `CONTRIBUTING.md` and a `.copywrite.hcl` file, which is HashiCorp's configuration for the copyright header tool that the Makefile runs.

The Makefile shows what the project's own checks are. The default goal is `test`, which runs `go test -v -race -timeout=60s -parallel=10 ./...`, so the race detector is on by default, which is the right call for a library that juggles goroutines and channels. There is a `cov` target that produces a coverage profile and opens it in a browser, a `lint` target running golangci-lint, a `deps` target that pins the tool versions by commit hash, a `tidy` target that fails the build if go.mod or go.sum is out of date, and a `copywriteheaders` target that verifies the copyright headers are correct.

## Where mdns is the wrong tool

The limits here are environmental and one of them is absolute.

The absolute one is multicast. If your service runs in a cloud instance, in most container orchestration setups, or on any shared infrastructure, mDNS will not work and no amount of configuration will change that. A service that needs to be discoverable across machines in that environment needs a different mechanism, whether that is a registry, a well known DNS name, or a service mesh. The library does not attempt a fallback, and it should not, since a silent fallback that connects to a different discovery system than you expected is worse than a failure.

The second limit is that the discovery is local and unauthenticated. mDNS runs on a local segment, and anything on that segment can publish a service claiming to be yours. If the thing you are discovering hands out capabilities, that is a security consideration, and the library provides no authentication layer, because the protocol it implements does not have one.

The third limit is the scope of the API. There is no service type registry, no TXT record validation, no filtering by attributes, and no way to ask for a service and get the one you meant rather than every host advertising that type. The channel gives you everything that matches, and the discrimination is your job.

The alternative approach is a conventional DNS name with a well known resolver, which is what almost every distributed system does. The difference in approach is that DNS gives you a globally unique, stable address that survives a change of network and can be pointed at a different machine without the clients noticing, while mDNS gives you local, self-configuring discovery that disappears the moment you leave the segment. A tool that a user picks up on their laptop and runs on a home network, where there is no infrastructure to configure, is exactly the case mDNS is built for. A service with users on the internet is exactly the case it is not.

There is also the option of a hybrid, where the tool uses mDNS when it finds it on a local network and falls back to a static configuration otherwise, which is a common pattern for printers and for local development tooling. Nothing in this library helps you build that, because the fallback is your own code, but the library is a reasonable half of it.

On maintenance, the project is in good shape. The last push was on 2026-10-18, the repository is not archived, and the recent releases are v1.0.5 from 2022-01-03, v1.0.6 from 2025-01-23 and v1.0.7 from 2026-06-12. The version has been at 1.0 for a long time, which for a small library like this means the API surface has been stable, and the licence is MIT.

## Conclusion

HashiCorp mdns fits a Go service or desktop tool that runs on a private network and needs other machines to find it, because the API is a handful of functions and the dependency surface is two libraries, so the cost of adding it is close to the cost of the code you write around it. Skip it in a container, on a cloud instance or on any network that blocks multicast, since the documentation says multicast cannot be used in any sort of cloud or shared infrastructure environment and the symptom is silence rather than an error. Before you rely on it, verify multicast actually reaches the segment you care about, because the failure mode is a lookup that simply never returns. The last push was on 2026-10-18 and the current release is v1.0.7 from 2026-06-12.

## FAQ

### What does the HashiCorp mdns library do?

It is a Go library for publishing and discovering services over multicast DNS on the local network, without an authoritative DNS server, which enables peer-to-peer discovery. The repository contains a client, a server and a zone type.

### Does mDNS work in the cloud?

No. The documentation states that multicast cannot be used in any sort of cloud, or shared infrastructure environment, and that many networks restrict multicasting. It works well in most office, home, or private infrastructure environments.

### How do I publish a service with the HashiCorp mdns library?

Create the service with `mdns.NewMDNSService(host, "_foobar._tcp", "", "", 8000, nil, info)`, then start a server with `mdns.NewServer(&mdns.Config{Zone: service})` and defer `server.Shutdown()`.

### How do I look up mDNS services in Go?

Create a channel for the results, start a goroutine ranging over it, then call `mdns.Lookup("_foobar._tcp", entriesCh)` and close the channel when you are done.

### What dependencies does the HashiCorp mdns library have?

Two direct ones: `github.com/miekg/dns` at v1.1.73 and `golang.org/x/net` at v0.59.0. The go.mod requires Go 1.26.0, and the licence is MIT.

## Sources

- [hashicorp/mdns on GitHub](https://github.com/hashicorp/mdns)
- [Issues](https://github.com/hashicorp/mdns/issues)
- [License: MIT](https://github.com/hashicorp/mdns/blob/main/LICENSE)
- [README](https://github.com/hashicorp/mdns/blob/main/README.md)
- [Releases](https://github.com/hashicorp/mdns/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hashicorp-mdns
