From 3896485d96f15095a2e6ae6540a42d122f7feb38 Mon Sep 17 00:00:00 2001 From: Matt Layher Date: Tue, 17 Jan 2017 00:26:34 -0500 Subject: [PATCH] wifi: add BSS, Client.BSS --- client.go | 6 ++ client_linux.go | 140 +++++++++++++++++++++++++++++- client_linux_test.go | 193 +++++++++++++++++++++++++++++++++++++++++- client_others.go | 5 ++ client_others_test.go | 5 ++ wifi.go | 105 +++++++++++++++++++++++ wifi_test.go | 112 ++++++++++++++++++++++++ 7 files changed, 561 insertions(+), 5 deletions(-) create mode 100644 wifi_test.go diff --git a/client.go b/client.go index ae62e19..3de6dac 100644 --- a/client.go +++ b/client.go @@ -45,6 +45,11 @@ func (c *Client) Interfaces() ([]*Interface, error) { return c.c.Interfaces() } +// BSS retrieves the BSS associated with a WiFi interface. +func (c *Client) BSS(ifi *Interface) (*BSS, error) { + return c.c.BSS(ifi) +} + // StationInfo retrieves statistics about a WiFi interface operating in // station mode. func (c *Client) StationInfo(ifi *Interface) (*StationInfo, error) { @@ -59,5 +64,6 @@ func (c *Client) StationInfo(ifi *Interface) (*StationInfo, error) { type osClient interface { Close() error Interfaces() ([]*Interface, error) + BSS(ifi *Interface) (*BSS, error) StationInfo(ifi *Interface) (*StationInfo, error) } diff --git a/client_linux.go b/client_linux.go index 1eb5398..ed54ec6 100644 --- a/client_linux.go +++ b/client_linux.go @@ -3,11 +3,13 @@ package wifi import ( + "bytes" "errors" "math" "net" "os" "time" + "unicode/utf8" "github.com/mdlayher/netlink" "github.com/mdlayher/netlink/genetlink" @@ -96,10 +98,40 @@ func (c *client) Interfaces() ([]*Interface, error) { return parseInterfaces(msgs) } +// BSS requests that nl80211 return the BSS for the specified Interface. +func (c *client) BSS(ifi *Interface) (*BSS, error) { + b, err := netlink.MarshalAttributes(ifi.idAttrs()) + if err != nil { + return nil, err + } + + // Ask nl80211 to retrieve BSS information for the interface specified + // by its attributes + req := genetlink.Message{ + Header: genetlink.Header{ + Command: nl80211.CmdGetScan, + Version: c.familyVersion, + }, + Data: b, + } + + flags := netlink.HeaderFlagsRequest | netlink.HeaderFlagsDump + msgs, err := c.c.Execute(req, c.familyID, flags) + if err != nil { + return nil, err + } + + if err := c.checkMessages(msgs, nl80211.CmdNewScanResults); err != nil { + return nil, err + } + + return parseBSS(msgs) +} + // StationInfo requests that nl80211 return station info for the specified // Interface. func (c *client) StationInfo(ifi *Interface) (*StationInfo, error) { - b, err := netlink.MarshalAttributes(ifi.stationInfoAttrs()) + b, err := netlink.MarshalAttributes(ifi.idAttrs()) if err != nil { return nil, err } @@ -176,9 +208,9 @@ func parseInterfaces(msgs []genetlink.Message) ([]*Interface, error) { return ifis, nil } -// stationInfoAttrs returns the netlink attributes required from an Interface -// to retrieve a StationInfo. -func (ifi *Interface) stationInfoAttrs() []netlink.Attribute { +// idAttrs returns the netlink attributes required from an Interface to retrieve +// more data about it. +func (ifi *Interface) idAttrs() []netlink.Attribute { return []netlink.Attribute{ { Type: nl80211.AttrIfindex, @@ -217,6 +249,80 @@ func (ifi *Interface) parseAttributes(attrs []netlink.Attribute) error { return nil } +// parseBSS parses a single BSS with a status attribute from nl80211 BSS messages. +func parseBSS(msgs []genetlink.Message) (*BSS, error) { + for _, m := range msgs { + attrs, err := netlink.UnmarshalAttributes(m.Data) + if err != nil { + return nil, err + } + + for _, a := range attrs { + if a.Type != nl80211.AttrBss { + continue + } + + nattrs, err := netlink.UnmarshalAttributes(a.Data) + if err != nil { + return nil, err + } + + // The BSS which is associated with an interface will have a status + // attribute + if !attrsContain(nattrs, nl80211.BssStatus) { + continue + } + + var bss BSS + if err := (&bss).parseAttributes(nattrs); err != nil { + return nil, err + } + + return &bss, nil + } + } + + return nil, os.ErrNotExist +} + +// parseAttributes parses netlink attributes into a BSS's fields. +func (b *BSS) parseAttributes(attrs []netlink.Attribute) error { + for _, a := range attrs { + switch a.Type { + case nl80211.BssBssid: + b.BSSID = net.HardwareAddr(a.Data) + case nl80211.BssFrequency: + b.Frequency = nlenc.Uint32(a.Data) + case nl80211.BssBeaconInterval: + // Raw value is in "Time Units (TU)". See: + // https://en.wikipedia.org/wiki/Beacon_frame + b.BeaconInterval = time.Duration(nlenc.Uint16(a.Data)) * 1024 * time.Microsecond + case nl80211.BssSeenMsAgo: + // * @NL80211_BSS_SEEN_MS_AGO: age of this BSS entry in ms + b.LastSeen = time.Duration(nlenc.Uint32(a.Data)) * time.Millisecond + case nl80211.BssStatus: + // NOTE: BSSStatus copies the ordering of nl80211's BSS status + // constants. This may not be the case on other operating systems. + b.Status = BSSStatus(nlenc.Uint32(a.Data)) + case nl80211.BssInformationElements: + ies, err := parseIEs(a.Data) + if err != nil { + return err + } + + // TODO(mdlayher): return more IEs if they end up being generally useful + for _, ie := range ies { + switch ie.ID { + case ieSSID: + b.SSID = decodeSSID(ie.Data) + } + } + } + } + + return nil +} + // parseStationInfo parses StationInfo attributes from a byte slice of // netlink attributes. func parseStationInfo(b []byte) (*StationInfo, error) { @@ -345,6 +451,32 @@ func parseRateInfo(b []byte) (*rateInfo, error) { return &info, nil } +// attrsContain checks if a slice of netlink attributes contains an attribute +// with the specified type. +func attrsContain(attrs []netlink.Attribute, typ uint16) bool { + for _, a := range attrs { + if a.Type == typ { + return true + } + } + + return false +} + +// decodeSSID safely parses a byte slice into UTF-8 runes, and returns the +// resulting string from the runes. +func decodeSSID(b []byte) string { + buf := bytes.NewBuffer(nil) + for len(b) > 0 { + r, size := utf8.DecodeRune(b) + b = b[size:] + + buf.WriteRune(r) + } + + return buf.String() +} + var _ genl = &sysGENL{} // sysGENL is the system implementation of genl, using generic netlink. diff --git a/client_linux_test.go b/client_linux_test.go index 0e0ce81..6e41e30 100644 --- a/client_linux_test.go +++ b/client_linux_test.go @@ -3,6 +3,7 @@ package wifi import ( + "bytes" "fmt" "log" "math" @@ -94,12 +95,165 @@ func TestLinux_clientInterfacesOK(t *testing.T) { } } +func TestLinux_clientBSSMissingBSSAttributeIsNotExist(t *testing.T) { + // One message without BSS attribute + msgs := []genetlink.Message{{ + Header: genetlink.Header{ + Command: nl80211.CmdNewScanResults, + }, + Data: mustMarshalAttributes([]netlink.Attribute{{ + Type: nl80211.AttrIfindex, + Data: nlenc.Uint32Bytes(1), + }}), + }} + c := testClient(t, msgs, nil) + + _, err := c.BSS(&Interface{ + Index: 1, + HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad}, + }) + if !os.IsNotExist(err) { + t.Fatalf("expected is not exist, got: %v", err) + } +} + +func TestLinux_clientBSSMissingBSSStatusAttributeIsNotExist(t *testing.T) { + msgs := []genetlink.Message{{ + Header: genetlink.Header{ + Command: nl80211.CmdNewScanResults, + }, + // BSS attribute, but no nested status attribute for the "active" BSS + Data: mustMarshalAttributes([]netlink.Attribute{{ + Type: nl80211.AttrBss, + Data: mustMarshalAttributes([]netlink.Attribute{{ + Type: nl80211.BssBssid, + Data: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + }}), + }}), + }} + c := testClient(t, msgs, nil) + + _, err := c.BSS(&Interface{ + Index: 1, + HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad}, + }) + if !os.IsNotExist(err) { + t.Fatalf("expected is not exist, got: %v", err) + } +} + +func TestLinux_clientBSSNoMessagesIsNotExist(t *testing.T) { + // No messages + c := testClient(t, nil, nil) + + _, err := c.BSS(&Interface{ + Index: 1, + HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad}, + }) + if !os.IsNotExist(err) { + t.Fatalf("expected is not exist, got: %v", err) + } +} + +func TestLinux_clientBSSOKSkipMissingStatus(t *testing.T) { + want := net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55} + + msgs := []genetlink.Message{ + // Multiple messages, but only second one has BSS status, so the + // others should be ignored + { + Header: genetlink.Header{ + Command: nl80211.CmdNewScanResults, + }, + Data: mustMarshalAttributes([]netlink.Attribute{{ + Type: nl80211.AttrBss, + // Does not contain BSS information and status + Data: mustMarshalAttributes([]netlink.Attribute{{ + Type: nl80211.BssBssid, + Data: net.HardwareAddr{0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}, + }}), + }}), + }, + { + Header: genetlink.Header{ + Command: nl80211.CmdNewScanResults, + }, + Data: mustMarshalAttributes([]netlink.Attribute{{ + Type: nl80211.AttrBss, + // Contains BSS information and status + Data: mustMarshalAttributes([]netlink.Attribute{ + { + Type: nl80211.BssBssid, + Data: want, + }, + { + Type: nl80211.BssStatus, + Data: nlenc.Uint32Bytes(uint32(BSSStatusAssociated)), + }, + }), + }}), + }, + } + c := testClient(t, msgs, nil) + + bss, err := c.BSS(&Interface{ + Index: 1, + HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got := bss.BSSID; !bytes.Equal(want, got) { + t.Fatalf("unexpected BSS BSSID:\n- want: %#v\n- got: %#v", + want, got) + } +} + +func TestLinux_clientBSSOK(t *testing.T) { + want := &BSS{ + SSID: "Hello, 世界", + BSSID: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + Frequency: 2492, + BeaconInterval: 100 * 1024 * time.Microsecond, + LastSeen: 10 * time.Second, + Status: BSSStatusAssociated, + } + + ifi := &Interface{ + Index: 1, + HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad}, + } + + msgs := mustMessages(t, nl80211.CmdNewScanResults, want) + + c := testClient(t, msgs, &check{ + Command: nl80211.CmdGetScan, + Flags: netlink.HeaderFlagsRequest | netlink.HeaderFlagsDump, + Attrs: ifi.idAttrs(), + }) + + got, err := c.BSS(ifi) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected BSS:\n- want: %v\n- got: %v", + want, got) + } +} + func TestLinux_clientStationInfoMissingAttributeIsNotExist(t *testing.T) { // One message without station info attribute msgs := []genetlink.Message{{ Header: genetlink.Header{ Command: nl80211.CmdNewStation, }, + Data: mustMarshalAttributes([]netlink.Attribute{{ + Type: nl80211.AttrIfindex, + Data: nlenc.Uint32Bytes(1), + }}), }} c := testClient(t, msgs, nil) @@ -169,7 +323,7 @@ func TestLinux_clientStationInfoOK(t *testing.T) { c := testClient(t, msgs, &check{ Command: nl80211.CmdGetStation, Flags: netlink.HeaderFlagsRequest | netlink.HeaderFlagsDump, - Attrs: ifi.stationInfoAttrs(), + Attrs: ifi.idAttrs(), }) got, err := c.StationInfo(ifi) @@ -289,6 +443,17 @@ func (g *testGENL) Execute(m genetlink.Message, family uint16, flags netlink.Hea // Helper functions for converting types back into their raw attribute formats +func marshalIEs(ies []ie) []byte { + buf := bytes.NewBuffer(nil) + for _, ie := range ies { + buf.WriteByte(ie.ID) + buf.WriteByte(uint8(len(ie.Data))) + buf.Write(ie.Data) + } + + return buf.Bytes() +} + func mustMarshalAttributes(attrs []netlink.Attribute) []byte { b, err := netlink.MarshalAttributes(attrs) if err != nil { @@ -304,6 +469,7 @@ type attributeser interface { var ( _ attributeser = &Interface{} + _ attributeser = &BSS{} _ attributeser = &StationInfo{} ) @@ -319,6 +485,29 @@ func (ifi *Interface) attributes() []netlink.Attribute { } } +func (b *BSS) attributes() []netlink.Attribute { + return []netlink.Attribute{ + // TODO(mdlayher): return more attributes for validation? + { + Type: nl80211.AttrBss, + Data: mustMarshalAttributes([]netlink.Attribute{ + {Type: nl80211.BssBssid, Data: b.BSSID}, + {Type: nl80211.BssFrequency, Data: nlenc.Uint32Bytes(b.Frequency)}, + {Type: nl80211.BssBeaconInterval, Data: nlenc.Uint16Bytes(uint16(b.BeaconInterval / 1024 / time.Microsecond))}, + {Type: nl80211.BssSeenMsAgo, Data: nlenc.Uint32Bytes(uint32(b.LastSeen / time.Millisecond))}, + {Type: nl80211.BssStatus, Data: nlenc.Uint32Bytes(uint32(b.Status))}, + { + Type: nl80211.BssInformationElements, + Data: marshalIEs([]ie{{ + ID: ieSSID, + Data: []byte(b.SSID), + }}), + }, + }), + }, + } +} + func (s *StationInfo) attributes() []netlink.Attribute { return []netlink.Attribute{ // TODO(mdlayher): return more attributes for validation? @@ -368,6 +557,8 @@ func mustMessages(t *testing.T, command uint8, want interface{}) []genetlink.Mes for _, x := range xs { as = append(as, x) } + case *BSS: + as = append(as, xs) case *StationInfo: as = append(as, xs) default: diff --git a/client_others.go b/client_others.go index 5fa7b2b..1848b88 100644 --- a/client_others.go +++ b/client_others.go @@ -22,6 +22,11 @@ func (c *client) Interfaces() ([]*Interface, error) { return nil, errUnimplemented } +// BSS always returns an error. +func (c *client) BSS(ifi *Interface) (*BSS, error) { + return nil, errUnimplemented +} + // StationInfo always returns an error. func (c *client) StationInfo(ifi *Interface) (*StationInfo, error) { return nil, errUnimplemented diff --git a/client_others_test.go b/client_others_test.go index e5eb9ef..11d49a0 100644 --- a/client_others_test.go +++ b/client_others_test.go @@ -20,6 +20,11 @@ func TestOthers_clientUnimplemented(t *testing.T) { want, got) } + if _, got := c.BSS(nil); want != got { + t.Fatalf("unexpected error during c.BSS\n- want: %v\n- got: %v", + want, got) + } + if _, got := c.StationInfo(nil); want != got { t.Fatalf("unexpected error during c.StationInfo\n- want: %v\n- got: %v", want, got) diff --git a/wifi.go b/wifi.go index d39b805..ee5ed76 100644 --- a/wifi.go +++ b/wifi.go @@ -1,10 +1,17 @@ package wifi import ( + "errors" + "fmt" "net" "time" ) +var ( + // errInvalidIE is returned when one or more IEs are malformed. + errInvalidIE = errors.New("invalid 802.11 information element") +) + // An InterfaceType is the operating mode of an Interface. type InterfaceType int @@ -125,3 +132,101 @@ type StationInfo struct { // The number of times a beacon loss was detected. BeaconLoss uint32 } + +// A BSS is an 802.11 basic service set. It contains information about a wireless +// network associated with an Interface. +type BSS struct { + // The service set identifier, or "network name" of the BSS. + SSID string + + // The BSS service set identifier. In infrastructure mode, this is the + // hardware address of the wireless access point that a client is associated + // with. + BSSID net.HardwareAddr + + // The frequency used by the BSS, in MHz. + Frequency uint32 + + // The interval between beacon transmissions for this BSS. + BeaconInterval time.Duration + + // The time since the client last scanned this BSS's information. + LastSeen time.Duration + + // The status of the client within the BSS. + Status BSSStatus +} + +// A BSSStatus indicates the current status of client within a BSS. +type BSSStatus int + +const ( + // BSSStatusAuthenticated indicates that a client is authenticated with a BSS. + BSSStatusAuthenticated BSSStatus = iota + + // BSSStatusAssociated indicates that a client is associated with a BSS. + BSSStatusAssociated + + // BSSStatusIBSSJoined indicates that a client has joined an independent BSS. + BSSStatusIBSSJoined +) + +// String returns the string representation of a BSSStatus. +func (s BSSStatus) String() string { + switch s { + case BSSStatusAuthenticated: + return "authenticated" + case BSSStatusAssociated: + return "associated" + case BSSStatusIBSSJoined: + return "IBSS joined" + default: + return fmt.Sprintf("unknown(%d)", s) + } +} + +// List of 802.11 Information Element types. +const ( + ieSSID = 0 +) + +// An ie is an 802.11 information element. +type ie struct { + ID uint8 + // Length field implied by length of data + Data []byte +} + +// parseIEs parses zero or more ies from a byte slice. +// Reference: +// https://www.safaribooksonline.com/library/view/80211-wireless-networks/0596100523/ch04.html#wireless802dot112-CHP-4-FIG-31 +func parseIEs(b []byte) ([]ie, error) { + var ies []ie + var i int + for { + if len(b[i:]) == 0 { + break + } + if len(b[i:]) < 2 { + return nil, errInvalidIE + } + + id := b[i] + i++ + l := int(b[i]) + i++ + + if len(b[i:]) < l { + return nil, errInvalidIE + } + + ies = append(ies, ie{ + ID: id, + Data: b[i : i+l], + }) + + i += l + } + + return ies, nil +} diff --git a/wifi_test.go b/wifi_test.go new file mode 100644 index 0000000..d31b7e3 --- /dev/null +++ b/wifi_test.go @@ -0,0 +1,112 @@ +package wifi + +import ( + "reflect" + "testing" +) + +func TestBSSStatusString(t *testing.T) { + tests := []struct { + t BSSStatus + s string + }{ + { + t: BSSStatusAuthenticated, + s: "authenticated", + }, + { + t: BSSStatusAssociated, + s: "associated", + }, + { + t: BSSStatusIBSSJoined, + s: "IBSS joined", + }, + { + t: 3, + s: "unknown(3)", + }, + } + + for _, tt := range tests { + t.Run(tt.s, func(t *testing.T) { + if want, got := tt.s, tt.t.String(); want != got { + t.Fatalf("unexpected BSS status string:\n- want: %q\n- got: %q", + want, got) + } + }) + } +} + +func Test_parseIEs(t *testing.T) { + tests := []struct { + name string + b []byte + ies []ie + err error + }{ + { + name: "empty", + }, + { + name: "too short", + b: []byte{0x00}, + err: errInvalidIE, + }, + { + name: "length too long", + b: []byte{0x00, 0xff, 0x00}, + err: errInvalidIE, + }, + { + name: "OK one", + b: []byte{0x00, 0x03, 'f', 'o', 'o'}, + ies: []ie{{ + ID: 0, + Data: []byte("foo"), + }}, + }, + { + name: "OK three", + b: []byte{ + 0x00, 0x03, 'f', 'o', 'o', + 0x01, 0x00, + 0x02, 0x06, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + }, + ies: []ie{ + { + ID: 0, + Data: []byte("foo"), + }, + { + ID: 1, + Data: []byte{}, + }, + { + ID: 2, + Data: []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ies, err := parseIEs(tt.b) + + if want, got := tt.err, err; want != got { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", + want, got) + } + if err != nil { + t.Logf("err: %v", err) + return + } + + if want, got := tt.ies, ies; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected ies:\n- want: %v\n- got: %v", + want, got) + } + }) + } +}