← Back to team overview

sts-sponsors team mailing list archive

Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master

 


Diff comments:

> diff --git a/src/maasagent/cmd/netmon/main.go b/src/maasagent/cmd/netmon/main.go
> index e83655f..5acd102 100644
> --- a/src/maasagent/cmd/netmon/main.go
> +++ b/src/maasagent/cmd/netmon/main.go
> @@ -1,9 +1,80 @@
>  package main
>  
> +/*
> +	Copyright 2023 Canonical Ltd.  This software is licensed under the
> +	GNU Affero General Public License version 3 (see the file LICENSE).
> +*/
> +
>  import (
> +	"context"
> +	"encoding/json"
> +	"errors"
> +	"os"
> +
> +	"github.com/rs/zerolog"
> +	"github.com/rs/zerolog/log"
> +	"golang.org/x/sync/errgroup"
> +
>  	"launchpad.net/maas/maas/src/maasagent/internal/netmon"
>  )
>  
> +var (
> +	ErrMissingIface = errors.New("Missing interface argument")
> +)
> +
> +func Run() int {
> +	zerolog.SetGlobalLevel(zerolog.InfoLevel)
> +	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
> +
> +	if envLogLevel, ok := os.LookupEnv("LOG_LEVEL"); ok {
> +		if logLevel, err := zerolog.ParseLevel(envLogLevel); err != nil {
> +			log.Warn().Str("LOG_LEVEL", envLogLevel).Msg("Unknown log level, defaulting to INFO")
> +		} else {
> +			zerolog.SetGlobalLevel(logLevel)
> +		}
> +	}
> +
> +	if len(os.Args) < 2 {
> +		log.Error().Err(ErrMissingIface).Msg("Please provide an interface to monitor")
> +		return 2
> +	}
> +	iface := os.Args[1]
> +
> +	ctx, cancel := context.WithCancel(context.Background())
> +
> +	sigC := make(chan os.Signal)
> +	resultC := make(chan netmon.Result)
> +
> +	g, ctx := errgroup.WithContext(ctx)
> +	g.SetLimit(2)
> +
> +	svc := netmon.NewService(iface)
> +	g.Go(func() error {
> +		return svc.Start(ctx, resultC)
> +	})
> +	g.Go(func() error {
> +		encoder := json.NewEncoder(os.Stdout)
> +		for {
> +			select {
> +			case <-sigC:
> +				cancel()
> +				return nil
> +			case res := <-resultC:

+1

> +				err := encoder.Encode(res)
> +				if err != nil {
> +					return err
> +				}
> +			}
> +		}
> +	})
> +	log.Info().Msg("Service netmon started")
> +	if err := g.Wait(); err != nil {
> +		log.Error().Err(err).Send()
> +		return 1
> +	}
> +	return 0
> +}
> +
>  func main() {
> -	netmon.NewService()
> +	os.Exit(Run())
>  }
> diff --git a/src/maasagent/internal/netmon/service.go b/src/maasagent/internal/netmon/service.go
> index 14ebdc6..5ea5b00 100644
> --- a/src/maasagent/internal/netmon/service.go
> +++ b/src/maasagent/internal/netmon/service.go
> @@ -1,3 +1,243 @@
>  package netmon
>  
> -func NewService() {}
> +/*
> +	Copyright 2023 Canonical Ltd.  This software is licensed under the
> +	GNU Affero General Public License version 3 (see the file LICENSE).
> +*/
> +
> +import (
> +	"bytes"
> +	"context"
> +	"errors"
> +	"fmt"
> +	"net"
> +	"net/netip"
> +	"time"
> +
> +	pcap "github.com/packetcap/go-pcap"
> +	"github.com/rs/zerolog/log"
> +
> +	"launchpad.net/maas/maas/src/maasagent/internal/arp"
> +)
> +
> +const (
> +	snapLen            int32         = 64
> +	timeout            time.Duration = -1
> +	seenAgainThreshold time.Duration = 600 * time.Second
> +)
> +
> +const (
> +	// EventNew is the Event value for a new Result
> +	EventNew = "NEW"
> +	// EventRefreshed is the Event value for a Result that is for
> +	// refreshed ARP values
> +	EventRefreshed = "REFRESHED"
> +	// EventMoved is the Event value for a Result where the IP has
> +	// changed its MAC address
> +	EventMoved = "MOVED"
> +)
> +
> +var (
> +	// ErrEmptyPacket is returned when a packet of 0 bytes has been received
> +	ErrEmptyPacket = errors.New("received an empty packet")
> +	// ErrPacketCaptureClosed is returned when the packet capture channel
> +	// has been closed unexpectedly
> +	ErrPacketCaptureClosed = errors.New("packet capture channel closed")
> +)
> +
> +// Binding represents the binding between an IP address and MAC address
> +type Binding struct {
> +	// IP is the IP a binding is tracking
> +	IP netip.Addr
> +	// MAC is the MAC address the IP is currently bound to
> +	MAC net.HardwareAddr
> +	// VID is the associated VLAN ID, if one exists
> +	VID *uint16
> +	// Time is the time the packet creating / updating the binding
> +	// was observed
> +	Time time.Time
> +}
> +
> +// Result is the result of observed ARP packets
> +type Result struct {
> +	// IP is the presentation format of an observed IP
> +	IP string `json:"ip"`
> +	// MAC is the presentation format of an observed MAC
> +	MAC string `json:"mac"`
> +	// Previous MAC is the presentation format of a previous MAC if
> +	// an EventMoved was observed
> +	PreviousMAC string `json:"previous_mac,omitempty"`
> +	// Event is the type of event the Result is
> +	Event string `json:"event"`
> +	// Time is the time the packet creating the Result was observed
> +	Time int64 `json:"time"`
> +	// VID is the VLAN ID if one exists
> +	VID *uint16 `json:"vid"`
> +}
> +
> +// Service is responsible for starting packet capture and
> +// converting observed ARP packets into discovered Results
> +type Service struct {
> +	iface    string
> +	bindings map[string]Binding
> +}
> +
> +// NewService returns a pointer to a Service. It
> +// takes the desired interface to observe's name as an argument
> +func NewService(iface string) *Service {
> +	return &Service{
> +		iface:    iface,
> +		bindings: make(map[string]Binding),
> +	}
> +}
> +
> +func (s *Service) updateBindings(pkt *arp.Packet, vid *uint16, timestamp time.Time) (res []Result) {
> +	if timestamp.IsZero() {
> +		timestamp = time.Now()
> +	}
> +
> +	var vidLabel int
> +	if vid != nil {
> +		vidLabel = int(*vid)
> +	}
> +
> +	discoveredBindings := []Binding{
> +		{
> +			IP:   pkt.SendIPAddr,
> +			MAC:  pkt.SendHwdAddr,
> +			VID:  vid,
> +			Time: timestamp,
> +		},
> +	}
> +	if pkt.OpCode == arp.OpReply {
> +		discoveredBindings = append(discoveredBindings, Binding{
> +			IP:   pkt.TgtIPAddr,
> +			MAC:  pkt.TgtHwdAddr,
> +			VID:  vid,
> +			Time: timestamp,
> +		})
> +	}
> +
> +	for _, discoveredBinding := range discoveredBindings {
> +		key := fmt.Sprintf("%d_%s", vidLabel, discoveredBinding.IP.String())
> +		binding, ok := s.bindings[key]
> +		if ok {
> +			if bytes.Compare(binding.MAC, discoveredBinding.MAC) != 0 {
> +				s.bindings[key] = discoveredBinding
> +				res = append(res, Result{
> +					IP:          discoveredBinding.IP.String(),
> +					PreviousMAC: binding.MAC.String(),
> +					MAC:         discoveredBinding.MAC.String(),
> +					VID:         discoveredBinding.VID,
> +					Time:        discoveredBinding.Time.Unix(),
> +					Event:       EventMoved,
> +				})
> +			} else if discoveredBinding.Time.Sub(binding.Time) >= seenAgainThreshold {
> +				s.bindings[key] = discoveredBinding
> +				res = append(res, Result{
> +					IP:    discoveredBinding.IP.String(),
> +					MAC:   discoveredBinding.MAC.String(),
> +					VID:   discoveredBinding.VID,
> +					Time:  discoveredBinding.Time.Unix(),
> +					Event: EventRefreshed,
> +				})
> +			}
> +		} else {
> +			s.bindings[key] = discoveredBinding
> +			res = append(res, Result{
> +				IP:    discoveredBinding.IP.String(),
> +				MAC:   discoveredBinding.MAC.String(),
> +				VID:   discoveredBinding.VID,
> +				Time:  discoveredBinding.Time.Unix(),
> +				Event: EventNew,
> +			})
> +		}
> +	}
> +
> +	return res
> +}
> +
> +func isValidARPPacket(pkt *arp.Packet) bool {
> +	if pkt.HardwareType != arp.HardwareTypeEthernet {
> +		return false
> +	}
> +	if pkt.ProtocolType != arp.ProtocolTypeIPv4 {
> +		return false
> +	}
> +	if pkt.HardwareAddrLen != 6 {
> +		return false
> +	}
> +	if pkt.ProtocolAddrLen != 4 {
> +		return false
> +	}
> +	return true
> +}
> +
> +func (s *Service) handlePacket(pkt pcap.Packet) ([]Result, error) {
> +	if pkt.Error != nil {
> +		return nil, pkt.Error
> +	}
> +	if len(pkt.B) == 0 {
> +		return nil, ErrEmptyPacket
> +	}
> +	eth := &arp.EthernetFrame{}
> +	err := eth.UnmarshalBinary(pkt.B)
> +	if err != nil {
> +		return nil, err
> +	}
> +
> +	if eth.EthernetType != arp.EthernetTypeVLAN && eth.EthernetType != arp.EthernetTypeARP {
> +		log.Debug().Msg("skipping non-ARP packet")

Ideally no, but currently yes. Ideally, in that a BPF filter should be set using `arp or (vlan and arp)`, prevent this from being hit unless there is some erroneous behavior, however, the pure Go packet capture lib we are using doesn't support compiling that to BPF instructions currently, but I'm looking into what it would take to contribute this upstream.

> +		return nil, nil
> +	}
> +
> +	var vid *uint16
> +	if eth.EthernetType == arp.EthernetTypeVLAN {
> +		vlan, err := eth.ExtractVLAN()
> +		if err != nil {
> +			return nil, err
> +		}
> +		vid = &vlan.ID
> +	}
> +
> +	arpPkt, err := eth.ExtractARPPacket()
> +	if err != nil {
> +		return nil, err
> +	}
> +
> +	if !isValidARPPacket(arpPkt) {
> +		log.Debug().Msg("skipping non-ethernet+IPv4 ARP packet")
> +		return nil, nil
> +	}
> +	return s.updateBindings(arpPkt, vid, pkt.Info.Timestamp), nil
> +}
> +
> +// Start will start packet capture and send results to a channel
> +func (s *Service) Start(ctx context.Context, resultC chan<- Result) error {
> +	defer close(resultC)
> +
> +	hndlr, err := pcap.OpenLive(s.iface, snapLen, false, timeout, true)
> +	if err != nil {
> +		return err
> +	}
> +	defer hndlr.Close()
> +	pkts := hndlr.Listen()
> +	for {
> +		select {
> +		case <-ctx.Done():
> +			return nil
> +		case pkt, ok := <-pkts:
> +			if !ok {
> +				log.Debug().Msg("packet capture has closed")
> +				return ErrPacketCaptureClosed
> +			}
> +			res, err := s.handlePacket(pkt)
> +			if err != nil {
> +				return err
> +			}
> +			for _, r := range res {
> +				resultC <- r
> +			}
> +		}
> +	}
> +}


-- 
https://code.launchpad.net/~cgrabowski/maas/+git/maas/+merge/441702
Your team MAAS Committers is subscribed to branch maas:master.



References