├── LICENSE-MIT ├── README.md ├── UNLICENSE ├── cmd └── root.go ├── config.toml ├── go.mod ├── go.sum ├── layers.txt ├── main.go ├── util └── log.go └── watch ├── config.go ├── event.go ├── layers.go ├── packet.go ├── subscriber.go └── watcher.go /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Henry Wallace 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netwatch 2 | 3 | netwatch is a library and command line tool for aggregating and inferring 4 | information about hosts in a network by passively sniffing packets. It fashions 5 | events based on changes to those hosts, e.g. a new host with new MAC address 6 | has entered the local network, a known host is using never before used port 7 | 22/tcp, or a host is performing an ARP scan. 8 | 9 | Dual-licensed under MIT or the UNLICENSE. 10 | 11 | ## Examples 12 | 13 | ``` 14 | % sudo -E go run main.go --only log 15 | INFO[2019-09-24 20:28:38] using first up interface: eth0 16 | INFO[2019-09-24 20:28:44] new Host(xx:xx:xx:xx:xx:xx, 192.168.86.50) 17 | INFO[2019-09-24 20:28:44] new 1900/udp on Host(xx:xx:xx:xx:xx:xx, 192.168.86.50) 18 | INFO[2019-09-24 20:28:46] new Host(yy:yy:yy:yy:yy:yy, 192.168.86.20) 19 | INFO[2019-09-24 20:28:46] new 44054/tcp on Host(yy:yy:yy:yy:yy:yy, 192.168.86.20) 20 | INFO[2019-09-24 20:28:46] new Host(zz:zz:zz:zz:zz:zz, 0.0.0.0) 21 | INFO[2019-09-24 20:28:46] new 443/tcp on Host(zz:zz:zz:zz:zz:zz, 0.0.0.0) 22 | ``` 23 | 24 | Using the config, you can also configure your own hook events, with builtin 25 | event names, and templated variables for use in commands: 26 | ```sh 27 | % cat > config.toml < config.toml < 25 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | 14 | "github.com/henrywallace/netwatch/util" 15 | "github.com/henrywallace/netwatch/watch" 16 | ) 17 | 18 | var rootCmd = &cobra.Command{ 19 | Use: "netwatch", 20 | Short: "Watch for activity on a LAN", 21 | RunE: main, 22 | } 23 | 24 | func init() { 25 | rootCmd.Flags().StringP("config", "c", "config.toml", "toml file to trigger config") 26 | rootCmd.Flags().StringSliceP("only", "o", nil, "config trigger names to only run") 27 | rootCmd.Flags().StringP("iface", "i", "", "which network interface to use, if not first active") 28 | rootCmd.Flags().StringP("pcap", "p", "", "whether to read from pcap file instead of live interface") 29 | } 30 | 31 | func main(cmd *cobra.Command, args []string) error { 32 | ctx := context.Background() 33 | log := util.NewLogger() 34 | 35 | var subs []watch.Subscriber 36 | path := mustString(log, cmd, "config") 37 | only := mustStringSlice(log, cmd, "only") 38 | if path != "" { 39 | sub, err := watch.NewSubConfig(log, path, only) 40 | if err != nil { 41 | return err 42 | } 43 | subs = append(subs, sub) 44 | } 45 | 46 | iface := mustString(log, cmd, "iface") 47 | pcap := mustString(log, cmd, "pcap") 48 | if iface != "" && pcap != "" { 49 | return errors.Errorf( 50 | "cannot specify both --iface=%s and --pcap=%s", 51 | iface, 52 | pcap, 53 | ) 54 | } 55 | 56 | w := watch.NewWatcher(log, subs...) 57 | if pcap != "" { 58 | return w.WatchPCAP(ctx, pcap) 59 | } 60 | if iface == "" { 61 | var err error 62 | iface, err = firstLiveInterface() 63 | if err != nil { 64 | return err 65 | } 66 | log.Infof("using first up interface: %s", iface) 67 | } 68 | return w.WatchLive(ctx, iface) 69 | } 70 | 71 | // return the name of the first live interface 72 | // https://unix.stackexchange.com/a/335082/162041 73 | func firstLiveInterface() (string, error) { 74 | ifaces, err := net.Interfaces() 75 | if err != nil { 76 | return "", err 77 | } 78 | for _, iface := range ifaces { 79 | if iface.Flags&net.FlagLoopback != 0 { 80 | continue 81 | } 82 | if iface.Flags&net.FlagUp == 0 { 83 | continue 84 | } 85 | if strings.Contains(iface.Name, "docker") { 86 | continue 87 | } 88 | return iface.Name, nil 89 | } 90 | return "", errors.Errorf("failed to find interface to scan") 91 | } 92 | 93 | // Execute adds all child commands to the root command and sets flags 94 | // appropriately. This is called by main.main(). It only needs to happen once 95 | // to the rootCmd. 96 | func Execute() { 97 | if err := rootCmd.Execute(); err != nil { 98 | fmt.Println(err) 99 | os.Exit(1) 100 | } 101 | } 102 | 103 | func mustString( 104 | log *logrus.Logger, 105 | cmd *cobra.Command, 106 | name string, 107 | ) string { 108 | val, err := cmd.Flags().GetString(name) 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | return val 113 | } 114 | 115 | func mustStringSlice( 116 | log *logrus.Logger, 117 | cmd *cobra.Command, 118 | name string, 119 | ) []string { 120 | val, err := cmd.Flags().GetStringSlice(name) 121 | if err != nil { 122 | log.Fatal(err) 123 | } 124 | return val 125 | } 126 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | [triggers] 2 | [triggers.null] 3 | disabled = true 4 | onAny = true 5 | doBuiltin = "null" 6 | [triggers.debug] 7 | disabled = true 8 | onAny = true 9 | doShell = "echo '{{.Description}}'" 10 | [triggers.log] 11 | onEventsExcept = ["host.touch", "port.touch"] 12 | doBuiltin = "log" 13 | [triggeres.example] 14 | disabled = true 15 | onEvents = ["host.new"] 16 | doShell = "echo Hello {{.Host.IPv4}} - $(date)" 17 | [triggers.gmail] 18 | disabled = true 19 | onEvents = ["host.new"] 20 | doShell = "notify -s 'New host {{.Host.MAC}} on {{.Host.IPv4}}'" 21 | [triggers.gmail-ssh] 22 | disabled = true 23 | onShell = '[ "{{.Host.IPv4}} {{.PortString}}" = "$HOST1 $HOST1_PORT1" ]' 24 | doShell = "echo '{{.Description}}'" 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/henrywallace/netwatch 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/coreos/go-etcd v2.0.0+incompatible // indirect 8 | github.com/cpuguy83/go-md2man v1.0.10 // indirect 9 | github.com/google/gopacket v1.1.18 10 | github.com/kr/pretty v0.1.0 // indirect 11 | github.com/pkg/errors v0.9.1 12 | github.com/sirupsen/logrus v1.6.0 13 | github.com/spf13/cobra v1.0.0 14 | github.com/spf13/pflag v1.0.5 // indirect 15 | github.com/stretchr/testify v1.4.0 // indirect 16 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect 17 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect 18 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect 19 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 10 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 13 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 14 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 15 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 16 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 17 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 18 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 19 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 24 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 25 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 26 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 27 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 28 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 29 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 30 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 31 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 32 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 33 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 34 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 35 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 36 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 37 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 40 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 41 | github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY= 42 | github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= 43 | github.com/google/gopacket v1.1.18 h1:lum7VRA9kdlvBi7/v2p7/zcbkduHaCH/SVVyurs7OpY= 44 | github.com/google/gopacket v1.1.18/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= 45 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 46 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 47 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 48 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 49 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 50 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 51 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 52 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 53 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 54 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 55 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 56 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 57 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 58 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 59 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 60 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 61 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 62 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 63 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 64 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 65 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 66 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 67 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 68 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 69 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 70 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 71 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 72 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 73 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 74 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 75 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 76 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 77 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 78 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 79 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 80 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 81 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 82 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 83 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 84 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 85 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 86 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 87 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 88 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 89 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 90 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 91 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 92 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 93 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 94 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 95 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 96 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 97 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 98 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 99 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 100 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 101 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 102 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 103 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 104 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 105 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 106 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 107 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 108 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 109 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 110 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 111 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 112 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= 113 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 114 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 115 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 116 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 117 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 118 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 119 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 120 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 121 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 122 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 123 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 124 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 125 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 126 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 127 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 128 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 129 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 130 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 131 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 132 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 133 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 134 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 135 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 136 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 137 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 138 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 139 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 140 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 141 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 142 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 143 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 144 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 145 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 146 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 147 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= 148 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 149 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 150 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 151 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 152 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 153 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 154 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 155 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 156 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 157 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 158 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 159 | golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 160 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 161 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 162 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= 164 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= 166 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 168 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 169 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 170 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 171 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 172 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 173 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 174 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 175 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 176 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 177 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 178 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 179 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 180 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 181 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 182 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 183 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 184 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 185 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 186 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 187 | -------------------------------------------------------------------------------- /layers.txt: -------------------------------------------------------------------------------- 1 | cd gopacket 2 | rg '\bLayerType[A-Z]\w+\b' -I -or '$0' | sort | uniq -c | sort -nr 3 | 4 | 148 LayerTypeMetadata 5 | 130 LayerTypeEthernet 6 | 86 LayerTypePayload 7 | 83 LayerTypeIPv4 8 | 45 LayerTypeIPv6 9 | 34 LayerTypeUDP 10 | 33 LayerTypeOSPF 11 | 32 LayerTypeTCP 12 | 31 LayerTypeZero 13 | 31 LayerTypeSFlow 14 | 27 LayerTypeTLS 15 | 25 LayerTypeDot11 16 | 24 LayerTypeDNS 17 | 22 LayerTypeRadioTap 18 | 22 LayerTypeICMPv4 19 | 19 LayerTypeIPv6HopByHop 20 | 19 LayerTypeICMPv6 21 | 18 LayerTypeDot11InformationElement 22 | 17 LayerTypeIGMP 23 | 16 LayerTypeLCM 24 | 13 LayerTypeGRE 25 | 13 LayerTypeDot1Q 26 | 12 LayerTypeLLC 27 | 11 LayerTypeMPLS 28 | 11 LayerTypeDHCPv4 29 | 11 LayerTypeARP 30 | 10 LayerTypeSNAP 31 | 10 LayerTypeGeneve 32 | 10 LayerTypeDHCPv6 33 | 9 LayerTypeMLDv1MulticastListenerQuery 34 | 9 LayerTypeIPSecAH 35 | 8 LayerTypeMLDv2MulticastListenerQuery 36 | 8 LayerTypeMLDv1MulticastListenerReport 37 | 8 LayerTypeMLDv1MulticastListenerDone 38 | 8 LayerTypeLinkLayerDiscovery 39 | 8 LayerTypeIPv6Destination 40 | 8 LayerTypeGTPv1U 41 | 8 LayerTypeFragment 42 | 8 LayerTypeEAPOL 43 | 8 LayerTypeDot11Data 44 | 7 LayerTypeVXLAN 45 | 7 LayerTypeVRRP 46 | 7 LayerTypePPP 47 | 7 LayerTypeLinkLayerDiscoveryInfo 48 | 7 LayerTypeICMPv6RouterAdvertisement 49 | 7 LayerTypeICMPv6NeighborSolicitation 50 | 7 LayerTypeEAPOLKey 51 | 7 LayerTypeDot11DataQOSData 52 | 6 LayerTypeSIP 53 | 6 LayerTypeSCTPSack 54 | 6 LayerTypeSCTPData 55 | 6 LayerTypeSCTP 56 | 6 LayerTypePrismHeader 57 | 6 LayerTypePPPoE 58 | 6 LayerTypeNTP 59 | 6 LayerTypeNortelDiscovery 60 | 6 LayerTypeMLDv2MulticastListenerReport 61 | 6 LayerTypeLoopback 62 | 6 LayerTypeIPSecESP 63 | 6 LayerTypeICMPv6Redirect 64 | 6 LayerTypeICMPv6NeighborAdvertisement 65 | 6 LayerTypeICMPv6Echo 66 | 6 LayerTypeDot11WEP 67 | 6 LayerTypeDot11MgmtBeacon 68 | 6 LayerTypeDot11MgmtAction 69 | 6 LayerTypeBFD 70 | 5 LayerTypeSTP 71 | 5 LayerTypeSCTPShutdownComplete 72 | 5 LayerTypeSCTPShutdownAck 73 | 5 LayerTypeSCTPShutdown 74 | 5 LayerTypeSCTPInitAck 75 | 5 LayerTypeSCTPInit 76 | 5 LayerTypeSCTPCookieEcho 77 | 5 LayerTypeSCTPCookieAck 78 | 5 LayerTypeRMCP 79 | 5 LayerTypePFLog 80 | 5 LayerTypeLinuxSLL 81 | 5 LayerTypeIPv6Routing 82 | 5 LayerTypeIPv6Fragment 83 | 5 LayerTypeICMPv6RouterSolicitation 84 | 5 LayerTypeEtherIP 85 | 5 LayerTypeEAP 86 | 5 LayerTypeDot11MgmtProbeReq 87 | 5 LayerTypeDot11DataNull 88 | 5 LayerTypeDot11DataCFPollNoData 89 | 5 LayerTypeDot11DataCFPoll 90 | 5 LayerTypeDot11DataCFAckPollNoData 91 | 5 LayerTypeDot11DataCFAckPoll 92 | 5 LayerTypeDot11DataCFAck 93 | 5 LayerTypeDot11Ctrl 94 | 5 LayerTypeDecodeFailure 95 | 5 LayerTypeCiscoDiscoveryInfo 96 | 5 LayerTypeCiscoDiscovery 97 | 5 LayerTypeASFPresencePong 98 | 5 LayerTypeASF 99 | 4 LayerTypeUSBInterrupt 100 | 4 LayerTypeUSB 101 | 4 LayerTypeUDPLite 102 | 4 LayerTypeSCTPUnknownChunkType 103 | 4 LayerTypeSCTPHeartbeat 104 | 4 LayerTypeSCTPError 105 | 4 LayerTypeSCTPAbort 106 | 4 LayerTypeRUDP 107 | 4 LayerTypeModbusTCP 108 | 4 LayerTypeEthernetCTP 109 | 4 LayerTypeDot11MgmtReassociationResp 110 | 4 LayerTypeDot11MgmtReassociationReq 111 | 4 LayerTypeDot11MgmtProbeResp 112 | 4 LayerTypeDot11MgmtMeasurementPilot 113 | 4 LayerTypeDot11MgmtDisassociation 114 | 4 LayerTypeDot11MgmtDeauthentication 115 | 4 LayerTypeDot11MgmtAuthentication 116 | 4 LayerTypeDot11MgmtATIM 117 | 4 LayerTypeDot11MgmtAssociationResp 118 | 4 LayerTypeDot11MgmtAssociationReq 119 | 4 LayerTypeDot11MgmtActionNoAck 120 | 4 LayerTypeDot11DataQOSNull 121 | 4 LayerTypeDot11DataQOSDataCFPoll 122 | 4 LayerTypeDot11DataQOSDataCFAckPoll 123 | 4 LayerTypeDot11DataQOSDataCFAck 124 | 4 LayerTypeDot11DataQOSCFPollNoData 125 | 4 LayerTypeDot11DataQOSCFAckPollNoData 126 | 4 LayerTypeDot11DataCFAckNoData 127 | 4 LayerTypeDot11CtrlRTS 128 | 4 LayerTypeDot11CtrlPowersavePoll 129 | 4 LayerTypeDot11CtrlCTS 130 | 4 LayerTypeDot11CtrlCFEndAck 131 | 4 LayerTypeDot11CtrlCFEnd 132 | 4 LayerTypeDot11CtrlBlockAckReq 133 | 4 LayerTypeDot11CtrlBlockAck 134 | 4 LayerTypeDot11CtrlAck 135 | 3 LayerTypeUSBRequestBlockSetup 136 | 3 LayerTypeUSBControl 137 | 3 LayerTypeUSBBulk 138 | 3 LayerTypeSCTPHeartbeatAck 139 | 3 LayerTypeFDDI 140 | 3 LayerTypeEthernetCTPReply 141 | 3 LayerTypeEthernetCTPForwardData 142 | 3 LayerTypeDot11MgmtArubaWLAN 143 | 2 LayerTypeSCTPEmptyLayer 144 | 1 LayerTypeGTPV1U 145 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/henrywallace/netwatch/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /util/log.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // NewLogger creates a new logger whose level can be controlled with the env 10 | // WATCHER_LOGLEVEL. 11 | func NewLogger() *logrus.Logger { 12 | log := logrus.StandardLogger() 13 | 14 | format := new(logrus.TextFormatter) 15 | format.TimestampFormat = "2006-01-02 15:04:05" 16 | format.FullTimestamp = true 17 | log.SetFormatter(format) 18 | 19 | val := os.Getenv("WATCHER_LOGLEVEL") 20 | if val == "" { 21 | val = "INFO" 22 | } 23 | lvl, err := logrus.ParseLevel(val) 24 | if err != nil { 25 | log.Fatalf("failed to parse level: %v", val) 26 | } 27 | log.SetLevel(lvl) 28 | 29 | return log 30 | } 31 | -------------------------------------------------------------------------------- /watch/config.go: -------------------------------------------------------------------------------- 1 | package watch 2 | 3 | // Config holds configuration for Triggers. 4 | type Config struct { 5 | Triggers map[string]TriggerSpec `toml:"triggers"` 6 | } 7 | 8 | // TriggerSpec describes specification for one trigger. 9 | type TriggerSpec struct { 10 | Disabled bool 11 | OnEvents []EventType 12 | OnEventsExcept []EventType 13 | OnAny bool 14 | OnShell string 15 | DoBuiltin string 16 | DoShell string 17 | } 18 | -------------------------------------------------------------------------------- /watch/event.go: -------------------------------------------------------------------------------- 1 | package watch 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Event represents network activity. Events are intended to provide 9 | // higher-level descriptions of network changes, possibly encapsulating packets 10 | // seen over an extended period of time. 11 | // 12 | // A body of information peritent to the event type is attached to each event. 13 | // For example, the relevant introcution of a new host's MAC address. 14 | type Event struct { 15 | Type EventType 16 | Timestamp time.Time 17 | Body interface{} 18 | } 19 | 20 | // EventType describes the type of Event that has occurred. 21 | type EventType int 22 | 23 | // A complete list of types of Events. 24 | const ( 25 | Invalid EventType = iota 26 | HostTouch 27 | HostNew 28 | HostLost 29 | HostFound 30 | HostARPScanStart 31 | HostARPScanStop 32 | PortTouch 33 | PortNew 34 | PortLost 35 | PortFound 36 | ) 37 | 38 | // MarshalText satisfies the encoding.TextMarshaler interface. 39 | func (ty EventType) MarshalText() ([]byte, error) { 40 | var s string 41 | switch ty { 42 | case Invalid: 43 | s = "invalid" 44 | case HostTouch: 45 | s = "host.touch" 46 | case HostNew: 47 | s = "host.new" 48 | case HostLost: 49 | s = "host.lost" 50 | case HostFound: 51 | s = "host.found" 52 | case HostARPScanStart: 53 | s = "host.arp-scan.start" 54 | case HostARPScanStop: 55 | s = "host.arp-scan.stop" 56 | case PortTouch: 57 | s = "port.touch" 58 | case PortNew: 59 | s = "port.new" 60 | case PortLost: 61 | s = "port.lost" 62 | case PortFound: 63 | s = "port.found" 64 | default: 65 | panic(fmt.Sprintf("unknown event type: %v", ty)) 66 | } 67 | return []byte(s), nil 68 | } 69 | 70 | // UnmarshalText satisfies the encoding.TextUnmarshaler interface. 71 | func (ty *EventType) UnmarshalText(text []byte) error { 72 | s := string(text) 73 | switch s { 74 | case "", "invalid": 75 | *ty = Invalid 76 | case "host.touch": 77 | *ty = HostTouch 78 | case "host.new": 79 | *ty = HostNew 80 | case "host.lost": 81 | *ty = HostLost 82 | case "host.found": 83 | *ty = HostFound 84 | case "host.arp-scan.start": 85 | *ty = HostARPScanStart 86 | case "host.arp-scan.stop": 87 | *ty = HostARPScanStop 88 | case "port.touch": 89 | *ty = PortTouch 90 | case "port.new": 91 | *ty = PortNew 92 | case "port.lost": 93 | *ty = PortLost 94 | case "port.found": 95 | *ty = PortFound 96 | default: 97 | panic(fmt.Sprintf("unknown event type: %v", ty)) 98 | } 99 | return nil 100 | } 101 | 102 | // 103 | // host 104 | // 105 | 106 | // TODO: All of these events should be copies of events. If these events are 107 | // fed into many different subscribers, then some of the attributes about the 108 | // Host or Port, may not be correct. 109 | 110 | // EventHostTouch happens when any activity updates the state of a host. 111 | type EventHostTouch struct { 112 | Host *Host 113 | // TODO: Add an id or number indicating which number this is, or some 114 | // other stats. Might be useful for other event bodies as well. 115 | } 116 | 117 | // EventHostNew happens upon the introduction of a new host not yet seen. 118 | // Becoming inactive does not make it unseen. 119 | type EventHostNew struct { 120 | Host *Host 121 | } 122 | 123 | // EventHostLost happens when a host becomes inactive, after having no activity 124 | // for some amount of time. 125 | type EventHostLost struct { 126 | Host *Host 127 | Up time.Duration 128 | } 129 | 130 | // EventHostFound happens whenever a host becomes active again after being 131 | // contiguously inactive for some period of time. 132 | type EventHostFound struct { 133 | Host *Host 134 | Down time.Duration 135 | } 136 | 137 | // EventHostARPScanStart indicates that a host has started an arp scan. That is 138 | // there are many ARP protocol packets originating from this host in a short 139 | // amount of time. 140 | type EventHostARPScanStart struct { 141 | Host *Host 142 | } 143 | 144 | // EventHostARPScanStop indicates that a host has stopped performing an ARP 145 | // scan when it previously was. 146 | type EventHostARPScanStop struct { 147 | Host *Host 148 | Up time.Duration 149 | } 150 | 151 | // 152 | // port 153 | // 154 | 155 | // EventPortTouch happens when any activity updates the state of a Port. 156 | type EventPortTouch struct { 157 | Port *Port 158 | Host *Host 159 | } 160 | 161 | // EventPortNew happens upon the introduction of a new port not yet seen. 162 | // Becoming inactive does not make it unseen. 163 | type EventPortNew struct { 164 | Port *Port 165 | Host *Host 166 | } 167 | 168 | // EventPortLost happens when a port becomes inactive, after having no activity 169 | // for some amount of time. 170 | type EventPortLost struct { 171 | Port *Port 172 | Up time.Duration 173 | Host *Host 174 | } 175 | 176 | // EventPortFound happens whenever a port becomes active again after being 177 | // contiguously inactive for some period of time. 178 | type EventPortFound struct { 179 | Port *Port 180 | Down time.Duration 181 | Host *Host 182 | } 183 | -------------------------------------------------------------------------------- /watch/layers.go: -------------------------------------------------------------------------------- 1 | package watch 2 | 3 | import ( 4 | "net" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/google/gopacket" 9 | "github.com/google/gopacket/layers" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // TODO: Support returning more than just a view pair. Perhaps more generally a 14 | // collection of views, and a set of relationships between them. 15 | func handlePacket( 16 | log *logrus.Logger, 17 | packet gopacket.Packet, 18 | ) ViewPair { 19 | vp := ViewPair{ 20 | Src: NewView(), 21 | Dst: NewView(), 22 | Layers: make(map[gopacket.LayerType]int), 23 | } 24 | for _, l := range packet.Layers() { 25 | vp.Layers[l.LayerType()]++ 26 | switch l.LayerType() { 27 | case layers.LayerTypeARP: 28 | handleARP(&vp, l.(*layers.ARP)) 29 | case layers.LayerTypeEthernet: 30 | handleEthernet(&vp, l.(*layers.Ethernet)) 31 | case layers.LayerTypeTCP: 32 | handleTCP(&vp, l.(*layers.TCP)) 33 | case layers.LayerTypeLCM: 34 | handleLCM(&vp, l.(*layers.LCM)) 35 | case layers.LayerTypeIPv4: 36 | handleIPv4(&vp, l.(*layers.IPv4)) 37 | case layers.LayerTypeIPv6: 38 | handleIPv6(&vp, l.(*layers.IPv6)) 39 | case layers.LayerTypeUDP: 40 | handleUDP(&vp, l.(*layers.UDP)) 41 | case layers.LayerTypeDNS: 42 | handleDNS(&vp, l.(*layers.DNS)) 43 | case layers.LayerTypeDHCPv4: 44 | handleDHCPv4(&vp, l.(*layers.DHCPv4)) 45 | case layers.LayerTypeDHCPv6: 46 | handleDHCPv6(&vp, l.(*layers.DHCPv6)) 47 | default: 48 | log.Debugf("unhandled layer type: %v", l.LayerType()) 49 | } 50 | } 51 | return vp 52 | } 53 | 54 | func handleEthernet(v *ViewPair, eth *layers.Ethernet) { 55 | mac := MAC(eth.SrcMAC.String()) 56 | v.Src.MAC = &mac 57 | } 58 | 59 | func handleTCP(v *ViewPair, tcp *layers.TCP) { 60 | v.Src.TCP[int(tcp.SrcPort)] = true 61 | v.Dst.TCP[int(tcp.DstPort)] = true 62 | } 63 | 64 | func handleLCM(v *ViewPair, lcm *layers.LCM) { 65 | } 66 | 67 | func handleIPv4(v *ViewPair, ip4 *layers.IPv4) { 68 | v.Src.IPv4 = ip4.SrcIP 69 | v.Dst.IPv4 = ip4.DstIP 70 | } 71 | 72 | func handleIPv6(v *ViewPair, ip6 *layers.IPv6) { 73 | v.Src.IPv6 = ip6.SrcIP 74 | v.Dst.IPv6 = ip6.DstIP 75 | } 76 | 77 | func handleUDP(v *ViewPair, udp *layers.UDP) { 78 | v.Src.UDP[int(udp.SrcPort)] = true 79 | v.Dst.UDP[int(udp.DstPort)] = true 80 | } 81 | 82 | func handleDNS(v *ViewPair, dns *layers.DNS) { 83 | } 84 | 85 | func handleDHCPv4(v *ViewPair, dhcp *layers.DHCPv4) { 86 | if dhcp.Operation == layers.DHCPOpRequest { 87 | for _, opt := range dhcp.Options { 88 | switch opt.Type { 89 | case layers.DHCPOptHostname: 90 | v.Src.Hostname = string(opt.Data) 91 | case layers.DHCPOptClassID: 92 | // TODO 93 | case layers.DHCPOptClientID: 94 | // TODO 95 | } 96 | } 97 | } 98 | } 99 | 100 | var reControl = regexp.MustCompile(`^\p{Cc}+`) 101 | 102 | func stripOptDHCPv6(s string) string { 103 | s = reControl.ReplaceAllString(s, "") 104 | s = strings.TrimSpace(s) 105 | return s 106 | } 107 | 108 | func handleDHCPv6(v *ViewPair, dhcp *layers.DHCPv6) { 109 | if dhcp.MsgType == layers.DHCPv6MsgTypeSolicit { 110 | for _, opt := range dhcp.Options { 111 | switch opt.Code { 112 | case layers.DHCPv6OptClientFQDN: 113 | v.Src.Hostname = stripOptDHCPv6(string(opt.Data)) 114 | case layers.DHCPv6OptClientID: 115 | // TODO 116 | case layers.DHCPv6OptVendorClass: 117 | // TODO 118 | } 119 | } 120 | } 121 | } 122 | 123 | func handleARP(v *ViewPair, arp *layers.ARP) { 124 | // TODO: Check for change. 125 | srcMAC := MAC(net.HardwareAddr(arp.SourceHwAddress).String()) 126 | dstMAC := MAC(net.HardwareAddr(arp.DstHwAddress).String()) 127 | v.Src.MAC = &srcMAC 128 | v.Dst.MAC = &dstMAC 129 | 130 | addIP(&v.Src, net.IP(arp.SourceProtAddress)) 131 | addIP(&v.Dst, net.IP(arp.DstProtAddress)) 132 | } 133 | -------------------------------------------------------------------------------- /watch/packet.go: -------------------------------------------------------------------------------- 1 | package watch 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sort" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/google/gopacket" 12 | "github.com/google/gopacket/layers" 13 | "github.com/henrywallace/netwatch/util" 14 | ) 15 | 16 | var ( 17 | ttlHost = 120 * time.Second 18 | ttlPort = 30 * time.Second 19 | ttlArpScan = 5 * time.Second 20 | arpScanFreq = 20.0 21 | arpWindow = 10 * time.Second 22 | ) 23 | 24 | // Activity holds episodic state for something. 25 | type Activity struct { 26 | IsActive bool 27 | FirstSeen time.Time 28 | FirstSeenEpisode time.Time 29 | LastSeen time.Time 30 | 31 | expireFunc func(a *Activity) 32 | expire *time.Timer 33 | ttl time.Duration 34 | } 35 | 36 | // NewActivity creates a new Activity with the given time-to-live, and callback 37 | // once it hasn't been touched after the given ttl. 38 | func NewActivity(ttl time.Duration, expireFunc func(a *Activity)) *Activity { 39 | a := &Activity{ 40 | ttl: ttl, 41 | expireFunc: expireFunc, 42 | } 43 | return a 44 | } 45 | 46 | // Touch resets the time to live for this Activity, and returns if already was 47 | // active. 48 | func (a *Activity) Touch(now time.Time) bool { 49 | if a.expire == nil { 50 | a.expire = time.AfterFunc(a.ttl, func() { 51 | a.IsActive = false 52 | a.expireFunc(a) 53 | }) 54 | } else { 55 | a.expire.Reset(a.ttl) 56 | } 57 | if a.FirstSeen.IsZero() { 58 | a.FirstSeen = now 59 | } 60 | var wasActive bool 61 | if a.IsActive { 62 | wasActive = true 63 | } else { 64 | a.FirstSeenEpisode = now 65 | } 66 | a.IsActive = true 67 | a.LastSeen = now 68 | return wasActive 69 | } 70 | 71 | // Age returns the time since this was first seen, regardless if it is 72 | // currently active or not. 73 | func (a Activity) Age() time.Duration { 74 | return time.Since(a.FirstSeen) 75 | } 76 | 77 | // Up returns the time since this was recently first seen. 78 | func (a Activity) Up() time.Duration { 79 | return time.Since(a.FirstSeenEpisode) 80 | } 81 | 82 | // Host is a tracked entity. 83 | type Host struct { 84 | Activity *Activity 85 | ActivityARPScan *Activity 86 | 87 | MAC MAC 88 | IPv4 net.IP 89 | IPv6 net.IP 90 | TCP map[int]*Port 91 | UDP map[int]*Port 92 | Hostname string 93 | 94 | arps *windowed 95 | } 96 | 97 | func (h Host) String() string { 98 | var parts []string 99 | if h.Hostname != "" { 100 | parts = append(parts, h.Hostname) 101 | } 102 | parts = append(parts, string(h.MAC), h.IPv4.String()) 103 | s := fmt.Sprintf("Host(%s)", strings.Join(parts, ", ")) 104 | return s 105 | } 106 | 107 | // NewHost returns a new Host whose activity is touched on creation. The given 108 | // expire function is called whenever the Host hasn't been touched after some 109 | // default amount of time, indicating that it is non-active. 110 | func NewHost( 111 | mac MAC, 112 | events chan<- Event, 113 | now time.Time, 114 | expire func(h *Host), 115 | ) *Host { 116 | h := Host{ 117 | MAC: mac, 118 | TCP: make(map[int]*Port), 119 | UDP: make(map[int]*Port), 120 | arps: newWindowed(arpWindow), 121 | } 122 | h.Activity = NewActivity(ttlHost, func(a *Activity) { 123 | expire(&h) 124 | }) 125 | h.Activity.Touch(now) 126 | h.ActivityARPScan = NewActivity(ttlArpScan, func(a *Activity) { 127 | events <- Event{ 128 | Type: HostARPScanStop, 129 | Body: EventHostARPScanStop{ 130 | Host: &h, 131 | Up: time.Since(a.FirstSeenEpisode), 132 | }, 133 | } 134 | }) 135 | return &h 136 | } 137 | 138 | // ActiveTCP returns all TCP ports for the given Host that are currently 139 | // active. 140 | // 141 | // NOTE: Due to the expiring nature of Ports, it may be that returned pointers 142 | // to Ports are inactive when received or used. 143 | func (h Host) ActiveTCP() []*Port { 144 | var active []*Port 145 | for _, p := range h.TCP { 146 | if p.Activity.IsActive { 147 | active = append(active, p) 148 | } 149 | } 150 | return active 151 | } 152 | 153 | // ActiveUDP returns all UDP ports for the given Host that are currently 154 | // active. 155 | // 156 | // NOTE: Due to the expiring nature of Ports, it may be that returned pointers 157 | // to Ports are inactive when received or used. 158 | func (h Host) ActiveUDP() []*Port { 159 | var active []*Port 160 | for _, p := range h.UDP { 161 | if p.Activity.IsActive { 162 | active = append(active, p) 163 | } 164 | } 165 | return active 166 | } 167 | 168 | // Port represents a TCP or UDP connection. A Port could both appear on either 169 | // a sending or receiving host. Each Port has a TTL, before being considered 170 | // inactive. But it can be "touched" to be kept alive. 171 | type Port struct { 172 | Activity *Activity 173 | Num int 174 | 175 | isTCP bool 176 | } 177 | 178 | // NewPortTCP returns a new TCP port of the given port number. And a function 179 | // one what to do when the port expires, given a pointer to the created Port. 180 | func NewPortTCP( 181 | num int, 182 | now time.Time, 183 | expire func(p *Port), 184 | ) *Port { 185 | p := Port{ 186 | Num: num, 187 | isTCP: true, 188 | } 189 | p.Activity = NewActivity(ttlHost, func(a *Activity) { 190 | expire(&p) 191 | }) 192 | p.Activity.Touch(now) 193 | return &p 194 | } 195 | 196 | // NewPortUDP returns a new UDP port of the given port number. And a function 197 | // one what to do when the port expires, given a pointer to the created Port. 198 | func NewPortUDP( 199 | num int, 200 | now time.Time, 201 | expire func(p *Port), 202 | ) *Port { 203 | p := Port{ 204 | Num: num, 205 | isTCP: false, 206 | } 207 | p.Activity = NewActivity(ttlHost, func(a *Activity) { 208 | expire(&p) 209 | }) 210 | p.Activity.Touch(now) 211 | return &p 212 | } 213 | 214 | func (p Port) String() string { 215 | var suffix string 216 | if p.isTCP { 217 | suffix = "tcp" 218 | } else { 219 | suffix = "udp" 220 | } 221 | 222 | return fmt.Sprintf("%d/%s", p.Num, suffix) 223 | 224 | } 225 | 226 | // MAC is a string form of a net.HardwareAddr, so as to be used as keys in 227 | // maps. 228 | type MAC string 229 | 230 | // View represents a subset of information depicted about a Host, from a single 231 | // packet. This can be used to be associate with a host, and update it's 232 | // information. A View's properties are intended to be updated as different 233 | // layers of the packet are decoded. Some packet layers may not yet influence a 234 | // View, but it aims to capture as much information from each packet as 235 | // possible before updating the hosts. 236 | type View struct { 237 | MAC *MAC 238 | IPv4 net.IP 239 | IPv6 net.IP 240 | TCP map[int]bool 241 | UDP map[int]bool 242 | Hostname string 243 | } 244 | 245 | // NewView returns a new 246 | func NewView() View { 247 | return View{ 248 | TCP: make(map[int]bool), 249 | UDP: make(map[int]bool), 250 | } 251 | } 252 | 253 | // ViewPair represents a pair of views that are communicating withing one 254 | // packet, all determined from a single packet. 255 | type ViewPair struct { 256 | Src View 257 | Dst View 258 | Layers map[gopacket.LayerType]int 259 | } 260 | 261 | // ScanPackets updates hosts with a given a stream of packets, and sends 262 | // events to a channel based on their updated activity, when applicable. 263 | // 264 | // A map of hosts known will be updated with the diffs. 265 | func (w *Watcher) ScanPackets( 266 | hosts map[MAC]*Host, 267 | packets <-chan gopacket.Packet, 268 | ) { 269 | defer close(w.events) 270 | for p := range packets { 271 | vp := handlePacket(w.log, p) 272 | w.updateHosts(vp, hosts) 273 | } 274 | } 275 | 276 | // Consider using a graph database for storing all directed interactions 277 | // between the hosts. 278 | 279 | // InvalidHost can be used to represent a newly found Host, where there is only 280 | // a non-empty Curr. 281 | var InvalidHost = Host{} 282 | 283 | func (w *Watcher) updateHosts( 284 | vp ViewPair, 285 | hosts map[MAC]*Host, 286 | ) { 287 | w.updateHostWithView(hosts, vp, vp.Src) 288 | // TODO: There are some bugs here with the double updating, with 289 | // duplicate new hosts being detected. 290 | // 291 | // w.updateHostWithView(v.Dst, hosts) 292 | } 293 | 294 | func (w *Watcher) updateHostWithView( 295 | hosts map[MAC]*Host, 296 | vp ViewPair, 297 | v View, 298 | ) { 299 | now := time.Now() 300 | 301 | // TODO: Relieve this handicap, which is an artifact of the hosts 302 | // map[MAC]*Host datastructure, which should be made more 303 | // flexible. 304 | if v.MAC == nil { 305 | return 306 | } 307 | 308 | prev := findHost(v, hosts) 309 | var curr *Host 310 | if prev == nil { 311 | curr = NewHost(*v.MAC, w.events, now, func(h *Host) { 312 | up := time.Since(h.Activity.FirstSeenEpisode) 313 | w.events <- Event{ 314 | Type: HostLost, 315 | Body: EventHostLost{h, up}, 316 | } 317 | }) 318 | hosts[*v.MAC] = curr 319 | w.events <- Event{ 320 | Type: HostNew, 321 | Body: EventHostNew{curr}, 322 | } 323 | } else { 324 | if time.Since(prev.Activity.LastSeen) > ttlHost { 325 | down := time.Since(prev.Activity.LastSeen) 326 | w.events <- Event{ 327 | Type: HostFound, 328 | Body: EventHostFound{prev, down}, 329 | } 330 | } 331 | curr = prev 332 | // TODO: Add timestamp arg to Touch method. 333 | curr.Activity.Touch(now) 334 | w.log.Debugf("touch host %s", curr) 335 | w.events <- Event{ 336 | Type: HostTouch, 337 | Body: EventHostTouch{curr}, 338 | } 339 | } 340 | 341 | if v.Hostname != "" { 342 | if curr.Hostname != v.Hostname { 343 | w.log.Warnf("hostname has changed %s -> %s", curr.Hostname, v.Hostname) 344 | } 345 | curr.Hostname = v.Hostname 346 | } 347 | 348 | // Update ARP scan. 349 | if vp.Layers[layers.LayerTypeARP] > 0 { 350 | // TODO: Use a View timestamp. 351 | curr.arps.Add(now) 352 | } 353 | freq := curr.arps.Freq() 354 | if freq >= arpScanFreq { 355 | if !curr.ActivityARPScan.Touch(now) { 356 | w.events <- Event{ 357 | Type: HostARPScanStart, 358 | Body: EventHostARPScanStart{curr}, 359 | } 360 | } 361 | } 362 | 363 | // TODO: Display differences, which may be a job for 364 | // findHost. 365 | if v.IPv4 != nil && !v.IPv4.Equal(net.IPv4zero) { 366 | if !curr.IPv4.Equal(v.IPv4) { 367 | w.log.Debugf("host %s changed ips %s -> %s", curr, curr.IPv4, v.IPv4) 368 | } 369 | curr.IPv4 = v.IPv4 370 | } 371 | if v.IPv6 != nil && !v.IPv6.Equal(net.IPv6zero) { 372 | if !curr.IPv6.Equal(v.IPv6) { 373 | w.log.Debugf("host %s changed ips %s -> %s", curr, curr.IPv6, v.IPv6) 374 | } 375 | curr.IPv6 = v.IPv6 376 | } 377 | 378 | w.updatePortsWithView(curr, v) 379 | } 380 | 381 | // TODO: Only update dst ports whenever the dst host is active. 382 | func (w *Watcher) updatePortsWithView(h *Host, v View) { 383 | now := time.Now() 384 | 385 | for num := range v.TCP { 386 | prev, ok := h.TCP[num] 387 | var curr *Port 388 | if !ok { 389 | curr = NewPortTCP(num, now, func(p *Port) { 390 | w.events <- Event{ 391 | Type: PortLost, 392 | Body: EventPortLost{p, p.Activity.Up(), h}, 393 | } 394 | }) 395 | h.TCP[num] = curr 396 | w.events <- Event{ 397 | Type: PortNew, 398 | Body: EventPortNew{curr, h}, 399 | } 400 | } else { 401 | if time.Since(prev.Activity.LastSeen) > ttlPort { 402 | // We consider the host to have been alive for 403 | // ttlPort nanoseconds after it was last seen. 404 | down := time.Since(prev.Activity.LastSeen) - ttlPort 405 | w.events <- Event{ 406 | Type: PortFound, 407 | Body: EventPortFound{prev, down, h}, 408 | } 409 | } 410 | curr = prev 411 | curr.Activity.Touch(now) 412 | w.log.Debugf("touch host %s on %s", curr, h.IPv4) 413 | } 414 | 415 | } 416 | for num := range v.UDP { 417 | prev, ok := h.UDP[num] 418 | var curr *Port 419 | if !ok { 420 | curr = NewPortUDP(num, now, func(p *Port) { 421 | w.events <- Event{ 422 | Type: PortLost, 423 | Body: EventPortLost{p, p.Activity.Up(), h}, 424 | } 425 | }) 426 | h.UDP[num] = curr 427 | w.events <- Event{ 428 | Type: PortNew, 429 | Body: EventPortNew{curr, h}, 430 | } 431 | } else { 432 | if time.Since(prev.Activity.LastSeen) > ttlPort { 433 | // We consider the host to have been alive for 434 | // ttlPort nanoseconds after it was last seen. 435 | down := time.Since(prev.Activity.LastSeen) - ttlPort 436 | w.events <- Event{ 437 | Type: PortFound, 438 | Body: EventPortFound{prev, down, h}, 439 | } 440 | } 441 | curr = prev 442 | curr.Activity.Touch(now) 443 | w.log.Debugf("touch host %s on %s", curr, h.IPv4) 444 | w.events <- Event{ 445 | Type: PortTouch, 446 | Body: EventPortTouch{curr, h}, 447 | } 448 | } 449 | } 450 | } 451 | 452 | // findHost tries to find a host associated with the given view. 453 | // 454 | // TODO: Use more sophisticated host association techniques, such as using 455 | // previously seen ip address. 456 | func findHost(v View, hosts map[MAC]*Host) *Host { 457 | if v.MAC == nil { 458 | return nil 459 | } 460 | return hosts[*v.MAC] 461 | } 462 | 463 | func addIP(v *View, ip net.IP) { 464 | if len(ip) == net.IPv4len { 465 | v.IPv4 = ip 466 | } else if len(ip) == net.IPv6len { 467 | v.IPv6 = ip 468 | } else { 469 | util.NewLogger().Warnf("invalid ip len=%d: %#v", len(ip), ip) 470 | } 471 | } 472 | 473 | type windowed struct { 474 | size time.Duration 475 | mu sync.Mutex 476 | entries []time.Time 477 | } 478 | 479 | func newWindowed(size time.Duration) *windowed { 480 | return &windowed{size: size} 481 | } 482 | 483 | // Add adds an entry with the given timestamp. 484 | func (w *windowed) Add(ts time.Time) { 485 | w.mu.Lock() 486 | w.entries = append(w.entries, ts) 487 | if len(w.entries)%50 == 0 { 488 | w.flush() 489 | } 490 | w.mu.Unlock() 491 | } 492 | 493 | func (w *windowed) flush() { 494 | now := time.Now() 495 | cut := now.Add(-w.size) 496 | i := sort.Search(len(w.entries), func(i int) bool { 497 | return w.entries[i].After(cut) 498 | }) 499 | w.entries = w.entries[i:] 500 | } 501 | 502 | // Count returns the nubmer of entries in the window size. 503 | func (w *windowed) Count() int { 504 | w.flush() 505 | return len(w.entries) 506 | } 507 | 508 | // Freq returns the current Count per second. 509 | func (w *windowed) Freq() float64 { 510 | return float64(w.Count()) / w.size.Seconds() 511 | } 512 | -------------------------------------------------------------------------------- /watch/subscriber.go: -------------------------------------------------------------------------------- 1 | package watch 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // Subscriber handles a single event and reacts to it. A Subscriber can be 10 | // wrapped within a Trigger if they wish to filter which Events are recieved by 11 | // the Subscriber. 12 | type Subscriber func(e Event) error 13 | 14 | // FilteredSubscriber combines a Subscriber with an event filter, so that only 15 | // those events that return true for ShouldDo are given to enclosed Subscriber. 16 | type FilteredSubscriber struct { 17 | Sub Subscriber 18 | ShouldDo func(e Event) bool 19 | } 20 | 21 | // NewSubNull does nothing for each event. This is useful for debugging 22 | // handling of events, where you don't necessarily want to do anything in 23 | // response to the events. 24 | func NewSubNull(log *logrus.Logger) Subscriber { 25 | return func(e Event) error { 26 | return nil 27 | } 28 | } 29 | 30 | // NewSubLogger returns a new logging Subscriber. For each event, some 31 | // hopefully useful information is logged. 32 | func NewSubLogger(log *logrus.Logger) Subscriber { 33 | return func(e Event) error { 34 | switch e.Type { 35 | case HostTouch: 36 | e := e.Body.(EventHostTouch) 37 | log.Infof("touch %s", e.Host) 38 | case HostNew: 39 | e := e.Body.(EventHostNew) 40 | log.Infof("new %s", e.Host) 41 | case HostLost: 42 | e := e.Body.(EventHostLost) 43 | log.Infof("drop %s (up %s)", e.Host, e.Up) 44 | case HostFound: 45 | e := e.Body.(EventHostFound) 46 | log.Infof("return %s (down %s)", e.Host, e.Down) 47 | case HostARPScanStart: 48 | e := e.Body.(EventHostARPScanStart) 49 | log.Infof("host started arp scan %s", e.Host) 50 | case HostARPScanStop: 51 | e := e.Body.(EventHostARPScanStop) 52 | log.Infof("host stopped arp scan %s (up %s)", e.Host, e.Up) 53 | case PortTouch: 54 | e := e.Body.(EventPortTouch) 55 | log.Infof("touch %s on %s", e.Port, e.Host) 56 | case PortNew: 57 | e := e.Body.(EventPortNew) 58 | log.Infof("new %s on %s", e.Port, e.Host) 59 | case PortLost: 60 | e := e.Body.(EventPortLost) 61 | log.Infof("drop %s (up %s) on %s", e.Port, e.Up, e.Host) 62 | case PortFound: 63 | e := e.Body.(EventPortFound) 64 | log.Infof("return %s (down %s) on %s", e.Port, e.Down, e.Host) 65 | default: 66 | panic(fmt.Sprintf("unhandled event type: %#v", e)) 67 | } 68 | return nil 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /watch/watcher.go: -------------------------------------------------------------------------------- 1 | package watch 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "html/template" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "time" 12 | 13 | "github.com/BurntSushi/toml" 14 | "github.com/google/gopacket" 15 | "github.com/google/gopacket/pcap" 16 | "github.com/pkg/errors" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | // Watcher watches network activity and sends resultant Events to all of it's 21 | // Subscribers. 22 | type Watcher struct { 23 | log *logrus.Logger 24 | events chan Event 25 | subs []Subscriber 26 | } 27 | 28 | // NewWatcher creates a new watcher initialized with the given subscribers. 29 | func NewWatcher(log *logrus.Logger, subs ...Subscriber) *Watcher { 30 | if len(subs) == 0 { 31 | subs = []Subscriber{NewSubLogger(log)} 32 | } 33 | return &Watcher{ 34 | log: log, 35 | events: make(chan Event, 32), 36 | subs: subs, 37 | } 38 | } 39 | 40 | // Watch scans the given src for packets, and publish resultant Events to all 41 | // of it's registered Subscribers. 42 | func (w *Watcher) Watch(ctx context.Context, src *gopacket.PacketSource) error { 43 | hosts := make(map[MAC]*Host) 44 | go w.ScanPackets(hosts, src.Packets()) 45 | return w.Publish() 46 | } 47 | 48 | // WatchLive watches from the first good interface, and blocks forever. 49 | func (w *Watcher) WatchLive(ctx context.Context, iface string) error { 50 | h, err := pcap.OpenLive(iface, 65536, true, pcap.BlockForever) 51 | if err != nil { 52 | return err 53 | } 54 | src := gopacket.NewPacketSource(h, h.LinkType()) 55 | return w.Watch(ctx, src) 56 | 57 | } 58 | 59 | // WatchPCAP watches from a predefined pcap file. 60 | func (w *Watcher) WatchPCAP(ctx context.Context, pcapPath string) error { 61 | h, err := pcap.OpenOffline(pcapPath) 62 | if err != nil { 63 | return err 64 | } 65 | src := gopacket.NewPacketSource(h, h.LinkType()) 66 | return w.Watch(ctx, src) 67 | } 68 | 69 | // Publish endlessly reads incomming events, and sends a shallow copy of that 70 | // event to each of this Watcher's Subscribers. 71 | func (w *Watcher) Publish() error { 72 | for e := range w.events { 73 | for _, sub := range w.subs { 74 | if err := sub(e); err != nil { 75 | w.log.WithError(err).Errorf("failed to respond to event") 76 | } 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | // NewSubConfig returns a new Subscriber 83 | func NewSubConfig( 84 | log *logrus.Logger, 85 | path string, 86 | only []string, 87 | ) (Subscriber, error) { 88 | var conf Config 89 | if _, err := toml.DecodeFile(path, &conf); err != nil { 90 | return nil, err 91 | } 92 | // TODO: validate config, e.g. not on event and on events, etc. 93 | 94 | triggers := make(map[string]FilteredSubscriber) 95 | onlySet := stringSet(only) 96 | for name, spec := range conf.Triggers { 97 | if len(onlySet) > 0 && !onlySet[name] { 98 | continue 99 | } 100 | log.Debugf("loading subscriber %s", name) 101 | trig := newTriggerFromConfig(log, name, spec) 102 | if spec.Disabled && !onlySet[name] { 103 | continue 104 | } 105 | triggers[name] = trig 106 | } 107 | if len(triggers) == 0 { 108 | log.Fatal("no subscribers loaded") 109 | } 110 | 111 | return func(e Event) error { 112 | for name, trig := range triggers { 113 | if !trig.ShouldDo(e) { 114 | continue 115 | } 116 | if err := trig.Sub(e); err != nil { 117 | log.WithError(err).Errorf("failed to execute sub: %s", name) 118 | } 119 | } 120 | return nil 121 | }, nil 122 | } 123 | 124 | func newTriggerFromConfig( 125 | log *logrus.Logger, 126 | name string, 127 | spec TriggerSpec, 128 | ) FilteredSubscriber { 129 | var sub Subscriber 130 | if spec.DoBuiltin != "" { 131 | sub = newSubFromBuiltin(log, spec.DoBuiltin) 132 | } 133 | if spec.DoShell != "" { 134 | sub = newSubFromShell(context.TODO(), log, spec.DoShell) 135 | } 136 | if sub == nil { 137 | log.Fatalf( 138 | "failed to construct a trigger, "+ 139 | "did you fill out doBuiltin or doShell?: %#v", 140 | spec, 141 | ) 142 | } 143 | var shouldDo func(e Event) bool 144 | if spec.OnShell != "" { 145 | shouldDo = newShouldDoFromShell(context.TODO(), log, spec.OnShell) 146 | } 147 | return FilteredSubscriber{ 148 | Sub: sub, 149 | ShouldDo: func(e Event) bool { 150 | if spec.OnAny { 151 | return true 152 | } 153 | if shouldDo != nil { 154 | return shouldDo(e) 155 | } 156 | if len(spec.OnEventsExcept) > 0 { 157 | for _, ty := range spec.OnEventsExcept { 158 | if ty == e.Type { 159 | return false 160 | } 161 | } 162 | return true 163 | } 164 | for _, ty := range spec.OnEvents { 165 | if ty == e.Type { 166 | return true 167 | } 168 | } 169 | return false 170 | }, 171 | } 172 | } 173 | 174 | func newSubFromBuiltin(log *logrus.Logger, builtin string) Subscriber { 175 | var sub Subscriber 176 | switch strings.ToLower(builtin) { 177 | case "null": 178 | sub = NewSubNull(log) 179 | case "log": 180 | sub = NewSubLogger(log) 181 | default: 182 | panic(fmt.Sprintf("unsupported sub name: '%s'", builtin)) 183 | } 184 | return sub 185 | } 186 | 187 | func newSubFromShell( 188 | ctx context.Context, 189 | log *logrus.Logger, 190 | shell string, 191 | ) Subscriber { 192 | return func(e Event) error { 193 | shell = os.ExpandEnv(shell) 194 | tmpl, err := template.New("").Parse(shell) 195 | if err != nil { 196 | return err 197 | } 198 | info := newEventInfo(e) 199 | var buf bytes.Buffer 200 | err = tmpl.Execute(&buf, info) 201 | if err != nil { 202 | return err 203 | } 204 | cmd := exec.CommandContext(ctx, "/bin/sh", "-c", buf.String()) 205 | b, err := cmd.CombinedOutput() 206 | if err != nil { 207 | return errors.Wrapf(err, "failed to run command: %v", string(b)) 208 | } 209 | out := strings.TrimSpace(string(b)) 210 | fmt.Println(out) 211 | return nil 212 | } 213 | } 214 | 215 | func newShouldDoFromShell( 216 | ctx context.Context, 217 | log *logrus.Logger, 218 | shell string, 219 | ) func(e Event) bool { 220 | shell = os.ExpandEnv(shell) 221 | tmpl, err := template.New("").Parse(shell) 222 | if err != nil { 223 | log.WithError(err).Fatalf("failed to template parse shell: %s", shell) 224 | } 225 | return func(e Event) bool { 226 | info := newEventInfo(e) 227 | var buf bytes.Buffer 228 | err = tmpl.Execute(&buf, info) 229 | if err != nil { 230 | log.WithError(err).Fatalf("failed to execute template") 231 | return false 232 | } 233 | cmd := exec.CommandContext(ctx, "/bin/sh", "-c", buf.String()) 234 | b, err := cmd.CombinedOutput() 235 | if err != nil { 236 | // The point of this shell command is to return a 237 | // non-zero exit code when an event should be skipped. 238 | // However, we also log so as to not preclude 239 | // debugging. 240 | log.Debugf("failed to run output: %v: %s", err, string(b)) 241 | return false 242 | } 243 | return true 244 | } 245 | } 246 | 247 | type printableEvent struct { 248 | Description string 249 | Host Host 250 | Port Port 251 | PortString string 252 | Up time.Duration 253 | Down time.Duration 254 | Age time.Duration 255 | } 256 | 257 | func newEventInfo(e Event) printableEvent { 258 | var pe printableEvent 259 | switch e.Type { 260 | case HostTouch: 261 | e := e.Body.(EventHostTouch) 262 | pe.Host = *e.Host 263 | pe.Description = fmt.Sprintf( 264 | "touched host %s at %s (up %s) (age %s)", 265 | e.Host.MAC, 266 | e.Host.IPv4, 267 | time.Since(e.Host.Activity.FirstSeenEpisode), 268 | e.Host.Activity.Age(), 269 | ) 270 | case HostNew: 271 | e := e.Body.(EventHostNew) 272 | pe.Host = *e.Host 273 | pe.Age = e.Host.Activity.Age() 274 | pe.Description = fmt.Sprintf( 275 | "new host %s at %s", 276 | e.Host.MAC, 277 | e.Host.IPv4, 278 | ) 279 | case HostLost: 280 | e := e.Body.(EventHostLost) 281 | pe.Host = *e.Host 282 | pe.Up = e.Up 283 | pe.Description = fmt.Sprintf( 284 | "new host %s at %s (up %s) (age %s)", 285 | e.Host.MAC, 286 | e.Host.IPv4, 287 | e.Up, 288 | e.Host.Activity.Age(), 289 | ) 290 | case HostFound: 291 | e := e.Body.(EventHostFound) 292 | pe.Host = *e.Host 293 | pe.Down = e.Down 294 | pe.Description = fmt.Sprintf( 295 | "found host %s at %s (down %s) (age %s)", 296 | e.Host.MAC, 297 | e.Host.IPv4, 298 | e.Down, 299 | e.Host.Activity.Age(), 300 | ) 301 | case HostARPScanStart: 302 | e := e.Body.(EventHostARPScanStart) 303 | pe.Host = *e.Host 304 | pe.Description = fmt.Sprintf( 305 | "%s started arp scan started", 306 | e.Host, 307 | ) 308 | case HostARPScanStop: 309 | e := e.Body.(EventHostARPScanStop) 310 | pe.Host = *e.Host 311 | pe.Description = fmt.Sprintf( 312 | "%s stopped arp scan", 313 | e.Host, 314 | ) 315 | case PortTouch: 316 | e := e.Body.(EventPortTouch) 317 | pe.Port = *e.Port 318 | pe.Host = *e.Host 319 | pe.PortString = e.Port.String() 320 | pe.Description = fmt.Sprintf( 321 | "touched port %s at %s (up %s)", 322 | pe.Port, 323 | pe.Host.IPv4, 324 | pe.Host.Activity.Age(), 325 | ) 326 | case PortNew: 327 | e := e.Body.(EventPortNew) 328 | pe.Port = *e.Port 329 | pe.Host = *e.Host 330 | pe.PortString = e.Port.String() 331 | pe.Description = fmt.Sprintf( 332 | "new port %s at %s (age %s)", 333 | e.Port, 334 | e.Host.IPv4, 335 | e.Port.Activity.Age(), 336 | ) 337 | case PortLost: 338 | e := e.Body.(EventPortLost) 339 | pe.Port = *e.Port 340 | pe.Up = e.Up 341 | pe.Host = *e.Host 342 | pe.PortString = e.Port.String() 343 | pe.Description = fmt.Sprintf( 344 | "new port %s at %s (up %s) (age %s)", 345 | e.Port, 346 | e.Host.IPv4, 347 | e.Up, 348 | e.Port.Activity.Age(), 349 | ) 350 | case PortFound: 351 | e := e.Body.(EventPortFound) 352 | pe.Port = *e.Port 353 | pe.Down = e.Down 354 | pe.Host = *e.Host 355 | pe.PortString = e.Port.String() 356 | pe.Description = fmt.Sprintf( 357 | "found port %s at %s (down %s) (age %s)", 358 | e.Port, 359 | e.Host.IPv4, 360 | e.Down, 361 | e.Port.Activity.Age(), 362 | ) 363 | default: 364 | panic(fmt.Sprintf("unhandled event type: %#v", e)) 365 | } 366 | return pe 367 | } 368 | 369 | func stringSet(slice []string) map[string]bool { 370 | m := make(map[string]bool) 371 | for _, s := range slice { 372 | m[s] = true 373 | } 374 | return m 375 | } 376 | --------------------------------------------------------------------------------