mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
312 lines
7.1 KiB
Go
312 lines
7.1 KiB
Go
package clientinfo
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
)
|
|
|
|
// IpResolver is the interface for retrieving IP from Mac.
|
|
type IpResolver interface {
|
|
fmt.Stringer
|
|
// LookupIP returns ip of the device with given mac.
|
|
LookupIP(mac string) string
|
|
// List returns list of ip known by the resolver.
|
|
List() []string
|
|
}
|
|
|
|
// MacResolver is the interface for retrieving Mac from IP.
|
|
type MacResolver interface {
|
|
fmt.Stringer
|
|
// LookupMac returns mac of the device with given ip.
|
|
LookupMac(ip string) string
|
|
}
|
|
|
|
// HostnameByIpResolver is the interface for retrieving hostname from IP.
|
|
type HostnameByIpResolver interface {
|
|
// LookupHostnameByIP returns hostname of the given ip.
|
|
LookupHostnameByIP(ip string) string
|
|
}
|
|
|
|
// HostnameByMacResolver is the interface for retrieving hostname from Mac.
|
|
type HostnameByMacResolver interface {
|
|
// LookupHostnameByMac returns hostname of the device with given mac.
|
|
LookupHostnameByMac(mac string) string
|
|
}
|
|
|
|
// HostnameResolver is the interface for retrieving hostname from either IP or Mac.
|
|
type HostnameResolver interface {
|
|
fmt.Stringer
|
|
HostnameByIpResolver
|
|
HostnameByMacResolver
|
|
}
|
|
|
|
type refresher interface {
|
|
refresh() error
|
|
}
|
|
|
|
type Client struct {
|
|
IP netip.Addr
|
|
Mac string
|
|
Hostname string
|
|
Source map[string]struct{}
|
|
}
|
|
|
|
type Table struct {
|
|
ipResolvers []IpResolver
|
|
macResolvers []MacResolver
|
|
hostnameResolvers []HostnameResolver
|
|
refreshers []refresher
|
|
|
|
dhcp *dhcp
|
|
merlin *merlinDiscover
|
|
arp *arpDiscover
|
|
ptr *ptrDiscover
|
|
mdns *mdns
|
|
cfg *ctrld.Config
|
|
quitCh chan struct{}
|
|
selfIP string
|
|
}
|
|
|
|
func NewTable(cfg *ctrld.Config, selfIP string) *Table {
|
|
return &Table{
|
|
cfg: cfg,
|
|
quitCh: make(chan struct{}),
|
|
selfIP: selfIP,
|
|
}
|
|
}
|
|
|
|
func (t *Table) AddLeaseFile(name string, format ctrld.LeaseFileFormat) {
|
|
if !t.discoverDHCP() {
|
|
return
|
|
}
|
|
clientInfoFiles[name] = format
|
|
}
|
|
|
|
func (t *Table) RefreshLoop(stopCh chan struct{}) {
|
|
timer := time.NewTicker(time.Minute * 5)
|
|
for {
|
|
select {
|
|
case <-timer.C:
|
|
for _, r := range t.refreshers {
|
|
_ = r.refresh()
|
|
}
|
|
case <-stopCh:
|
|
close(t.quitCh)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Table) Init() {
|
|
if t.discoverDHCP() || t.discoverARP() {
|
|
t.merlin = &merlinDiscover{}
|
|
if err := t.merlin.refresh(); err != nil {
|
|
ctrld.ProxyLog.Error().Err(err).Msg("could not init Merlin discover")
|
|
} else {
|
|
t.hostnameResolvers = append(t.hostnameResolvers, t.merlin)
|
|
t.refreshers = append(t.refreshers, t.merlin)
|
|
}
|
|
}
|
|
if t.discoverDHCP() {
|
|
t.dhcp = &dhcp{selfIP: t.selfIP}
|
|
ctrld.ProxyLog.Debug().Msg("start dhcp discovery")
|
|
if err := t.dhcp.init(); err != nil {
|
|
ctrld.ProxyLog.Error().Err(err).Msg("could not init DHCP discover")
|
|
} else {
|
|
t.ipResolvers = append(t.ipResolvers, t.dhcp)
|
|
t.macResolvers = append(t.macResolvers, t.dhcp)
|
|
t.hostnameResolvers = append(t.hostnameResolvers, t.dhcp)
|
|
}
|
|
go t.dhcp.watchChanges()
|
|
}
|
|
if t.discoverARP() {
|
|
t.arp = &arpDiscover{}
|
|
ctrld.ProxyLog.Debug().Msg("start arp discovery")
|
|
if err := t.arp.refresh(); err != nil {
|
|
ctrld.ProxyLog.Error().Err(err).Msg("could not init ARP discover")
|
|
} else {
|
|
t.ipResolvers = append(t.ipResolvers, t.arp)
|
|
t.macResolvers = append(t.macResolvers, t.arp)
|
|
t.refreshers = append(t.refreshers, t.arp)
|
|
}
|
|
}
|
|
if t.discoverPTR() {
|
|
t.ptr = &ptrDiscover{resolver: ctrld.NewPrivateResolver()}
|
|
ctrld.ProxyLog.Debug().Msg("start ptr discovery")
|
|
if err := t.ptr.refresh(); err != nil {
|
|
ctrld.ProxyLog.Error().Err(err).Msg("could not init PTR discover")
|
|
} else {
|
|
t.hostnameResolvers = append(t.hostnameResolvers, t.ptr)
|
|
t.refreshers = append(t.refreshers, t.ptr)
|
|
}
|
|
}
|
|
if t.discoverMDNS() {
|
|
t.mdns = &mdns{}
|
|
ctrld.ProxyLog.Debug().Msg("start mdns discovery")
|
|
if err := t.mdns.init(t.quitCh); err != nil {
|
|
ctrld.ProxyLog.Error().Err(err).Msg("could not init mDNS discover")
|
|
} else {
|
|
t.hostnameResolvers = append(t.hostnameResolvers, t.mdns)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Table) LookupIP(mac string) string {
|
|
for _, r := range t.ipResolvers {
|
|
if ip := r.LookupIP(mac); ip != "" {
|
|
return ip
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (t *Table) LookupMac(ip string) string {
|
|
for _, r := range t.macResolvers {
|
|
if mac := r.LookupMac(ip); mac != "" {
|
|
return mac
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (t *Table) LookupHostname(ip, mac string) string {
|
|
for _, r := range t.hostnameResolvers {
|
|
if name := r.LookupHostnameByIP(ip); name != "" {
|
|
return name
|
|
}
|
|
if name := r.LookupHostnameByMac(mac); name != "" {
|
|
return name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type macEntry struct {
|
|
mac string
|
|
src string
|
|
}
|
|
|
|
type hostnameEntry struct {
|
|
name string
|
|
src string
|
|
}
|
|
|
|
func (t *Table) lookupMacAll(ip string) []*macEntry {
|
|
var res []*macEntry
|
|
for _, r := range t.macResolvers {
|
|
res = append(res, &macEntry{mac: r.LookupMac(ip), src: r.String()})
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (t *Table) lookupHostnameAll(ip, mac string) []*hostnameEntry {
|
|
var res []*hostnameEntry
|
|
for _, r := range t.hostnameResolvers {
|
|
src := r.String()
|
|
if name := r.LookupHostnameByIP(ip); name != "" {
|
|
res = append(res, &hostnameEntry{name: name, src: src})
|
|
continue
|
|
}
|
|
if name := r.LookupHostnameByMac(mac); name != "" {
|
|
res = append(res, &hostnameEntry{name: name, src: src})
|
|
continue
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// ListClients returns list of clients discovered by ctrld.
|
|
func (t *Table) ListClients() []*Client {
|
|
for _, r := range t.refreshers {
|
|
_ = r.refresh()
|
|
}
|
|
ipMap := make(map[string]*Client)
|
|
for _, ir := range t.ipResolvers {
|
|
for _, ip := range ir.List() {
|
|
c, ok := ipMap[ip]
|
|
if !ok {
|
|
c = &Client{
|
|
IP: netip.MustParseAddr(ip),
|
|
Source: map[string]struct{}{ir.String(): {}},
|
|
}
|
|
ipMap[ip] = c
|
|
} else {
|
|
c.Source[ir.String()] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
for ip := range ipMap {
|
|
c := ipMap[ip]
|
|
for _, e := range t.lookupMacAll(ip) {
|
|
if c.Mac == "" && e.mac != "" {
|
|
c.Mac = e.mac
|
|
}
|
|
if e.mac != "" {
|
|
c.Source[e.src] = struct{}{}
|
|
}
|
|
}
|
|
for _, e := range t.lookupHostnameAll(ip, c.Mac) {
|
|
if c.Hostname == "" && e.name != "" {
|
|
c.Hostname = e.name
|
|
}
|
|
if e.name != "" {
|
|
c.Source[e.src] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
clients := make([]*Client, 0, len(ipMap))
|
|
for _, c := range ipMap {
|
|
clients = append(clients, c)
|
|
}
|
|
return clients
|
|
}
|
|
|
|
func (t *Table) discoverDHCP() bool {
|
|
if t.cfg.Service.DiscoverDHCP == nil {
|
|
return true
|
|
}
|
|
return *t.cfg.Service.DiscoverDHCP
|
|
}
|
|
|
|
func (t *Table) discoverARP() bool {
|
|
if t.cfg.Service.DiscoverARP == nil {
|
|
return true
|
|
}
|
|
return *t.cfg.Service.DiscoverARP
|
|
}
|
|
|
|
func (t *Table) discoverMDNS() bool {
|
|
if t.cfg.Service.DiscoverMDNS == nil {
|
|
return true
|
|
}
|
|
return *t.cfg.Service.DiscoverMDNS
|
|
}
|
|
|
|
func (t *Table) discoverPTR() bool {
|
|
if t.cfg.Service.DiscoverPtr == nil {
|
|
return true
|
|
}
|
|
return *t.cfg.Service.DiscoverPtr
|
|
}
|
|
|
|
// normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file.
|
|
func normalizeIP(in string) string {
|
|
// dnsmasq may put ip with interface index in lease file, strip it here.
|
|
ip, _, found := strings.Cut(in, "%")
|
|
if found {
|
|
return ip
|
|
}
|
|
return in
|
|
}
|
|
|
|
func normalizeHostname(name string) string {
|
|
if before, _, found := strings.Cut(name, "."); found {
|
|
return before // remove ".local.", ".lan.", ... suffix
|
|
}
|
|
return name
|
|
}
|