all: add clients list command to debug Mac discovery

This commit is contained in:
Cuong Manh Le
2023-07-21 16:28:08 +00:00
committed by Cuong Manh Le
parent 61b6431b6e
commit 437fb1b16d
10 changed files with 236 additions and 6 deletions

View File

@@ -5,6 +5,7 @@ import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
@@ -26,6 +27,7 @@ import (
"github.com/go-playground/validator/v10"
"github.com/kardianos/service"
"github.com/miekg/dns"
"github.com/olekukonko/tablewriter"
"github.com/pelletier/go-toml/v2"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
@@ -35,6 +37,7 @@ import (
"tailscale.com/net/interfaces"
"github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/internal/clientinfo"
"github.com/Control-D-Inc/ctrld/internal/controld"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
"github.com/Control-D-Inc/ctrld/internal/router"
@@ -751,6 +754,66 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
uninstallCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
uninstallCmdAlias.Flags().AddFlagSet(stopCmd.Flags())
rootCmd.AddCommand(uninstallCmdAlias)
listClientsCmd := &cobra.Command{
Use: "list",
Short: "List clients that ctrld discovered",
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege()
},
Run: func(cmd *cobra.Command, args []string) {
dir, err := userHomeDir()
if err != nil {
mainLog.Fatal().Err(err).Msg("failed to find ctrld home dir")
}
cc := newControlClient(filepath.Join(dir, ctrldControlUnixSock))
resp, err := cc.post(listClientsPath, nil)
if err != nil {
mainLog.Fatal().Err(err).Msg("failed to get clients list")
}
defer resp.Body.Close()
var clients []*clientinfo.Client
if err := json.NewDecoder(resp.Body).Decode(&clients); err != nil {
mainLog.Fatal().Err(err).Msg("failed to decode clients list result")
}
map2Slice := func(m map[string]struct{}) []string {
s := make([]string, 0, len(m))
for k := range m {
s = append(s, k)
}
sort.Strings(s)
return s
}
data := make([][]string, len(clients))
for i, c := range clients {
row := []string{
c.IP.String(),
c.Hostname,
c.Mac,
strings.Join(map2Slice(c.Source), ","),
}
data[i] = row
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"IP", "Hostname", "Mac", "Discovered"})
table.SetAutoFormatHeaders(false)
table.AppendBulk(data)
table.Render()
},
}
clientsCmd := &cobra.Command{
Use: "clients",
Short: "Manage clients",
Args: cobra.OnlyValidArgs,
ValidArgs: []string{
listClientsCmd.Use,
},
}
clientsCmd.AddCommand(listClientsCmd)
rootCmd.AddCommand(clientsCmd)
}
func writeConfigFile() error {

View File

@@ -2,13 +2,18 @@ package main
import (
"context"
"encoding/json"
"net"
"net/http"
"os"
"sort"
"time"
)
const contentTypeJson = "application/json"
const (
contentTypeJson = "application/json"
listClientsPath = "/clients"
)
type controlServer struct {
server *http.Server
@@ -48,7 +53,16 @@ func (s *controlServer) register(pattern string, handler http.Handler) {
}
func (p *prog) registerControlServerHandler() {
// TODO: register handler here.
p.cs.mux.Handle(listClientsPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
clients := p.ciTable.ListClients()
sort.Slice(clients, func(i, j int) bool {
return clients[i].IP.Less(clients[j].IP)
})
if err := json.NewEncoder(w).Encode(&clients); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}))
}
func jsonResponse(next http.Handler) http.Handler {

5
go.mod
View File

@@ -15,10 +15,12 @@ require (
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/kardianos/service v1.2.1
github.com/miekg/dns v1.1.55
github.com/olekukonko/tablewriter v0.0.5
github.com/pelletier/go-toml/v2 v2.0.8
github.com/quic-go/quic-go v0.32.0
github.com/rs/zerolog v1.28.0
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.3
github.com/vishvananda/netlink v1.2.1-beta.2
@@ -48,6 +50,7 @@ require (
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
@@ -60,11 +63,11 @@ require (
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
github.com/vishvananda/netns v0.0.4 // indirect

8
go.sum
View File

@@ -193,6 +193,9 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
@@ -206,6 +209,8 @@ github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
@@ -230,6 +235,9 @@ github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV5
github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA=
github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=

View File

@@ -27,3 +27,20 @@ func (a *arpDiscover) LookupMac(ip string) string {
}
return val.(string)
}
func (a *arpDiscover) String() string {
return "arp"
}
func (a *arpDiscover) List() []string {
var ips []string
a.ip.Range(func(key, value any) bool {
ips = append(ips, value.(string))
return true
})
a.mac.Range(func(key, value any) bool {
ips = append(ips, key.(string))
return true
})
return ips
}

View File

@@ -1,6 +1,8 @@
package clientinfo
import (
"fmt"
"net/netip"
"strings"
"time"
@@ -9,25 +11,35 @@ import (
// 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
}
@@ -36,6 +48,13 @@ 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
@@ -146,9 +165,6 @@ func (t *Table) LookupIP(mac string) string {
}
func (t *Table) LookupMac(ip string) string {
t.arp.mac.Range(func(key, value any) bool {
return true
})
for _, r := range t.macResolvers {
if mac := r.LookupMac(ip); mac != "" {
return mac
@@ -169,6 +185,86 @@ func (t *Table) LookupHostname(ip, mac string) string {
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

View File

@@ -100,6 +100,23 @@ func (d *dhcp) LookupHostnameByMac(mac string) string {
return val.(string)
}
func (d *dhcp) String() string {
return "dhcp"
}
func (d *dhcp) List() []string {
var ips []string
d.ip.Range(func(key, value any) bool {
ips = append(ips, value.(string))
return true
})
d.mac.Range(func(key, value any) bool {
ips = append(ips, key.(string))
return true
})
return ips
}
// AddLeaseFile adds given lease file for reading/watching clients info.
func (d *dhcp) addLeaseFile(name string, format ctrld.LeaseFileFormat) error {
if d.watcher == nil {

View File

@@ -43,6 +43,10 @@ func (m *mdns) LookupHostnameByMac(mac string) string {
return ""
}
func (m *mdns) String() string {
return "mdns"
}
func (m *mdns) init(quitCh chan struct{}) error {
ifaces, err := multicastInterfaces()
if err != nil {

View File

@@ -65,3 +65,7 @@ func (m *merlinDiscover) parseMerlinCustomClientList(data string) {
m.hostname.Store(mac, hostname)
}
}
func (m *merlinDiscover) String() string {
return "merlin"
}

View File

@@ -36,6 +36,10 @@ func (p *ptrDiscover) LookupHostnameByMac(mac string) string {
return ""
}
func (p *ptrDiscover) String() string {
return "ptr"
}
func (p *ptrDiscover) lookupHostname(ip string) string {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()