sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #07446
[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:
MAAS Maintainers (maas-maintainers)
For more details, see:
https://code.launchpad.net/~cgrabowski/maas/+git/maas/+merge/441702
--
Your team MAAS Maintainers is requested to review the proposed merge of ~cgrabowski/maas:go_network_discovery into maas:master.
diff --git a/src/maasagent/cmd/netmon/main.go b/src/maasagent/cmd/netmon/main.go
index e83655f..c6e79ea 100644
--- a/src/maasagent/cmd/netmon/main.go
+++ b/src/maasagent/cmd/netmon/main.go
@@ -1,9 +1,78 @@
package main
import (
+ "context"
+ "encoding/json"
+ "errors"
+ "os"
+ "strconv"
+
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+
"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)
+
+ svc := netmon.NewService(iface)
+ go svc.Start(ctx)
+ log.Info().Msg("Service netmon started")
+
+ for {
+ select {
+ case <-sigC:
+ cancel()
+ return 0
+ case res := <-svc.ResC:
+ err = json.NewEncoder(os.Stdout).Encode(res)
+ if err != nil {
+ log.Error().Err(err).Msg("")
+ return 1
+ }
+ case err = <-svc.ErrC:
+ log.Error().Err(err).Msg("")
+ return 1
+ }
+ }
+}
+
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..e2cfc5b
--- /dev/null
+++ b/src/maasagent/internal/arp/ethernet.go
@@ -0,0 +1,99 @@
+package arp
+
+import (
+ "encoding/binary"
+ "errors"
+ "io"
+ "net"
+)
+
+const (
+ minEthernetLen = 14
+)
+
+const (
+ EthernetTypeLLC uint16 = 0
+ EthernetTypeIPv4 uint16 = 0x0800
+ EthernetTypeARP uint16 = 0x0806
+ EthernetTypeIPv6 uint16 = 0x086dd
+ EthernetTypeVLAN uint16 = 0x8100
+
+ NonStdLenEthernetTypes uint16 = 0x600
+)
+
+var (
+ ErrNotVLAN = errors.New("ethernet frame not of type VLAN")
+ ErrMalformedVLAN = errors.New("VLAN tag is malformed")
+)
+
+type VLAN struct {
+ Priority uint8
+ DropEligible bool
+ ID uint16
+ EthernetType uint16
+}
+
+func UnmarshalVLAN(buf []byte) (*VLAN, error) {
+ if len(buf) < 4 {
+ return nil, ErrMalformedVLAN
+ }
+
+ var v VLAN
+
+ // 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 &v, nil
+}
+
+type EthernetFrame struct {
+ SrcMAC net.HardwareAddr
+ DstMAC net.HardwareAddr
+ EthernetType uint16
+ Len uint16
+ Payload []byte
+}
+
+func (e *EthernetFrame) ExtractARPPacket() (*Packet, error) {
+ if e.EthernetType == EthernetTypeVLAN {
+ return Unmarshal(e.Payload[4:])
+ }
+ return Unmarshal(e.Payload)
+}
+
+func (e *EthernetFrame) ExtractVLAN() (*VLAN, error) {
+ if e.EthernetType != EthernetTypeVLAN {
+ return nil, ErrNotVLAN
+ }
+ return UnmarshalVLAN(e.Payload[0:4])
+}
+
+func UnmarshalEthernet(buf []byte) (*EthernetFrame, error) {
+ if len(buf) < minEthernetLen {
+ return nil, io.ErrUnexpectedEOF
+ }
+
+ var eth EthernetFrame
+
+ 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 nil, 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..2bb301b
--- /dev/null
+++ b/src/maasagent/internal/arp/ethernet_test.go
@@ -0,0 +1,231 @@
+package arp
+
+import (
+ "io"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUnmarshalVLAN(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, err := UnmarshalVLAN(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)
+ } else {
+ assert.Nil(tt, res, "expected UnmarshalVLAN to not return a VLAN")
+ }
+ })
+ }
+}
+
+func TestUnmarshalEthernet(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, err := UnmarshalEthernet(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)
+ } else {
+ assert.Nil(tt, res, "did not expect an ethernet frame to be returned from UnmarshalEthernet")
+ }
+ })
+ }
+}
+
+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, err := UnmarshalEthernet(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)
+ } else {
+ assert.Nil(tt, vlan, "expected no VLAN to be extracted")
+ }
+ })
+ }
+}
+
+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, err := UnmarshalEthernet(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)
+ if tcase.Out != nil {
+ compareARPPacket(tt, tcase.Out, pkt)
+ } else {
+ assert.Nil(tt, pkt, "did not expect ExtractARPPacket to return a Packet")
+ }
+ })
+ }
+}
diff --git a/src/maasagent/internal/arp/helpers_test.go b/src/maasagent/internal/arp/helpers_test.go
new file mode 100644
index 0000000..acdc1dd
--- /dev/null
+++ b/src/maasagent/internal/arp/helpers_test.go
@@ -0,0 +1,99 @@
+package arp
+
+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..a81f036
--- /dev/null
+++ b/src/maasagent/internal/arp/packet.go
@@ -0,0 +1,118 @@
+package arp
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "net/netip"
+)
+
+const (
+ HardwareTypeEthernet uint16 = iota + 1
+)
+
+const (
+ ProtocolTypeIPv4 uint16 = 0x0800
+)
+
+const (
+ OpRequest uint16 = iota + 1
+ OpReply
+)
+
+var (
+ 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
+}
+
+func Unmarshal(buf []byte) (*Packet, error) {
+ var (
+ pkt Packet
+ bytesRead int
+ )
+
+ err := checkPacketLen(buf, bytesRead, 8)
+ if err != nil {
+ return nil, 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 nil, 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 nil, 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 nil, fmt.Errorf("%w: invalid sender IP address", ErrMalformedPacket)
+ }
+ bytesRead += ipAddrLen
+
+ err = checkPacketLen(buf, bytesRead, hwdAddrLen)
+ if err != nil {
+ return nil, 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 nil, 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 nil, fmt.Errorf("%w: invalid target IP address", ErrMalformedPacket)
+ }
+
+ return &pkt, 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..992d9a7
--- /dev/null
+++ b/src/maasagent/internal/arp/packet_test.go
@@ -0,0 +1,83 @@
+package arp
+
+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, err := Unmarshal(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)
+ } else {
+ assert.Nil(tt, res, "did not expect a packet to be returned from Unmarshal")
+ }
+ })
+ }
+}
diff --git a/src/maasagent/internal/netmon/service.go b/src/maasagent/internal/netmon/service.go
index 14ebdc6..1792028 100644
--- a/src/maasagent/internal/netmon/service.go
+++ b/src/maasagent/internal/netmon/service.go
@@ -1,3 +1,202 @@
package netmon
-func NewService() {}
+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 = 64
+ timeout = 5 * time.Minute
+ seenAgainThreshold = 600 * time.Second
+)
+
+const (
+ EventNew = "NEW"
+ EventRefreshed = "REFRESHED"
+ EventMoved = "MOVED"
+)
+
+var (
+ emptyTime time.Time
+)
+
+var (
+ ErrEmptyPacket = errors.New("received an empty packet")
+)
+
+type Binding struct {
+ IP netip.Addr
+ MAC net.HardwareAddr
+ VID *uint16
+ Time time.Time
+}
+
+type Result struct {
+ IP string `json:"ip"`
+ MAC string `json:"mac"`
+ PreviousMAC string `json:"previous_mac,omitempty"`
+ Event string `json:"event"`
+ Time int64 `json:"time"`
+ VID *uint16 `json:"vid"`
+}
+
+type Service struct {
+ iface string
+ bindings map[string]Binding
+ ResC chan Result
+ ErrC chan error
+}
+
+func NewService(iface string) *Service {
+ return &Service{
+ iface: iface,
+ bindings: make(map[string]Binding),
+ ResC: make(chan Result),
+ ErrC: make(chan error),
+ }
+}
+
+func (s *Service) updateBindings(pkt *arp.Packet, vid *uint16, timestamp time.Time) (res []Result) {
+ if timestamp == emptyTime {
+ 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 (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, err := arp.UnmarshalEthernet(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
+ }
+ return s.updateBindings(arpPkt, vid, pkt.Info.Timestamp), nil
+}
+
+func (s *Service) Start(ctx context.Context) {
+ defer func() {
+ close(s.ResC)
+ close(s.ErrC)
+ }()
+
+ hndlr, err := pcap.OpenLive(s.iface, snapLen, false, timeout, true)
+ if err != nil {
+ s.ErrC <- err
+ return
+ }
+ defer hndlr.Close()
+ pkts := hndlr.Listen()
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case pkt, ok := <-pkts:
+ if !ok {
+ log.Debug().Msg("packet capture has closed")
+ }
+ res, err := s.handlePacket(pkt)
+ if err != nil {
+ s.ErrC <- err
+ return
+ }
+ for _, r := range res {
+ s.ResC <- 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..20c4fae
--- /dev/null
+++ b/src/maasagent/internal/netmon/service_test.go
@@ -0,0 +1,84 @@
+package netmon
+
+import (
+ "io"
+ "testing"
+
+ pcap "github.com/packetcap/go-pcap"
+ "github.com/stretchr/testify/assert"
+
+ "launchpad.net/maas/maas/src/maasagent/internal/arp"
+)
+
+type handlePacketCase struct {
+ Name string
+ In pcap.Packet
+ Out *Result
+ Err error
+}
+
+func TestServiceHandlePacket(t *testing.T) {
+ 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,
+ },
+ },
+ Out: &Result{},
+ },
+ {
+ 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{},
+ },
+ {
+ 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, 0x00, 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,
+ },
+ },
+ Err: arp.ErrMalformedPacket,
+ },
+ {
+ 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)
+ assert.Equalf(tt, tcase.Out, res, "expected handlePacket to return a value of: %+v", tcase.Out)
+ })
+ }
+}
diff --git a/src/maasagent/scripts/get_golangci-lint.sh b/src/maasagent/scripts/get_golangci-lint.sh
new file mode 100644
index 0000000..291c47a
--- /dev/null
+++ b/src/maasagent/scripts/get_golangci-lint.sh
@@ -0,0 +1,412 @@
+#!/bin/sh
+set -e
+
+usage() {
+ this=$1
+ cat <<EOF
+$this: download go binaries for golangci/golangci-lint
+
+Usage: $this [-b] bindir [-d] [tag]
+ -b sets bindir or installation directory, Defaults to ./bin
+ -d turns on debug logging
+ [tag] is a tag from
+ https://github.com/golangci/golangci-lint/releases
+ If tag is missing, then the latest will be used.
+
+ Generated by godownloader
+ https://github.com/goreleaser/godownloader
+
+EOF
+ exit 2
+}
+
+parse_args() {
+ #BINDIR is ./bin unless set be ENV
+ # over-ridden by flag below
+
+ BINDIR=${BINDIR:-./bin}
+ while getopts "b:dh?x" arg; do
+ case "$arg" in
+ b) BINDIR="$OPTARG" ;;
+ d) log_set_priority 10 ;;
+ h | \?) usage "$0" ;;
+ x) set -x ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+ TAG=$1
+}
+# this function wraps all the destructive operations
+# if a curl|bash cuts off the end of the script due to
+# network, either nothing will happen or will syntax error
+# out preventing half-done work
+execute() {
+ tmpdir=$(mktemp -d)
+ log_debug "downloading files into ${tmpdir}"
+ http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
+ http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
+ hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
+ srcdir="${tmpdir}/${NAME}"
+ rm -rf "${srcdir}"
+ (cd "${tmpdir}" && untar "${TARBALL}")
+ test ! -d "${BINDIR}" && install -d "${BINDIR}"
+ for binexe in $BINARIES; do
+ if [ "$OS" = "windows" ]; then
+ binexe="${binexe}.exe"
+ fi
+ install "${srcdir}/${binexe}" "${BINDIR}/"
+ log_info "installed ${BINDIR}/${binexe}"
+ done
+ rm -rf "${tmpdir}"
+}
+get_binaries() {
+ case "$PLATFORM" in
+ darwin/amd64) BINARIES="golangci-lint" ;;
+ darwin/arm64) BINARIES="golangci-lint" ;;
+ darwin/armv6) BINARIES="golangci-lint" ;;
+ darwin/armv7) BINARIES="golangci-lint" ;;
+ darwin/mips64) BINARIES="golangci-lint" ;;
+ darwin/mips64le) BINARIES="golangci-lint" ;;
+ darwin/ppc64le) BINARIES="golangci-lint" ;;
+ darwin/s390x) BINARIES="golangci-lint" ;;
+ freebsd/386) BINARIES="golangci-lint" ;;
+ freebsd/amd64) BINARIES="golangci-lint" ;;
+ freebsd/armv6) BINARIES="golangci-lint" ;;
+ freebsd/armv7) BINARIES="golangci-lint" ;;
+ freebsd/mips64) BINARIES="golangci-lint" ;;
+ freebsd/mips64le) BINARIES="golangci-lint" ;;
+ freebsd/ppc64le) BINARIES="golangci-lint" ;;
+ freebsd/s390x) BINARIES="golangci-lint" ;;
+ linux/386) BINARIES="golangci-lint" ;;
+ linux/amd64) BINARIES="golangci-lint" ;;
+ linux/arm64) BINARIES="golangci-lint" ;;
+ linux/armv6) BINARIES="golangci-lint" ;;
+ linux/armv7) BINARIES="golangci-lint" ;;
+ linux/mips64) BINARIES="golangci-lint" ;;
+ linux/mips64le) BINARIES="golangci-lint" ;;
+ linux/ppc64le) BINARIES="golangci-lint" ;;
+ linux/s390x) BINARIES="golangci-lint" ;;
+ linux/riscv64) BINARIES="golangci-lint" ;;
+ netbsd/386) BINARIES="golangci-lint" ;;
+ netbsd/amd64) BINARIES="golangci-lint" ;;
+ netbsd/armv6) BINARIES="golangci-lint" ;;
+ netbsd/armv7) BINARIES="golangci-lint" ;;
+ windows/386) BINARIES="golangci-lint" ;;
+ windows/amd64) BINARIES="golangci-lint" ;;
+ windows/arm64) BINARIES="golangci-lint" ;;
+ windows/armv6) BINARIES="golangci-lint" ;;
+ windows/armv7) BINARIES="golangci-lint" ;;
+ windows/mips64) BINARIES="golangci-lint" ;;
+ windows/mips64le) BINARIES="golangci-lint" ;;
+ windows/ppc64le) BINARIES="golangci-lint" ;;
+ windows/s390x) BINARIES="golangci-lint" ;;
+ *)
+ log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
+ exit 1
+ ;;
+ esac
+}
+tag_to_version() {
+ if [ -z "${TAG}" ]; then
+ log_info "checking GitHub for latest tag"
+ else
+ log_info "checking GitHub for tag '${TAG}'"
+ fi
+ REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
+ if test -z "$REALTAG"; then
+ log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
+ exit 1
+ fi
+ # if version starts with 'v', remove it
+ TAG="$REALTAG"
+ VERSION=${TAG#v}
+}
+adjust_format() {
+ # change format (tar.gz or zip) based on OS
+ case ${OS} in
+ windows) FORMAT=zip ;;
+ esac
+ true
+}
+adjust_os() {
+ # adjust archive name based on OS
+ true
+}
+adjust_arch() {
+ # adjust archive name based on ARCH
+ true
+}
+
+cat /dev/null <<EOF
+------------------------------------------------------------------------
+https://github.com/client9/shlib - portable posix shell functions
+Public domain - http://unlicense.org
+https://github.com/client9/shlib/blob/master/LICENSE.md
+but credit (and pull requests) appreciated.
+------------------------------------------------------------------------
+EOF
+is_command() {
+ command -v "$1" >/dev/null
+}
+echoerr() {
+ echo "$@" 1>&2
+}
+log_prefix() {
+ echo "$0"
+}
+_logp=6
+log_set_priority() {
+ _logp="$1"
+}
+log_priority() {
+ if test -z "$1"; then
+ echo "$_logp"
+ return
+ fi
+ [ "$1" -le "$_logp" ]
+}
+log_tag() {
+ case $1 in
+ 0) echo "emerg" ;;
+ 1) echo "alert" ;;
+ 2) echo "crit" ;;
+ 3) echo "err" ;;
+ 4) echo "warning" ;;
+ 5) echo "notice" ;;
+ 6) echo "info" ;;
+ 7) echo "debug" ;;
+ *) echo "$1" ;;
+ esac
+}
+log_debug() {
+ log_priority 7 || return 0
+ echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
+}
+log_info() {
+ log_priority 6 || return 0
+ echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
+}
+log_err() {
+ log_priority 3 || return 0
+ echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
+}
+log_crit() {
+ log_priority 2 || return 0
+ echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
+}
+uname_os() {
+ os=$(uname -s | tr '[:upper:]' '[:lower:]')
+ case "$os" in
+ msys*) os="windows" ;;
+ mingw*) os="windows" ;;
+ cygwin*) os="windows" ;;
+ win*) os="windows" ;;
+ esac
+ echo "$os"
+}
+uname_arch() {
+ arch=$(uname -m)
+ case $arch in
+ x86_64) arch="amd64" ;;
+ x86) arch="386" ;;
+ i686) arch="386" ;;
+ i386) arch="386" ;;
+ aarch64) arch="arm64" ;;
+ armv5*) arch="armv5" ;;
+ armv6*) arch="armv6" ;;
+ armv7*) arch="armv7" ;;
+ esac
+ echo ${arch}
+}
+uname_os_check() {
+ os=$(uname_os)
+ case "$os" in
+ darwin) return 0 ;;
+ dragonfly) return 0 ;;
+ freebsd) return 0 ;;
+ linux) return 0 ;;
+ android) return 0 ;;
+ nacl) return 0 ;;
+ netbsd) return 0 ;;
+ openbsd) return 0 ;;
+ plan9) return 0 ;;
+ solaris) return 0 ;;
+ windows) return 0 ;;
+ esac
+ log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value."
+ return 1
+}
+uname_arch_check() {
+ arch=$(uname_arch)
+ case "$arch" in
+ 386) return 0 ;;
+ amd64) return 0 ;;
+ arm64) return 0 ;;
+ armv5) return 0 ;;
+ armv6) return 0 ;;
+ armv7) return 0 ;;
+ ppc64) return 0 ;;
+ ppc64le) return 0 ;;
+ mips) return 0 ;;
+ mipsle) return 0 ;;
+ mips64) return 0 ;;
+ mips64le) return 0 ;;
+ s390x) return 0 ;;
+ riscv64) return 0 ;;
+ amd64p32) return 0 ;;
+ esac
+ log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value."
+ return 1
+}
+untar() {
+ tarball=$1
+ case "${tarball}" in
+ *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;;
+ *.tar) tar --no-same-owner -xf "${tarball}" ;;
+ *.zip) unzip "${tarball}" ;;
+ *)
+ log_err "untar unknown archive format for ${tarball}"
+ return 1
+ ;;
+ esac
+}
+http_download_curl() {
+ local_file=$1
+ source_url=$2
+ header=$3
+ if [ -z "$header" ]; then
+ code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
+ else
+ code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
+ fi
+ if [ "$code" != "200" ]; then
+ log_debug "http_download_curl received HTTP status $code"
+ return 1
+ fi
+ return 0
+}
+http_download_wget() {
+ local_file=$1
+ source_url=$2
+ header=$3
+ if [ -z "$header" ]; then
+ wget -q -O "$local_file" "$source_url"
+ else
+ wget -q --header "$header" -O "$local_file" "$source_url"
+ fi
+}
+http_download() {
+ log_debug "http_download $2"
+ if is_command curl; then
+ http_download_curl "$@"
+ return
+ elif is_command wget; then
+ http_download_wget "$@"
+ return
+ fi
+ log_crit "http_download unable to find wget or curl"
+ return 1
+}
+http_copy() {
+ tmp=$(mktemp)
+ http_download "${tmp}" "$1" "$2" || return 1
+ body=$(cat "$tmp")
+ rm -f "${tmp}"
+ echo "$body"
+}
+github_release() {
+ owner_repo=$1
+ version=$2
+ test -z "$version" && version="latest"
+ giturl="https://github.com/${owner_repo}/releases/${version}"
+ json=$(http_copy "$giturl" "Accept:application/json")
+ test -z "$json" && return 1
+ version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
+ test -z "$version" && return 1
+ echo "$version"
+}
+hash_sha256() {
+ TARGET=${1:-/dev/stdin}
+ if is_command gsha256sum; then
+ hash=$(gsha256sum "$TARGET") || return 1
+ echo "$hash" | cut -d ' ' -f 1
+ elif is_command sha256sum; then
+ hash=$(sha256sum "$TARGET") || return 1
+ echo "$hash" | cut -d ' ' -f 1
+ elif is_command shasum; then
+ hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
+ echo "$hash" | cut -d ' ' -f 1
+ elif is_command openssl; then
+ hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
+ echo "$hash" | cut -d ' ' -f a
+ else
+ log_crit "hash_sha256 unable to find command to compute sha-256 hash"
+ return 1
+ fi
+}
+hash_sha256_verify() {
+ TARGET=$1
+ checksums=$2
+ if [ -z "$checksums" ]; then
+ log_err "hash_sha256_verify checksum file not specified in arg2"
+ return 1
+ fi
+ BASENAME=${TARGET##*/}
+ want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
+ if [ -z "$want" ]; then
+ log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
+ return 1
+ fi
+ got=$(hash_sha256 "$TARGET")
+ if [ "$want" != "$got" ]; then
+ log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
+ return 1
+ fi
+}
+cat /dev/null <<EOF
+------------------------------------------------------------------------
+End of functions from https://github.com/client9/shlib
+------------------------------------------------------------------------
+EOF
+
+PROJECT_NAME="golangci-lint"
+OWNER=golangci
+REPO="golangci-lint"
+BINARY=golangci-lint
+FORMAT=tar.gz
+OS=$(uname_os)
+ARCH=$(uname_arch)
+PREFIX="$OWNER/$REPO"
+
+# use in logging routines
+log_prefix() {
+ echo "$PREFIX"
+}
+PLATFORM="${OS}/${ARCH}"
+GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
+
+uname_os_check "$OS"
+uname_arch_check "$ARCH"
+
+parse_args "$@"
+
+get_binaries
+
+tag_to_version
+
+adjust_format
+
+adjust_os
+
+adjust_arch
+
+log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
+
+NAME=${BINARY}-${VERSION}-${OS}-${ARCH}
+TARBALL=${NAME}.${FORMAT}
+TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
+CHECKSUM=${PROJECT_NAME}-${VERSION}-checksums.txt
+CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
+
+
+execute
References