Browse Source

Detect presence with arp and arping on FreeBSD

Douglas Thrift 1 year ago
parent
commit
c30f2acf60
5 changed files with 176 additions and 0 deletions
  1. 29 0
      cmd/presence/main.go
  2. 3 0
      go.mod
  3. 11 0
      neighbors/arp.go
  4. 86 0
      neighbors/arp_freebsd.go
  5. 47 0
      neighbors/arping.go

+ 29 - 0
cmd/presence/main.go

@@ -0,0 +1,29 @@
+package main
+
+import (
+	"context"
+	"log"
+	"os"
+
+	"douglasthrift.net/presence/neighbors"
+)
+
+func main() {
+	ifs := map[string]bool{os.Args[1]: true}
+	hws := make(map[string]bool, len(os.Args[2:]))
+	for _, hw := range os.Args[2:] {
+		hws[hw] = true
+	}
+
+	ctx := context.Background()
+	a, err := neighbors.NewARP(1)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	ok, err := a.Present(ctx, ifs, hws)
+	if err != nil {
+		log.Fatal(err)
+	}
+	log.Println(ok)
+}

+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module douglasthrift.net/presence
+
+go 1.17

+ 11 - 0
neighbors/arp.go

@@ -0,0 +1,11 @@
+package neighbors
+
+import (
+	"context"
+)
+
+type (
+	ARP interface {
+		Present(ctx context.Context, ifs map[string]bool, addrs map[string]bool) (bool, error)
+	}
+)

+ 86 - 0
neighbors/arp_freebsd.go

@@ -0,0 +1,86 @@
+package neighbors
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net"
+	"os/exec"
+)
+
+const (
+	arpOutputVersion = "1"
+)
+
+type (
+	arp struct {
+		cmd    string
+		arping ARPing
+	}
+
+	arpOutput struct {
+		Version string `json:"__version"`
+		ARP     struct {
+			Cache []arpEntry `json:"arp-cache"`
+		} `json:"arp"`
+	}
+
+	arpEntry struct {
+		IPAddress  string `json:"ip-address"`
+		MACAddress string `json:"mac-address"`
+		Interface  string `json:"interface"`
+	}
+)
+
+func NewARP(count uint) (ARP, error) {
+	cmd, err := exec.LookPath("arp")
+	if err != nil {
+		return nil, err
+	}
+
+	arping, err := NewARPing(count)
+	if err != nil {
+		return nil, err
+	}
+
+	return &arp{cmd: cmd, arping: arping}, nil
+}
+
+func (a *arp) Present(ctx context.Context, ifs map[string]bool, hws map[string]bool) (ok bool, err error) {
+	cmd := exec.CommandContext(ctx, a.cmd, "--libxo=json", "-an")
+	b, err := cmd.Output()
+	if err != nil {
+		return
+	}
+
+	o := &arpOutput{}
+	err = json.Unmarshal(b, o)
+	if err != nil {
+		return
+	}
+
+	if o.Version != arpOutputVersion {
+		err = fmt.Errorf("arp output version mismatch (got %v, expected %v)", o.Version, arpOutputVersion)
+		return
+	}
+
+	for _, e := range o.ARP.Cache {
+		if ifs[e.Interface] {
+			var hwa net.HardwareAddr
+			hwa, err = net.ParseMAC(e.MACAddress)
+			if err != nil {
+				return
+			}
+			hw := hwa.String()
+
+			if hws[hw] {
+				ok, err = a.arping.Ping(ctx, e.Interface, hw, e.IPAddress)
+				if ok || err != nil {
+					return
+				}
+			}
+		}
+	}
+
+	return
+}

+ 47 - 0
neighbors/arping.go

@@ -0,0 +1,47 @@
+package neighbors
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os/exec"
+)
+
+type (
+	ARPing interface {
+		Ping(ctx context.Context, ifi, hw, ip string) (bool, error)
+	}
+
+	arping struct {
+		cmd, sudoCmd, count string
+	}
+)
+
+func NewARPing(count uint) (ARPing, error) {
+	cmd, err := exec.LookPath("arping")
+	if err != nil {
+		return nil, err
+	}
+
+	sudoCmd, err := exec.LookPath("sudo")
+	if err != nil {
+		return nil, err
+	}
+
+	return &arping{cmd: cmd, sudoCmd: sudoCmd, count: fmt.Sprint(count)}, nil
+}
+
+func (a *arping) Ping(ctx context.Context, ifi, hw, ip string) (ok bool, err error) {
+	cmd := exec.CommandContext(ctx, a.sudoCmd, a.cmd, "-c", a.count, "-i", ifi, "-t", hw, "-q", ip)
+	err = cmd.Run()
+	if err == nil {
+		ok = true
+	} else {
+		var exitError *exec.ExitError
+		if errors.As(err, &exitError) && len(exitError.Stderr) == 0 {
+			err = nil
+		}
+	}
+
+	return
+}