Browse Source

Progress on detection as a daemon

Douglas Thrift 1 year ago
parent
commit
3271c51b4d

+ 13 - 15
cmd/presence/check.go

@@ -4,6 +4,7 @@ import (
 	"goa.design/clue/log"
 
 	"douglasthrift.net/presence"
+	"douglasthrift.net/presence/neighbors"
 )
 
 type (
@@ -12,24 +13,21 @@ type (
 	}
 )
 
-func (c *Check) Run(cli *CLI) error {
+func (c *Check) Run(cli *CLI) (err error) {
 	ctx := cli.Context()
-	config, err := presence.ParseConfig(cli.Config)
+	if c.Values {
+		_, err = presence.ParseConfigWithContext(ctx, cli.Config, wNet)
+	} else {
+		_, err = presence.ParseConfig(cli.Config, wNet)
+	}
 	if err != nil {
-		log.Error(ctx, err, log.Fields{"config": cli.Config})
-		return err
+		log.Fatal(ctx, err, log.KV{K: "msg", V: "error parsing config"}, log.KV{K: "config", V: cli.Config})
 	}
 
-	if c.Values {
-		log.Info(ctx, log.Fields{"interval": config.Interval})
-
-		as := make([]string, len(config.MACAddresses))
-		for i, a := range config.MACAddresses {
-			as[i] = a.String()
-		}
-		log.Info(ctx, log.Fields{"mac_addresses": as})
-
-		log.Info(ctx, log.Fields{"ping_count": config.PingCount})
+	_, err = neighbors.NewARP(0)
+	if err != nil {
+		log.Fatal(ctx, err, log.KV{K: "msg", V: "error finding dependencies"})
 	}
-	return nil
+
+	return
 }

+ 65 - 15
cmd/presence/detect.go

@@ -1,39 +1,89 @@
 package main
 
 import (
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
 	"goa.design/clue/log"
 
+	"douglasthrift.net/presence"
 	"douglasthrift.net/presence/neighbors"
 )
 
 type (
 	Detect struct {
-		Interface     string   `arg:""`
-		HardwareAddrs []string `arg:""`
+		Iterations uint `help:"Only detect for N iterations." placeholder:"N" short:"i"`
 	}
 )
 
 func (d *Detect) Run(cli *CLI) error {
 	ctx := cli.Context()
-
-	ifs := neighbors.Interfaces{d.Interface: true}
-	hws := make(neighbors.HardwareAddrStates, len(d.HardwareAddrs))
-	for _, hw := range d.HardwareAddrs {
-		hws[hw] = neighbors.NewState()
+	config, err := presence.ParseConfigWithContext(ctx, cli.Config, wNet)
+	if err != nil {
+		log.Fatal(ctx, err, log.KV{K: "msg", V: "error parsing config"}, log.KV{K: "config", V: cli.Config})
 	}
 
-	a, err := neighbors.NewARP(1)
+	arp, err := neighbors.NewARP(config.PingCount)
 	if err != nil {
-		return err
+		log.Fatal(ctx, err, log.KV{K: "msg", V: "error finding dependencies"})
 	}
 
-	ok, err := a.Present(ctx, ifs, hws)
+	var (
+		detector = presence.NewDetector(config, arp)
+		ticker   = time.NewTicker(config.Interval)
+		stop     = make(chan os.Signal, 1)
+		reload   = make(chan os.Signal, 1)
+		i        uint
+	)
+
+	err = detector.Detect(ctx)
 	if err != nil {
-		return err
+		log.Error(ctx, err, log.KV{K: "msg", V: "error detecting presence"})
 	}
-	log.Info(ctx, log.KV{K: "present", V: ok})
-	for hw, state := range hws {
-		log.Info(ctx, log.KV{K: "hw", V: hw}, log.KV{K: "present", V: state.Present()}, log.KV{K: "changed", V: state.Changed()})
+
+	if d.Iterations != 0 {
+		i++
+		if i >= d.Iterations {
+			ticker.Stop()
+			return nil
+		}
+	}
+
+	signal.Ignore(syscall.SIGHUP)
+	signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
+	signal.Notify(reload, syscall.SIGUSR1)
+
+	for {
+		select {
+		case <-ticker.C:
+			err = detector.Detect(ctx)
+			if err != nil {
+				log.Error(ctx, err, log.KV{K: "msg", V: "error detecting presence"})
+			}
+
+			if d.Iterations != 0 {
+				i++
+				if i >= d.Iterations {
+					ticker.Stop()
+					return nil
+				}
+			}
+		case s := <-stop:
+			log.Print(ctx, log.Fields{"msg": "received stop signal"}, log.Fields{"signal": s})
+			ticker.Stop()
+			return nil
+		case s := <-reload:
+			log.Print(ctx, log.Fields{"msg": "received reload signal"}, log.Fields{"signal": s})
+			config, err = presence.ParseConfigWithContext(ctx, cli.Config, wNet)
+			if err != nil {
+				log.Error(ctx, err, log.KV{K: "msg", V: "error parsing config"}, log.KV{K: "config", V: cli.Config})
+			} else {
+				arp.Count(config.PingCount)
+				detector.Config(config)
+				ticker.Reset(config.Interval)
+			}
+		}
 	}
-	return nil
 }

+ 4 - 1
cmd/presence/main.go

@@ -8,6 +8,8 @@ import (
 
 	"github.com/alecthomas/kong"
 	"goa.design/clue/log"
+
+	"douglasthrift.net/presence/wrap"
 )
 
 type (
@@ -26,6 +28,7 @@ var (
 	commit       = "none"
 	date         = "unknown"
 	configPrefix = ""
+	wNet         = wrap.NewNet()
 )
 
 func main() {
@@ -47,7 +50,7 @@ func (cli *CLI) Context() (ctx context.Context) {
 	if cli.Debug {
 		ctx = log.Context(ctx, log.WithDebug())
 	} else {
-		ctx = log.Context(ctx, log.WithDisableBuffering(func(context.Context) bool { return true }))
+		ctx = log.Context(ctx)
 	}
 	return
 }

+ 55 - 18
config.go

@@ -1,27 +1,32 @@
 package presence
 
 import (
+	"context"
 	"fmt"
 	"net"
 	"os"
 	"time"
 
+	"goa.design/clue/log"
 	"gopkg.in/yaml.v3"
+
+	"douglasthrift.net/presence/wrap"
 )
 
 type (
-	MACAddress struct {
-		net.HardwareAddr
-	}
-
 	Config struct {
 		Interval     time.Duration `yaml:"interval"`
-		MACAddresses []MACAddress  `yaml:"mac_addresses"`
+		Interfaces   []string      `yaml:"interfaces"`
+		MACAddresses []string      `yaml:"mac_addresses"`
 		PingCount    uint          `yaml:"ping_count"`
 	}
 )
 
-func ParseConfig(name string) (*Config, error) {
+func ParseConfig(name string, wNet wrap.Net) (*Config, error) {
+	return ParseConfigWithContext(context.Background(), name, wNet)
+}
+
+func ParseConfigWithContext(ctx context.Context, name string, wNet wrap.Net) (*Config, error) {
 	f, err := os.Open(name)
 	if err != nil {
 		return nil, err
@@ -39,23 +44,55 @@ func ParseConfig(name string) (*Config, error) {
 
 	if c.Interval < 0 {
 		return nil, fmt.Errorf("negative interval (%v)", c.Interval)
-	}
-	if c.Interval == 0 {
+	} else if c.Interval == 0 {
 		c.Interval = 30 * time.Second
 	}
+
+	if len(c.Interfaces) == 0 {
+		ifs, err := wNet.Interfaces()
+		if err != nil {
+			return nil, err
+		}
+
+		c.Interfaces = make([]string, 0, len(ifs))
+		for _, i := range ifs {
+			c.Interfaces = append(c.Interfaces, i.Name)
+		}
+	} else {
+		for _, i := range c.Interfaces {
+			_, err = wNet.InterfaceByName(i)
+			if err != nil {
+				return nil, fmt.Errorf("interface %v: %w", i, err)
+			}
+		}
+	}
+
+	if len(c.MACAddresses) == 0 {
+		return nil, fmt.Errorf("no MAC addresses")
+	}
+	as := make(map[string]bool, len(c.MACAddresses))
+	for i, a := range c.MACAddresses {
+		hw, err := net.ParseMAC(a)
+		if err != nil {
+			return nil, err
+		}
+
+		a = hw.String()
+		if as[a] {
+			return nil, fmt.Errorf("duplicate MAC address (%v)", a)
+		}
+		as[a] = true
+		c.MACAddresses[i] = a
+	}
+
 	if c.PingCount == 0 {
 		c.PingCount = 1
 	}
-	return c, nil
-}
 
-func (a *MACAddress) UnmarshalYAML(value *yaml.Node) (err error) {
-	var s string
-	err = value.Decode(&s)
-	if err != nil {
-		return
-	}
+	log.Print(ctx, log.KV{K: "msg", V: "interval"}, log.KV{K: "value", V: c.Interval})
+	log.Print(ctx, log.KV{K: "msg", V: "interfaces"}, log.KV{K: "value", V: c.Interfaces})
+	log.Print(ctx, log.KV{K: "msg", V: "MAC addresses"}, log.KV{K: "value", V: c.MACAddresses})
+	log.Print(ctx, log.KV{K: "msg", V: "ping count"}, log.KV{K: "value", V: c.PingCount})
 
-	a.HardwareAddr, err = net.ParseMAC(s)
-	return
+	return c, nil
 }

+ 61 - 14
config_test.go

@@ -1,42 +1,49 @@
 package presence
 
 import (
+	"fmt"
 	"net"
 	"path/filepath"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/assert"
+
+	mockwrap "douglasthrift.net/presence/wrap/mocks"
 )
 
 func TestParseConfig(t *testing.T) {
 	cases := []struct {
 		name, file string
+		setup      func(wNet *mockwrap.Net)
 		config     *Config
 		err        string
 	}{
 		{
 			name: "success",
 			file: "success.yml",
+			setup: func(wNet *mockwrap.Net) {
+				wNet.Mock.On("InterfaceByName", "eth0").Return(&net.Interface{}, nil)
+				wNet.Mock.On("InterfaceByName", "eth1").Return(&net.Interface{}, nil)
+			},
 			config: &Config{
-				Interval: 1 * time.Minute,
-				MACAddresses: []MACAddress{
-					{net.HardwareAddr{0, 0, 0, 0, 0, 0xa}},
-					{net.HardwareAddr{0, 0, 0, 0, 0, 0xb}},
-				},
-				PingCount: 5,
+				Interval:     1 * time.Minute,
+				Interfaces:   []string{"eth0", "eth1"},
+				MACAddresses: []string{"00:00:00:00:00:0a", "00:00:00:00:00:0b"},
+				PingCount:    5,
 			},
 		},
 		{
 			name: "defaults",
 			file: "defaults.yml",
+			setup: func(wNet *mockwrap.Net) {
+				wNet.Mock.On("Interfaces").Return([]net.Interface{{Name: "eth0"}, {Name: "eth1"}, {Name: "lo"}}, nil)
+			},
 			config: &Config{
-				Interval: 30 * time.Second,
-				MACAddresses: []MACAddress{
-					{net.HardwareAddr{0, 0, 0, 0, 0, 1}},
-					{net.HardwareAddr{0, 0, 0, 0, 0, 2}},
-				},
-				PingCount: 1,
+				Interval:     30 * time.Second,
+				Interfaces:   []string{"eth0", "eth1", "lo"},
+				MACAddresses: []string{"00:00:00:00:00:01", "00:00:00:00:00:02"},
+				PingCount:    1,
 			},
 		},
 		{
@@ -52,13 +59,48 @@ func TestParseConfig(t *testing.T) {
 		{
 			name: "bad MAC encoding",
 			file: "bad_mac_encoding.yml",
-			err:  "address 00-00-00-00-00-0x: invalid MAC address",
+			setup: func(wNet *mockwrap.Net) {
+				wNet.Mock.On("Interfaces").Return([]net.Interface{{Name: "eth0"}, {Name: "eth1"}, {Name: "lo"}}, nil)
+			},
+			err: "address 00-00-00-00-00-0x: invalid MAC address",
 		},
 		{
 			name: "negative interval",
 			file: "negative_interval.yml",
 			err:  "negative interval (-1ns)",
 		},
+		{
+			name: "error listing interfaces",
+			file: "defaults.yml",
+			setup: func(wNet *mockwrap.Net) {
+				wNet.Mock.On("Interfaces").Return(nil, fmt.Errorf("no network interfaces"))
+			},
+			err: "no network interfaces",
+		},
+		{
+			name: "error getting interface by name",
+			file: "success.yml",
+			setup: func(wNet *mockwrap.Net) {
+				wNet.Mock.On("InterfaceByName", "eth0").Return(nil, fmt.Errorf("no such network interface"))
+			},
+			err: "interface eth0: no such network interface",
+		},
+		{
+			name: "no MAC addresses",
+			file: "no_mac_addresses.yml",
+			setup: func(wNet *mockwrap.Net) {
+				wNet.Mock.On("InterfaceByName", "eth0").Return(&net.Interface{}, nil)
+			},
+			err: "no MAC addresses",
+		},
+		{
+			name: "duplicate MAC address",
+			file: "duplicate_mac_address.yml",
+			setup: func(wNet *mockwrap.Net) {
+				wNet.Mock.On("InterfaceByName", "eth0").Return(&net.Interface{}, nil)
+			},
+			err: "duplicate MAC address (00:00:00:00:00:0e)",
+		},
 	}
 
 	for _, tc := range cases {
@@ -69,7 +111,12 @@ func TestParseConfig(t *testing.T) {
 
 			assert := assert.New(t)
 
-			c, err := ParseConfig(filepath.Join("tests", tc.file))
+			wNet := mockwrap.NewNet(t)
+			if tc.setup != nil {
+				tc.setup(wNet)
+			}
+
+			c, err := ParseConfig(filepath.Join("tests", tc.file), wNet)
 			if tc.err != "" {
 				assert.ErrorContains(err, tc.err)
 			} else {

+ 79 - 0
detector.go

@@ -0,0 +1,79 @@
+package presence
+
+import (
+	"context"
+
+	"goa.design/clue/log"
+
+	"douglasthrift.net/presence/neighbors"
+)
+
+type (
+	Detector interface {
+		Detect(ctx context.Context) error
+		Config(config *Config)
+	}
+
+	detector struct {
+		config     *Config
+		arp        neighbors.ARP
+		interfaces neighbors.Interfaces
+		state      neighbors.State
+		states     neighbors.HardwareAddrStates
+	}
+)
+
+func NewDetector(config *Config, arp neighbors.ARP) Detector {
+	d := &detector{
+		arp:    arp,
+		state:  neighbors.NewState(),
+		states: make(neighbors.HardwareAddrStates, len(config.MACAddresses)),
+	}
+	d.Config(config)
+	return d
+}
+
+func (d *detector) Detect(ctx context.Context) error {
+	log.Print(ctx, log.KV{K: "msg", V: "detecting presence"}, log.KV{K: "present", V: d.state.Present()})
+	err := d.arp.Present(ctx, d.interfaces, d.state, d.states)
+	if err != nil {
+		return err
+	}
+
+	for _, a := range d.config.MACAddresses {
+		state := d.states[a]
+		log.Print(ctx, log.KV{K: "msg", V: a}, log.KV{K: "present", V: state.Present()}, log.KV{K: "changed", V: state.Changed()})
+	}
+
+	log.Print(ctx, log.KV{K: "msg", V: "detected presence"}, log.KV{K: "present", V: d.state.Present()}, log.KV{K: "changed", V: d.state.Changed()})
+	if d.state.Changed() {
+		// TODO IFTTT
+	}
+
+	return nil
+}
+
+func (d *detector) Config(config *Config) {
+	d.config = config
+	d.interfaces = make(neighbors.Interfaces, len(config.Interfaces))
+	for _, i := range config.Interfaces {
+		d.interfaces[i] = true
+	}
+
+	states := make(map[string]bool, len(d.states))
+	for a := range d.states {
+		states[a] = true
+	}
+	for _, a := range config.MACAddresses {
+		if states[a] {
+			states[a] = false
+		} else {
+			d.states[a] = neighbors.NewState()
+		}
+	}
+	for a, ok := range states {
+		if ok {
+			delete(d.states, a)
+		}
+	}
+}

+ 1 - 0
go.mod

@@ -17,6 +17,7 @@ require (
 	github.com/kr/text v0.2.0 // indirect
 	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/stretchr/objx v0.4.0 // indirect
 	go.opentelemetry.io/otel v1.7.0 // indirect
 	go.opentelemetry.io/otel/trace v1.7.0 // indirect
 	goa.design/goa/v3 v3.7.5 // indirect

+ 1 - 0
go.sum

@@ -337,6 +337,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

+ 2 - 1
neighbors/arp.go

@@ -9,6 +9,7 @@ type (
 	HardwareAddrStates map[string]State
 
 	ARP interface {
-		Present(ctx context.Context, ifs Interfaces, addrs HardwareAddrStates) (bool, error)
+		Present(ctx context.Context, ifs Interfaces, state State, addrStates HardwareAddrStates) error
+		Count(count uint)
 	}
 )

+ 15 - 5
neighbors/arp_freebsd.go

@@ -45,12 +45,15 @@ func NewARP(count uint) (ARP, error) {
 		return nil, err
 	}
 
-	return &arp{cmd: cmd, arping: arping}, nil
+	return &arp{
+		cmd:    cmd,
+		arping: arping,
+	}, nil
 }
 
-func (a *arp) Present(ctx context.Context, ifs Interfaces, hws HardwareAddrStates) (present bool, err error) {
-	as := make(map[string]bool, len(hws))
-	for hw := range hws {
+func (a *arp) Present(ctx context.Context, ifs Interfaces, state State, addrStates HardwareAddrStates) (err error) {
+	as := make(map[string]bool, len(addrStates))
+	for hw := range addrStates {
 		as[hw] = false
 	}
 
@@ -73,6 +76,7 @@ func (a *arp) Present(ctx context.Context, ifs Interfaces, hws HardwareAddrState
 	}
 
 	for _, e := range o.ARP.Cache {
+		log.Debug(ctx, log.KV{K: "IP address", V: e.IPAddress}, log.KV{K: "MAC address", V: e.MACAddress}, log.KV{K: "interface", V: e.Interface})
 		if ifs[e.Interface] {
 			var hwa net.HardwareAddr
 			hwa, err = net.ParseMAC(e.MACAddress)
@@ -91,12 +95,18 @@ func (a *arp) Present(ctx context.Context, ifs Interfaces, hws HardwareAddrState
 		}
 	}
 
+	present := false
 	for hw, ok := range as {
-		hws[hw].Set(ok)
+		addrStates[hw].Set(ok)
 		if ok {
 			present = true
 		}
 	}
+	state.Set(present)
 
 	return
 }
+
+func (a *arp) Count(count uint) {
+	a.arping.Count(count)
+}

+ 15 - 5
neighbors/arp_linux.go

@@ -33,12 +33,15 @@ func NewARP(count uint) (ARP, error) {
 		return nil, err
 	}
 
-	return &arp{cmd: cmd, arping: arping}, nil
+	return &arp{
+		cmd:    cmd,
+		arping: arping,
+	}, nil
 }
 
-func (a *arp) Present(ctx context.Context, ifs Interfaces, hws HardwareAddrStates) (present bool, err error) {
-	as := make(map[string]bool, len(hws))
-	for hw := range hws {
+func (a *arp) Present(ctx context.Context, ifs Interfaces, state State, addrStates HardwareAddrStates) (err error) {
+	as := make(map[string]bool, len(addrStates))
+	for hw := range addrStates {
 		as[hw] = false
 	}
 
@@ -53,6 +56,7 @@ func (a *arp) Present(ctx context.Context, ifs Interfaces, hws HardwareAddrState
 	err = json.Unmarshal(b, &es)
 
 	for _, e := range es {
+		log.Debug(ctx, log.KV{K: "IP address", V: e.IPAddress}, log.KV{K: "MAC address", V: e.MACAddress}, log.KV{K: "interface", V: e.Interface})
 		if ifs[e.Interface] {
 			var hwa net.HardwareAddr
 			hwa, err = net.ParseMAC(e.MACAddress)
@@ -71,12 +75,18 @@ func (a *arp) Present(ctx context.Context, ifs Interfaces, hws HardwareAddrState
 		}
 	}
 
+	present := false
 	for hw, ok := range as {
-		hws[hw].Set(ok)
+		addrStates[hw].Set(ok)
 		if ok {
 			present = true
 		}
 	}
+	state.Set(present)
 
 	return
 }
+
+func (a *arp) Count(count uint) {
+	a.arping.Count(count)
+}

+ 13 - 1
neighbors/arping.go

@@ -15,6 +15,7 @@ import (
 type (
 	ARPing interface {
 		Ping(ctx context.Context, ifi, hw, ip string) (bool, error)
+		Count(count uint)
 	}
 
 	arping struct {
@@ -58,7 +59,11 @@ func NewARPing(count uint) (ARPing, error) {
 		return nil, err
 	}
 
-	return &arping{arpingCmd: arpingCmd, sudoCmd: sudoCmd, count: fmt.Sprint(count)}, nil
+	return &arping{
+		arpingCmd: arpingCmd,
+		sudoCmd:   sudoCmd,
+		count:     fmt.Sprint(count),
+	}, nil
 }
 
 func (a *arping) Ping(ctx context.Context, ifi, hw, ip string) (ok bool, err error) {
@@ -71,8 +76,15 @@ func (a *arping) Ping(ctx context.Context, ifi, hw, ip string) (ok bool, err err
 		var exitError *exec.ExitError
 		if errors.As(err, &exitError) && len(exitError.Stderr) == 0 {
 			err = nil
+		} else {
+			return
 		}
 	}
+	log.Debug(ctx, log.KV{K: "cmd", V: cmd}, log.KV{K: "ok", V: ok})
 
 	return
 }
+
+func (a *arping) Count(count uint) {
+	a.count = fmt.Sprint(count)
+}

+ 1 - 1
tests/defaults.yml

@@ -1,3 +1,3 @@
 mac_addresses:
   - 00:00:00:00:00:01
-  - 00:00:00:00:00:02
+  - 00-00-00-00-00-02

+ 5 - 0
tests/duplicate_mac_address.yml

@@ -0,0 +1,5 @@
+interfaces: [eth0]
+mac_addresses:
+  - 00:00:00:00:00:0e
+  - 00:00:00:00:00:0f
+  - 00-00-00-00-00-0e

+ 1 - 0
tests/no_mac_addresses.yml

@@ -0,0 +1 @@
+interfaces: [eth0]

+ 2 - 1
tests/success.yml

@@ -1,5 +1,6 @@
 interval: 1m
+interfaces: [eth0, eth1]
 mac_addresses:
   - 00:00:00:00:00:0a
-  - 00:00:00:00:00:0b
+  - 00-00-00-00-00-0b
 ping_count: 5

+ 75 - 0
wrap/mocks/net.go

@@ -0,0 +1,75 @@
+// Code generated by mockery v2.13.1. DO NOT EDIT.
+
+package mocks
+
+import (
+	net "net"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Net is an autogenerated mock type for the Net type
+type Net struct {
+	mock.Mock
+}
+
+// InterfaceByName provides a mock function with given fields: name
+func (_m *Net) InterfaceByName(name string) (*net.Interface, error) {
+	ret := _m.Called(name)
+
+	var r0 *net.Interface
+	if rf, ok := ret.Get(0).(func(string) *net.Interface); ok {
+		r0 = rf(name)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*net.Interface)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(string) error); ok {
+		r1 = rf(name)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Interfaces provides a mock function with given fields:
+func (_m *Net) Interfaces() ([]net.Interface, error) {
+	ret := _m.Called()
+
+	var r0 []net.Interface
+	if rf, ok := ret.Get(0).(func() []net.Interface); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]net.Interface)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func() error); ok {
+		r1 = rf()
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+type mockConstructorTestingTNewNet interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewNet creates a new instance of Net. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewNet(t mockConstructorTestingTNewNet) *Net {
+	mock := &Net{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 26 - 0
wrap/net.go

@@ -0,0 +1,26 @@
+package wrap
+
+import (
+	"net"
+)
+
+type (
+	Net interface {
+		InterfaceByName(name string) (*net.Interface, error)
+		Interfaces() ([]net.Interface, error)
+	}
+
+	netImpl struct{}
+)
+
+func NewNet() Net {
+	return &netImpl{}
+}
+
+func (_ *netImpl) InterfaceByName(name string) (*net.Interface, error) {
+	return net.InterfaceByName(name)
+}
+
+func (_ *netImpl) Interfaces() ([]net.Interface, error) {
+	return net.Interfaces()
+}