first commit
This commit is contained in:
306
clients/clients.go
Normal file
306
clients/clients.go
Normal file
@@ -0,0 +1,306 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user