diff --git a/client.go b/client.go index 3361aff..eec7b56 100644 --- a/client.go +++ b/client.go @@ -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) diff --git a/client_linux.go b/client_linux.go index cc8c584..f5985a5 100644 --- a/client_linux.go +++ b/client_linux.go @@ -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) @@ -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 { diff --git a/client_linux_integration_test.go b/client_linux_integration_test.go index d90532d..a2a7dec 100644 --- a/client_linux_integration_test.go +++ b/client_linux_integration_test.go @@ -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]++ } } diff --git a/client_linux_test.go b/client_linux_test.go index de51ba8..86541a5 100644 --- a/client_linux_test.go +++ b/client_linux_test.go @@ -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) } @@ -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) } @@ -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]) + } + } +} diff --git a/client_others.go b/client_others.go index 84eebe2..f50fc5f 100644 --- a/client_others.go +++ b/client_others.go @@ -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 } diff --git a/wifi.go b/wifi.go index 475b76e..39a814f 100644 --- a/wifi.go +++ b/wifi.go @@ -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 +}