Skip to content

Commit

Permalink
wifi: initial commit, working on Linux
Browse files Browse the repository at this point in the history
  • Loading branch information
mdlayher committed Jan 6, 2017
1 parent f5e8772 commit ce63de2
Show file tree
Hide file tree
Showing 4 changed files with 892 additions and 0 deletions.
36 changes: 36 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package wifi

// A Client is a type which can access WiFi device actions and statistics
// using operating system-specific operations.
type Client struct {
c osClient
}

// New creates a new Client.
func New() (*Client, error) {
c, err := newClient()
if err != nil {
return nil, err
}

return &Client{
c: c,
}, nil
}

// Interfaces returns a list of the system's WiFi network interfaces.
func (c *Client) Interfaces() ([]*Interface, error) {
return c.c.Interfaces()
}

// StationInfo retrieves statistics about a WiFi interface operating in
// station mode.
func (c *Client) StationInfo(ifi *Interface) (*StationInfo, error) {
return c.c.StationInfo(ifi)
}

// An osClient is the operating system-specific implementation of Client.
type osClient interface {
Interfaces() ([]*Interface, error)
StationInfo(ifi *Interface) (*StationInfo, error)
}
347 changes: 347 additions & 0 deletions client_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
//+build linux

package wifi

import (
"errors"
"math"
"net"
"os"
"time"

"github.com/mdlayher/netlink"
"github.com/mdlayher/netlink/genetlink"
"github.com/mdlayher/netlink/nlenc"
"github.com/mdlayher/wifi/internal/nl80211"
)

// Errors which may occur when interacting with generic netlink.
var (
errMultipleMessages = errors.New("expected only one generic netlink message")
errInvalidCommand = errors.New("invalid generic netlink response command")
errInvalidFamilyVersion = errors.New("invalid generic netlink response family version")
)

var _ osClient = &client{}

// A client is the Linux implementation of osClient, which makes use of
// netlink, generic netlink, and nl80211 to provide access to WiFi device
// actions and statistics.
type client struct {
c genl
familyID uint16
familyVersion uint8
}

// genl is an interface over generic netlink, so netlink interactions can
// be stubbed in tests.
type genl interface {
GetFamily(name string) (genetlink.Family, error)
Execute(m genetlink.Message, family uint16, flags netlink.HeaderFlags) ([]genetlink.Message, error)
}

// newClient dials a generic netlink connection and verifies that nl80211
// is available for use by this package.
func newClient() (*client, error) {
c, err := genetlink.Dial(nil)
if err != nil {
return nil, err
}

g := &sysGENL{Conn: c}
return initClient(g)
}

// initClient is the internal constructor for a client, used in tests.
func initClient(c genl) (*client, error) {
family, err := c.GetFamily(nl80211.GenlName)
if err != nil {
return nil, err
}

return &client{
c: c,
familyID: family.ID,
familyVersion: family.Version,
}, nil
}

// Interfaces requests that nl80211 return a list of all WiFi interfaces present
// on this system.
func (c *client) Interfaces() ([]*Interface, error) {
// Ask nl80211 to dump a list of all WiFi interfaces
req := genetlink.Message{
Header: genetlink.Header{
Command: nl80211.CmdGetInterface,
Version: c.familyVersion,
},
}

flags := netlink.HeaderFlagsRequest | netlink.HeaderFlagsDump
msgs, err := c.c.Execute(req, c.familyID, flags)
if err != nil {
return nil, err
}

if err := c.checkMessages(msgs, nl80211.CmdNewInterface); err != nil {
return nil, err
}

return parseInterfaces(msgs)
}

// StationInfo requests that nl80211 return station info for the specified
// Interface.
func (c *client) StationInfo(ifi *Interface) (*StationInfo, error) {
b, err := netlink.MarshalAttributes(ifi.stationInfoAttrs())
if err != nil {
return nil, err
}

// Ask nl80211 to retrieve station info for the interface specified
// by its attributes
req := genetlink.Message{
Header: genetlink.Header{
// From nl80211.h:
// * @NL80211_CMD_GET_STATION: Get station attributes for station identified by
// * %NL80211_ATTR_MAC on the interface identified by %NL80211_ATTR_IFINDEX.
Command: nl80211.CmdGetStation,
Version: c.familyVersion,
},
Data: b,
}

flags := netlink.HeaderFlagsRequest | netlink.HeaderFlagsDump
msgs, err := c.c.Execute(req, c.familyID, flags)
if err != nil {
return nil, err
}

if len(msgs) > 1 {
return nil, errMultipleMessages
}

if err := c.checkMessages(msgs, nl80211.CmdNewStation); err != nil {
return nil, err
}

return parseStationInfo(msgs[0].Data)
}

// checkMessages verifies that response messages from generic netlink contain
// the command and family version we expect.
func (c *client) checkMessages(msgs []genetlink.Message, command uint8) error {
for _, m := range msgs {
if m.Header.Command != command {
return errInvalidCommand
}

if m.Header.Version != c.familyVersion {
return errInvalidFamilyVersion
}
}

return nil
}

// parseInterfaces parses zero or more Interfaces from nl80211 interface
// messages.
func parseInterfaces(msgs []genetlink.Message) ([]*Interface, error) {
ifis := make([]*Interface, 0, len(msgs))
for _, m := range msgs {
attrs, err := netlink.UnmarshalAttributes(m.Data)
if err != nil {
return nil, err
}

var ifi Interface
if err := (&ifi).parseAttributes(attrs); err != nil {
return nil, err
}

ifis = append(ifis, &ifi)
}

return ifis, nil
}

// stationInfoAttrs returns the netlink attributes required from an Interface
// to retrieve a StationInfo.
func (ifi *Interface) stationInfoAttrs() []netlink.Attribute {
return []netlink.Attribute{
{
Type: nl80211.AttrIfindex,
Data: nlenc.Uint32Bytes(uint32(ifi.Index)),
},
{
Type: nl80211.AttrMac,
Data: ifi.HardwareAddr,
},
}
}

// parseAttributes parses netlink attributes into an Interface's fields.
func (ifi *Interface) parseAttributes(attrs []netlink.Attribute) error {
for _, a := range attrs {
switch a.Type {
case nl80211.AttrIfindex:
ifi.Index = int(nlenc.Uint32(a.Data))
case nl80211.AttrIfname:
ifi.Name = nlenc.String(a.Data)
case nl80211.AttrMac:
ifi.HardwareAddr = net.HardwareAddr(a.Data)
case nl80211.AttrWiphy:
ifi.PHY = int(nlenc.Uint32(a.Data))
case nl80211.AttrIftype:
// NOTE: InterfaceType copies the ordering of nl80211's interface type
// constants. This may not be the case on other operating systems.
ifi.Type = InterfaceType(nlenc.Uint32(a.Data))
case nl80211.AttrWdev:
ifi.Device = int(nlenc.Uint64(a.Data))
case nl80211.AttrWiphyFreq:
ifi.Frequency = int(nlenc.Uint32(a.Data))
}
}

return nil
}

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

for _, a := range attrs {
// The other attributes that are returned here appear to indicate the
// interface index and MAC address, which is information we already
// possess. No need to parse them for now.
if a.Type != nl80211.AttrStaInfo {
continue
}

nattrs, err := netlink.UnmarshalAttributes(a.Data)
if err != nil {
return nil, err
}

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

return &info, nil
}

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

// parseAttributes parses netlink attributes into a StationInfo's fields.
func (info *StationInfo) parseAttributes(attrs []netlink.Attribute) error {
for _, a := range attrs {
switch a.Type {
case nl80211.StaInfoConnectedTime:
// Though nl80211 does not specify, this value appears to be in seconds:
// * @NL80211_STA_INFO_CONNECTED_TIME: time since the station is last connected
info.Connected = time.Duration(nlenc.Uint32(a.Data)) * time.Second
case nl80211.StaInfoInactiveTime:
// * @NL80211_STA_INFO_INACTIVE_TIME: time since last activity (u32, msecs)
info.Inactive = time.Duration(nlenc.Uint32(a.Data)) * time.Millisecond
case nl80211.StaInfoRxBytes64:
info.ReceivedBytes = nlenc.Uint64(a.Data)
case nl80211.StaInfoTxBytes64:
info.TransmittedBytes = nlenc.Uint64(a.Data)
case nl80211.StaInfoSignal:
// Converted into the typical negative strength format
// * @NL80211_STA_INFO_SIGNAL: signal strength of last received PPDU (u8, dBm)
info.Signal = int(a.Data[0]) - math.MaxUint8
case nl80211.StaInfoRxPackets:
info.ReceivedPackets = nlenc.Uint32(a.Data)
case nl80211.StaInfoTxPackets:
info.TransmittedPackets = nlenc.Uint32(a.Data)
case nl80211.StaInfoTxRetries:
info.TransmitRetries = nlenc.Uint32(a.Data)
case nl80211.StaInfoTxFailed:
info.TransmitFailed = nlenc.Uint32(a.Data)
case nl80211.StaInfoBeaconLoss:
info.BeaconLoss = nlenc.Uint32(a.Data)
case nl80211.StaInfoRxBitrate, nl80211.StaInfoTxBitrate:
rate, err := parseRateInfo(a.Data)
if err != nil {
return err
}

// TODO(mdlayher): return more statistics if they end up being
// generally useful
switch a.Type {
case nl80211.StaInfoRxBitrate:
info.ReceiveBitrate = rate.Bitrate
case nl80211.StaInfoTxBitrate:
info.TransmitBitrate = rate.Bitrate
}
}

// Only use 32-bit counters if the 64-bit counters are not present.
// If the 64-bit counters appear later in the slice, they will overwrite
// these values.
if info.ReceivedBytes == 0 && a.Type == nl80211.StaInfoRxBytes {
info.ReceivedBytes = uint64(nlenc.Uint32(a.Data))
}
if info.TransmittedBytes == 0 && a.Type == nl80211.StaInfoTxBytes {
info.TransmittedBytes = uint64(nlenc.Uint32(a.Data))
}
}

return nil
}

// rateInfo provides statistics about the receive or transmit rate of
// an interface.
type rateInfo struct {
// Bitrate in bits per second.
Bitrate int
}

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

var info rateInfo
for _, a := range attrs {
switch a.Type {
case nl80211.RateInfoBitrate32:
info.Bitrate = int(nlenc.Uint32(a.Data))
}

// Only use 16-bit counters if the 32-bit counters are not present.
// If the 32-bit counters appear later in the slice, they will overwrite
// these values.
if info.Bitrate == 0 && a.Type == nl80211.RateInfoBitrate {
info.Bitrate = int(nlenc.Uint16(a.Data))
}
}

// Scale bitrate to bits/second as base unit instead of 100kbits/second.
// * @NL80211_RATE_INFO_BITRATE: total bitrate (u16, 100kbit/s)
info.Bitrate *= 100 * 1000

return &info, nil
}

var _ genl = &sysGENL{}

// sysGENL is the system implementation of genl, using generic netlink.
type sysGENL struct {
*genetlink.Conn
}

// GetFamily is a small adapter to make *genetlink.Conn implement genl.
func (g *sysGENL) GetFamily(name string) (genetlink.Family, error) {
return g.Conn.Family.Get(name)
}
Loading

0 comments on commit ce63de2

Please sign in to comment.