mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-03-13 10:26:06 +00:00
Capitalize the first letter of all log messages throughout the codebase to improve readability and consistency in logging output. Key improvements: - All log messages now start with capital letters - Consistent formatting across all logging statements - Improved readability for debugging and monitoring - Enhanced user experience with better formatted messages Files updated: - CLI commands and service management - Internal client information discovery - Network operations and configuration - DNS resolver and proxy operations - Platform-specific implementations This completes the final phase of the logging improvement project, ensuring all log messages follow consistent capitalization standards for better readability and professional appearance.
258 lines
6.1 KiB
Go
258 lines
6.1 KiB
Go
package clientinfo
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mdlayher/ndp"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
|
)
|
|
|
|
// ndpDiscover provides client discovery functionality using NDP protocol.
|
|
type ndpDiscover struct {
|
|
mac sync.Map // ip => mac
|
|
ip sync.Map // mac => ip
|
|
logger *ctrld.Logger
|
|
}
|
|
|
|
// refresh re-scans the NDP table.
|
|
func (nd *ndpDiscover) refresh() error {
|
|
nd.scan()
|
|
return nil
|
|
}
|
|
|
|
// LookupIP returns the ipv6 associated with the input MAC address.
|
|
func (nd *ndpDiscover) LookupIP(mac string) string {
|
|
val, ok := nd.ip.Load(mac)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return val.(string)
|
|
}
|
|
|
|
// LookupMac returns the MAC address of the given IP address.
|
|
func (nd *ndpDiscover) LookupMac(ip string) string {
|
|
val, ok := nd.mac.Load(ip)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return val.(string)
|
|
}
|
|
|
|
// String returns human-readable format of ndpDiscover.
|
|
func (nd *ndpDiscover) String() string {
|
|
return "ndp"
|
|
}
|
|
|
|
// List returns all known IP addresses.
|
|
func (nd *ndpDiscover) List() []string {
|
|
if nd == nil {
|
|
return nil
|
|
}
|
|
var ips []string
|
|
nd.ip.Range(func(key, value any) bool {
|
|
ips = append(ips, value.(string))
|
|
return true
|
|
})
|
|
nd.mac.Range(func(key, value any) bool {
|
|
ips = append(ips, key.(string))
|
|
return true
|
|
})
|
|
return ips
|
|
}
|
|
|
|
// saveInfo saves ip and mac info to mapping table.
|
|
func (nd *ndpDiscover) saveInfo(ip, mac string) {
|
|
ip = normalizeIP(ip)
|
|
// Store ip => map mapping,
|
|
nd.mac.Store(ip, mac)
|
|
|
|
// Do not store mac => ip mapping if new ip is a link local unicast.
|
|
if ctrldnet.IsLinkLocalUnicastIPv6(ip) {
|
|
return
|
|
}
|
|
|
|
// If there is old ip => mac mapping, delete it.
|
|
if old, existed := nd.ip.Load(mac); existed {
|
|
oldIP := old.(string)
|
|
if oldIP != ip {
|
|
nd.mac.Delete(oldIP)
|
|
}
|
|
}
|
|
// Store mac => ip mapping.
|
|
nd.ip.Store(mac, ip)
|
|
}
|
|
|
|
// listen listens on ipv6 link local for Neighbor Solicitation message
|
|
// to update new neighbors information to ndp table.
|
|
func (nd *ndpDiscover) listen(ctx context.Context) {
|
|
ifis, err := allInterfacesWithV6LinkLocal()
|
|
if err != nil {
|
|
nd.logger.Debug().Err(err).Msg("Failed to find valid ipv6 interfaces")
|
|
return
|
|
}
|
|
for _, ifi := range ifis {
|
|
go func(ifi *net.Interface) {
|
|
nd.listenOnInterface(ctx, ifi)
|
|
}(ifi)
|
|
}
|
|
}
|
|
|
|
func (nd *ndpDiscover) listenOnInterface(ctx context.Context, ifi *net.Interface) {
|
|
c, ip, err := ndp.Listen(ifi, ndp.Unspecified)
|
|
if err != nil {
|
|
nd.logger.Debug().Err(err).Msg("Ndp listen failed")
|
|
return
|
|
}
|
|
defer c.Close()
|
|
nd.logger.Debug().Msgf("Listening ndp on: %s", ip.String())
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
_ = c.SetReadDeadline(time.Now().Add(30 * time.Second))
|
|
msg, _, from, readErr := c.ReadFrom()
|
|
if readErr != nil {
|
|
var opErr *net.OpError
|
|
if errors.As(readErr, &opErr) && (opErr.Timeout() || opErr.Temporary()) {
|
|
continue
|
|
}
|
|
nd.logger.Debug().Err(readErr).Msg("Ndp read loop error")
|
|
return
|
|
}
|
|
|
|
// Only looks for neighbor solicitation message, since new clients
|
|
// which join network will broadcast this message to us.
|
|
am, ok := msg.(*ndp.NeighborSolicitation)
|
|
if !ok {
|
|
continue
|
|
}
|
|
fromIP := from.String()
|
|
for _, opt := range am.Options {
|
|
if lla, ok := opt.(*ndp.LinkLayerAddress); ok {
|
|
mac := lla.Addr.String()
|
|
nd.saveInfo(fromIP, mac)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// scanWindows populates NDP table using information from "netsh" command.
|
|
func (nd *ndpDiscover) scanWindows(r io.Reader) {
|
|
scanner := bufio.NewScanner(r)
|
|
for scanner.Scan() {
|
|
fields := strings.Fields(scanner.Text())
|
|
if len(fields) < 3 {
|
|
continue
|
|
}
|
|
if mac := parseMAC(fields[1]); mac != "" {
|
|
nd.saveInfo(fields[0], mac)
|
|
}
|
|
}
|
|
}
|
|
|
|
// scanUnix populates NDP table using information from "ndp" command.
|
|
func (nd *ndpDiscover) scanUnix(r io.Reader) {
|
|
scanner := bufio.NewScanner(r)
|
|
scanner.Scan() // skip header
|
|
for scanner.Scan() {
|
|
fields := strings.Fields(scanner.Text())
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
if mac := parseMAC(fields[1]); mac != "" {
|
|
ip := fields[0]
|
|
// Remove interface suffix from IPv6 addresses
|
|
// Unix systems append interface names to IPv6 addresses (e.g., "fe80::1%eth0")
|
|
// This suffix needs to be removed for proper IP parsing
|
|
if idx := strings.IndexByte(ip, '%'); idx != -1 {
|
|
ip = ip[:idx]
|
|
}
|
|
nd.saveInfo(ip, mac)
|
|
}
|
|
}
|
|
}
|
|
|
|
// normalizeMac ensure the given MAC address have the proper format
|
|
// before being parsed.
|
|
//
|
|
// Example, changing "00:0:00:0:00:01" to "00:00:00:00:00:01", which
|
|
// can be seen on Darwin.
|
|
func normalizeMac(mac string) string {
|
|
if len(mac) == 17 {
|
|
return mac
|
|
}
|
|
// Windows use "-" instead of ":" as separator.
|
|
// This normalization is needed because different operating systems use different
|
|
// separators for MAC addresses, but net.ParseMAC expects ":" format
|
|
mac = strings.ReplaceAll(mac, "-", ":")
|
|
parts := strings.Split(mac, ":")
|
|
if len(parts) != 6 {
|
|
return ""
|
|
}
|
|
// Pad single-digit hex values with leading zero
|
|
// This ensures consistent formatting for MAC address parsing
|
|
for i, c := range parts {
|
|
if len(c) == 1 {
|
|
parts[i] = "0" + c
|
|
}
|
|
}
|
|
return strings.Join(parts, ":")
|
|
}
|
|
|
|
// parseMAC parses the input MAC, doing normalization,
|
|
// and return the result after calling net.ParseMac function.
|
|
func parseMAC(mac string) string {
|
|
hw, _ := net.ParseMAC(normalizeMac(mac))
|
|
return hw.String()
|
|
}
|
|
|
|
// allInterfacesWithV6LinkLocal returns all interfaces which is capable of using NDP.
|
|
func allInterfacesWithV6LinkLocal() ([]*net.Interface, error) {
|
|
ifis, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res := make([]*net.Interface, 0, len(ifis))
|
|
for _, ifi := range ifis {
|
|
ifi := ifi
|
|
// Skip if iface is down/loopback/non-multicast.
|
|
if ifi.Flags&net.FlagUp == 0 || ifi.Flags&net.FlagLoopback != 0 || ifi.Flags&net.FlagMulticast == 0 {
|
|
continue
|
|
}
|
|
|
|
addrs, err := ifi.Addrs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
ipNet, ok := addr.(*net.IPNet)
|
|
if !ok {
|
|
continue
|
|
}
|
|
ip, ok := netip.AddrFromSlice(ipNet.IP)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid ip address: %s", ipNet.String())
|
|
}
|
|
if ip.Is6() && !ip.Is4In6() {
|
|
res = append(res, &ifi)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|