Skip to content

Commit

Permalink
feat: add SurveyInfo (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
berezovskyi-oleksandr authored Jan 27, 2025
1 parent bf70b98 commit 0676b7a
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 0 deletions.
3 changes: 3 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ func (c *Client) StationInfo(ifi *Interface) ([]*StationInfo, error) {
return c.c.StationInfo(ifi)
}

// SurveyInfo retrieves the survey information about a WiFi interface.
func (c *Client) SurveyInfo(ifi *Interface) ([]*SurveyInfo, error) { return c.c.SurveyInfo(ifi) }

// SetDeadline sets the read and write deadlines associated with the connection.
func (c *Client) SetDeadline(t time.Time) error {
return c.c.SetDeadline(t)
Expand Down
86 changes: 86 additions & 0 deletions client_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,32 @@ func (c *client) StationInfo(ifi *Interface) ([]*StationInfo, error) {
return stations, nil
}

// SurveyInfo requests that nl80211 return a list of survey information for the
// specified Interface.
func (c *client) SurveyInfo(ifi *Interface) ([]*SurveyInfo, error) {
msgs, err := c.get(
unix.NL80211_CMD_GET_SURVEY,
netlink.Dump,
ifi,
func(ae *netlink.AttributeEncoder) {
if ifi.HardwareAddr != nil {
ae.Bytes(unix.NL80211_ATTR_MAC, ifi.HardwareAddr)
}
},
)
if err != nil {
return nil, err
}

surveys := make([]*SurveyInfo, len(msgs))
for i := range msgs {
if surveys[i], err = parseSurveyInfo(msgs[i].Data); err != nil {
return nil, err
}
}
return surveys, nil
}

// SetDeadline sets the read and write deadlines associated with the connection.
func (c *client) SetDeadline(t time.Time) error {
return c.c.SetDeadline(t)
Expand Down Expand Up @@ -539,6 +565,66 @@ func parseRateInfo(b []byte) (*rateInfo, error) {
return &info, nil
}

// parseSurveyInfo parses a single SurveyInfo from a byte slice of netlink
// attributes.
func parseSurveyInfo(b []byte) (*SurveyInfo, error) {
attrs, err := netlink.UnmarshalAttributes(b)
if err != nil {
return nil, err
}

var info SurveyInfo
for _, a := range attrs {
switch a.Type {
case unix.NL80211_ATTR_SURVEY_INFO:
nattrs, err := netlink.UnmarshalAttributes(a.Data)
if err != nil {
return nil, err
}

if err := (&info).parseAttributes(nattrs); err != nil {
return nil, err
}

// Parsed the necessary data.
return &info, nil
}
}

// No survey info found
return nil, os.ErrNotExist
}

// parseAttributes parses netlink attributes into a SurveyInfo's fields.
func (s *SurveyInfo) parseAttributes(attrs []netlink.Attribute) error {
for _, a := range attrs {
switch a.Type {
case unix.NL80211_SURVEY_INFO_FREQUENCY:
s.Frequency = int(nlenc.Uint32(a.Data))
case unix.NL80211_SURVEY_INFO_NOISE:
s.Noise = int(int8(a.Data[0]))
case unix.NL80211_SURVEY_INFO_IN_USE:
s.InUse = true
case unix.NL80211_SURVEY_INFO_TIME:
s.ChannelTime = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
case unix.NL80211_SURVEY_INFO_TIME_BUSY:
s.ChannelTimeBusy = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
case unix.NL80211_SURVEY_INFO_TIME_EXT_BUSY:
s.ChannelTimeExtBusy = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
case unix.NL80211_SURVEY_INFO_TIME_BSS_RX:
s.ChannelTimeBssRx = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
case unix.NL80211_SURVEY_INFO_TIME_RX:
s.ChannelTimeRx = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
case unix.NL80211_SURVEY_INFO_TIME_TX:
s.ChannelTimeTx = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
case unix.NL80211_SURVEY_INFO_TIME_SCAN:
s.ChannelTimeScan = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
}
}

return nil
}

// attrsContain checks if a slice of netlink attributes contains an attribute
// with the specified type.
func attrsContain(attrs []netlink.Attribute, typ uint16) bool {
Expand Down
5 changes: 5 additions & 0 deletions client_linux_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ func execN(t *testing.T, n int, expect []string, worker_id int) {
}
}

if _, err := c.SurveyInfo(ifi); err != nil {
if !errors.Is(err, os.ErrNotExist) {
panicf("[worker_id %d; iteration %d] failed to retrieve survey info for device %s: %v", worker_id, i, ifi.Name, err)
}
}
names[ifi.Name]++
}
}
Expand Down
138 changes: 138 additions & 0 deletions client_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,31 @@ func (s *StationInfo) attributes() []netlink.Attribute {
}
}

func (s *SurveyInfo) attributes() []netlink.Attribute {
attributes := []netlink.Attribute{
{Type: unix.NL80211_SURVEY_INFO_FREQUENCY, Data: nlenc.Uint32Bytes(uint32(s.Frequency))},
{Type: unix.NL80211_SURVEY_INFO_NOISE, Data: []byte{byte(int8(s.Noise))}},
}
if s.InUse {
attributes = append(attributes, netlink.Attribute{Type: unix.NL80211_SURVEY_INFO_IN_USE})
}
attributes = append(attributes, []netlink.Attribute{
{Type: unix.NL80211_SURVEY_INFO_TIME, Data: nlenc.Uint64Bytes(uint64(s.ChannelTime / time.Millisecond))},
{Type: unix.NL80211_SURVEY_INFO_TIME_BUSY, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeBusy / time.Millisecond))},
{Type: unix.NL80211_SURVEY_INFO_TIME_EXT_BUSY, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeExtBusy / time.Millisecond))},
{Type: unix.NL80211_SURVEY_INFO_TIME_BSS_RX, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeBssRx / time.Millisecond))},
{Type: unix.NL80211_SURVEY_INFO_TIME_RX, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeRx / time.Millisecond))},
{Type: unix.NL80211_SURVEY_INFO_TIME_TX, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeTx / time.Millisecond))},
{Type: unix.NL80211_SURVEY_INFO_TIME_SCAN, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeScan / time.Millisecond))},
}...)
return []netlink.Attribute{
{
Type: unix.NL80211_ATTR_SURVEY_INFO,
Data: mustMarshalAttributes(attributes),
},
}
}

func bitrateAttr(bitrate int) uint32 {
return uint32(bitrate / 100 / 1000)
}
Expand All @@ -542,6 +567,10 @@ func mustMessages(t *testing.T, command uint8, want interface{}) genltest.Func {
for _, x := range xs {
as = append(as, x)
}
case []*SurveyInfo:
for _, x := range xs {
as = append(as, x)
}
default:
t.Fatalf("cannot make messages for type: %T", xs)
}
Expand Down Expand Up @@ -606,3 +635,112 @@ func Test_decodeBSSLoadError(t *testing.T) {
t.Error("want error on bogus IE with wrong length")
}
}

func TestLinux_clientSurveryInfoMissingAttributeIsNotExist(t *testing.T) {
c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) {
// One message without station info attribute
return []genetlink.Message{{
Header: genetlink.Header{
Command: unix.NL80211_CMD_GET_SURVEY,
},
Data: mustMarshalAttributes([]netlink.Attribute{{
Type: unix.NL80211_ATTR_IFINDEX,
Data: nlenc.Uint32Bytes(1),
}}),
}}, nil
})

_, err := c.StationInfo(&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_clientSurveyInfoNoMessagesIsNotExist(t *testing.T) {
c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) {
// No messages about station info at the generic netlink level.
// Caller will interpret this as no station info.
return nil, io.EOF
})

info, err := c.SurveyInfo(&Interface{
Index: 1,
HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad},
})
if err != nil {
t.Fatalf("undexpected error: %v", err)
}
if !reflect.DeepEqual(info, []*SurveyInfo{}) {
t.Fatalf("expected info to be an empty slice, got %v", info)
}
}

func TestLinux_clientSurveyInfoOK(t *testing.T) {
want := []*SurveyInfo{
{
Frequency: 2412,
Noise: -95,
InUse: true,
ChannelTime: 100 * time.Millisecond,
ChannelTimeBusy: 50 * time.Millisecond,
ChannelTimeExtBusy: 10 * time.Millisecond,
ChannelTimeBssRx: 20 * time.Millisecond,
ChannelTimeRx: 30 * time.Millisecond,
ChannelTimeTx: 40 * time.Millisecond,
ChannelTimeScan: 5 * time.Millisecond,
},
{
Frequency: 2437,
Noise: -90,
InUse: false,
ChannelTime: 200 * time.Millisecond,
ChannelTimeBusy: 100 * time.Millisecond,
ChannelTimeExtBusy: 20 * time.Millisecond,
ChannelTimeBssRx: 40 * time.Millisecond,
ChannelTimeRx: 60 * time.Millisecond,
ChannelTimeTx: 80 * time.Millisecond,
ChannelTimeScan: 10 * time.Millisecond,
},
}

ifi := &Interface{
Index: 1,
HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad},
}

const flags = netlink.Request | netlink.Dump

msgsFn := mustMessages(t, unix.NL80211_CMD_GET_SURVEY, want)

c := testClient(t, genltest.CheckRequest(familyID, unix.NL80211_CMD_GET_SURVEY, flags,
func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) {
// Also verify that the correct interface attributes are
// present in the request.
attrs, err := netlink.UnmarshalAttributes(greq.Data)
if err != nil {
t.Fatalf("failed to unmarshal attributes: %v", err)
}

if diff := diffNetlinkAttributes(ifi.idAttrs(), attrs); diff != "" {
t.Fatalf("unexpected request netlink attributes (-want +got):\n%s", diff)
}

return msgsFn(greq, nreq)
},
))

got, err := c.SurveyInfo(ifi)
if err != nil {
log.Fatalf("unexpected error: %v", err)
}

for i := range want {
if !reflect.DeepEqual(want[i], got[i]) {
t.Fatalf("unexpected station info:\n- want: %v\n- got: %v",
want[i], got[i])
}
}
}
1 change: 1 addition & 0 deletions client_others.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func (*client) Close() error { return errUni
func (*client) Interfaces() ([]*Interface, error) { return nil, errUnimplemented }
func (*client) BSS(_ *Interface) (*BSS, error) { return nil, errUnimplemented }
func (*client) StationInfo(_ *Interface) ([]*StationInfo, error) { return nil, errUnimplemented }
func (*client) SurveyInfo(_ *Interface) ([]*SurveyInfo, error) { return nil, errUnimplemented }
func (*client) Connect(_ *Interface, _ string) error { return errUnimplemented }
func (*client) Disconnect(_ *Interface) error { return errUnimplemented }
func (*client) ConnectWPAPSK(_ *Interface, _, _ string) error { return errUnimplemented }
Expand Down
35 changes: 35 additions & 0 deletions wifi.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,38 @@ func parseIEs(b []byte) ([]ie, error) {

return ies, nil
}

type SurveyInfo struct {
// The frequency in MHz of the channel.
Frequency int

// The noise level in dBm.
Noise int

// The time the radio has spent on this channel.
ChannelTime time.Duration

// The time the radio has spent on this channel while it was active.
ChannelTimeActive time.Duration

// The time the radio has spent on this channel while it was busy.
ChannelTimeBusy time.Duration

// The time the radio has spent on this channel while it was busy with external traffic.
ChannelTimeExtBusy time.Duration

// The time the radio has spent on this channel receiving data from a BSS.
ChannelTimeBssRx time.Duration

// The time the radio has spent on this channel receiving data.
ChannelTimeRx time.Duration

// The time the radio has spent on this channel transmitting data.
ChannelTimeTx time.Duration

// The time the radio has spent on this channel while it was scanning.
ChannelTimeScan time.Duration

// Indicates if the channel is currently in use.
InUse bool
}

0 comments on commit 0676b7a

Please sign in to comment.