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

feat: add SurveyInfo #94

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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
}