diff --git a/client.go b/client.go index 39dabfc..22d0c83 100644 --- a/client.go +++ b/client.go @@ -53,6 +53,9 @@ func (c *Client) BSS(ifi *Interface) (*BSS, error) { return c.c.BSS(ifi) } +// PHYs returns a list of the system's WiFi devices. +func (c *Client) PHYs() ([]*PHY, error) { return c.c.PHYs() } + // AccessPoints retrieves the currently known BSS around the specified Interface. func (c *Client) AccessPoints(ifi *Interface) ([]*BSS, error) { return c.c.AccessPoints(ifi) diff --git a/client_linux.go b/client_linux.go index 9be6a41..e616220 100644 --- a/client_linux.go +++ b/client_linux.go @@ -8,6 +8,7 @@ import ( "crypto/sha1" "encoding/binary" "errors" + "fmt" "net" "os" "sync" @@ -100,6 +101,31 @@ func (c *client) Interfaces() ([]*Interface, error) { return ParseInterfaces(msgs) } +// PHYs requests that nl80211 return information for all wireless physical +// devices. +func (c *client) PHYs() ([]*PHY, error) { + return c.getPHYs(nil) +} + +// getPHYs is the back-end for PHY() and PHYs(): building and making the netlink +// call, and parsing the response. +func (c *client) getPHYs(n *uint32) ([]*PHY, error) { + // The kernel, as of 3713b4e364eff (3.10), doesn't emit all information + // unless SplitWiphyDump is set. We could check for it by issuing + // CmdGetProtocolFeatures and seeing if ProtocolFeatureSplitWiphyDump is + // set, if we care about kernels that old ... + msgs, err := c.get(unix.NL80211_CMD_GET_WIPHY, netlink.Dump, nil, func(ae *netlink.AttributeEncoder) { + ae.Flag(unix.NL80211_ATTR_SPLIT_WIPHY_DUMP, true) + if n != nil { + ae.Uint32(unix.NL80211_ATTR_WIPHY, *n) + } + }) + if err != nil { + return nil, err + } + return parsePHYs(msgs) +} + // Connect starts connecting the interface to the specified ssid. func (c *client) Connect(ifi *Interface, ssid string) error { // Ask nl80211 to connect to the specified SSID. @@ -618,6 +644,354 @@ func parseBSS(msgs []genetlink.Message) (*BSS, error) { return nil, os.ErrNotExist } +func parsePHYs(msgs []genetlink.Message) ([]*PHY, error) { + phys := make([]*PHY, 0) + var phy *PHY + curphynum := -1 + for _, m := range msgs { + attrs, err := netlink.UnmarshalAttributes(m.Data) + if err != nil { + return nil, err + } + + // Because we get a single stream of messages spanning multiple + // PHYs, we have to peek into the attributes to see if it's the + // same PHY as we've been processing. + phynum, err := phyNumber(attrs) + if err != nil { + return nil, err + } + if phynum != curphynum { + phy = new(PHY) + phy.Extra = make(map[uint16][]byte, 0) + phys = append(phys, phy) + curphynum = phynum + } + + if err := phy.parseAttributes(attrs); err != nil { + return nil, err + } + } + return phys, nil +} + +// phyNumber extracts the first integer device index (AttrWiphy) from a list of +// netlink attributes. +func phyNumber(attrs []netlink.Attribute) (int, error) { + for _, a := range attrs { + switch a.Type { + case unix.NL80211_ATTR_WIPHY: + return int(nlenc.Uint32(a.Data)), nil + } + } + return 0, fmt.Errorf("there was no wiphy attribute") +} + +// parseAttributes parses netlink attributes into a PHY's fields. +func (p *PHY) parseAttributes(attrs []netlink.Attribute) error { + for _, a := range attrs { + switch a.Type { + case unix.NL80211_ATTR_WIPHY: + p.Index = int(nlenc.Uint32(a.Data)) + + case unix.NL80211_ATTR_WIPHY_NAME: + p.Name = nlenc.String(a.Data) + + case unix.NL80211_ATTR_SUPPORTED_IFTYPES: + // This contains nested attributes with no data; the + // data we care about is the type. + nattrs, err := netlink.UnmarshalAttributes(a.Data) + if err != nil { + return err + } + for _, na := range nattrs { + p.SupportedIftypes = append(p.SupportedIftypes, InterfaceType(na.Type)) + } + + case unix.NL80211_ATTR_SOFTWARE_IFTYPES: + // This contains nested attributes with no data; the + // data we care about is the type. + nattrs, err := netlink.UnmarshalAttributes(a.Data) + if err != nil { + return err + } + for _, na := range nattrs { + p.SoftwareIftypes = append(p.SoftwareIftypes, InterfaceType(na.Type)) + } + + case unix.NL80211_ATTR_WIPHY_BANDS: + nattrs, err := netlink.UnmarshalAttributes(a.Data) + if err != nil { + return err + } + for i, band := range nattrs { + // band.Type has the band number + err := p.parseBandAttributes(band) + if err != nil { + return fmt.Errorf("could not decode band %d (attr#%d) data: %s", + band.Type, i, err) + } + } + + case unix.NL80211_ATTR_INTERFACE_COMBINATIONS: + nattrs, err := netlink.UnmarshalAttributes(a.Data) + if err != nil { + return err + } + for i, combo := range nattrs { + c, err := parseCombo(combo) + if err != nil { + return fmt.Errorf("could not decode combo %d data: %s", i, err) + } + p.InterfaceCombinations = append(p.InterfaceCombinations, *c) + } + + default: + p.Extra[a.Type] = a.Data + } + } + return nil +} + +// parseCombo parses a netlink attribute into an InterfaceCombination. +func parseCombo(comboNLA netlink.Attribute) (*InterfaceCombination, error) { + attrs, err := netlink.UnmarshalAttributes(comboNLA.Data) + if err != nil { + return nil, err + } + + combo := &InterfaceCombination{} + for _, attr := range attrs { + switch attr.Type { + case unix.NL80211_IFACE_COMB_LIMITS: + lattrs, err := netlink.UnmarshalAttributes(attr.Data) + if err != nil { + return nil, err + } + + for _, l := range lattrs { + comboLimit := InterfaceCombinationLimit{} + ltypes, err := netlink.UnmarshalAttributes(l.Data) + if err != nil { + return nil, err + } + + for _, la := range ltypes { + switch la.Type { + case unix.NL80211_IFACE_LIMIT_MAX: + comboLimit.Max = int(nlenc.Uint32(la.Data)) + case unix.NL80211_IFACE_LIMIT_TYPES: + types, err := netlink.UnmarshalAttributes(la.Data) + if err != nil { + return nil, err + } + + for _, typ := range types { + comboLimit.InterfaceTypes = append(comboLimit.InterfaceTypes, InterfaceType(typ.Type)) + } + } + } + combo.CombinationLimits = append(combo.CombinationLimits, comboLimit) + } + + case unix.NL80211_IFACE_COMB_NUM_CHANNELS: + combo.NumChannels = int(nlenc.Uint32(attr.Data)) + + case unix.NL80211_IFACE_COMB_MAXNUM: + combo.Total = int(nlenc.Uint32(attr.Data)) + + case unix.NL80211_IFACE_COMB_STA_AP_BI_MATCH: + combo.StaApBiMatch = true + } + } + return combo, nil +} + +// parseBandAttributes parses a netlink attribute into the band-specific data of +// a PHY. +func (p *PHY) parseBandAttributes(nlband netlink.Attribute) error { + attrs, err := netlink.UnmarshalAttributes(nlband.Data) + if err != nil { + return err + } + + // We'll get called multiple times for individual attributes of a band, + // so be sure to use the right element of the BandAttributes array, or + // add new ones if we haven't seen the band before. The expectation is + // that we'll get them in order, 0..n, but this should work for any + // ordering. + for int(nlband.Type)+1 > len(p.BandAttributes) { + ba := &BandAttributes{} + p.BandAttributes = append(p.BandAttributes, *ba) + } + ba := &p.BandAttributes[nlband.Type] + + for _, attr := range attrs { + switch attr.Type { + case unix.NL80211_BAND_ATTR_HT_CAPA: + ba.HTCapabilities = decodeHTCapabilities(ba.HTCapabilities, nlenc.Uint16(attr.Data)) + + case unix.NL80211_BAND_ATTR_HT_AMPDU_FACTOR: + exponent := nlenc.Uint8(attr.Data) + // The exponent comes from three bits of OTA data, but + // netlink gives it to us as an 8-bit value. + if exponent < 4 { + // If we haven't seen BandAttrHtCapa yet, we + // need to create the struct first. + if ba.HTCapabilities == nil { + ba.HTCapabilities = new(HTCapabilities) + } + ba.HTCapabilities.MaxRxAMPDULength = (1 << (13 + exponent)) - 1 + } + + case unix.NL80211_BAND_ATTR_HT_AMPDU_DENSITY: + spacing := nlenc.Uint8(attr.Data) + if spacing > 0 { + ba.MinRxAMPDUSpacing = (1 << (spacing - 1)) * time.Microsecond / 4 + } + + case unix.NL80211_BAND_ATTR_VHT_CAPA: + ba.VHTCapabilities = decodeVHTCapabilities(ba.VHTCapabilities, nlenc.Uint32(attr.Data)) + case unix.NL80211_BAND_ATTR_HT_MCS_SET: + if ba.HTCapabilities == nil { + ba.HTCapabilities = new(HTCapabilities) + } + copy(ba.HTCapabilities.SupportedMCS[:], attr.Data) + case unix.NL80211_BAND_ATTR_VHT_MCS_SET: + if ba.VHTCapabilities == nil { + ba.VHTCapabilities = new(VHTCapabilities) + } + copy(ba.VHTCapabilities.SupportedMCS[:], attr.Data) + + case unix.NL80211_BAND_ATTR_RATES: + nattrs, err := netlink.UnmarshalAttributes(attr.Data) + if err != nil { + return err + } + // It doesn't look like we need to take as much care to + // build up the BitrateAttributes array as we do the + // FrequenceAttributes array, since it appears we get + // all of the former back in a single message. But just + // in case ... + for _, nlbra := range nattrs { + brattrs, err := netlink.UnmarshalAttributes(nlbra.Data) + if err != nil { + return err + } + var bra BitrateAttrs + for _, bra2 := range brattrs { + switch bra2.Type { + case unix.NL80211_BITRATE_ATTR_RATE: + bra.Bitrate = 0.1 * float32(nlenc.Uint32(bra2.Data)) + case unix.NL80211_BITRATE_ATTR_2GHZ_SHORTPREAMBLE: + bra.ShortPreamble = true + } + } + ba.BitrateAttributes = append(ba.BitrateAttributes, bra) + } + + case unix.NL80211_BAND_ATTR_FREQS: + nattrs, err := netlink.UnmarshalAttributes(attr.Data) + if err != nil { + return err + } + for _, nlfa := range nattrs { + fattrs, err := netlink.UnmarshalAttributes(nlfa.Data) + if err != nil { + return err + } + var fa FrequencyAttrs + for _, fa2 := range fattrs { + switch fa2.Type { + case unix.NL80211_FREQUENCY_ATTR_FREQ: + fa.Frequency = int(nlenc.Uint32(fa2.Data)) + case unix.NL80211_FREQUENCY_ATTR_DISABLED: + fa.Disabled = true + // In 8fe02e167efa8 (3.14), Linux renamed the + // PASSIVE_SCAN frequency attribute to NO_IR, + // and deprecated NO_IBSS (4). It sends both, + // but we don't need to support old kernels. + case unix.NL80211_FREQUENCY_ATTR_NO_IR: + fa.NoIR = true + case unix.NL80211_FREQUENCY_ATTR_RADAR: + fa.RadarDetection = true + case unix.NL80211_FREQUENCY_ATTR_MAX_TX_POWER: + fa.MaxTxPower = 0.01 * float32(nlenc.Uint32(fa2.Data)) + } + } + ba.FrequencyAttributes = append(ba.FrequencyAttributes, fa) + } + } + } + + return nil +} + +// decodeHTCapabilities parses a 16-bit integer into an HTCapabilities struct +// based on information from an HT Capabilities Info field (NL80211_BAND_ATTR_HT_CAPA). +// Create a new one if nil is passed in, but allow for the struct to have other +// fields already set. +func decodeHTCapabilities(htcap *HTCapabilities, capability uint16) *HTCapabilities { + if htcap == nil { + htcap = new(HTCapabilities) + } + + htcap.RxLDPC = capability&(1<<0) != 0 + htcap.CW40 = capability&(1<<1) != 0 + htcap.HTGreenfield = capability&(1<<4) != 0 + htcap.SGI20 = capability&(1<<5) != 0 + htcap.SGI40 = capability&(1<<6) != 0 + htcap.TxSTBC = capability&(1<<7) != 0 + htcap.RxSTBCStreams = uint8((capability >> 8) & 0x3) + htcap.HTDelayedBlockAck = capability&(1<<10) != 0 + htcap.LongMaxAMSDULength = capability&(1<<11) != 0 + htcap.DSSSCCKHT40 = capability&(1<<12) != 0 + htcap.FortyMhzIntolerant = capability&(1<<14) != 0 + htcap.LSIGTxOPProtection = capability&(1<<15) != 0 + + return htcap +} + +// decodeVHTCapabilities parses a 32-bit integer into an VHTCapabilities struct +// based on information from an VHT Capabilities Info field (NL80211_BAND_ATTR_VHT_CAPA). +// Create a new one if nil is passed in, but allow for the struct to have other +// fields already set. +func decodeVHTCapabilities(vhtcap *VHTCapabilities, capability uint32) *VHTCapabilities { + if vhtcap == nil { + vhtcap = new(VHTCapabilities) + } + switch int(capability & 0x3) { + case 0: + vhtcap.MaxMPDULength = 3895 + case 1: + vhtcap.MaxMPDULength = 7991 + case 2: + vhtcap.MaxMPDULength = 11454 + } + vhtcap.VHT160 = capability&(1<<2) != 0 + vhtcap.VHT8080 = capability&(1<<3) != 0 + vhtcap.RXLDPC = capability&(1<<4) != 0 + vhtcap.ShortGI80 = capability&(1<<5) != 0 + vhtcap.ShortGI160 = capability&(1<<6) != 0 + vhtcap.TXSTBC = capability&(1<<7) != 0 + vhtcap.RXSTBC = int((capability >> 8) & 0x7) + vhtcap.SuBeamFormer = capability&(1<<11) != 0 + vhtcap.SuBeamFormee = capability&(1<<12) != 0 + vhtcap.BFAntenna = int((capability>>13)&0x7) - 1 + vhtcap.SoundingDimension = int((capability >> 16) & 0x7) + vhtcap.MuBeamformer = capability&(1<<19) != 0 + vhtcap.MuBeamformee = capability&(1<<20) != 0 + vhtcap.VTHTXOPPS = capability&(1<<21) != 0 + vhtcap.HTCVHT = capability&(1<<22) != 0 + vhtcap.MaxAMPDU = 2 ^ (13 + int((capability>>23)&0x2)) - 1 + vhtcap.VHTLinkAdapt = int((capability >> 27) & 0x3) + vhtcap.RXAntennaPattern = capability&(1<<28) != 0 + vhtcap.TXAntennaPattern = capability&(1<<29) != 0 + vhtcap.ExtendedNSSBW = int((capability >> 30) & 0x7) + + return vhtcap +} + // parseAttributes parses netlink attributes into a BSS's fields. func (b *BSS) parseAttributes(attrs []netlink.Attribute) error { for _, a := range attrs { diff --git a/client_others.go b/client_others.go index 6f10434..e565dee 100644 --- a/client_others.go +++ b/client_others.go @@ -20,6 +20,8 @@ func newClient() (*client, error) { return nil, errUnimplemented } func (*client) Close() error { return errUnimplemented } func (*client) Interfaces() ([]*Interface, error) { return nil, errUnimplemented } +func (c *client) PHY(_ uint32) (*PHY, error) { return nil, errUnimplemented } +func (c *client) PHYs() ([]*PHY, error) { return nil, errUnimplemented } func (*client) BSS(_ *Interface) (*BSS, error) { return nil, errUnimplemented } func (client) AccessPoints(ifi *Interface) ([]*BSS, error) { return nil, errUnimplemented } func (*client) StationInfo(_ *Interface) ([]*StationInfo, error) { return nil, errUnimplemented } diff --git a/wifi.go b/wifi.go index 290b3a3..1910eca 100644 --- a/wifi.go +++ b/wifi.go @@ -100,6 +100,8 @@ func (t InterfaceType) String() string { return "station" case InterfaceTypeAP: return "access point" + case InterfaceTypeAPVLAN: + return "access point/VLAN" case InterfaceTypeWDS: return "wireless distribution" case InterfaceTypeMonitor: @@ -356,6 +358,249 @@ func (s BSSStatus) String() string { } } +// A PHY represents the physical attributes of a wireless device. +type PHY struct { + // The index of the interface. + Index int + + // The name of the interface. + Name string + + // The interface types this device supports. + SupportedIftypes []InterfaceType + + // The software-only interface types this device supports. + SoftwareIftypes []InterfaceType + + // An array of attributes related to each radio frequency band. + BandAttributes []BandAttributes + + // A description of what combinations of interfaces the device can + // support running simultaneously, on virtual MACs. + InterfaceCombinations []InterfaceCombination + + // All the attributes the kernel has told us about, but we haven't + // parsed. + Extra map[uint16][]byte +} + +// BandAttributes represent the RF band-specific attributes. +type BandAttributes struct { + // High Throughput (802.11n) device capabilities (nil if not supported). + HTCapabilities *HTCapabilities + + // Very High Throughput (802.11ac) device capabilities (nil if not supported). + VHTCapabilities *VHTCapabilities + + // Minimum spacing between A-MPDU frames. Used for both HT and VHT + // capable devices. + MinRxAMPDUSpacing time.Duration + + // Per-frequency (channel) attributes. + FrequencyAttributes []FrequencyAttrs + + // Per-bitrate attributes. + BitrateAttributes []BitrateAttrs +} + +// HTCapabilities represents 802.11n (High Throughput) capabilities. This group +// of attributes is specific to each band of frequencies. Failure to support +// any given attribute may be due to lack support in the driver or the firmware, +// not only in the hardware. Some of them may also be overridden during station +// association. +// +// The fields represent those in the HT Capabilities element (802.11-2016, +// 9.4.2.56). Notably missing is information about the device's Spatial +// Multiplexing Power Save (SMPS) capability. SMPS support must be determined +// by retrieving the device feature flags (not yet supported). +type HTCapabilities struct { + // Device supports Low Density Parity Check codes. + RxLDPC bool + + // Device supports 40MHz channels (in addition to 20MHz channels). + CW40 bool + + // Device supports HT Greenfield (802.11n-only) mode, in which a/b/g + // frames will be ignored. + HTGreenfield bool + + // Device supports short guard intervals in 20MHz channels. + SGI20 bool + + // Device supports short guard intervals in 40MHz channels. + SGI40 bool + + // Device supports Space-Time Block Coding transmission. + TxSTBC bool + + // Number of STBC receive streams supported by the device. Valid values + // are 0-3. + RxSTBCStreams uint8 + + // Device supports delayed Block Ack frames when acknowledging an + // A-MPDU. + HTDelayedBlockAck bool + + // Device supports long (7935 bytes) maximum A-MSDU length, compared to + // standard 3839 bytes. + LongMaxAMSDULength bool + + // Device supports DSSS/CCK in 40MHz channels. + DSSSCCKHT40 bool + + // (2.4GHz) Band cannot tolerate 40MHz channels because someone has + // requested it support 20MHz channels. + FortyMhzIntolerant bool + + // Device supports L-SIG (non-HT) Transmit Oppportunity protection. + LSIGTxOPProtection bool + + // Maximum receivable A-MPDU (Aggregated MAC Protocol Data Unit) frame + // size. + MaxRxAMPDULength int + + // Supported MCS for HT mode + // Todo: + // - Parse them are according to Section 7.3.2.56.4 IEEE 80211n + SupportedMCS [16]byte +} + +// VHTCapabilities represents 802.11ac (Very High Throughput) capabilities. +// +// The fields represent those in the VHT Capabilities element (802.11-2020, +// 9.4.2.157). +type VHTCapabilities struct { + // Maximum MPDU length supported by the device. + MaxMPDULength int + + // Device supports 160MHz channel width. + VHT160 bool + + // Device supports 80+80MHz channel width (non-contiguous 160MHz) along with 160MHz channel. + VHT8080 bool + + // Device supports receiving Low Density Parity Check codes. + RXLDPC bool + + // Device supports short guard intervals in 80MHz channels. + ShortGI80 bool + + // Device supports short guard intervals in 160MHz and 80+80MHz channels. + ShortGI160 bool + + // Device supports transmission of at least 2x1 Space-Time Block Coding transmission. + TXSTBC bool + + // Number of STBC receive streams supported by the device. Valid values are 0-4. + RXSTBC int + + // Device supports SU (Single User) Beamforming as a transmitter. + SuBeamFormer bool + + // Device supports SU (Single User) Beamforming as a receiver. + SuBeamFormee bool + + // Number of sounding antennas supported by the device for SU Beamforming transmission. + BFAntenna int + + // Maximum sounding dimensions supported by the device for SU Beamforming. + SoundingDimension int + + // Device supports MU (Multi-User) Beamforming as a transmitter. + MuBeamformer bool + + // Device supports MU (Multi-User) Beamforming as a receiver. + MuBeamformee bool + + // Device supports VHT TXOP power save mode. + VTHTXOPPS bool + + // Device supports HT Control field when operating in VHT mode. + HTCVHT bool + + // Maximum A-MPDU (Aggregated MAC Protocol Data Unit) frame size supported by the device. + MaxAMPDU int + + // Device supports VHT Link Adaptation capabilities. Valid values + // specify the type of link adaptation supported (e.g., no feedback, + // unsolicited feedback, or both). + VHTLinkAdapt int + + // Device supports receive antenna pattern consistency. + RXAntennaPattern bool + + // Device supports transmit antenna pattern consistency. + TXAntennaPattern bool + + //Indicates whether the STA is capable of interpreting the Extended NSS BW + //Support subfield of the VHT Capabilities Information field. + ExtendedNSSBW int + + // Supported MCS for VHT mode + // Todo: + // - Parse them according to Section 8.4.2.160.3 IEEE Std 80211ac-2013 + SupportedMCS [8]byte +} + +// FrequencyAttrs represents the attributes of a WiFi frequency/channel. +type FrequencyAttrs struct { + // Frequency is the radio frequency in MHz. + Frequency int + + // Disabled indicates that the channel is disabled due to regulatory + // requirements. + Disabled bool + + // NoIR indicates that no mechanisms that initiate radiation are + // permitted on this channel. + NoIR bool + + // RadarDetection indicates that radar detection is mandatory on this + // channel. + RadarDetection bool + + // MaxTxPower gives the maximum transmission power in mBm (100 * dBm). + MaxTxPower float32 +} + +// BitrateAttrs represents the attributes of a bitrate. +type BitrateAttrs struct { + // Bitrate is the bitrate in units of 100kbps. + Bitrate float32 + + // ShortPreamble indicates that a short preamble is supported in the + // 2.4GHz band. + ShortPreamble bool +} + +// InterfaceCombination represents a group of valid combinations of interface +// types which can be simultaneously supported on a device. +type InterfaceCombination struct { + CombinationLimits []InterfaceCombinationLimit + + // Total is the maximum number of interfaces that can be created in this + // group. + Total int + + // NumChannels is the number of different channels which may be used in + // this group. + NumChannels int + + // StaApBiMatch indicates that beacon intervals within this group must + // all be the same, regardless of interface type. + StaApBiMatch bool +} + +// InterfaceCombinationLimit represents a single combination of interface types +// which may be run simultaneously on a device. +type InterfaceCombinationLimit struct { + InterfaceTypes []InterfaceType + + // Max is the maximum number of interfaces that can be chosen from the + // set of interface types in InterfaceTypes. + Max int +} + // List of 802.11 Information Element types. const ( ieSSID = 0