306 lines
6.0 KiB
Go
306 lines
6.0 KiB
Go
package clients
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"alpine-router/config"
|
|
"alpine-router/traffic"
|
|
)
|
|
|
|
const LeasesFile = "/var/lib/misc/dnsmasq.leases"
|
|
|
|
type Client struct {
|
|
IP string `json:"ip"`
|
|
MAC string `json:"mac"`
|
|
Hostname string `json:"hostname"`
|
|
Interface string `json:"interface"`
|
|
LeaseExpires int64 `json:"lease_expires"`
|
|
IsDHCP bool `json:"is_dhcp"`
|
|
Online bool `json:"online"`
|
|
TxBytes uint64 `json:"tx_bytes"`
|
|
RxBytes uint64 `json:"rx_bytes"`
|
|
LastActive int64 `json:"last_active"`
|
|
Known bool `json:"known"`
|
|
Blocked bool `json:"blocked"`
|
|
StaticIP string `json:"static_ip"`
|
|
}
|
|
|
|
func GetAll() ([]Client, error) {
|
|
leases, err := parseDNSMasqLeases()
|
|
if err != nil {
|
|
leases = map[string]*Client{}
|
|
}
|
|
|
|
arpEntries, err := parseARPTable()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("arp: %w", err)
|
|
}
|
|
|
|
byIP := make(map[string]*Client, len(arpEntries))
|
|
|
|
for ip, c := range arpEntries {
|
|
byIP[ip] = c
|
|
}
|
|
|
|
for ip, lease := range leases {
|
|
if c, exists := byIP[ip]; exists {
|
|
c.IsDHCP = true
|
|
c.LeaseExpires = lease.LeaseExpires
|
|
if lease.Hostname != "" {
|
|
c.Hostname = lease.Hostname
|
|
}
|
|
if c.MAC == "" {
|
|
c.MAC = lease.MAC
|
|
}
|
|
} else {
|
|
byIP[ip] = lease
|
|
}
|
|
}
|
|
|
|
blockedByMAC := make(map[string]bool)
|
|
|
|
cfg, cfgErr := config.Load()
|
|
if cfgErr == nil && cfg != nil {
|
|
knownByMAC := make(map[string]bool)
|
|
for _, c := range byIP {
|
|
if c.MAC != "" {
|
|
knownByMAC[c.MAC] = true
|
|
}
|
|
}
|
|
|
|
for _, kd := range cfg.KnownDevices {
|
|
key := kd.IP
|
|
found := false
|
|
|
|
for ip, c := range byIP {
|
|
if kd.MAC != "" && c.MAC == kd.MAC {
|
|
c.Blocked = kd.Blocked
|
|
c.StaticIP = kd.StaticIP
|
|
if kd.Hostname != "" {
|
|
c.Hostname = kd.Hostname
|
|
}
|
|
if kd.StaticIP != "" {
|
|
c.IP = kd.StaticIP
|
|
}
|
|
found = true
|
|
break
|
|
}
|
|
if kd.IP != "" && ip == kd.IP && (kd.MAC == "" || c.MAC == kd.MAC) {
|
|
c.Blocked = kd.Blocked
|
|
c.StaticIP = kd.StaticIP
|
|
if kd.Hostname != "" {
|
|
c.Hostname = kd.Hostname
|
|
}
|
|
if kd.StaticIP != "" {
|
|
c.IP = kd.StaticIP
|
|
}
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found && key != "" {
|
|
knownByMACKey := kd.MAC
|
|
if knownByMACKey != "" && knownByMAC[knownByMACKey] {
|
|
continue
|
|
}
|
|
displayIP := kd.IP
|
|
if kd.StaticIP != "" {
|
|
displayIP = kd.StaticIP
|
|
}
|
|
byIP[key] = &Client{
|
|
IP: displayIP,
|
|
MAC: kd.MAC,
|
|
Hostname: kd.Hostname,
|
|
Known: true,
|
|
Blocked: kd.Blocked,
|
|
StaticIP: kd.StaticIP,
|
|
}
|
|
}
|
|
|
|
if kd.Blocked && kd.MAC != "" {
|
|
blockedByMAC[kd.MAC] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
trafficAvailable := traffic.Available()
|
|
for ip, c := range byIP {
|
|
if c.Known && c.IP == "" {
|
|
continue
|
|
}
|
|
if trafficAvailable {
|
|
traffic.EnsureIPTracked(ip)
|
|
}
|
|
ts := traffic.Get(ip)
|
|
c.TxBytes = ts.TxBytes
|
|
c.RxBytes = ts.RxBytes
|
|
if !ts.LastActive.IsZero() {
|
|
c.LastActive = ts.LastActive.Unix()
|
|
}
|
|
if trafficAvailable {
|
|
c.Online = traffic.IsOnline(ip)
|
|
}
|
|
}
|
|
|
|
go syncKnownDevices(byIP)
|
|
|
|
result := make([]Client, 0, len(byIP))
|
|
for _, c := range byIP {
|
|
result = append(result, *c)
|
|
}
|
|
|
|
sort.Slice(result, func(i, j int) bool {
|
|
if result[i].Online != result[j].Online {
|
|
return result[i].Online
|
|
}
|
|
if result[i].Known != result[j].Known {
|
|
return result[i].Known
|
|
}
|
|
return ipLess(result[i].IP, result[j].IP)
|
|
})
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func syncKnownDevices(byIP map[string]*Client) {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
savedHostnames := make(map[string]string)
|
|
savedBlocked := make(map[string]bool)
|
|
savedStaticIPs := make(map[string]string)
|
|
for _, kd := range cfg.KnownDevices {
|
|
key := kd.MAC
|
|
if key == "" {
|
|
key = kd.IP
|
|
}
|
|
savedHostnames[key] = kd.Hostname
|
|
if kd.Blocked {
|
|
savedBlocked[key] = true
|
|
}
|
|
if kd.StaticIP != "" {
|
|
savedStaticIPs[key] = kd.StaticIP
|
|
}
|
|
}
|
|
|
|
var seen []config.KnownDevice
|
|
for _, c := range byIP {
|
|
if c.MAC != "" && c.IP != "" {
|
|
key := c.MAC
|
|
hostname := c.Hostname
|
|
if saved, ok := savedHostnames[key]; ok {
|
|
hostname = saved
|
|
}
|
|
kd := config.KnownDevice{
|
|
IP: c.IP,
|
|
MAC: c.MAC,
|
|
Hostname: hostname,
|
|
}
|
|
if savedBlocked[key] {
|
|
kd.Blocked = true
|
|
}
|
|
if sip, ok := savedStaticIPs[key]; ok {
|
|
kd.StaticIP = sip
|
|
}
|
|
seen = append(seen, kd)
|
|
}
|
|
}
|
|
_ = config.UpdateKnownDevices(seen)
|
|
}
|
|
|
|
func parseDNSMasqLeases() (map[string]*Client, error) {
|
|
f, err := os.Open(LeasesFile)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return map[string]*Client{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
out := map[string]*Client{}
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
fields := strings.Fields(scanner.Text())
|
|
if len(fields) < 4 {
|
|
continue
|
|
}
|
|
exp, _ := strconv.ParseInt(fields[0], 10, 64)
|
|
mac := fields[1]
|
|
ip := fields[2]
|
|
hostname := fields[3]
|
|
if hostname == "*" {
|
|
hostname = ""
|
|
}
|
|
out[ip] = &Client{
|
|
IP: ip,
|
|
MAC: mac,
|
|
Hostname: hostname,
|
|
LeaseExpires: exp,
|
|
IsDHCP: true,
|
|
}
|
|
}
|
|
return out, scanner.Err()
|
|
}
|
|
|
|
func parseARPTable() (map[string]*Client, error) {
|
|
f, err := os.Open("/proc/net/arp")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
out := map[string]*Client{}
|
|
scanner := bufio.NewScanner(f)
|
|
scanner.Scan()
|
|
|
|
for scanner.Scan() {
|
|
fields := strings.Fields(scanner.Text())
|
|
if len(fields) < 6 {
|
|
continue
|
|
}
|
|
ip := fields[0]
|
|
flags := fields[2]
|
|
mac := fields[3]
|
|
iface := fields[5]
|
|
|
|
if mac == "00:00:00:00:00:00" || iface == "lo" {
|
|
continue
|
|
}
|
|
|
|
arpOnline := flags == "0x2" || flags == "0x6"
|
|
|
|
out[ip] = &Client{
|
|
IP: ip,
|
|
MAC: mac,
|
|
Interface: iface,
|
|
Online: arpOnline,
|
|
}
|
|
}
|
|
return out, scanner.Err()
|
|
}
|
|
|
|
func ipLess(a, b string) bool {
|
|
return ipToUint32(a) < ipToUint32(b)
|
|
}
|
|
|
|
func ipToUint32(ip string) uint32 {
|
|
parts := strings.SplitN(ip, ".", 4)
|
|
if len(parts) != 4 {
|
|
return 0
|
|
}
|
|
var v uint32
|
|
for _, p := range parts {
|
|
n, _ := strconv.ParseUint(p, 10, 8)
|
|
v = v<<8 | uint32(n)
|
|
}
|
|
return v
|
|
} |