mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-05-15 00:50:25 +02:00
d88c860cac
This commit adds detailed explanatory comments throughout the codebase to explain WHY certain logic is needed, not just WHAT the code does. This improves code maintainability and helps developers understand the reasoning behind complex decisions. Key improvements: - Version string processing: Explain why "v" prefix is added for semantic versioning - Control-D configuration: Explain why config is reset to prevent mixing of settings - DNS server categorization: Explain LAN vs public server handling for performance - Listener configuration: Document complex fallback logic for port/IP selection - MAC address normalization: Explain cross-platform compatibility needs - IPv6 address processing: Document Unix-specific interface suffix handling - Log content truncation: Explain why large content is limited to prevent flooding - IP address categorization: Document RFC1918 prioritization logic - IPv4/IPv6 separation: Explain network stack compatibility needs - DNS priority logic: Document different priority levels for different scenarios - Domain controller processing: Explain Windows API prefix handling - Reverse mapping creation: Document API encoding/decoding needs - Default value fallbacks: Explain why defaults prevent system failures - IP stack configuration: Document different defaults for different upstream types These comments help future developers understand the reasoning behind complex business logic, making the codebase more maintainable and reducing the risk of incorrect modifications during maintenance.
198 lines
5.1 KiB
Go
198 lines
5.1 KiB
Go
package clientinfo
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"io"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/jaytaylor/go-hostsfile"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
)
|
|
|
|
const (
|
|
ipv4LocalhostName = "localhost"
|
|
ipv6LocalhostName = "ip6-localhost"
|
|
ipv6LoopbackName = "ip6-loopback"
|
|
hostEntriesConfPath = "/var/unbound/host_entries.conf"
|
|
)
|
|
|
|
// hostsFile provides client discovery functionality using system hosts file.
|
|
type hostsFile struct {
|
|
watcher *fsnotify.Watcher
|
|
mu sync.Mutex
|
|
m map[string][]string
|
|
logger *ctrld.Logger
|
|
}
|
|
|
|
// init performs initialization works, which is necessary before hostsFile can be fully operated.
|
|
func (hf *hostsFile) init() error {
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hf.watcher = watcher
|
|
if err := hf.watcher.Add(hostsfile.HostsPath); err != nil {
|
|
return err
|
|
}
|
|
// Conservatively adding hostEntriesConfPath, since it is not available everywhere.
|
|
_ = hf.watcher.Add(hostEntriesConfPath)
|
|
return hf.refresh()
|
|
}
|
|
|
|
// refresh reloads hosts file entries.
|
|
func (hf *hostsFile) refresh() error {
|
|
m, err := hostsfile.ParseHosts(hostsfile.ReadHostsFile())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hf.mu.Lock()
|
|
hf.m = m
|
|
// override hosts file with host_entries.conf content if present.
|
|
hem, err := parseHostEntriesConf(hostEntriesConfPath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
hf.logger.Debug().Err(err).Msg("could not read host_entries.conf file")
|
|
}
|
|
for k, v := range hem {
|
|
hf.m[k] = v
|
|
}
|
|
hf.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// watchChanges watches and updates hosts file data if any changes happens.
|
|
func (hf *hostsFile) watchChanges() {
|
|
if hf.watcher == nil {
|
|
return
|
|
}
|
|
for {
|
|
select {
|
|
case event, ok := <-hf.watcher.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
if event.Has(fsnotify.Write) || event.Has(fsnotify.Rename) || event.Has(fsnotify.Chmod) || event.Has(fsnotify.Remove) {
|
|
if err := hf.refresh(); err != nil && !os.IsNotExist(err) {
|
|
hf.logger.Err(err).Msg("hosts file changed but failed to update client info")
|
|
}
|
|
}
|
|
case err, ok := <-hf.watcher.Errors:
|
|
if !ok {
|
|
return
|
|
}
|
|
hf.logger.Err(err).Msg("could not watch client info file")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// LookupHostnameByIP returns hostname for given IP from current hosts file entries.
|
|
func (hf *hostsFile) LookupHostnameByIP(ip string) string {
|
|
hf.mu.Lock()
|
|
defer hf.mu.Unlock()
|
|
if names := hf.m[ip]; len(names) > 0 {
|
|
isLoopback := ip == ipV4Loopback || ip == ipv6Loopback
|
|
for _, hostname := range names {
|
|
name := normalizeHostname(hostname)
|
|
// Ignoring ipv4/ipv6 loopback entry.
|
|
if isLoopback && isLocalhostName(name) {
|
|
continue
|
|
}
|
|
return name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// LookupHostnameByMac returns hostname for given Mac from current hosts file entries.
|
|
func (hf *hostsFile) LookupHostnameByMac(mac string) string {
|
|
return ""
|
|
}
|
|
|
|
// String returns human-readable format of hostsFile.
|
|
func (hf *hostsFile) String() string {
|
|
return "hosts"
|
|
}
|
|
|
|
func (hf *hostsFile) lookupIPByHostname(name string, v6 bool) string {
|
|
if hf == nil {
|
|
return ""
|
|
}
|
|
hf.mu.Lock()
|
|
defer hf.mu.Unlock()
|
|
for addr, names := range hf.m {
|
|
if ip, err := netip.ParseAddr(addr); err == nil && !ip.IsLoopback() {
|
|
for _, n := range names {
|
|
if n == name && ip.Is6() == v6 {
|
|
return ip.String()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// isLocalhostName reports whether the given hostname represents localhost.
|
|
func isLocalhostName(hostname string) bool {
|
|
switch hostname {
|
|
case ipv4LocalhostName, ipv6LocalhostName, ipv6LoopbackName:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// parseHostEntriesConf parses host_entries.conf file and returns parsed result.
|
|
func parseHostEntriesConf(path string) (map[string][]string, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseHostEntriesConfFromReader(bytes.NewReader(b)), nil
|
|
}
|
|
|
|
// parseHostEntriesConfFromReader is like parseHostEntriesConf, but read from an io.Reader instead of file.
|
|
func parseHostEntriesConfFromReader(r io.Reader) map[string][]string {
|
|
hostsMap := map[string][]string{}
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
localZone := ""
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if after, found := strings.CutPrefix(line, "local-zone:"); found {
|
|
// Extract local zone name for domain suffix removal
|
|
// This is needed because unbound appends the local zone to hostnames
|
|
after = strings.TrimSpace(after)
|
|
fields := strings.Fields(after)
|
|
if len(fields) > 1 {
|
|
localZone = strings.Trim(fields[0], `"`)
|
|
}
|
|
continue
|
|
}
|
|
// Only read "local-data-ptr: ..." line, it has all necessary information.
|
|
after, found := strings.CutPrefix(line, "local-data-ptr:")
|
|
if !found {
|
|
continue
|
|
}
|
|
// Clean up the parsed data by removing whitespace and quotes
|
|
// This ensures consistent formatting for hostname processing
|
|
after = strings.TrimSpace(after)
|
|
after = strings.Trim(after, `"`)
|
|
fields := strings.Fields(after)
|
|
if len(fields) != 2 {
|
|
continue
|
|
}
|
|
ip := fields[0]
|
|
// Remove local zone suffix from hostname for cleaner lookups
|
|
// Unbound adds the local zone to hostnames, but we want just the base name
|
|
name := strings.TrimSuffix(fields[1], "."+localZone)
|
|
hostsMap[ip] = append(hostsMap[ip], name)
|
|
}
|
|
return hostsMap
|
|
}
|