From 5cea0a4ca2fdcd6ae18269a1bd4114d55563d0b9 Mon Sep 17 00:00:00 2001 From: Martin/Geno Date: Sat, 2 Jun 2018 01:00:54 +0200 Subject: [PATCH] capture packages --- .drone.yml | 26 +++++++ capture/collector.go | 161 ++++++++++++++++++++++++++++++++++++++++ capture/const.go | 12 +++ capture/iface.go | 8 ++ cmd/dump.go | 62 ++++++++++++++++ cmd/root.go | 33 ++++++++ data/handler.go | 5 ++ data/socket_msg.go | 125 +++++++++++++++++++++++++++++++ data/socket_msg_test.go | 58 +++++++++++++++ data/wifi_client.go | 30 ++++++++ main.go | 7 ++ 11 files changed, 527 insertions(+) create mode 100644 .drone.yml create mode 100644 capture/collector.go create mode 100644 capture/const.go create mode 100644 capture/iface.go create mode 100644 cmd/dump.go create mode 100644 cmd/root.go create mode 100644 data/handler.go create mode 100644 data/socket_msg.go create mode 100644 data/socket_msg_test.go create mode 100644 data/wifi_client.go create mode 100644 main.go diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..8e930d3 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,26 @@ +workspace: + base: /go + path: src/dev.sum7.eu/wifictld/analyzer + +pipeline: + build: + image: golang:latest + commands: + - go get ./... + - go build + codestyle: + image: golang:latest + commands: + - go get github.com/client9/misspell/cmd/misspell + - misspell -error . + - if [ -n "$(gofmt -s -l .)" ]; then echo "Go code is not formatted, run 'gofmt -s -w .'" >&2; exit 1; fi + test: + image: golang:latest + commands: + - go get github.com/stretchr/testify/assert + - go test ./... -v -cover + test-race: + image: golang:latest + commands: + - go get github.com/stretchr/testify/assert + - go test ./... -v -race diff --git a/capture/collector.go b/capture/collector.go new file mode 100644 index 0000000..c99cb35 --- /dev/null +++ b/capture/collector.go @@ -0,0 +1,161 @@ +package capture + +import ( + "net" + + "dev.sum7.eu/wifictld/analyzer/data" + log "github.com/sirupsen/logrus" +) + +//Collector for capture +type Collector struct { + connections map[string]*net.UDPConn + handler data.Handler + queue chan *Packet + stop chan interface{} +} + +// NewCollector creates a Collector struct +func NewCollector(handler data.Handler, ifaces []IFaceConfig) *Collector { + + coll := &Collector{ + handler: handler, + queue: make(chan *Packet, 400), + stop: make(chan interface{}), + connections: make(map[string]*net.UDPConn), + } + + for _, iface := range ifaces { + coll.listenUDP(iface) + } + + go coll.parser() + + return coll +} + +// Close Collector +func (coll *Collector) Close() { + close(coll.stop) + for _, conn := range coll.connections { + conn.Close() + } + close(coll.queue) +} + +func (coll *Collector) listenUDP(iface IFaceConfig) { + ip := net.ParseIP(iface.IPAddress) + var conn *net.UDPConn + var err error + + if ip.IsMulticast() { + ifx, err := net.InterfaceByName(iface.InterfaceName) + if err != nil { + log.Panic(err) + } + + // Open socket + conn, err = net.ListenMulticastUDP("udp", ifx, &net.UDPAddr{ + IP: ip, + Port: iface.Port, + }) + if err != nil { + log.Panic(err) + } + } else { + // Open socket + conn, err = net.ListenUDP("udp", &net.UDPAddr{ + IP: ip, + Port: iface.Port, + Zone: iface.InterfaceName, + }) + if err != nil { + log.Panic(err) + } + } + + conn.SetReadBuffer(MaxDataGramSize) + + coll.connections[iface.InterfaceName] = conn + + // Start receiver + go coll.receiver(conn) +} + +// Packet of the respond request +type Packet struct { + Address *net.UDPAddr + Raw []byte +} + +func (coll *Collector) parser() { + for obj := range coll.queue { + msg, err := data.NewSocketMSG(obj.Raw) + if err != nil { + log.Warnf("unable to unmarshal request from %s: %s", obj.Address.String(), err) + continue + } + response, err := coll.handler(obj.Address, msg) + if err != nil { + log.Warnf("unable to handle request from %s: %s", obj.Address.String(), err) + continue + } + if response != nil { + coll.SendTo(obj.Address, response) + } + } +} + +// SendTo a specifical address +func (coll *Collector) SendTo(addr *net.UDPAddr, msg *data.SocketMSG) { + data, err := msg.Marshal() + if err != nil { + log.Warnf("unable to marshal response for %s: %s", addr.String(), err) + return + } + conn, ok := coll.connections[addr.Zone] + if ok { + conn.WriteToUDP(data, addr) + return + } + for _, conn := range coll.connections { + conn.WriteToUDP(data, addr) + } +} + +// Send to every connection to default address +func (coll *Collector) Send(msg *data.SocketMSG) { + data, err := msg.Marshal() + if err != nil { + log.Warnf("unable to marshal response: %s", err) + return + } + for ifname, conn := range coll.connections { + conn.WriteToUDP(data, &net.UDPAddr{ + IP: net.ParseIP(MulticastAddressDefault), + Port: Port, + Zone: ifname, + }) + } +} + +func (coll *Collector) receiver(conn *net.UDPConn) { + buf := make([]byte, MaxDataGramSize) + + for { + n, src, err := conn.ReadFromUDP(buf) + + if err != nil { + log.Warnf("ReadFromUDP failed: %s", err) + return + } + + raw := make([]byte, n) + copy(raw, buf) + + coll.queue <- &Packet{ + Address: src, + Raw: raw, + } + } +} diff --git a/capture/const.go b/capture/const.go new file mode 100644 index 0000000..2942e40 --- /dev/null +++ b/capture/const.go @@ -0,0 +1,12 @@ +package capture + +const ( + // MulticastAddressDefault default multicast group used by announced + MulticastAddressDefault = "ff02::31f1" + + // Port default udp port used by announced + Port = 1000 + + // MaxDataGramSize maximum receivable size + MaxDataGramSize = 256 +) diff --git a/capture/iface.go b/capture/iface.go new file mode 100644 index 0000000..198e101 --- /dev/null +++ b/capture/iface.go @@ -0,0 +1,8 @@ +package capture + +// IFaceConfig where to listen +type IFaceConfig struct { + InterfaceName string `toml:"ifname"` + Port int `toml:"port"` + IPAddress string `toml:"ip_address"` +} diff --git a/cmd/dump.go b/cmd/dump.go new file mode 100644 index 0000000..50553f4 --- /dev/null +++ b/cmd/dump.go @@ -0,0 +1,62 @@ +package cmd + +import ( + "net" + "os" + "os/signal" + "strings" + "syscall" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "dev.sum7.eu/wifictld/analyzer/capture" + "dev.sum7.eu/wifictld/analyzer/data" +) + +var ( + port int + ipAddress string +) + +// queryCmd represents the query command +var dumpCmd = &cobra.Command{ + Use: "dump ", + Short: "capture wifictld traffic and just display the values (like wireshark)", + Example: `analyzer dump "eth0,wlan0"`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ifaces := strings.Split(args[0], ",") + + log.Infof("listen on: %s", ifaces) + + var ifacesConfigs []capture.IFaceConfig + for _, iface := range ifaces { + ifaceConfig := capture.IFaceConfig{ + InterfaceName: iface, + Port: port, + IPAddress: ipAddress, + } + ifacesConfigs = append(ifacesConfigs, ifaceConfig) + } + + coll := capture.NewCollector(func(addr *net.UDPAddr, msg *data.SocketMSG) (*data.SocketMSG, error) { + log.Infof("recv[%s]: %s", addr, msg.String()) + return nil, nil + }, ifacesConfigs) + defer coll.Close() + + // Wait for INT/TERM + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + sig := <-sigs + log.Println("received", sig) + + }, +} + +func init() { + RootCmd.AddCommand(dumpCmd) + dumpCmd.Flags().IntVar(&port, "port", capture.Port, "define a port to listen (if not set or set to 0 the kernel will use a random free port at its own)") + dumpCmd.Flags().StringVar(&ipAddress, "listen", capture.MulticastAddressDefault, "") +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..69b897c --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,33 @@ +package cmd + +import ( + log "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" +) + +var debug bool + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "analyzer", + Short: "wifictld analyzer", + Long: `capture wifictld traffic and display thus`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := RootCmd.Execute(); err != nil { + log.Error(err) + } +} +func init() { + cobra.OnInitialize(func() { + if debug { + log.SetLevel(log.DebugLevel) + } + log.Debug("show debug") + }) + RootCmd.PersistentFlags().BoolVar(&debug, "v", false, "show debug log") +} diff --git a/data/handler.go b/data/handler.go new file mode 100644 index 0000000..bc0d878 --- /dev/null +++ b/data/handler.go @@ -0,0 +1,5 @@ +package data + +import "net" + +type Handler func(addr *net.UDPAddr, msg *SocketMSG) (*SocketMSG, error) diff --git a/data/socket_msg.go b/data/socket_msg.go new file mode 100644 index 0000000..6d7ead9 --- /dev/null +++ b/data/socket_msg.go @@ -0,0 +1,125 @@ +package data + +import ( + "encoding/binary" + "fmt" + "net" + "time" + + log "github.com/sirupsen/logrus" +) + +// SocketMSGType kind of packages +type SocketMSGType uint32 + +const ( + SocketMSGTypeRequest SocketMSGType = (1 << 0) + SocketMSGTypeResponse SocketMSGType = (1 << 1) + SocketMSGTypeClient SocketMSGType = (1 << 2) + SocketMSGTypeStats SocketMSGType = (1 << 3) +) + +func (a SocketMSGType) Is(b SocketMSGType) bool { + log.Debugf("SocketType: %x & %x = %x -> %b", a, b, (a & b), (a&b) > 0) + return (a & b) > 0 +} + +// SocketMSG package of wifictld format +type SocketMSG struct { + Types SocketMSGType + Client *WifiClient +} + +func NewSocketMSG(obj []byte) (*SocketMSG, error) { + msg := &SocketMSG{} + if err := msg.Unmarshal(obj); err != nil { + return nil, err + } + return msg, nil +} + +func (msg *SocketMSG) Marshal() ([]byte, error) { + obj := make([]byte, MaxDataGramSize) + pos := 0 + + binary.BigEndian.PutUint32(obj[pos:(pos+4)], uint32(msg.Types)) + pos += 4 + + if msg.Types.Is(SocketMSGTypeClient) { + obj[pos] = msg.Client.Addr[0] + pos++ + obj[pos] = msg.Client.Addr[1] + pos++ + obj[pos] = msg.Client.Addr[2] + pos++ + obj[pos] = msg.Client.Addr[3] + pos++ + obj[pos] = msg.Client.Addr[4] + pos++ + obj[pos] = msg.Client.Addr[5] + pos++ + binary.BigEndian.PutUint32(obj[pos:(pos+4)], uint32(msg.Client.Time.Unix())) + pos += 4 + binary.BigEndian.PutUint16(obj[pos:(pos+2)], msg.Client.TryProbe) + pos += 2 + binary.BigEndian.PutUint16(obj[pos:(pos+2)], msg.Client.TryAuth) + pos += 2 + if msg.Client.Connected { + obj[pos] = byte(1) + } + pos++ + if msg.Client.Authed { + obj[pos] = byte(1) + } + pos++ + binary.BigEndian.PutUint16(obj[pos:(pos+2)], msg.Client.FreqHighest) + pos += 2 + binary.BigEndian.PutUint16(obj[pos:(pos+2)], uint16(msg.Client.SignalLowFreq)) + pos += 2 + binary.BigEndian.PutUint16(obj[pos:(pos+2)], uint16(msg.Client.SignalHighFreq)) + //pos += 2 + } + + return obj, nil +} + +func (msg *SocketMSG) Unmarshal(obj []byte) error { + log.Debugf("hex unmarshal: %x", obj) + + pos := 0 + + msg.Types = SocketMSGType(binary.BigEndian.Uint32(obj[pos:(pos + 4)])) + pos += 4 + + if msg.Types.Is(SocketMSGTypeClient) { + msg.Client = &WifiClient{ + Addr: net.HardwareAddr(obj[pos:(pos + 6)]), + Time: time.Unix(int64(binary.BigEndian.Uint32(obj[(pos+6):(pos+10)])), 0), + TryProbe: binary.BigEndian.Uint16(obj[(pos + 10):(pos + 12)]), + TryAuth: binary.BigEndian.Uint16(obj[(pos + 12):(pos + 14)]), + Connected: (obj[(pos+14)] == 1), + Authed: (obj[(pos+15)] == 1), + FreqHighest: binary.BigEndian.Uint16(obj[(pos + 16):(pos + 18)]), + SignalLowFreq: int16(binary.BigEndian.Uint16(obj[(pos + 18):(pos + 20)])), + SignalHighFreq: int16(binary.BigEndian.Uint16(obj[(pos + 20):(pos + 22)])), + } + //pos += 22 + } + return nil +} + +func (msg *SocketMSG) String() string { + answer := "" + + if msg.Types.Is(SocketMSGTypeRequest) { + answer += "request " + } + if msg.Types.Is(SocketMSGTypeResponse) { + answer += "response " + } + if msg.Types.Is(SocketMSGTypeClient) { + answer += fmt.Sprintf("client(mac=%s freq=%d ssi_l=%d ssi_h=%d tprobe=%d tauth=%d authed=%v time=%s) ", msg.Client.Addr, msg.Client.FreqHighest, msg.Client.SignalLowFreq, msg.Client.SignalHighFreq, msg.Client.TryProbe, msg.Client.TryAuth, msg.Client.Authed, msg.Client.Time.String()) + } + + return answer +} diff --git a/data/socket_msg_test.go b/data/socket_msg_test.go new file mode 100644 index 0000000..12c4136 --- /dev/null +++ b/data/socket_msg_test.go @@ -0,0 +1,58 @@ +package data + +import ( + "encoding/hex" + "log" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestMsgIsTypes(t *testing.T) { + assert := assert.New(t) + types := SocketMSGType(5) + + assert.True(types.Is(SocketMSGTypeRequest)) + assert.False(types.Is(SocketMSGTypeResponse)) + assert.True(types.Is(SocketMSGTypeClient)) + assert.False(types.Is(SocketMSGTypeStats)) + + assert.Equal(SocketMSGType(5), SocketMSGType(SocketMSGTypeRequest|SocketMSGTypeClient)) +} + +func TestMsgString(t *testing.T) { + assert := assert.New(t) + msg := SocketMSG{ + Types: SocketMSGTypeRequest | SocketMSGTypeResponse | SocketMSGTypeClient, + Client: &WifiClient{ + FreqHighest: 2464, + }, + } + assert.Equal("request response client(mac= freq=2464 ssi_l=0 ssi_h=0 tprobe=0 tauth=0 authed=false time=0001-01-01 00:00:00 +0000 UTC) ", msg.String()) +} + +func TestMsgMarshal(t *testing.T) { + assert := assert.New(t) + + dst := make([]byte, MaxDataGramSize) + _, err := hex.Decode(dst, []byte("00000005f0cfa13375295b0f8b92000000000101099effa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")) + if err != nil { + log.Fatal(err) + } + + msg, _ := NewSocketMSG(dst) + assert.Equal(SocketMSGType(SocketMSGTypeRequest|SocketMSGTypeClient), msg.Types) + assert.Equal("f0:cf:a1:33:75:29", msg.Client.Addr.String()) + assert.Equal("2018-05-31T05:43:46Z", msg.Client.Time.UTC().Format(time.RFC3339)) + assert.Equal(0, int(msg.Client.TryProbe)) + assert.Equal(0, int(msg.Client.TryAuth)) + assert.True(msg.Client.Connected) + assert.True(msg.Client.Authed) + assert.Equal(2462, int(msg.Client.FreqHighest)) + assert.Equal(-93, int(msg.Client.SignalLowFreq)) + assert.Equal(0, int(msg.Client.SignalHighFreq)) + + result, _ := msg.Marshal() + assert.Equal(dst, result) +} diff --git a/data/wifi_client.go b/data/wifi_client.go new file mode 100644 index 0000000..d335cdc --- /dev/null +++ b/data/wifi_client.go @@ -0,0 +1,30 @@ +package data + +import ( + "net" + "time" +) + +const ( + // default multicast group used by announced + MulticastAddressDefault = "ff02::31f1" + + // default udp port used by announced + Port = 1000 + + // maximum receivable size + MaxDataGramSize = 256 +) + +// WifiClient datatype of wifictld +type WifiClient struct { + Addr net.HardwareAddr + Time time.Time + TryProbe uint16 + TryAuth uint16 + Connected bool + Authed bool + FreqHighest uint16 + SignalLowFreq int16 + SignalHighFreq int16 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..38ee27f --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "dev.sum7.eu/wifictld/analyzer/cmd" + +func main() { + cmd.Execute() +}