This package implements a custom gRPC Name Resolver for the mdns:// scheme, using mDNS (Multicast DNS) via the github.com/hashicorp/mdns library.
It is designed for dynamic service discovery within a local network without relying on centralized service discovery systems (such as Consul, etcd, or Kubernetes DNS).
The implementation follows architectural patterns used in official gRPC resolvers (dns, passthrough) and typical mDNS solutions in the Go ecosystem.
- IPv4 and IPv6 support
- Address deduplication
- Instance name filtering
- Interface-specific resolution
- Streaming delivery of address updates
- Optimized handling of
ResolveNow()calls
By default, the resolver expects an endpoint in the following format:
mdns:///my-instance._myservice._tcp.local.
Parsing follows this structure:
| Part | Example | Description |
|---|---|---|
| instance | my-instance | service instance name |
| service | _myservice | service name |
| proto | _tcp | transport protocol |
| domain | local. | mDNS domain |
The resulting service type is constructed as:
<service>.<proto>
type BuilderOptions struct {
ResolverIfaces []net.Interface
ShouldResolveIpv6 bool
ShouldReportError bool
ShouldDedupeAddresses bool
ParseFunc func(endpoint string) (instanceName, service, proto, domain string, err error)
}List of network interfaces used for mDNS lookup.
- if empty → all interfaces are used (
net.Interfaces())
Enables inclusion of IPv6 addresses from mDNS responses:
entry.AddrV6IPAddrIf true, errors from:
mdns.QueryclientConn.UpdateState
are propagated via:
clientConn.ReportError(err)Enables comparison between previous and current address sets.
If there are no changes, the gRPC state is not updated.
Allows replacing the default mDNS endpoint parser.
The default parser uses the following regular expression:
^([^.]+)\.(_[^.]+)\.(_[^.]+)\.((?:[^.]+\.)+)$On package import:
resolver.Register(&mdnsBuilder{})Registers the scheme:
mdns
When calling grpc.NewClient:
- The scheme (
mdns) is validated - The endpoint is parsed
- Network interfaces are determined
- A context with cancel is created
mdnsResolver.Start()is launched
For each interface:
mdns.Query(&mdns.QueryParam{
Service: r.targetInfo.serviceType,
Domain: r.targetInfo.domain,
})-
Filter by instance name
-
Skip nil entries
Supported:
AddrV4.String()AddrV6IPAddr.IP.String()Format:
ip:port
clientConn.UpdateState(resolver.State{Addresses: addrs})If ShouldDedupeAddresses is enabled, the resolver compares:
- list length
- presence of all addresses in the previous set
If no changes are detected, UpdateState is not called.
The resolver uses two goroutines:
- listens on
resolveNowChan - triggers mDNS queries
- reads results from
entries - builds gRPC state
- sends
UpdateState
This model is optimized to coalesce multiple rapid ResolveNow calls from gRPC.
Defined errors:
ErrUnsupportedScheme
ErrEmptyEndpoint
ErrInvalidFormat
ErrInvalidServicemdnsBuilder := resolver.NewBuilder(resolver.BuilderOptions{
ShouldResolveIpv6: false,
ShouldReportError: true,
ShouldDedupeAddresses: true,
})
conn, err := grpc.NewClient(
"mdns:///my-instance._myservice._tcp.local.",
grpc.WithResolvers(mdnsBuilder),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := NewYourServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = client.YourMethod(ctx, &YourRequest{})
if err != nil {
log.Fatal(err)
}Uses standard interfaces:
resolver.Builderresolver.Resolverresolver.ClientConn
ResolveNow→ triggers lookup- watcher → pushes updates
- Works only in local networks with mDNS
- Depends on multicast availability
Suitable for:
- local development
- IoT networks
- P2P services
- zeroconf infrastructures
MIT License — Copyright (c) 2026 1ight181
Found a bug? — GitHub Issues Email: danil.odinzov181@gmail.com