diff --git a/client_linux.go b/client_linux.go index 805d091..9829697 100644 --- a/client_linux.go +++ b/client_linux.go @@ -6,6 +6,7 @@ package wifi import ( "bytes" "crypto/sha1" + "encoding/binary" "net" "os" "time" @@ -384,6 +385,12 @@ func (b *BSS) parseAttributes(attrs []netlink.Attribute) error { switch ie.ID { case ieSSID: b.SSID = decodeSSID(ie.Data) + case ieBSSLoad: + Bssload, err := decodeBSSLoad(ie.Data) + if err != nil { + continue // This IE is malformed + } + b.Load = *Bssload } } } @@ -544,3 +551,28 @@ func decodeSSID(b []byte) string { return buf.String() } + +// decodeBSSLoad Decodes the BSSLoad IE. Supports Version 1 and Version 2 +// values according to https://raw.githubusercontent.com/wireshark/wireshark/master/epan/dissectors/packet-ieee80211.c +// See also source code of iw (v5.19) scan.c Line 1634ff +// BSS Load ELement (with length 5) is defined by chapter 9.4.2.27 (page 1066) of the current IEEE 802.11-2020 +func decodeBSSLoad(b []byte) (*BSSLoad, error) { + var load BSSLoad + if len(b) == 5 { + // Wireshark calls this "802.11e CCA Version" + // This is the version defined in IEEE 802.11 (Versions 2007, 2012, 2016 and 2020) + load.Version = 2 + load.StationCount = binary.LittleEndian.Uint16(b[0:2]) // first 2 bytes + load.ChannelUtilization = b[2] // next 1 byte + load.AvailableAdmissionCapacity = binary.LittleEndian.Uint16(b[3:5]) // last 2 bytes + } else if len(b) == 4 { + // Wireshark calls this "Cisco QBSS Version 1 - non CCA" + load.Version = 1 + load.StationCount = binary.LittleEndian.Uint16(b[0:2]) // first 2 bytes + load.ChannelUtilization = b[2] // next 1 byte + load.AvailableAdmissionCapacity = uint16(b[3]) // next 1 byte + } else { + return nil, errInvalidBSSLoad + } + return &load, nil +} diff --git a/client_linux_integration_test.go b/client_linux_integration_test.go index cd0d318..d90532d 100644 --- a/client_linux_integration_test.go +++ b/client_linux_integration_test.go @@ -45,21 +45,21 @@ func TestIntegrationLinuxConcurrent(t *testing.T) { defer wg.Wait() for i := 0; i < workers; i++ { - go func() { + go func(differentI int) { defer wg.Done() - execN(t, iterations, names) - }() + execN(t, iterations, names, differentI) + }(i) } } -func execN(t *testing.T, n int, expect []string) { +func execN(t *testing.T, n int, expect []string, worker_id int) { c := testClient(t) names := make(map[string]int) for i := 0; i < n; i++ { ifis, err := c.Interfaces() if err != nil { - panicf("failed to retrieve interfaces: %v", err) + panicf("[worker_id %d; iteration %d] failed to retrieve interfaces: %v", worker_id, i, err) } for _, ifi := range ifis { @@ -69,7 +69,7 @@ func execN(t *testing.T, n int, expect []string) { if _, err := c.StationInfo(ifi); err != nil { if !errors.Is(err, os.ErrNotExist) { - panicf("failed to retrieve station info for device %s: %v", ifi.Name, err) + panicf("[worker_id %d; iteration %d] failed to retrieve station info for device %s: %v", worker_id, i, ifi.Name, err) } } @@ -80,10 +80,10 @@ func execN(t *testing.T, n int, expect []string) { for _, e := range expect { nn, ok := names[e] if !ok { - panicf("did not find interface %q during test", e) + panicf("[worker_id %d] did not find interface %q during test", worker_id, e) } if nn != n { - panicf("wanted to find %q %d times, found %d", e, n, nn) + panicf("[worker_id %d] wanted to find %q %d times, found %d", worker_id, e, n, nn) } } } diff --git a/client_linux_test.go b/client_linux_test.go index e23cb42..0b4612b 100644 --- a/client_linux_test.go +++ b/client_linux_test.go @@ -554,3 +554,49 @@ func mustMessages(t *testing.T, command uint8, want interface{}) genltest.Func { return msgs, nil } } + +func Test_decodeBSSLoad(t *testing.T) { + type args struct { + b []byte + } + tests := []struct { + name string + args args + wantVersion uint16 + wantStationCount uint16 + wantChannelUtilization uint8 + wantAvailableAdmissionCapacity uint16 + }{ + {name: "Parse BSS Load Normal", args: args{b: []byte{3, 0, 8, 0x8D, 0x5B}}, wantVersion: 2, wantStationCount: 3, wantChannelUtilization: 8, wantAvailableAdmissionCapacity: 23437}, + {name: "Parse BSS Load Version 1", args: args{b: []byte{9, 0, 8, 0x8D}}, wantVersion: 1, wantStationCount: 9, wantChannelUtilization: 8, wantAvailableAdmissionCapacity: 141}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bssLoad, _ := decodeBSSLoad(tt.args.b) + gotVersion := bssLoad.Version + gotStationCount := bssLoad.StationCount + gotChannelUtilization := bssLoad.ChannelUtilization + gotAvailableAdmissionCapacity := bssLoad.AvailableAdmissionCapacity + if uint16(gotVersion) != tt.wantVersion { + t.Errorf("decodeBSSLoad() gotVersion = %v, want %v", gotVersion, tt.wantVersion) + } + if gotStationCount != tt.wantStationCount { + t.Errorf("decodeBSSLoad() gotStationCount = %v, want %v", gotStationCount, tt.wantStationCount) + } + if gotChannelUtilization != tt.wantChannelUtilization { + t.Errorf("decodeBSSLoad() gotChannelUtilization = %v, want %v", gotChannelUtilization, tt.wantChannelUtilization) + } + if gotAvailableAdmissionCapacity != tt.wantAvailableAdmissionCapacity { + t.Errorf("decodeBSSLoad() gotAvailableAdmissionCapacity = %v, want %v", gotAvailableAdmissionCapacity, tt.wantAvailableAdmissionCapacity) + } + }) + } +} + +func Test_decodeBSSLoadError(t *testing.T) { + t.Parallel() + _, err := decodeBSSLoad([]byte{3, 0, 8}) + if err == nil { + t.Error("want error on bogus IE with wrong length") + } +} diff --git a/wifi.go b/wifi.go index ac1155f..4eef586 100644 --- a/wifi.go +++ b/wifi.go @@ -10,6 +10,9 @@ import ( // errInvalidIE is returned when one or more IEs are malformed. var errInvalidIE = errors.New("invalid 802.11 information element") +// errInvalidBSSLoad is returned when BSSLoad IE has wrong length. +var errInvalidBSSLoad = errors.New("802.11 information element BSSLoad has wrong length") + // An InterfaceType is the operating mode of an Interface. type InterfaceType int @@ -166,28 +169,61 @@ type StationInfo struct { BeaconLoss int } +// BSSLoad is an Information Element containing measurements of the load on the BSS. +type BSSLoad struct { + // Version: Indicates the version of the BSS Load Element. Can be 1 or 2. + Version int + + // StationCount: total number of STA currently associated with this BSS. + StationCount uint16 + + // ChannelUtilization: Percentage of time (linearly scaled 0 to 255) that the AP sensed the medium was busy. Calculated only for the primary channel. + ChannelUtilization uint8 + + // AvailableAdmissionCapacity: remaining amount of medium time availible via explicit admission controll in units of 32 us/s. + AvailableAdmissionCapacity uint16 +} + +// String returns the string representation of a BSSLoad. +func (l BSSLoad) String() string { + if l.Version == 1 { + return fmt.Sprintf("BSSLoad Version: %d stationCount: %d channelUtilization: %d/255 availableAdmissionCapacity: %d\n", + l.Version, l.StationCount, l.ChannelUtilization, l.AvailableAdmissionCapacity, + ) + } else if l.Version == 2 { + return fmt.Sprintf("BSSLoad Version: %d stationCount: %d channelUtilization: %d/255 availableAdmissionCapacity: %d [*32us/s]\n", + l.Version, l.StationCount, l.ChannelUtilization, l.AvailableAdmissionCapacity, + ) + } else { + return fmt.Sprintf("invalid BSSLoad Version: %d", l.Version) + } +} + // 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 + // BSSID: 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: The frequency used by the BSS, in MHz. Frequency int - // The interval between beacon transmissions for this BSS. + // BeaconInterval: The time interval between beacon transmissions for this BSS. BeaconInterval time.Duration - // The time since the client last scanned this BSS's information. + // LastSeen: The time since the client last scanned this BSS's information. LastSeen time.Duration - // The status of the client within the BSS. + // Status: The status of the client within the BSS. Status BSSStatus + + // Load: The load element of the BSS (contains StationCount, ChannelUtilization and AvailableAdmissionCapacity). + Load BSSLoad } // A BSSStatus indicates the current status of client within a BSS. @@ -220,7 +256,8 @@ func (s BSSStatus) String() string { // List of 802.11 Information Element types. const ( - ieSSID = 0 + ieSSID = 0 + ieBSSLoad = 11 ) // An ie is an 802.11 information element. @@ -232,7 +269,8 @@ type ie struct { // 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 +// +// 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