Documentation
¶
Index ¶
- Constants
- func DefaultGateway() netip.Addr
- func FormatMAC(mac string) string
- func IsRandomized(mac string) bool
- func ListenMDNS(ctx context.Context, out func(Advert)) error
- func ListenMulticast(ctx context.Context, group netip.Addr, port int, bufSize int, ...) error
- func ListenSSDP(ctx context.Context, out func(Advert)) error
- func LocalAddrs() (ips map[netip.Addr]bool, macs map[string]bool)
- func NormalizeMAC(mac string) string
- func PortIsInteresting(port uint16) bool
- func QueryMDNS(ctx context.Context) error
- func ScanPortCount() int
- func ServiceForPort(port uint16, proto string) string
- func Sweep(ctx context.Context) (int, error)
- func SweepTargets() ([]netip.Addr, error)
- func Vendor(mac string) string
- type Advert
- type Inference
- type Neighbour
- type OpenPort
- type Packet
- type Service
Constants ¶
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.
const ( ByService = "service" ByModel = "model" ByVendor = "vendor" ByGateway = "gateway" BySelf = "self" )
Evidence codes for Inference.Because.
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.
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.
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.
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 ¶
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 ¶
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 ¶
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:
- There is no vendor to look up. The address belongs to nobody.
- 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 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.