mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
all: add clients list command to debug Mac discovery
This commit is contained in:
committed by
Cuong Manh Le
parent
61b6431b6e
commit
437fb1b16d
@@ -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 {
|
||||
|
||||
@@ -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
5
go.mod
@@ -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
8
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -65,3 +65,7 @@ func (m *merlinDiscover) parseMerlinCustomClientList(data string) {
|
||||
m.hostname.Store(mac, hostname)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *merlinDiscover) String() string {
|
||||
return "merlin"
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user