Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse BSS Load Information Elements #64

Merged
merged 8 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any documentation other than wireshark source code for these formats? Linking to a > 2MiB source file isn't my preference.

Copy link
Contributor Author

@lukas-mbag lukas-mbag Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation in iw scan command is comparable in size.

See Line 1640ff in scan.c and Line 1826

But I will do another search for better references.

Edit: Links that do not point to a rolling master branch but to a fixed release Line 1634ff in scan.c of release v5.19

Copy link
Contributor Author

@lukas-mbag lukas-mbag Feb 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also checked the IEEE Std 802.11-2020 as a primary source. That document is approx 4000 pages long, The relevant definition of the BSS Load Information Element can be found in chapter 9.4.2.27.

I will add the reference to the primary source and change the descriptions to closer match the language in the primary source to reduce any confusion.

What do you think about that solution?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

// 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) {
SuperQ marked this conversation as resolved.
Show resolved Hide resolved
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
Loading