sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #07547
[Merge] ~cgrabowski/maas:go_network_discovery into maas:master
Christian Grabowski has proposed merging ~cgrabowski/maas:go_network_discovery into maas:master.
Commit message:
print results as json
add netmon service
add package for parsing ARP packets
Requested reviews:
Mark Laing (markylaing)
Thomas Parrott (tomparrott)
MAAS Maintainers (maas-maintainers)
For more details, see:
https://code.launchpad.net/~cgrabowski/maas/+git/maas/+merge/441702
--
Your team MAAS Committers is subscribed to branch maas:master.
diff --git a/src/maasagent/cmd/netmon/main.go b/src/maasagent/cmd/netmon/main.go
index e83655f..09306fa 100644
--- a/src/maasagent/cmd/netmon/main.go
+++ b/src/maasagent/cmd/netmon/main.go
@@ -1,9 +1,93 @@
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"
+ "strconv"
+
+ "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 {
+ var (
+ debug bool
+ err error
+ )
+
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+
+ debugStr, ok := os.LookupEnv("DEBUG")
+ if ok {
+ debug, err = strconv.ParseBool(debugStr)
+ if err != nil {
+ log.Error().Err(err).Msg("Unable to parse debug flag")
+ return 2
+ }
+ }
+
+ if debug {
+ zerolog.SetGlobalLevel(zerolog.DebugLevel)
+ } else {
+ zerolog.SetGlobalLevel(zerolog.InfoLevel)
+ }
+
+ if len(os.Args) < 2 {
+ log.Error().Err(ErrMissingIface).Msg("Please provide an interface to monitor")
+ return 2
+ }
+ iface := os.Args[1]
+
+ bkg := context.Background()
+ ctx, cancel := context.WithCancel(bkg)
+
+ 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:
+ 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).Msg("")
+ return 1
+ }
+ return 0
+}
+
func main() {
- netmon.NewService()
+ os.Exit(Run())
}
diff --git a/src/maasagent/go.mod b/src/maasagent/go.mod
index 9bee24c..6adeafc 100644
--- a/src/maasagent/go.mod
+++ b/src/maasagent/go.mod
@@ -1,3 +1,30 @@
module launchpad.net/maas/maas/src/maasagent
go 1.18
+
+require (
+ github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875
+ github.com/packetcap/go-pcap v0.0.0-20230225181818-eba71accde5b
+ github.com/rs/zerolog v1.29.1
+ github.com/stretchr/testify v1.7.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/google/go-cmp v0.5.8 // indirect
+ github.com/google/gopacket v1.1.17 // indirect
+ github.com/josharian/native v1.0.0 // indirect
+ github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
+ github.com/mattn/go-colorable v0.1.12 // indirect
+ github.com/mattn/go-isatty v0.0.14 // indirect
+ github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 // indirect
+ github.com/mdlayher/packet v1.0.0 // indirect
+ github.com/mdlayher/socket v0.2.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/sirupsen/logrus v1.4.2 // indirect
+ golang.org/x/net v0.7.0 // indirect
+ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
+ golang.org/x/sys v0.5.0 // indirect
+ gopkg.in/yaml.v2 v2.2.8 // indirect
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
+)
diff --git a/src/maasagent/go.sum b/src/maasagent/go.sum
index e69de29..045d92e 100644
--- a/src/maasagent/go.sum
+++ b/src/maasagent/go.sum
@@ -0,0 +1,210 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY=
+github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
+github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875 h1:ql8x//rJsHMjS+qqEag8n3i4azw1QneKh5PieH9UEbY=
+github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875/go.mod h1:kfOoFJuHWp76v1RgZCb9/gVUc7XdY877S2uVYbNliGc=
+github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 h1:2oDp6OOhLxQ9JBoUuysVz9UZ9uI6oLUbvAZu0x8o+vE=
+github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118/go.mod h1:ZFUnHIVchZ9lJoWoEGUg8Q3M4U8aNNWA3CVSUTkW4og=
+github.com/mdlayher/packet v1.0.0 h1:InhZJbdShQYt6XV2GPj5XHxChzOfhJJOMbvnGAmOfQ8=
+github.com/mdlayher/packet v1.0.0/go.mod h1:eE7/ctqDhoiRhQ44ko5JZU2zxB88g+JH/6jmnjzPjOU=
+github.com/mdlayher/socket v0.2.1 h1:F2aaOwb53VsBE+ebRS9bLd7yPOfYUMC8lOODdCBDY6w=
+github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/packetcap/go-pcap v0.0.0-20230225181818-eba71accde5b h1:M2uMUEhJoonOZxXMFb9xs5Bq573t7xt6b2waNsluzCU=
+github.com/packetcap/go-pcap v0.0.0-20230225181818-eba71accde5b/go.mod h1:IwL7NJSMD5mvRco6A6uxPca1Zv0OJp0tY5Gf++9LBYQ=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
+github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/src/maasagent/internal/arp/ethernet.go b/src/maasagent/internal/arp/ethernet.go
new file mode 100644
index 0000000..0d3e7d3
--- /dev/null
+++ b/src/maasagent/internal/arp/ethernet.go
@@ -0,0 +1,133 @@
+package arp
+
+/*
+ Copyright 2023 Canonical Ltd. This software is licensed under the
+ GNU Affero General Public License version 3 (see the file LICENSE).
+*/
+
+import (
+ "encoding/binary"
+ "errors"
+ "io"
+ "net"
+)
+
+const (
+ minEthernetLen = 14
+)
+
+const (
+ // EthernetTypeLLC is a special ethernet type, if found the frame is truncated
+ EthernetTypeLLC uint16 = 0
+ // EthernetTypeIPv4 is the ethernet type for a frame containing an IPv4 packet
+ EthernetTypeIPv4 uint16 = 0x0800
+ // EthernetTypeARP is the ethernet type for a frame containing an ARP packet
+ EthernetTypeARP uint16 = 0x0806
+ // EthernetTypeIPv6 is the ethernet type for a frame containing an IPv6 packet
+ EthernetTypeIPv6 uint16 = 0x86dd
+ // EthernetTypeVLAN is the ethernet type for a frame containing a VLAN tag,
+ // the VLAN tag bytes will indicate the actual type of packet the frame contains
+ EthernetTypeVLAN uint16 = 0x8100
+
+ // NonStdLenEthernetTypes is a magic number to find any non-standard types
+ // and mark them as EthernetTypeLLC
+ NonStdLenEthernetTypes uint16 = 0x600
+)
+
+var (
+ // ErrNotVLAN is an error returned when calling EthernetFrame.ExtractVLAN
+ // if the frame is not of type EthernetTypeVLAN
+ ErrNotVLAN = errors.New("ethernet frame not of type VLAN")
+ // ErrMalformedVLAN is an error returned when parsing a VLAN tag
+ // that is malformed
+ ErrMalformedVLAN = errors.New("VLAN tag is malformed")
+)
+
+// VLAN represents a VLAN tag within an ethernet frame
+type VLAN struct {
+ Priority uint8
+ DropEligible bool
+ ID uint16
+ EthernetType uint16
+}
+
+// UnmarshalBinary will take the ethernet frame's payload
+// and extract a VLAN tag if one is present
+func (v *VLAN) UnmarshalBinary(buf []byte) error {
+ if len(buf) < 4 {
+ return ErrMalformedVLAN
+ }
+
+ // extract the first 3 bits
+ v.Priority = (buf[0] & 0xe0) >> 5
+ // extract the next bit and turn it into a bool
+ v.DropEligible = buf[0]&0x10 != 0
+ // extract the next 12 bits for an ID
+ v.ID = binary.BigEndian.Uint16(buf[:2]) & 0x0fff
+ // last 2 bytes are ethernet type
+ v.EthernetType = binary.BigEndian.Uint16(buf[2:])
+ return nil
+}
+
+// EthernetFrame represents an ethernet frame
+type EthernetFrame struct {
+ SrcMAC net.HardwareAddr
+ DstMAC net.HardwareAddr
+ EthernetType uint16
+ Len uint16
+ Payload []byte
+}
+
+// ExtractARPPacket will extract an ARP packet from the ethernet frame's
+// payload
+func (e *EthernetFrame) ExtractARPPacket() (*Packet, error) {
+ var buf []byte
+ if e.EthernetType == EthernetTypeVLAN {
+ buf = e.Payload[4:]
+ } else {
+ buf = e.Payload
+ }
+ a := &Packet{}
+ err := a.UnmarshalBinary(buf)
+ if err != nil {
+ return nil, err
+ }
+ return a, nil
+}
+
+// ExtractVLAN will extract the VLAN tag from the ethernet frame's
+// payload if one is present and return ErrNotVLAN if not
+func (e *EthernetFrame) ExtractVLAN() (*VLAN, error) {
+ if e.EthernetType != EthernetTypeVLAN {
+ return nil, ErrNotVLAN
+ }
+ v := &VLAN{}
+ err := v.UnmarshalBinary(e.Payload[0:4])
+ if err != nil {
+ return nil, err
+ }
+ return v, nil
+}
+
+// UnmarshalBinary parses ethernet frame bytes into an EthernetFrame
+func (eth *EthernetFrame) UnmarshalBinary(buf []byte) error {
+ if len(buf) < minEthernetLen {
+ return io.ErrUnexpectedEOF
+ }
+
+ eth.DstMAC = buf[0:6]
+ eth.SrcMAC = buf[6:12]
+ eth.EthernetType = binary.BigEndian.Uint16(buf[12:14])
+ eth.Payload = buf[14:]
+ if eth.EthernetType < NonStdLenEthernetTypes {
+ eth.Len = eth.EthernetType
+ eth.EthernetType = EthernetTypeLLC
+ cmp := len(eth.Payload) - int(eth.Len)
+ if cmp < 0 {
+ return io.ErrUnexpectedEOF
+ } else if cmp > 0 {
+ eth.Payload = eth.Payload[:len(eth.Payload)-cmp]
+ }
+ }
+ return nil
+}
diff --git a/src/maasagent/internal/arp/ethernet_test.go b/src/maasagent/internal/arp/ethernet_test.go
new file mode 100644
index 0000000..fc6bed5
--- /dev/null
+++ b/src/maasagent/internal/arp/ethernet_test.go
@@ -0,0 +1,234 @@
+package arp
+
+/*
+ Copyright 2023 Canonical Ltd. This software is licensed under the
+ GNU Affero General Public License version 3 (see the file LICENSE).
+*/
+
+import (
+ "fmt"
+ "io"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestVLANUnmarshal(t *testing.T) {
+ table := []unmarshalVLANCase{
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "ValidVLANTag",
+ In: []byte{0x00, 0x02, 0x08, 0x06},
+ },
+ Out: &VLAN{
+ Priority: 0,
+ DropEligible: false,
+ ID: 2,
+ EthernetType: EthernetTypeARP,
+ },
+ },
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "InvalidVLANTag",
+ In: []byte{0x00, 0x02},
+ Err: ErrMalformedVLAN,
+ },
+ },
+ }
+
+ for _, tcase := range table {
+ t.Run(tcase.Name, func(tt *testing.T) {
+ res := &VLAN{}
+ err := res.UnmarshalBinary(tcase.In)
+ assert.ErrorIsf(tt, err, tcase.Err, "expected UnmarshalVLAN to return an error of %s", tcase.Err)
+ if tcase.Out != nil {
+ compareVLANs(tt, tcase.Out, res)
+ }
+ })
+ }
+}
+
+func TestEthernetUnmarshal(t *testing.T) {
+ table := []unmarshalEthernetCase{
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "ValidEthernetFrameWithoutVLAN",
+ In: []byte{
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0x08, 0x06, 0x00, 0x01,
+ 0x08, 0x00, 0x06, 0x04, 0x00, 0x02, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0xc0, 0xa8, 0x01, 0x6c,
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0xc0, 0xa8, 0x01, 0x50,
+ },
+ },
+ Out: &EthernetFrame{
+ SrcMAC: parseMACNoError("80:61:5f:08:fc:16"),
+ DstMAC: parseMACNoError("24:4b:fe:e1:ea:26"),
+ EthernetType: EthernetTypeARP,
+ Payload: []byte{
+ 0x00, 0x01,
+ 0x08, 0x00, 0x06, 0x04, 0x00, 0x02, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0xc0, 0xa8, 0x01, 0x6c,
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0xc0, 0xa8, 0x01, 0x50,
+ },
+ },
+ },
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "ValidEthernetFrameWithVLAN",
+ In: []byte{
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25, 0x81, 0x00, 0x00, 0x02,
+ 0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25,
+ 0xc0, 0xa8, 0x0a, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x0a, 0x19, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ },
+ },
+ Out: &EthernetFrame{
+ SrcMAC: parseMACNoError("84:39:c0:0b:22:25"),
+ DstMAC: parseMACNoError("ff:ff:ff:ff:ff:ff"),
+ EthernetType: EthernetTypeVLAN,
+ Payload: []byte{
+ 0x00, 0x02,
+ 0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25,
+ 0xc0, 0xa8, 0x0a, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x0a, 0x19, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ },
+ },
+ },
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "NoEthernetFrame",
+ Err: io.ErrUnexpectedEOF,
+ },
+ },
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "InvalidEthernetFrame",
+ In: []byte{
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25, 0x00, 0x02,
+ 0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25,
+ 0xc0, 0xa8, 0x0a, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x0a, 0x19,
+ },
+ Err: io.ErrUnexpectedEOF,
+ },
+ },
+ }
+
+ for _, tcase := range table {
+ t.Run(tcase.Name, func(tt *testing.T) {
+ res := &EthernetFrame{}
+ err := res.UnmarshalBinary(tcase.In)
+ assert.ErrorIsf(tt, err, tcase.Err, "expected UnmarshalEthernet to return an error of %s", tcase.Err)
+ if tcase.Out != nil {
+ compareEthernetFrames(tt, tcase.Out, res)
+ }
+ })
+ }
+}
+
+func TestEthernetFrameExtractVLAN(t *testing.T) {
+ table := []unmarshalVLANCase{
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "EthernetFrameIsVLAN",
+ In: []byte{
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25, 0x81, 0x00, 0x00, 0x02,
+ 0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25,
+ 0xc0, 0xa8, 0x0a, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x0a, 0x19, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ },
+ },
+ Out: &VLAN{
+ ID: 2,
+ EthernetType: EthernetTypeARP,
+ },
+ },
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "EthernetFrameIsNotVLAN",
+ In: []byte{
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0x08, 0x06, 0x00, 0x01,
+ 0x08, 0x00, 0x06, 0x04, 0x00, 0x02, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0xc0, 0xa8, 0x01, 0x6c,
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0xc0, 0xa8, 0x01, 0x50,
+ },
+ Err: ErrNotVLAN,
+ },
+ },
+ }
+
+ for _, tcase := range table {
+ t.Run(tcase.Name, func(tt *testing.T) {
+ eth := &EthernetFrame{}
+ err := eth.UnmarshalBinary(tcase.In)
+ if err != nil {
+ tt.Fatal(err)
+ }
+ vlan, err := eth.ExtractVLAN()
+ assert.ErrorIsf(tt, err, tcase.Err, "expected ExtractVLAN to return an error of: %s", tcase.Err)
+ if tcase.Out != nil {
+ compareVLANs(tt, tcase.Out, vlan)
+ }
+ })
+ }
+}
+
+func TestEthernetFrameExtractARP(t *testing.T) {
+ table := []unmarshalCase{
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "EthernetFrameIsVLAN",
+ In: []byte{
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25, 0x81, 0x00, 0x00, 0x02,
+ 0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25,
+ 0xc0, 0xa8, 0x0a, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x0a, 0x19, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ },
+ },
+ Out: &Packet{
+ HardwareType: HardwareTypeEthernet,
+ ProtocolType: ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ OpCode: OpRequest,
+ SendHwdAddr: parseMACNoError("84:39:c0:0b:22:25"),
+ SendIPAddr: parseAddrNoError("192.168.10.26"),
+ TgtHwdAddr: parseMACNoError("00:00:00:00:00:00"),
+ TgtIPAddr: parseAddrNoError("192.168.10.25"),
+ },
+ },
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "EthernetFrameIsNotVLAN",
+ In: []byte{
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0x08, 0x06, 0x00, 0x01,
+ 0x08, 0x00, 0x06, 0x04, 0x00, 0x02, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0xc0, 0xa8, 0x01, 0x6c,
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0xc0, 0xa8, 0x01, 0x50,
+ },
+ },
+ Out: &Packet{
+ HardwareType: HardwareTypeEthernet,
+ ProtocolType: ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ OpCode: OpReply,
+ SendHwdAddr: parseMACNoError("80:61:5f:08:fc:16"),
+ SendIPAddr: parseAddrNoError("192.168.1.108"),
+ TgtHwdAddr: parseMACNoError("24:4b:fe:e1:ea:26"),
+ TgtIPAddr: parseAddrNoError("192.168.1.80"),
+ },
+ },
+ }
+
+ for _, tcase := range table {
+ t.Run(tcase.Name, func(tt *testing.T) {
+ eth := &EthernetFrame{}
+ err := eth.UnmarshalBinary(tcase.In)
+ if err != nil {
+ tt.Fatal(err)
+ }
+ pkt, err := eth.ExtractARPPacket()
+ assert.ErrorIsf(tt, err, tcase.Err, "expected ExtractARPPacket to return an error of: %s", tcase.Err)
+ fmt.Printf("%+v\n", pkt)
+ if tcase.Out != nil {
+ compareARPPacket(tt, tcase.Out, pkt)
+ }
+ })
+ }
+}
diff --git a/src/maasagent/internal/arp/helpers_test.go b/src/maasagent/internal/arp/helpers_test.go
new file mode 100644
index 0000000..2c2b024
--- /dev/null
+++ b/src/maasagent/internal/arp/helpers_test.go
@@ -0,0 +1,104 @@
+package arp
+
+/*
+ Copyright 2023 Canonical Ltd. This software is licensed under the
+ GNU Affero General Public License version 3 (see the file LICENSE).
+*/
+
+import (
+ "net"
+ "net/netip"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type basePacketTestCase struct {
+ Name string
+ In []byte
+ Err error
+}
+
+type unmarshalVLANCase struct {
+ basePacketTestCase
+ Out *VLAN
+}
+
+type unmarshalEthernetCase struct {
+ basePacketTestCase
+ Out *EthernetFrame
+}
+
+type unmarshalCase struct {
+ basePacketTestCase
+ Out *Packet
+}
+
+func parseMACNoError(s string) net.HardwareAddr {
+ addr, _ := net.ParseMAC(s)
+ return addr
+}
+
+func parseAddrNoError(s string) netip.Addr {
+ addr, _ := netip.ParseAddr(s)
+ return addr
+}
+
+func ethernetTypeToString(t uint16) string {
+ switch t {
+ case EthernetTypeLLC:
+ return "LLC"
+ case EthernetTypeIPv4:
+ return "IPv4"
+ case EthernetTypeARP:
+ return "ARP"
+ case EthernetTypeIPv6:
+ return "IPv6"
+ case EthernetTypeVLAN:
+ return "VLAN"
+ }
+ return "unknown"
+}
+
+func hardwareTypeToString(t uint16) string {
+ switch t {
+ case HardwareTypeEthernet:
+ return "Ethernet"
+ }
+ return "unknown"
+}
+
+func protocolTypeToString(t uint16) string {
+ switch t {
+ case ProtocolTypeIPv4:
+ return "IPv4"
+ }
+ return "unknown"
+}
+
+func compareVLANs(t *testing.T, expected, actual *VLAN) {
+ assert.Equalf(t, expected.Priority, actual.Priority, "expected Priority to be %d", int(expected.Priority))
+ assert.Equalf(t, expected.DropEligible, actual.DropEligible, "exptected DropEligible to be %v", expected.DropEligible)
+ assert.Equalf(t, expected.ID, actual.ID, "expected ID to be %d", int(expected.ID))
+ assert.Equalf(t, expected.EthernetType, actual.EthernetType, "expected EthernetType to be %s", ethernetTypeToString(expected.EthernetType))
+}
+
+func compareEthernetFrames(t *testing.T, expected, actual *EthernetFrame) {
+ assert.Equalf(t, expected.SrcMAC, actual.SrcMAC, "expected SrcMAC to be %s", expected.SrcMAC)
+ assert.Equalf(t, expected.DstMAC, actual.DstMAC, "expected DstMAC to be %s", expected.DstMAC)
+ assert.Equalf(t, expected.EthernetType, actual.EthernetType, "expected EthernetType to be %s", ethernetTypeToString(expected.EthernetType))
+ assert.Equalf(t, expected.Len, actual.Len, "expected a length of %d", int(expected.Len))
+ assert.Equalf(t, expected.Payload, actual.Payload, "expected a payload of %x", expected.Payload)
+}
+
+func compareARPPacket(t *testing.T, expected, actual *Packet) {
+ assert.Equalf(t, expected.HardwareType, actual.HardwareType, "expected a HardwareType of %s", hardwareTypeToString(expected.HardwareType))
+ assert.Equalf(t, expected.ProtocolType, actual.ProtocolType, "expected a ProtocolType of %s", protocolTypeToString(expected.ProtocolType))
+ assert.Equalf(t, expected.HardwareAddrLen, actual.HardwareAddrLen, "expected a HardwareAddrLen of %d", int(expected.HardwareAddrLen))
+ assert.Equalf(t, expected.ProtocolAddrLen, actual.ProtocolAddrLen, "expected a ProtocolAddrLen of %d", int(expected.ProtocolAddrLen))
+ assert.Equalf(t, expected.OpCode, actual.OpCode, "expected a ProtocolLen of %d", int(expected.OpCode))
+ assert.Equalf(t, expected.SendHwdAddr, actual.SendHwdAddr, "expected a SendHwdAddr of %s", expected.SendHwdAddr)
+ assert.Equalf(t, expected.SendIPAddr, actual.SendIPAddr, "expected a SendIPAddr of %s", expected.SendIPAddr)
+ assert.Equalf(t, expected.TgtHwdAddr, actual.TgtHwdAddr, "expected a TgtHwdAddr of %s", expected.TgtHwdAddr)
+ assert.Equalf(t, expected.TgtIPAddr, actual.TgtIPAddr, "expected a TgtIPAddr of %s", expected.TgtIPAddr)
+}
diff --git a/src/maasagent/internal/arp/packet.go b/src/maasagent/internal/arp/packet.go
new file mode 100644
index 0000000..d397e85
--- /dev/null
+++ b/src/maasagent/internal/arp/packet.go
@@ -0,0 +1,163 @@
+package arp
+
+/*
+ Copyright 2023 Canonical Ltd. This software is licensed under the
+ GNU Affero General Public License version 3 (see the file LICENSE).
+*/
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "net/netip"
+)
+
+const (
+ // HardwareTypeReserved is a special value for hardware type
+ HardwareTypeReserved uint16 = iota // see RFC5494
+ // HardwareTypeEthernet is the hardware type value for Ethernet
+ // we only care about ethernet, but additional types are defined for
+ // testing and possible future use
+ HardwareTypeEthernet
+ // HardwareTypeExpEth is the hardware type for experimental ethernet
+ HardwareTypeExpEth
+ // HardwareTypeAX25 is the hardware type for Radio AX.25
+ HardwareTypeAX25
+ _
+ // HardwareTypeChaos is a chaos value for hardware type
+ HardwareTypeChaos
+ // HardwareTypeIEEE802 is for IEEE 802 networks
+ HardwareTypeIEEE802
+
+ // skipping propriatary networks
+
+ // HardwareTypeFiberChannel is the hardware type for fiber channel
+ HardwareTypeFiberChannel uint16 = 18
+ // HardwareTypeSerialLine is the hardware type for serial line
+ HardwareTypeSerialLine uint16 = 19
+ // HardwareTypeHIPARP is the hardware type for HIPARP
+ HardwareTypeHIPARP uint16 = iota + 28
+ // HardwareTypeIPARPISO7163 is the hardware type for IP and ARP over ISO 7816-3
+ HardwareTypeIPARPISO7163
+ // HardwareTypeARPSec is the hardware type for ARPSec
+ HardwareTypeARPSec
+ // HardwareTypeIPSec is the hardware type for IPSec tunnel
+ HardwareTypeIPSec
+ // HardwareTypeInfiniBand is the hardware type for InfiniBand
+ HardwareTypeInfiniBand
+)
+
+const (
+ // ProtocolTypeIPv4 is the value for IPv4 ARP packets
+ ProtocolTypeIPv4 uint16 = 0x0800
+ // ProtocolTypeIPv6 is the value for IPv6 ARP packets,
+ // which shouldn't be used, this is defined for testing purposes
+ ProtocolTypeIPv6 uint16 = 0x86dd
+)
+
+const (
+ // OpReserved is a special reserved OpCode
+ OpReserved uint16 = iota // see RFC5494
+ // OpRequest is the OpCode for ARP requests
+ OpRequest
+ // OpReply is the OpCode for ARP replies
+ OpReply
+)
+
+var (
+ // ErrMalformedPacket is an error returned when parsing a malformed ARP packet
+ ErrMalformedPacket = errors.New("malformed ARP packet")
+)
+
+// Packet is a struct containing the data of an ARP packet
+type Packet struct {
+ HardwareType uint16
+ ProtocolType uint16
+ HardwareAddrLen uint8
+ ProtocolAddrLen uint8
+ OpCode uint16
+ SendHwdAddr net.HardwareAddr
+ SendIPAddr netip.Addr
+ TgtHwdAddr net.HardwareAddr
+ TgtIPAddr netip.Addr
+}
+
+func checkPacketLen(buf []byte, bytesRead, length int) error {
+ if len(buf[bytesRead:]) < length {
+ return io.ErrUnexpectedEOF
+ }
+ return nil
+}
+
+// UnmarsahalBinary takes the ARP packet bytes and parses it into a Packet
+func (pkt *Packet) UnmarshalBinary(buf []byte) error {
+ var (
+ bytesRead int
+ )
+
+ err := checkPacketLen(buf, bytesRead, 8)
+ if err != nil {
+ return fmt.Errorf("%w: packet missing initial ARP fields", err)
+ }
+
+ pkt.HardwareType = binary.BigEndian.Uint16(buf[0:2])
+ pkt.ProtocolType = binary.BigEndian.Uint16(buf[2:4])
+ pkt.HardwareAddrLen = buf[4]
+ pkt.ProtocolAddrLen = buf[5]
+ pkt.OpCode = binary.BigEndian.Uint16(buf[6:8])
+
+ bytesRead = 8
+ hwdAddrLen := int(pkt.HardwareAddrLen)
+ ipAddrLen := int(pkt.ProtocolAddrLen)
+
+ err = checkPacketLen(buf, bytesRead, hwdAddrLen)
+ if err != nil {
+ return fmt.Errorf("%w: packet too short for sender hardware address", err)
+ }
+
+ sendHwdAddrBuf := make([]byte, hwdAddrLen)
+ copy(sendHwdAddrBuf[:], buf[bytesRead:bytesRead+hwdAddrLen])
+ pkt.SendHwdAddr = sendHwdAddrBuf
+ bytesRead += hwdAddrLen
+
+ err = checkPacketLen(buf, bytesRead, ipAddrLen)
+ if err != nil {
+ return fmt.Errorf("%w: packet too short for sender IP address", err)
+ }
+
+ var ok bool
+
+ sendIPAddrBuf := make([]byte, ipAddrLen)
+ copy(sendIPAddrBuf[:], buf[bytesRead:bytesRead+ipAddrLen])
+ pkt.SendIPAddr, ok = netip.AddrFromSlice(sendIPAddrBuf)
+ if !ok {
+ return fmt.Errorf("%w: invalid sender IP address", ErrMalformedPacket)
+ }
+ bytesRead += ipAddrLen
+
+ err = checkPacketLen(buf, bytesRead, hwdAddrLen)
+ if err != nil {
+ return fmt.Errorf("%w: packet too short for target hardware address", err)
+ }
+
+ tgtHwdAddrBuf := make([]byte, hwdAddrLen)
+ copy(tgtHwdAddrBuf[:], buf[bytesRead:bytesRead+hwdAddrLen])
+ pkt.TgtHwdAddr = tgtHwdAddrBuf
+ bytesRead += hwdAddrLen
+
+ err = checkPacketLen(buf, bytesRead, ipAddrLen)
+ if err != nil {
+ return fmt.Errorf("%w: packet too short for target IP address", err)
+ }
+
+ tgtIPAddrBuf := make([]byte, ipAddrLen)
+ copy(tgtIPAddrBuf[:], buf[bytesRead:bytesRead+ipAddrLen])
+ pkt.TgtIPAddr, ok = netip.AddrFromSlice(tgtIPAddrBuf)
+ if !ok {
+ return fmt.Errorf("%w: invalid target IP address", ErrMalformedPacket)
+ }
+
+ return nil
+}
diff --git a/src/maasagent/internal/arp/packet_test.go b/src/maasagent/internal/arp/packet_test.go
new file mode 100644
index 0000000..ce7091b
--- /dev/null
+++ b/src/maasagent/internal/arp/packet_test.go
@@ -0,0 +1,87 @@
+package arp
+
+/*
+ Copyright 2023 Canonical Ltd. This software is licensed under the
+ GNU Affero General Public License version 3 (see the file LICENSE).
+*/
+
+import (
+ "io"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUnmarshal(t *testing.T) {
+ table := []unmarshalCase{
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "ValidRequestPacket",
+ // generated from tcpdump
+ In: []byte{
+ 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25,
+ 0xc0, 0xa8, 0x0a, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x0a, 0x19,
+ },
+ },
+ Out: &Packet{
+ HardwareType: HardwareTypeEthernet,
+ ProtocolType: ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ OpCode: OpRequest,
+ SendHwdAddr: parseMACNoError("84:39:c0:0b:22:25"),
+ SendIPAddr: parseAddrNoError("192.168.10.26"),
+ TgtHwdAddr: parseMACNoError("00:00:00:00:00:00"),
+ TgtIPAddr: parseAddrNoError("192.168.10.25"),
+ },
+ },
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "ValidReplyPacket",
+ // generated from tcpdump
+ In: []byte{
+ 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x02, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0xc0, 0xa8,
+ 0x01, 0x6c, 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0xc0, 0xa8, 0x01, 0x50,
+ },
+ },
+ Out: &Packet{
+ HardwareType: HardwareTypeEthernet,
+ ProtocolType: ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ OpCode: OpReply,
+ SendHwdAddr: parseMACNoError("80:61:5f:08:fc:16"),
+ SendIPAddr: parseAddrNoError("192.168.1.108"),
+ TgtHwdAddr: parseMACNoError("24:4b:fe:e1:ea:26"),
+ TgtIPAddr: parseAddrNoError("192.168.1.80"),
+ },
+ },
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "EmptyPacket",
+ Err: io.ErrUnexpectedEOF,
+ },
+ },
+ {
+ basePacketTestCase: basePacketTestCase{
+ Name: "TooShortPacket",
+ In: []byte{
+ 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25,
+ 0xc0, 0xa8,
+ },
+ Err: io.ErrUnexpectedEOF,
+ },
+ },
+ }
+
+ for _, tcase := range table {
+ t.Run(tcase.Name, func(tt *testing.T) {
+ res := &Packet{}
+ err := res.UnmarshalBinary(tcase.In)
+ assert.ErrorIsf(tt, err, tcase.Err, "expected Unmarshal to return an error of %s", tcase.Err)
+ if tcase.Out != nil {
+ compareARPPacket(tt, tcase.Out, res)
+ }
+ })
+ }
+}
diff --git a/src/maasagent/internal/netmon/service.go b/src/maasagent/internal/netmon/service.go
index 14ebdc6..68f7860 100644
--- a/src/maasagent/internal/netmon/service.go
+++ b/src/maasagent/internal/netmon/service.go
@@ -1,3 +1,246 @@
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 = 0
+ } else {
+ 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")
+ 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 Service.ResC
+// if an error occurs, it is sent to Service.ErrC
+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
+ }
+ }
+ }
+}
diff --git a/src/maasagent/internal/netmon/service_test.go b/src/maasagent/internal/netmon/service_test.go
new file mode 100644
index 0000000..eac4aed
--- /dev/null
+++ b/src/maasagent/internal/netmon/service_test.go
@@ -0,0 +1,383 @@
+package netmon
+
+/*
+ Copyright 2023 Canonical Ltd. This software is licensed under the
+ GNU Affero General Public License version 3 (see the file LICENSE).
+*/
+
+import (
+ "io"
+ "net"
+ "net/netip"
+ "testing"
+ "time"
+
+ "github.com/google/gopacket"
+ pcap "github.com/packetcap/go-pcap"
+ "github.com/stretchr/testify/assert"
+
+ "launchpad.net/maas/maas/src/maasagent/internal/arp"
+)
+
+func uint16Pointer(v uint16) *uint16 {
+ return &v
+}
+
+type isValidARPPacketCase struct {
+ Name string
+ In *arp.Packet
+ Out bool
+}
+
+func TestIsValidARPPacket(t *testing.T) {
+ table := []isValidARPPacketCase{
+ {
+ Name: "ValidARPPacket",
+ In: &arp.Packet{
+ HardwareType: arp.HardwareTypeEthernet,
+ ProtocolType: arp.ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ },
+ Out: true,
+ },
+ {
+ Name: "InvalidHardwareTypeARPPacket",
+ In: &arp.Packet{
+ HardwareType: arp.HardwareTypeChaos,
+ ProtocolType: arp.ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ },
+ Out: false,
+ },
+ {
+ Name: "InvalidProtocolTypeARPPacket",
+ In: &arp.Packet{
+ HardwareType: arp.HardwareTypeEthernet,
+ ProtocolType: arp.ProtocolTypeIPv6,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ },
+ Out: false,
+ },
+ {
+ Name: "InvalidHardwareAddrLenARPPacket",
+ In: &arp.Packet{
+ HardwareType: arp.HardwareTypeEthernet,
+ ProtocolType: arp.ProtocolTypeIPv4,
+ HardwareAddrLen: 8,
+ ProtocolAddrLen: 4,
+ },
+ Out: false,
+ },
+ {
+ Name: "InvalidProtocolAddrLenARPPacket",
+ In: &arp.Packet{
+ HardwareType: arp.HardwareTypeEthernet,
+ ProtocolType: arp.ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 16,
+ },
+ Out: false,
+ },
+ }
+ for _, tcase := range table {
+ t.Run(tcase.Name, func(tt *testing.T) {
+ assert.Equalf(tt, tcase.Out, isValidARPPacket(tcase.In), "expected the result to be %v", tcase.Out)
+ })
+ }
+}
+
+type updateBindingsArgs struct {
+ Pkt *arp.Packet
+ VID *uint16
+ Time time.Time
+}
+
+type updateBindingsCase struct {
+ Name string
+ BindingsFixture map[string]Binding
+ In updateBindingsArgs
+ Out []Result
+}
+
+func TestUpdateBindings(t *testing.T) {
+ timestamp := time.Now()
+ testIP1 := net.ParseIP("10.0.0.1").To4()
+ testIP2 := net.ParseIP("10.0.0.2").To4()
+ table := []updateBindingsCase{
+ {
+ Name: "NewRequestPacket",
+ In: updateBindingsArgs{
+ Pkt: &arp.Packet{
+ HardwareType: arp.HardwareTypeEthernet,
+ ProtocolType: arp.ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ OpCode: arp.OpRequest,
+ SendHwdAddr: net.HardwareAddr{0xc0, 0xff, 0xee, 0x15, 0xc0, 0x01},
+ SendIPAddr: netip.AddrFrom4([4]byte{testIP1[0], testIP1[1], testIP1[2], testIP1[3]}),
+ TgtIPAddr: netip.AddrFrom4([4]byte{testIP2[0], testIP2[1], testIP2[2], testIP2[3]}),
+ },
+ Time: timestamp,
+ },
+ Out: []Result{
+ {
+ IP: "10.0.0.1",
+ MAC: "c0:ff:ee:15:c0:01",
+ Time: timestamp.Unix(),
+ Event: EventNew,
+ },
+ },
+ },
+ {
+ Name: "NewReplyPacket",
+ In: updateBindingsArgs{
+ Pkt: &arp.Packet{
+ HardwareType: arp.HardwareTypeEthernet,
+ ProtocolType: arp.ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ OpCode: arp.OpReply,
+ SendHwdAddr: net.HardwareAddr{0xc0, 0xff, 0xee, 0x15, 0xc0, 0x01},
+ SendIPAddr: netip.AddrFrom4([4]byte{testIP1[0], testIP1[1], testIP1[2], testIP1[3]}),
+ TgtHwdAddr: net.HardwareAddr{0xc0, 0xff, 0xee, 0x15, 0xc0, 0x1d},
+ TgtIPAddr: netip.AddrFrom4([4]byte{testIP2[0], testIP2[1], testIP2[2], testIP2[3]}),
+ },
+ Time: timestamp,
+ },
+ Out: []Result{
+ {
+ IP: "10.0.0.1",
+ MAC: "c0:ff:ee:15:c0:01",
+ Time: timestamp.Unix(),
+ Event: EventNew,
+ },
+ {
+ IP: "10.0.0.2",
+ MAC: "c0:ff:ee:15:c0:1d",
+ Time: timestamp.Unix(),
+ Event: EventNew,
+ },
+ },
+ },
+ {
+ Name: "NewVLANPacket",
+ In: updateBindingsArgs{
+ Pkt: &arp.Packet{
+ HardwareType: arp.HardwareTypeEthernet,
+ ProtocolType: arp.ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ OpCode: arp.OpRequest,
+ SendHwdAddr: net.HardwareAddr{0xc0, 0xff, 0xee, 0x15, 0xc0, 0x01},
+ SendIPAddr: netip.AddrFrom4([4]byte{testIP1[0], testIP1[1], testIP1[2], testIP1[3]}),
+ TgtIPAddr: netip.AddrFrom4([4]byte{testIP2[0], testIP2[1], testIP2[2], testIP2[3]}),
+ },
+ VID: uint16Pointer(2),
+ Time: timestamp,
+ },
+ Out: []Result{
+ {
+ IP: "10.0.0.1",
+ MAC: "c0:ff:ee:15:c0:01",
+ Time: timestamp.Unix(),
+ VID: uint16Pointer(2),
+ Event: EventNew,
+ },
+ },
+ },
+ {
+ Name: "Refresh",
+ BindingsFixture: map[string]Binding{
+ "0_10.0.0.1": Binding{
+ IP: netip.AddrFrom4([4]byte{testIP1[0], testIP1[1], testIP1[2], testIP1[3]}),
+ MAC: net.HardwareAddr{0xc0, 0xff, 0xee, 0x15, 0xc0, 0x01},
+ Time: timestamp,
+ },
+ },
+ In: updateBindingsArgs{
+ Pkt: &arp.Packet{
+ HardwareType: arp.HardwareTypeEthernet,
+ ProtocolType: arp.ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ OpCode: arp.OpRequest,
+ SendHwdAddr: net.HardwareAddr{0xc0, 0xff, 0xee, 0x15, 0xc0, 0x01},
+ SendIPAddr: netip.AddrFrom4([4]byte{testIP1[0], testIP1[1], testIP1[2], testIP1[3]}),
+ TgtIPAddr: netip.AddrFrom4([4]byte{testIP2[0], testIP2[1], testIP2[2], testIP2[3]}),
+ },
+ Time: timestamp.Add(seenAgainThreshold + time.Second),
+ },
+ Out: []Result{
+ {
+ IP: "10.0.0.1",
+ MAC: "c0:ff:ee:15:c0:01",
+ Time: timestamp.Add(seenAgainThreshold + time.Second).Unix(),
+ Event: EventRefreshed,
+ },
+ },
+ },
+ {
+ Name: "Move",
+ BindingsFixture: map[string]Binding{
+ "0_10.0.0.1": Binding{
+ IP: netip.AddrFrom4([4]byte{testIP1[0], testIP1[1], testIP1[2], testIP1[3]}),
+ MAC: net.HardwareAddr{0xc0, 0xff, 0xee, 0x15, 0xc0, 0x01},
+ Time: timestamp,
+ },
+ },
+ In: updateBindingsArgs{
+ Pkt: &arp.Packet{
+ HardwareType: arp.HardwareTypeEthernet,
+ ProtocolType: arp.ProtocolTypeIPv4,
+ HardwareAddrLen: 6,
+ ProtocolAddrLen: 4,
+ OpCode: arp.OpRequest,
+ SendHwdAddr: net.HardwareAddr{0xc0, 0xff, 0xee, 0x15, 0xc0, 0x1d},
+ SendIPAddr: netip.AddrFrom4([4]byte{testIP1[0], testIP1[1], testIP1[2], testIP1[3]}),
+ TgtIPAddr: netip.AddrFrom4([4]byte{testIP2[0], testIP2[1], testIP2[2], testIP2[3]}),
+ },
+ Time: timestamp,
+ },
+ Out: []Result{
+ {
+ IP: "10.0.0.1",
+ MAC: "c0:ff:ee:15:c0:1d",
+ Time: timestamp.Unix(),
+ Event: EventMoved,
+ },
+ },
+ },
+ }
+ for _, tcase := range table {
+ t.Run(tcase.Name, func(tt *testing.T) {
+ svc := NewService("lo")
+ if tcase.BindingsFixture != nil {
+ svc.bindings = tcase.BindingsFixture
+ }
+ res := svc.updateBindings(tcase.In.Pkt, tcase.In.VID, tcase.In.Time)
+ for i, expected := range tcase.Out {
+ var expectedVID int
+ if expected.VID != nil {
+ expectedVID = int(*expected.VID)
+ }
+ assert.Equalf(tt, expected.IP, res[i].IP, "expected Result at index of %d to have the IP %s", i, expected.IP)
+ assert.Equalf(tt, expected.MAC, res[i].MAC, "expected Result at index of %d to have the MAC %s", i, expected.MAC)
+ assert.Equalf(tt, expected.VID, res[i].VID, "expected Result at index of %d to have the VID %d", i, expectedVID)
+ assert.Equalf(tt, expected.Time, res[i].Time, "expected Result at index of %d to have the Time of %d", i, int(expected.Time))
+ assert.Equalf(tt, expected.Event, res[i].Event, "expected Result at index of %d to have the Event of %s", i, expected.Event)
+ }
+ })
+ }
+}
+
+type handlePacketCase struct {
+ Name string
+ In pcap.Packet
+ Out []Result
+ Err error
+}
+
+func TestServiceHandlePacket(t *testing.T) {
+ timestamp := time.Now()
+ table := []handlePacketCase{
+ {
+ Name: "ValidRequestPacket",
+ In: pcap.Packet{
+ // generated from tcpdump
+ B: []byte{
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25, 0x81, 0x00, 0x00, 0x02,
+ 0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x84, 0x39, 0xc0, 0x0b, 0x22, 0x25,
+ 0xc0, 0xa8, 0x0a, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x0a, 0x19, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ },
+ Info: gopacket.CaptureInfo{
+ Timestamp: timestamp,
+ },
+ },
+ Out: []Result{
+ {
+ IP: "192.168.10.26",
+ MAC: "84:39:c0:0b:22:25",
+ VID: uint16Pointer(2),
+ Time: timestamp.Unix(),
+ Event: EventNew,
+ },
+ },
+ },
+ {
+ Name: "ValidReplyPacket",
+ In: pcap.Packet{
+ B: []byte{
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0x08, 0x06, 0x00, 0x01,
+ 0x08, 0x00, 0x06, 0x04, 0x00, 0x02, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0xc0, 0xa8, 0x01, 0x6c,
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0xc0, 0xa8, 0x01, 0x50,
+ },
+ },
+ Out: []Result{
+ {
+ IP: "192.168.1.108",
+ MAC: "80:61:5f:08:fc:16",
+ VID: nil,
+ Time: timestamp.Unix(),
+ Event: EventNew,
+ },
+ {
+ IP: "192.168.1.80",
+ MAC: "24:4b:fe:e1:ea:26",
+ VID: nil,
+ Time: timestamp.Unix(),
+ Event: EventNew,
+ },
+ },
+ },
+ {
+ Name: "EmptyPacket",
+ Err: ErrEmptyPacket,
+ },
+ {
+ Name: "MalformedPacket",
+ In: pcap.Packet{
+ B: []byte{
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x84, 0x39, 0xc0, 0x0b, 0x22,
+ 0x08, 0x06, 0x00, 0x01, 0x08, 0x06, 0x04, 0x00, 0x01, 0x84,
+ 0xc0, 0xa8, 0x0a, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ },
+ },
+ // should return nil, nil
+ },
+ {
+ Name: "ShortPacket",
+ In: pcap.Packet{
+ B: []byte{
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0x80, 0x61, 0x5f, 0x08, 0xfc, 0x16, 0x08, 0x06, 0x00, 0x01,
+ 0x08, 0x00, 0x06, 0x04, 0x00, 0x02, 0x80, 0xfc, 0x16, 0xc0, 0xa8, 0x01, 0x6c,
+ 0x24, 0x4b, 0xfe, 0xe1, 0xea, 0x26, 0xc0, 0xa8, 0x01, 0x50,
+ },
+ },
+ Err: io.ErrUnexpectedEOF,
+ },
+ }
+
+ svc := NewService("")
+
+ for _, tcase := range table {
+ t.Run(tcase.Name, func(tt *testing.T) {
+ res, err := svc.handlePacket(tcase.In)
+ assert.ErrorIsf(tt, err, tcase.Err, "expected handlePacket to return an error of: %s", tcase.Err)
+ if tcase.Out != nil {
+ for i, expected := range tcase.Out {
+ assert.Equalf(tt, expected.IP, res[i].IP, "expected result at index %d to have an IP address of %s", i, expected.IP)
+ assert.Equalf(tt, expected.MAC, res[i].MAC, "expected result at index %d to have a MAC address of %s", i, expected.MAC)
+ assert.Equalf(tt, expected.VID, res[i].VID, "expected result at index %d to have a VID of %v", i, expected.VID)
+ assert.Equalf(tt, expected.Time, res[i].Time, "expected result at index %d to have a Time of %s", i, expected.Time)
+ }
+ } else {
+ assert.Nil(tt, res)
+ }
+ })
+ }
+}
Follow ups
-
[Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: MAAS Lander, 2023-05-04
-
[Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Christian Grabowski, 2023-05-04
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS PASS
From: MAAS Lander, 2023-05-03
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS FAILED
From: MAAS Lander, 2023-05-03
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Anton Troyanov, 2023-05-03
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Christian Grabowski, 2023-05-03
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Christian Grabowski, 2023-05-03
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Mark Laing, 2023-05-03
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Mark Laing, 2023-05-03
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS FAILED
From: MAAS Lander, 2023-05-03
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Christian Grabowski, 2023-05-03
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Thomas Parrott, 2023-05-03
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS PASS
From: MAAS Lander, 2023-05-01
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Christian Grabowski, 2023-05-01
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Anton Troyanov, 2023-04-30
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Anton Troyanov, 2023-04-30
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Anton Troyanov, 2023-04-30
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS FAILED
From: MAAS Lander, 2023-04-29
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS PASS
From: MAAS Lander, 2023-04-28
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS PASS
From: MAAS Lander, 2023-04-27
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS FAILED
From: MAAS Lander, 2023-04-27
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Christian Grabowski, 2023-04-27
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Anton Troyanov, 2023-04-27
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Alberto Donato, 2023-04-27
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS PASS
From: MAAS Lander, 2023-04-25
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Christian Grabowski, 2023-04-25
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Alberto Donato, 2023-04-25
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS PASS
From: MAAS Lander, 2023-04-24
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Alexsander de Souza, 2023-04-24
-
Re: [UNITTESTS] -b go_network_discovery lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas - TESTS FAILED
From: MAAS Lander, 2023-04-24
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Christian Grabowski, 2023-04-24
-
Re: [Merge] ~cgrabowski/maas:go_network_discovery into maas:master
From: Christian Grabowski, 2023-04-24