Skip to content

Commit 3147e15

Browse files
feat: add SurveyInfo
1 parent bcd874d commit 3147e15

6 files changed

+268
-0
lines changed

client.go

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ func (c *Client) StationInfo(ifi *Interface) ([]*StationInfo, error) {
6060
return c.c.StationInfo(ifi)
6161
}
6262

63+
// SurveyInfo retrieves the survey information about a WiFi interface.
64+
func (c *Client) SurveyInfo(ifi *Interface) ([]*SurveyInfo, error) { return c.c.SurveyInfo(ifi) }
65+
6366
// SetDeadline sets the read and write deadlines associated with the connection.
6467
func (c *Client) SetDeadline(t time.Time) error {
6568
return c.c.SetDeadline(t)

client_linux.go

+86
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,32 @@ func (c *client) StationInfo(ifi *Interface) ([]*StationInfo, error) {
195195
return stations, nil
196196
}
197197

198+
// SurveyInfo requests that nl80211 return a list of survey information for the
199+
// specified Interface.
200+
func (c *client) SurveyInfo(ifi *Interface) ([]*SurveyInfo, error) {
201+
msgs, err := c.get(
202+
unix.NL80211_CMD_GET_SURVEY,
203+
netlink.Dump,
204+
ifi,
205+
func(ae *netlink.AttributeEncoder) {
206+
if ifi.HardwareAddr != nil {
207+
ae.Bytes(unix.NL80211_ATTR_MAC, ifi.HardwareAddr)
208+
}
209+
},
210+
)
211+
if err != nil {
212+
return nil, err
213+
}
214+
215+
surveys := make([]*SurveyInfo, len(msgs))
216+
for i := range msgs {
217+
if surveys[i], err = parseSurveyInfo(msgs[i].Data); err != nil {
218+
return nil, err
219+
}
220+
}
221+
return surveys, nil
222+
}
223+
198224
// SetDeadline sets the read and write deadlines associated with the connection.
199225
func (c *client) SetDeadline(t time.Time) error {
200226
return c.c.SetDeadline(t)
@@ -539,6 +565,66 @@ func parseRateInfo(b []byte) (*rateInfo, error) {
539565
return &info, nil
540566
}
541567

568+
// parseSurveyInfo parses a single SurveyInfo from a byte slice of netlink
569+
// attributes.
570+
func parseSurveyInfo(b []byte) (*SurveyInfo, error) {
571+
attrs, err := netlink.UnmarshalAttributes(b)
572+
if err != nil {
573+
return nil, err
574+
}
575+
576+
var info SurveyInfo
577+
for _, a := range attrs {
578+
switch a.Type {
579+
case unix.NL80211_ATTR_SURVEY_INFO:
580+
nattrs, err := netlink.UnmarshalAttributes(a.Data)
581+
if err != nil {
582+
return nil, err
583+
}
584+
585+
if err := (&info).parseAttributes(nattrs); err != nil {
586+
return nil, err
587+
}
588+
589+
// Parsed the necessary data.
590+
return &info, nil
591+
}
592+
}
593+
594+
// No survey info found
595+
return nil, os.ErrNotExist
596+
}
597+
598+
// parseAttributes parses netlink attributes into a SurveyInfo's fields.
599+
func (s *SurveyInfo) parseAttributes(attrs []netlink.Attribute) error {
600+
for _, a := range attrs {
601+
switch a.Type {
602+
case unix.NL80211_SURVEY_INFO_FREQUENCY:
603+
s.Frequency = int(nlenc.Uint32(a.Data))
604+
case unix.NL80211_SURVEY_INFO_NOISE:
605+
s.Noise = int(int8(a.Data[0]))
606+
case unix.NL80211_SURVEY_INFO_IN_USE:
607+
s.InUse = true
608+
case unix.NL80211_SURVEY_INFO_TIME:
609+
s.ChannelTime = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
610+
case unix.NL80211_SURVEY_INFO_TIME_BUSY:
611+
s.ChannelTimeBusy = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
612+
case unix.NL80211_SURVEY_INFO_TIME_EXT_BUSY:
613+
s.ChannelTimeExtBusy = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
614+
case unix.NL80211_SURVEY_INFO_TIME_BSS_RX:
615+
s.ChannelTimeBssRx = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
616+
case unix.NL80211_SURVEY_INFO_TIME_RX:
617+
s.ChannelTimeRx = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
618+
case unix.NL80211_SURVEY_INFO_TIME_TX:
619+
s.ChannelTimeTx = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
620+
case unix.NL80211_SURVEY_INFO_TIME_SCAN:
621+
s.ChannelTimeScan = time.Duration(nlenc.Uint64(a.Data)) * time.Millisecond
622+
}
623+
}
624+
625+
return nil
626+
}
627+
542628
// attrsContain checks if a slice of netlink attributes contains an attribute
543629
// with the specified type.
544630
func attrsContain(attrs []netlink.Attribute, typ uint16) bool {

client_linux_integration_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ func execN(t *testing.T, n int, expect []string, worker_id int) {
7373
}
7474
}
7575

76+
if _, err := c.SurveyInfo(ifi); err != nil {
77+
if !errors.Is(err, os.ErrNotExist) {
78+
panicf("[worker_id %d; iteration %d] failed to retrieve survey info for device %s: %v", worker_id, i, ifi.Name, err)
79+
}
80+
}
7681
names[ifi.Name]++
7782
}
7883
}

client_linux_test.go

+138
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,31 @@ func (s *StationInfo) attributes() []netlink.Attribute {
523523
}
524524
}
525525

526+
func (s *SurveyInfo) attributes() []netlink.Attribute {
527+
attributes := []netlink.Attribute{
528+
{Type: unix.NL80211_SURVEY_INFO_FREQUENCY, Data: nlenc.Uint32Bytes(uint32(s.Frequency))},
529+
{Type: unix.NL80211_SURVEY_INFO_NOISE, Data: []byte{byte(int8(s.Noise))}},
530+
}
531+
if s.InUse {
532+
attributes = append(attributes, netlink.Attribute{Type: unix.NL80211_SURVEY_INFO_IN_USE})
533+
}
534+
attributes = append(attributes, []netlink.Attribute{
535+
{Type: unix.NL80211_SURVEY_INFO_TIME, Data: nlenc.Uint64Bytes(uint64(s.ChannelTime / time.Millisecond))},
536+
{Type: unix.NL80211_SURVEY_INFO_TIME_BUSY, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeBusy / time.Millisecond))},
537+
{Type: unix.NL80211_SURVEY_INFO_TIME_EXT_BUSY, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeExtBusy / time.Millisecond))},
538+
{Type: unix.NL80211_SURVEY_INFO_TIME_BSS_RX, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeBssRx / time.Millisecond))},
539+
{Type: unix.NL80211_SURVEY_INFO_TIME_RX, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeRx / time.Millisecond))},
540+
{Type: unix.NL80211_SURVEY_INFO_TIME_TX, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeTx / time.Millisecond))},
541+
{Type: unix.NL80211_SURVEY_INFO_TIME_SCAN, Data: nlenc.Uint64Bytes(uint64(s.ChannelTimeScan / time.Millisecond))},
542+
}...)
543+
return []netlink.Attribute{
544+
{
545+
Type: unix.NL80211_ATTR_SURVEY_INFO,
546+
Data: mustMarshalAttributes(attributes),
547+
},
548+
}
549+
}
550+
526551
func bitrateAttr(bitrate int) uint32 {
527552
return uint32(bitrate / 100 / 1000)
528553
}
@@ -542,6 +567,10 @@ func mustMessages(t *testing.T, command uint8, want interface{}) genltest.Func {
542567
for _, x := range xs {
543568
as = append(as, x)
544569
}
570+
case []*SurveyInfo:
571+
for _, x := range xs {
572+
as = append(as, x)
573+
}
545574
default:
546575
t.Fatalf("cannot make messages for type: %T", xs)
547576
}
@@ -606,3 +635,112 @@ func Test_decodeBSSLoadError(t *testing.T) {
606635
t.Error("want error on bogus IE with wrong length")
607636
}
608637
}
638+
639+
func TestLinux_clientSurveryInfoMissingAttributeIsNotExist(t *testing.T) {
640+
c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) {
641+
// One message without station info attribute
642+
return []genetlink.Message{{
643+
Header: genetlink.Header{
644+
Command: unix.NL80211_CMD_GET_SURVEY,
645+
},
646+
Data: mustMarshalAttributes([]netlink.Attribute{{
647+
Type: unix.NL80211_ATTR_IFINDEX,
648+
Data: nlenc.Uint32Bytes(1),
649+
}}),
650+
}}, nil
651+
})
652+
653+
_, err := c.StationInfo(&Interface{
654+
Index: 1,
655+
HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad},
656+
})
657+
if !os.IsNotExist(err) {
658+
t.Fatalf("expected is not exist, got: %v", err)
659+
}
660+
}
661+
662+
func TestLinux_clientSurveyInfoNoMessagesIsNotExist(t *testing.T) {
663+
c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) {
664+
// No messages about station info at the generic netlink level.
665+
// Caller will interpret this as no station info.
666+
return nil, io.EOF
667+
})
668+
669+
info, err := c.SurveyInfo(&Interface{
670+
Index: 1,
671+
HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad},
672+
})
673+
if err != nil {
674+
t.Fatalf("undexpected error: %v", err)
675+
}
676+
if !reflect.DeepEqual(info, []*SurveyInfo{}) {
677+
t.Fatalf("expected info to be an empty slice, got %v", info)
678+
}
679+
}
680+
681+
func TestLinux_clientSurveyInfoOK(t *testing.T) {
682+
want := []*SurveyInfo{
683+
{
684+
Frequency: 2412,
685+
Noise: -95,
686+
InUse: true,
687+
ChannelTime: 100 * time.Millisecond,
688+
ChannelTimeBusy: 50 * time.Millisecond,
689+
ChannelTimeExtBusy: 10 * time.Millisecond,
690+
ChannelTimeBssRx: 20 * time.Millisecond,
691+
ChannelTimeRx: 30 * time.Millisecond,
692+
ChannelTimeTx: 40 * time.Millisecond,
693+
ChannelTimeScan: 5 * time.Millisecond,
694+
},
695+
{
696+
Frequency: 2437,
697+
Noise: -90,
698+
InUse: false,
699+
ChannelTime: 200 * time.Millisecond,
700+
ChannelTimeBusy: 100 * time.Millisecond,
701+
ChannelTimeExtBusy: 20 * time.Millisecond,
702+
ChannelTimeBssRx: 40 * time.Millisecond,
703+
ChannelTimeRx: 60 * time.Millisecond,
704+
ChannelTimeTx: 80 * time.Millisecond,
705+
ChannelTimeScan: 10 * time.Millisecond,
706+
},
707+
}
708+
709+
ifi := &Interface{
710+
Index: 1,
711+
HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad},
712+
}
713+
714+
const flags = netlink.Request | netlink.Dump
715+
716+
msgsFn := mustMessages(t, unix.NL80211_CMD_GET_SURVEY, want)
717+
718+
c := testClient(t, genltest.CheckRequest(familyID, unix.NL80211_CMD_GET_SURVEY, flags,
719+
func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) {
720+
// Also verify that the correct interface attributes are
721+
// present in the request.
722+
attrs, err := netlink.UnmarshalAttributes(greq.Data)
723+
if err != nil {
724+
t.Fatalf("failed to unmarshal attributes: %v", err)
725+
}
726+
727+
if diff := diffNetlinkAttributes(ifi.idAttrs(), attrs); diff != "" {
728+
t.Fatalf("unexpected request netlink attributes (-want +got):\n%s", diff)
729+
}
730+
731+
return msgsFn(greq, nreq)
732+
},
733+
))
734+
735+
got, err := c.SurveyInfo(ifi)
736+
if err != nil {
737+
log.Fatalf("unexpected error: %v", err)
738+
}
739+
740+
for i := range want {
741+
if !reflect.DeepEqual(want[i], got[i]) {
742+
t.Fatalf("unexpected station info:\n- want: %v\n- got: %v",
743+
want[i], got[i])
744+
}
745+
}
746+
}

client_others.go

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func (*client) Close() error { return errUni
2222
func (*client) Interfaces() ([]*Interface, error) { return nil, errUnimplemented }
2323
func (*client) BSS(_ *Interface) (*BSS, error) { return nil, errUnimplemented }
2424
func (*client) StationInfo(_ *Interface) ([]*StationInfo, error) { return nil, errUnimplemented }
25+
func (*client) SurveyInfo(_ *Interface) ([]*SurveyInfo, error) { return nil, errUnimplemented }
2526
func (*client) Connect(_ *Interface, _ string) error { return errUnimplemented }
2627
func (*client) Disconnect(_ *Interface) error { return errUnimplemented }
2728
func (*client) ConnectWPAPSK(_ *Interface, _, _ string) error { return errUnimplemented }

wifi.go

+35
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,38 @@ func parseIEs(b []byte) ([]ie, error) {
304304

305305
return ies, nil
306306
}
307+
308+
type SurveyInfo struct {
309+
// The frequency in MHz of the channel.
310+
Frequency int
311+
312+
// The noise level in dBm.
313+
Noise int
314+
315+
// The time the radio has spent on this channel.
316+
ChannelTime time.Duration
317+
318+
// The time the radio has spent on this channel while it was active.
319+
ChannelTimeActive time.Duration
320+
321+
// The time the radio has spent on this channel while it was busy.
322+
ChannelTimeBusy time.Duration
323+
324+
// The time the radio has spent on this channel while it was busy with external traffic.
325+
ChannelTimeExtBusy time.Duration
326+
327+
// The time the radio has spent on this channel receiving data from a BSS.
328+
ChannelTimeBssRx time.Duration
329+
330+
// The time the radio has spent on this channel receiving data.
331+
ChannelTimeRx time.Duration
332+
333+
// The time the radio has spent on this channel transmitting data.
334+
ChannelTimeTx time.Duration
335+
336+
// The time the radio has spent on this channel while it was scanning.
337+
ChannelTimeScan time.Duration
338+
339+
// Indicates if the channel is currently in use.
340+
InUse bool
341+
}

0 commit comments

Comments
 (0)