discover

package
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: AGPL-3.0 Imports: 24 Imported by: 0

Documentation

Index

Constants

View Source
const (
	TypeThisMachine = "this-machine"
	TypeRouter      = "router"
	TypePrinter     = "printer"
	TypeTV          = "tv"
	TypeSpeaker     = "speaker"
	TypePhone       = "phone"
	TypeTablet      = "tablet"
	TypeComputer    = "computer"
	TypeSBC         = "single-board-computer"
	TypeNAS         = "nas"
	TypeCamera      = "camera"
	TypeConsole     = "games-console"
	TypeSmartHome   = "smart-home"
	TypeUnknown     = ""
)

Device type codes. Stable identifiers, changing one changes stored data and every translation key that refers to it.

View Source
const (
	ByService = "service"
	ByModel   = "model"
	ByVendor  = "vendor"
	ByGateway = "gateway"
	BySelf    = "self"
)

Evidence codes for Inference.Because.

View Source
const NeighbourInterval = 10 * time.Second

NeighbourInterval is how often the neighbour table is re-read.

The table is a kernel cache that changes on its own schedule, so polling is the only way to observe it. Ten seconds is frequent enough that a device appearing on the network shows up promptly, and cheap enough to be irrelevant: the read is a few kilobytes and no packets are sent.

View Source
const QueryInterval = 15 * time.Minute

QueryInterval is how often the enumeration query is repeated after startup.

Long, because its purpose is to fill in a newly opened Roster rather than to track changes: the passive listeners already catch a device that joins or changes. Fifteen minutes is far below what a network notices and far above anything that could be called chatty.

View Source
const SweepInterval = 15 * time.Minute

SweepInterval is how often the gentle sweep repeats.

Long, because its job is to find what passive observation misses, and a device that has been silent for fifteen minutes will still be there in another fifteen. The neighbour table is re-read far more often; this only has to keep it stocked.

View Source
const SweepRate = 10

SweepRate is how many addresses are probed per second.

Ten is slower than any scanner and fast enough to cover a /24 in under half a minute. The figure is deliberately conservative: this runs unattended on somebody's home network, and the cost of being slow is that the Roster fills in a little later.

Variables

This section is empty.

Functions

func DefaultGateway

func DefaultGateway() netip.Addr

DefaultGateway returns the address of the network's default route.

An invalid address means it could not be determined, which is not an error: device typing falls back to its other signals.

func FormatMAC

func FormatMAC(mac string) string

FormatMAC renders any hardware-address spelling as the colon-separated uppercase form people recognise.

Stored rather than normalized hex, because this string is shown to the user; identity comparison uses the normalized form held in device_keys, so display and matching do not have to agree on a single representation.

func IsRandomized

func IsRandomized(mac string) bool

IsRandomized reports whether a MAC address was generated rather than assigned to a manufacturer.

Phones and laptops randomize their address to avoid being tracked between networks. Two consequences, and it is worth being precise about the second because the obvious reading of it is wrong:

  1. There is no vendor to look up. The address belongs to nobody.
  2. The address is still a usable identity *on this network*. Apple's Private Wi-Fi Address and Android's equivalent derive one address per SSID and keep it, so a phone that has joined this network keeps the same randomized address across reconnections. What it cannot do is correlate that phone to any other network, or to a manufacturer.

So randomization weakens identity rather than destroying it: the address is treated as a real key, but a rotatable one, and a device that reappears under a new randomized address is re-identified by hostname instead.

func ListenMDNS

func ListenMDNS(ctx context.Context, out func(Advert)) error

ListenMDNS delivers a normalized Advert for every mDNS announcement seen.

It is passive: nothing is sent. Devices announce on their own schedule, on joining the network and periodically after, so a Roster fills in over minutes without this putting a single packet on the wire.

func ListenMulticast

func ListenMulticast(
	ctx context.Context,
	group netip.Addr,
	port int,
	bufSize int,
	handle func(Packet),
) error

ListenMulticast joins group:port on every suitable interface and delivers datagrams until ctx is cancelled.

The same passive listener the Roster is built on. It puts nothing on the wire; senders are a separate concern for the caller.

func ListenSSDP

func ListenSSDP(ctx context.Context, out func(Advert)) error

ListenSSDP delivers a normalized Advert for every SSDP announcement seen.

Passive, like the mDNS listener: devices send NOTIFY messages on joining the network and periodically to refresh their advertised lifetime, so listening alone is enough.

func LocalAddrs

func LocalAddrs() (ips map[netip.Addr]bool, macs map[string]bool)

LocalAddrs returns this machine's own IP addresses and hardware addresses.

Used to mark the Roster entry for this machine rather than trusting a "permanent" flag from the neighbour table: the flag's meaning varies between operating systems, whereas comparing against the interface list is exact everywhere.

func NormalizeMAC

func NormalizeMAC(mac string) string

NormalizeMAC reduces a MAC address to bare uppercase hex, accepting the aa:bb:cc, AA-BB-CC and aabbcc forms that different sources produce.

func PortIsInteresting

func PortIsInteresting(port uint16) bool

PortIsInteresting reports whether a listening port is worth recording.

Ephemeral ports are where outbound connections come *from*, not where services live. Recording them would fill a device's service list with noise that changes every connection.

func QueryMDNS

func QueryMDNS(ctx context.Context) error

QueryMDNS sends the DNS-SD enumeration query and asks directly for the service types a household commonly runs.

Failure is not an error worth surfacing: the listeners still work, discovery is simply slower.

func ScanPortCount

func ScanPortCount() int

ScanPortCount is how many ports a check covers, for the UI to say so before the user asks for one.

func ServiceForPort

func ServiceForPort(port uint16, proto string) string

ServiceForPort names the service conventionally found on a port, or empty if the port carries no useful convention.

proto is "tcp" or "udp"; a few numbers mean different things on each.

func Sweep

func Sweep(ctx context.Context) (int, error)

Sweep probes every address on the local segment once, paced.

Errors from individual sends are ignored: an unreachable host is the expected case and is exactly as informative as a reachable one, because the neighbour table was populated either way.

func SweepTargets

func SweepTargets() ([]netip.Addr, error)

SweepTargets lists the addresses a sweep would probe, without sending anything.

Separate from the sending so the decision about what to probe can be tested without putting packets on a network.

func Vendor

func Vendor(mac string) string

Vendor returns the manufacturer registered to a MAC address, or "" when the address is empty, malformed, randomized, or in an unregistered range.

An empty result rather than "Unknown": the caller decides how to present an absence, and a literal "Unknown" stored in a database is a value that later has to be special-cased everywhere.

Types

type Advert struct {
	Addr netip.AddrPort
	// Source is "mdns" or "ssdp", kept because the two differ in how much they
	// can be trusted: a name a device publishes about itself is better evidence
	// than one inferred from a service type.
	Source string
	// Hostname is the device's own claimed name, such as "living-room-tv.local".
	Hostname string
	// Name is a human-facing label, such as "Kitchen HomePod".
	Name string
	// Model is a manufacturer model string where one is published.
	Model string
	// Services are the service types advertised, such as "_airplay._tcp".
	Services []string
	// Interface is the local interface the advert arrived on.
	Interface string
	SeenAt    time.Time
}

Advert is what a device said about itself.

mDNS and SSDP carry different fields but answer the same three questions: what is this thing called, what kind of thing is it, and what does it offer. They are normalized into one type so the Roster does not need to care which protocol named a device.

type Inference

type Inference struct {
	Type string
	// Because names the evidence, as a stable code the dashboard translates.
	Because string
	// Confidence ranks competing conclusions. Not shown to the user.
	Confidence int
}

Inference is a conclusion about what a device is, with the reason for it.

The reason is carried so the UI can explain itself. "Printer, because it advertises IPP" is a claim a user can check; "Printer" alone is something they have to take on faith, and being wrong without explanation is worse than being vague.

func InferType

func InferType(d types.Device, services []string, isGateway bool) Inference

InferType decides what a device is from everything known about it.

isGateway is whether this device is the network's default route, which is the single most reliable signal available: whatever else it does, that device is the router.

type Neighbour

type Neighbour struct {
	Addr netip.Addr
	MAC  string
	// Interface is the local interface the device was seen on, which is what
	// distinguishes a LAN device from something on a VPN or container bridge.
	Interface string
	// Self marks this machine, established by comparing against the local
	// interface list rather than by trusting a platform-specific table flag.
	Self bool
	// Virtual marks an entry seen on a container bridge, VPN or VM network
	// rather than a real local segment.
	Virtual bool
	SeenAt  time.Time
}

Neighbour is one entry from the operating system's IP-to-MAC table.

func Neighbours

func Neighbours() ([]Neighbour, error)

Neighbours returns the devices in the operating system's IP-to-MAC table.

Broadcast and multicast entries are removed, and entries on virtual interfaces are marked rather than dropped so that a caller can decide: the Roster hides them, but a diagnostic view may want them.

An unsupported platform returns no entries and no error: discovery degrades to whatever the listeners can find, rather than failing.

type OpenPort

type OpenPort struct {
	Port    uint16 `json:"port"`
	Service string `json:"service,omitempty"`
}

OpenPort is a port that answered.

func ScanPorts

func ScanPorts(ctx context.Context, addr netip.Addr) ([]OpenPort, error)

ScanPorts checks the conventional ports on one address and reports which answered.

The caller is responsible for this being a deliberate, user-initiated act.

type Packet

type Packet = multicastPacket

Packet is one datagram received on a multicast group.

Exported alongside ListenMulticast for The Dispatch, which needs the same join-and-read machinery on a different group. Duplicating it there would mean two implementations of the interface-selection and SO_REUSEPORT handling that took several platform quirks to get right.

type Service

type Service struct {
	// Out receives every sighting. Required.
	Out func(types.Sighting)
	// OnError is called when a source cannot start, so the UI can say which
	// capabilities are missing. Optional.
	OnError func(source string, err error)

	// Sweep enables the gentle sweep, which finds devices that never speak to
	// this machine. See sweep.go for what "gentle" is constrained to mean.
	Sweep bool
	// contains filtered or unexported fields
}

Service runs the discovery sources and reports what they find.

func (*Service) Active

func (s *Service) Active() []string

Active reports which discovery sources are running.

func (*Service) Start

func (s *Service) Start(ctx context.Context)

Start begins discovery and returns immediately.

A source that cannot start is reported and skipped rather than failing the call. Discovery is a best-effort enrichment: a machine where multicast is firewalled should still get a Roster from the neighbour table.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL