Skip to content

Commit

Permalink
Merge pull request #64 from mercedes-benz/feat-QBSSLoad-IEs
Browse files Browse the repository at this point in the history
Parse BSS Load Information Elements
  • Loading branch information
SuperQ authored May 21, 2024
2 parents a09d1bc + 508c95c commit e031f90
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 15 deletions.
32 changes: 32 additions & 0 deletions client_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package wifi
import (
"bytes"
"crypto/sha1"
"encoding/binary"
"net"
"os"
"time"
Expand Down Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -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
}
16 changes: 8 additions & 8 deletions client_linux_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}

Expand All @@ -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)
}
}
}
Expand Down
46 changes: 46 additions & 0 deletions client_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
52 changes: 45 additions & 7 deletions wifi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down

0 comments on commit e031f90

Please sign in to comment.