pax_global_header00006660000000000000000000000064144423326520014517gustar00rootroot0000000000000052 comment=2e12db85563c9c6187d2ab69bcfd4c64e83f41ef golang-github-mdlayher-wifi-0.1.0/000077500000000000000000000000001444233265200170035ustar00rootroot00000000000000golang-github-mdlayher-wifi-0.1.0/.github/000077500000000000000000000000001444233265200203435ustar00rootroot00000000000000golang-github-mdlayher-wifi-0.1.0/.github/workflows/000077500000000000000000000000001444233265200224005ustar00rootroot00000000000000golang-github-mdlayher-wifi-0.1.0/.github/workflows/static-analysis.yml000066400000000000000000000013341444233265200262340ustar00rootroot00000000000000name: Static Analysis on: push: branches: - "*" pull_request: branches: - "*" jobs: build: strategy: matrix: go-version: ["1.20"] runs-on: ubuntu-latest steps: - name: Set up Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v3 - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@latest - name: Print staticcheck version run: staticcheck -version - name: Run staticcheck run: staticcheck ./... - name: Run go vet run: go vet ./... golang-github-mdlayher-wifi-0.1.0/.github/workflows/test.yml000066400000000000000000000012541444233265200241040ustar00rootroot00000000000000name: Test on: push: branches: - '*' pull_request: branches: - '*' jobs: build: strategy: fail-fast: false matrix: go-version: ["1.20"] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - name: Set up Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v3 # Run basic tests, we just want to make sure there is parity on Linux and # macOS, and back to the oldest version of Go this library supports. - name: Run tests run: go test ./... golang-github-mdlayher-wifi-0.1.0/LICENSE.md000066400000000000000000000020631444233265200204100ustar00rootroot00000000000000# MIT License Copyright (C) 2016-2022 Matt Layher Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-mdlayher-wifi-0.1.0/README.md000066400000000000000000000007511444233265200202650ustar00rootroot00000000000000# wifi [![Test Status](https://github.com/mdlayher/wifi/workflows/Test/badge.svg)](https://github.com/mdlayher/wifi/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/mdlayher/wifi.svg)](https://pkg.go.dev/github.com/mdlayher/wifi) [![Go Report Card](https://goreportcard.com/badge/github.com/mdlayher/wifi)](https://goreportcard.com/report/github.com/mdlayher/wifi) Package `wifi` provides access to IEEE 802.11 WiFi device operations on Linux using `nl80211`. MIT Licensed. golang-github-mdlayher-wifi-0.1.0/client.go000066400000000000000000000025341444233265200206140ustar00rootroot00000000000000package wifi // A Client is a type which can access WiFi device actions and statistics // using operating system-specific operations. type Client struct { c *client } // New creates a new Client. func New() (*Client, error) { c, err := newClient() if err != nil { return nil, err } return &Client{ c: c, }, nil } // Close releases resources used by a Client. func (c *Client) Close() error { return c.c.Close() } // Connect starts connecting the interface to the specified ssid. func (c *Client) Connect(ifi *Interface, ssid string) error { return c.c.Connect(ifi, ssid) } // Dissconnect disconnects the interface. func (c *Client) Disconnect(ifi *Interface) error { return c.c.Disconnect(ifi) } // Connect starts connecting the interface to the specified ssid using WPA. func (c *Client) ConnectWPAPSK(ifi *Interface, ssid, psk string) error { return c.c.ConnectWPAPSK(ifi, ssid, psk) } // Interfaces returns a list of the system's WiFi network interfaces. func (c *Client) Interfaces() ([]*Interface, error) { return c.c.Interfaces() } // BSS retrieves the BSS associated with a WiFi interface. func (c *Client) BSS(ifi *Interface) (*BSS, error) { return c.c.BSS(ifi) } // StationInfo retrieves all station statistics about a WiFi interface. func (c *Client) StationInfo(ifi *Interface) ([]*StationInfo, error) { return c.c.StationInfo(ifi) } golang-github-mdlayher-wifi-0.1.0/client_linux.go000066400000000000000000000350421444233265200220330ustar00rootroot00000000000000//go:build linux // +build linux package wifi import ( "bytes" "crypto/sha1" "net" "os" "time" "unicode/utf8" "github.com/mdlayher/genetlink" "github.com/mdlayher/netlink" "github.com/mdlayher/netlink/nlenc" "golang.org/x/crypto/pbkdf2" "golang.org/x/sys/unix" ) // 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 *genetlink.Conn familyID uint16 familyVersion uint8 } // 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 } // Make a best effort to apply the strict options set to provide better // errors and validation. We don't apply Strict in the constructor because // this library is widely used on a range of kernels and we can't guarantee // it will always work on older kernels. for _, o := range []netlink.ConnOption{ netlink.ExtendedAcknowledge, netlink.GetStrictCheck, } { _ = c.SetOption(o, true) } return initClient(c) } func initClient(c *genetlink.Conn) (*client, error) { family, err := c.GetFamily(unix.NL80211_GENL_NAME) if err != nil { // Ensure the genl socket is closed on error to avoid leaking file // descriptors. _ = c.Close() return nil, err } return &client{ c: c, familyID: family.ID, familyVersion: family.Version, }, nil } // Close closes the client's generic netlink connection. func (c *client) Close() error { return c.c.Close() } // 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 msgs, err := c.get( unix.NL80211_CMD_GET_INTERFACE, netlink.Dump, nil, nil, ) if err != nil { return nil, err } return parseInterfaces(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. _, err := c.get( unix.NL80211_CMD_CONNECT, netlink.Acknowledge, ifi, func(ae *netlink.AttributeEncoder) { ae.Bytes(unix.NL80211_ATTR_SSID, []byte(ssid)) ae.Uint32(unix.NL80211_ATTR_AUTH_TYPE, unix.NL80211_AUTHTYPE_OPEN_SYSTEM) }, ) return err } // Disconnect disconnects the interface. func (c *client) Disconnect(ifi *Interface) error { // Ask nl80211 to disconnect. _, err := c.get( unix.NL80211_CMD_DISCONNECT, netlink.Acknowledge, ifi, nil, ) return err } // ConnectWPAPSK starts connecting the interface to the specified SSID using // WPA. func (c *client) ConnectWPAPSK(ifi *Interface, ssid, psk string) error { // Ask nl80211 to connect to the specified SSID with key.. _, err := c.get( unix.NL80211_CMD_CONNECT, netlink.Acknowledge, ifi, func(ae *netlink.AttributeEncoder) { // TODO(mdlayher): document these or build from bitflags. const ( cipherSuites = 0xfac04 akmSuites = 0xfac02 ) ae.Bytes(unix.NL80211_ATTR_SSID, []byte(ssid)) ae.Uint32(unix.NL80211_ATTR_WPA_VERSIONS, unix.NL80211_WPA_VERSION_2) ae.Uint32(unix.NL80211_ATTR_CIPHER_SUITE_GROUP, cipherSuites) ae.Uint32(unix.NL80211_ATTR_CIPHER_SUITES_PAIRWISE, cipherSuites) ae.Uint32(unix.NL80211_ATTR_AKM_SUITES, akmSuites) ae.Flag(unix.NL80211_ATTR_WANT_1X_4WAY_HS, true) ae.Bytes( unix.NL80211_ATTR_PMK, wpaPassphrase([]byte(ssid), []byte(psk)), ) ae.Uint32(unix.NL80211_ATTR_AUTH_TYPE, unix.NL80211_AUTHTYPE_OPEN_SYSTEM) }, ) return err } // wpaPassphrase computes a WPA passphrase given an SSID and preshared key. func wpaPassphrase(ssid, psk []byte) []byte { return pbkdf2.Key(psk, ssid, 4096, 32, sha1.New) } // BSS requests that nl80211 return the BSS for the specified Interface. func (c *client) BSS(ifi *Interface) (*BSS, error) { msgs, err := c.get( unix.NL80211_CMD_GET_SCAN, 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 } return parseBSS(msgs) } // StationInfo requests that nl80211 return all station info for the specified // Interface. func (c *client) StationInfo(ifi *Interface) ([]*StationInfo, error) { msgs, err := c.get( unix.NL80211_CMD_GET_STATION, 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 } if len(msgs) == 0 { return nil, os.ErrNotExist } stations := make([]*StationInfo, len(msgs)) for i := range msgs { if stations[i], err = parseStationInfo(msgs[i].Data); err != nil { return nil, err } } return stations, nil } // get performs a request/response interaction with nl80211. func (c *client) get( cmd uint8, flags netlink.HeaderFlags, ifi *Interface, // May be nil; used to apply optional parameters. params func(ae *netlink.AttributeEncoder), ) ([]genetlink.Message, error) { ae := netlink.NewAttributeEncoder() ifi.encode(ae) if params != nil { // Optionally apply more parameters to the attribute encoder. params(ae) } // Note: don't send netlink.Acknowledge or we get an extra message back from // the kernel which doesn't seem useful as of now. return c.execute(cmd, flags, ae) } // execute executes the specified command with additional header flags and input // netlink request attributes. The netlink.Request header flag is automatically // set. func (c *client) execute( cmd uint8, flags netlink.HeaderFlags, ae *netlink.AttributeEncoder, ) ([]genetlink.Message, error) { b, err := ae.Encode() if err != nil { return nil, err } return c.c.Execute( genetlink.Message{ Header: genetlink.Header{ Command: cmd, Version: c.familyVersion, }, Data: b, }, // Always pass the genetlink family ID and request flag. c.familyID, netlink.Request|flags, ) } // 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 } // encode provides an encoding function for ifi's attributes. If ifi is nil, // encode is a no-op. func (ifi *Interface) encode(ae *netlink.AttributeEncoder) { if ifi == nil { return } // Mandatory. ae.Uint32(unix.NL80211_ATTR_IFINDEX, uint32(ifi.Index)) } // idAttrs returns the netlink attributes required from an Interface to retrieve // more data about it. func (ifi *Interface) idAttrs() []netlink.Attribute { return []netlink.Attribute{ { Type: unix.NL80211_ATTR_IFINDEX, Data: nlenc.Uint32Bytes(uint32(ifi.Index)), }, { Type: unix.NL80211_ATTR_MAC, 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 unix.NL80211_ATTR_IFINDEX: ifi.Index = int(nlenc.Uint32(a.Data)) case unix.NL80211_ATTR_IFNAME: ifi.Name = nlenc.String(a.Data) case unix.NL80211_ATTR_MAC: ifi.HardwareAddr = net.HardwareAddr(a.Data) case unix.NL80211_ATTR_WIPHY: ifi.PHY = int(nlenc.Uint32(a.Data)) case unix.NL80211_ATTR_IFTYPE: // 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 unix.NL80211_ATTR_WDEV: ifi.Device = int(nlenc.Uint64(a.Data)) case unix.NL80211_ATTR_WIPHY_FREQ: ifi.Frequency = int(nlenc.Uint32(a.Data)) } } return nil } // parseBSS parses a single BSS with a status attribute from nl80211 BSS messages. func parseBSS(msgs []genetlink.Message) (*BSS, error) { for _, m := range msgs { attrs, err := netlink.UnmarshalAttributes(m.Data) if err != nil { return nil, err } for _, a := range attrs { if a.Type != unix.NL80211_ATTR_BSS { continue } nattrs, err := netlink.UnmarshalAttributes(a.Data) if err != nil { return nil, err } // The BSS which is associated with an interface will have a status // attribute if !attrsContain(nattrs, unix.NL80211_BSS_STATUS) { continue } var bss BSS if err := (&bss).parseAttributes(nattrs); err != nil { return nil, err } return &bss, nil } } return nil, os.ErrNotExist } // parseAttributes parses netlink attributes into a BSS's fields. func (b *BSS) parseAttributes(attrs []netlink.Attribute) error { for _, a := range attrs { switch a.Type { case unix.NL80211_BSS_BSSID: b.BSSID = net.HardwareAddr(a.Data) case unix.NL80211_BSS_FREQUENCY: b.Frequency = int(nlenc.Uint32(a.Data)) case unix.NL80211_BSS_BEACON_INTERVAL: // Raw value is in "Time Units (TU)". See: // https://en.wikipedia.org/wiki/Beacon_frame b.BeaconInterval = time.Duration(nlenc.Uint16(a.Data)) * 1024 * time.Microsecond case unix.NL80211_BSS_SEEN_MS_AGO: // * @NL80211_BSS_SEEN_MS_AGO: age of this BSS entry in ms b.LastSeen = time.Duration(nlenc.Uint32(a.Data)) * time.Millisecond case unix.NL80211_BSS_STATUS: // NOTE: BSSStatus copies the ordering of nl80211's BSS status // constants. This may not be the case on other operating systems. b.Status = BSSStatus(nlenc.Uint32(a.Data)) case unix.NL80211_BSS_INFORMATION_ELEMENTS: ies, err := parseIEs(a.Data) if err != nil { return err } // TODO(mdlayher): return more IEs if they end up being generally useful for _, ie := range ies { switch ie.ID { case ieSSID: b.SSID = decodeSSID(ie.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 } var info StationInfo for _, a := range attrs { switch a.Type { case unix.NL80211_ATTR_MAC: info.HardwareAddr = net.HardwareAddr(a.Data) case unix.NL80211_ATTR_STA_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 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 unix.NL80211_STA_INFO_CONNECTED_TIME: // 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 unix.NL80211_STA_INFO_INACTIVE_TIME: // * @NL80211_STA_INFO_INACTIVE_TIME: time since last activity (u32, msecs) info.Inactive = time.Duration(nlenc.Uint32(a.Data)) * time.Millisecond case unix.NL80211_STA_INFO_RX_BYTES64: info.ReceivedBytes = int(nlenc.Uint64(a.Data)) case unix.NL80211_STA_INFO_TX_BYTES64: info.TransmittedBytes = int(nlenc.Uint64(a.Data)) case unix.NL80211_STA_INFO_SIGNAL: // * @NL80211_STA_INFO_SIGNAL: signal strength of last received PPDU (u8, dBm) // Should just be cast to int8, see code here: https://git.kernel.org/pub/scm/linux/kernel/git/jberg/iw.git/tree/station.c#n378 info.Signal = int(int8(a.Data[0])) case unix.NL80211_STA_INFO_RX_PACKETS: info.ReceivedPackets = int(nlenc.Uint32(a.Data)) case unix.NL80211_STA_INFO_TX_PACKETS: info.TransmittedPackets = int(nlenc.Uint32(a.Data)) case unix.NL80211_STA_INFO_TX_RETRIES: info.TransmitRetries = int(nlenc.Uint32(a.Data)) case unix.NL80211_STA_INFO_TX_FAILED: info.TransmitFailed = int(nlenc.Uint32(a.Data)) case unix.NL80211_STA_INFO_BEACON_LOSS: info.BeaconLoss = int(nlenc.Uint32(a.Data)) case unix.NL80211_STA_INFO_RX_BITRATE, unix.NL80211_STA_INFO_TX_BITRATE: 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 unix.NL80211_STA_INFO_RX_BITRATE: info.ReceiveBitrate = rate.Bitrate case unix.NL80211_STA_INFO_TX_BITRATE: 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 == unix.NL80211_STA_INFO_RX_BYTES { info.ReceivedBytes = int(nlenc.Uint32(a.Data)) } if info.TransmittedBytes == 0 && a.Type == unix.NL80211_STA_INFO_TX_BYTES { info.TransmittedBytes = int(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 unix.NL80211_RATE_INFO_BITRATE32: 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 == unix.NL80211_RATE_INFO_BITRATE { 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 } // attrsContain checks if a slice of netlink attributes contains an attribute // with the specified type. func attrsContain(attrs []netlink.Attribute, typ uint16) bool { for _, a := range attrs { if a.Type == typ { return true } } return false } // decodeSSID safely parses a byte slice into UTF-8 runes, and returns the // resulting string from the runes. func decodeSSID(b []byte) string { buf := bytes.NewBuffer(nil) for len(b) > 0 { r, size := utf8.DecodeRune(b) b = b[size:] buf.WriteRune(r) } return buf.String() } golang-github-mdlayher-wifi-0.1.0/client_linux_integration_test.go000066400000000000000000000037011444233265200254720ustar00rootroot00000000000000//go:build linux // +build linux package wifi_test import ( "errors" "fmt" "os" "sync" "testing" "github.com/mdlayher/wifi" ) func TestIntegrationLinuxConcurrent(t *testing.T) { const ( workers = 4 iterations = 1000 ) c := testClient(t) ifis, err := c.Interfaces() if err != nil { t.Fatalf("failed to retrieve interfaces: %v", err) } if len(ifis) == 0 { t.Skip("skipping, found no WiFi interfaces") } var names []string for _, ifi := range ifis { if ifi.Name == "" || ifi.Type != wifi.InterfaceTypeStation { continue } names = append(names, ifi.Name) } t.Logf("workers: %d, iterations: %d, interfaces: %v", workers, iterations, names) var wg sync.WaitGroup wg.Add(workers) defer wg.Wait() for i := 0; i < workers; i++ { go func() { defer wg.Done() execN(t, iterations, names) }() } } func execN(t *testing.T, n int, expect []string) { c := testClient(t) names := make(map[string]int) for i := 0; i < n; i++ { ifis, err := c.Interfaces() if err != nil { panicf("failed to retrieve interfaces: %v", err) } for _, ifi := range ifis { if ifi.Name == "" || ifi.Type != wifi.InterfaceTypeStation { continue } if _, err := c.StationInfo(ifi); err != nil { if !errors.Is(err, os.ErrNotExist) { panicf("failed to retrieve station info for device %s: %v", ifi.Name, err) } } names[ifi.Name]++ } } for _, e := range expect { nn, ok := names[e] if !ok { panicf("did not find interface %q during test", e) } if nn != n { panicf("wanted to find %q %d times, found %d", e, n, nn) } } } func testClient(t *testing.T) *wifi.Client { t.Helper() c, err := wifi.New() if err != nil { if errors.Is(err, os.ErrNotExist) { t.Skipf("skipping, nl80211 not found: %v", err) } t.Fatalf("failed to create client: %v", err) } t.Cleanup(func() { _ = c.Close() }) return c } func panicf(format string, a ...interface{}) { panic(fmt.Sprintf(format, a...)) } golang-github-mdlayher-wifi-0.1.0/client_linux_test.go000066400000000000000000000401431444233265200230700ustar00rootroot00000000000000//go:build linux // +build linux package wifi import ( "bytes" "fmt" "io" "log" "net" "os" "reflect" "syscall" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/mdlayher/genetlink" "github.com/mdlayher/genetlink/genltest" "github.com/mdlayher/netlink" "github.com/mdlayher/netlink/nlenc" "golang.org/x/sys/unix" ) func TestLinux_clientInterfacesOK(t *testing.T) { want := []*Interface{ { Index: 1, Name: "wlan0", HardwareAddr: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}, PHY: 0, Device: 1, Type: InterfaceTypeStation, Frequency: 2412, }, { HardwareAddr: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xae}, PHY: 0, Device: 2, Type: InterfaceTypeP2PDevice, }, } const flags = netlink.Request | netlink.Dump c := testClient(t, genltest.CheckRequest(familyID, unix.NL80211_CMD_GET_INTERFACE, flags, mustMessages(t, unix.NL80211_CMD_NEW_INTERFACE, want), )) got, err := c.Interfaces() if err != nil { t.Fatalf("unexpected error: %v", err) } if diff := cmp.Diff(want, got); diff != "" { t.Fatalf("unexpected interfaces (-want +got):\n%s", diff) } } func TestLinux_clientBSSMissingBSSAttributeIsNotExist(t *testing.T) { c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { // One message without BSS attribute return []genetlink.Message{{ Header: genetlink.Header{ Command: unix.NL80211_CMD_NEW_SCAN_RESULTS, }, Data: mustMarshalAttributes([]netlink.Attribute{{ Type: unix.NL80211_ATTR_IFINDEX, Data: nlenc.Uint32Bytes(1), }}), }}, nil }) _, err := c.BSS(&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_clientBSSMissingBSSStatusAttributeIsNotExist(t *testing.T) { c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { return []genetlink.Message{{ Header: genetlink.Header{ Command: unix.NL80211_CMD_NEW_SCAN_RESULTS, }, // BSS attribute, but no nested status attribute for the "active" BSS Data: mustMarshalAttributes([]netlink.Attribute{{ Type: unix.NL80211_ATTR_BSS, Data: mustMarshalAttributes([]netlink.Attribute{{ Type: unix.NL80211_BSS_BSSID, Data: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, }}), }}), }}, nil }) _, err := c.BSS(&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_clientBSSNoMessagesIsNotExist(t *testing.T) { c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { // No messages about the BSS at the generic netlink level. // Caller will interpret this as no BSS. return nil, io.EOF }) _, err := c.BSS(&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_clientBSSOKSkipMissingStatus(t *testing.T) { want := net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55} c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { return []genetlink.Message{ // Multiple messages, but only second one has BSS status, so the // others should be ignored { Header: genetlink.Header{ Command: unix.NL80211_CMD_NEW_SCAN_RESULTS, }, Data: mustMarshalAttributes([]netlink.Attribute{{ Type: unix.NL80211_ATTR_BSS, // Does not contain BSS information and status Data: mustMarshalAttributes([]netlink.Attribute{{ Type: unix.NL80211_BSS_BSSID, Data: net.HardwareAddr{0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}, }}), }}), }, { Header: genetlink.Header{ Command: unix.NL80211_CMD_NEW_SCAN_RESULTS, }, Data: mustMarshalAttributes([]netlink.Attribute{{ Type: unix.NL80211_ATTR_BSS, // Contains BSS information and status Data: mustMarshalAttributes([]netlink.Attribute{ { Type: unix.NL80211_BSS_BSSID, Data: want, }, { Type: unix.NL80211_BSS_STATUS, Data: nlenc.Uint32Bytes(uint32(BSSStatusAssociated)), }, }), }}), }, }, nil }) bss, err := c.BSS(&Interface{ Index: 1, HardwareAddr: net.HardwareAddr{0xe, 0xad, 0xbe, 0xef, 0xde, 0xad}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if got := bss.BSSID; !bytes.Equal(want, got) { t.Fatalf("unexpected BSS BSSID:\n- want: %#v\n- got: %#v", want, got) } } func TestLinux_clientBSSOK(t *testing.T) { want := &BSS{ SSID: "Hello, 世界", BSSID: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, Frequency: 2492, BeaconInterval: 100 * 1024 * time.Microsecond, LastSeen: 10 * time.Second, Status: BSSStatusAssociated, } 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_NEW_SCAN_RESULTS, want) c := testClient(t, genltest.CheckRequest(familyID, unix.NL80211_CMD_GET_SCAN, 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.BSS(ifi) if err != nil { log.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(want, got) { t.Fatalf("unexpected BSS:\n- want: %v\n- got: %v", want, got) } } func TestLinux_clientStationInfoMissingAttributeIsNotExist(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_NEW_STATION, }, 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_clientStationInfoNoMessagesIsNotExist(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 }) _, 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_clientStationInfoOK(t *testing.T) { want := []*StationInfo{ { HardwareAddr: net.HardwareAddr{0xb8, 0x27, 0xeb, 0xd5, 0xf3, 0xef}, Connected: 30 * time.Minute, Inactive: 4 * time.Millisecond, ReceivedBytes: 1000, TransmittedBytes: 2000, ReceivedPackets: 10, TransmittedPackets: 20, Signal: -50, TransmitRetries: 5, TransmitFailed: 2, BeaconLoss: 3, ReceiveBitrate: 130000000, TransmitBitrate: 130000000, }, { HardwareAddr: net.HardwareAddr{0x40, 0xa5, 0xef, 0xd9, 0x96, 0x6f}, Connected: 60 * time.Minute, Inactive: 8 * time.Millisecond, ReceivedBytes: 2000, TransmittedBytes: 4000, ReceivedPackets: 20, TransmittedPackets: 40, Signal: -25, TransmitRetries: 10, TransmitFailed: 4, BeaconLoss: 6, ReceiveBitrate: 260000000, TransmitBitrate: 260000000, }, } 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_NEW_STATION, want) c := testClient(t, genltest.CheckRequest(familyID, unix.NL80211_CMD_GET_STATION, 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.StationInfo(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]) } } } func TestLinux_initClientErrorCloseConn(t *testing.T) { c := genltest.Dial(func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { // Assume that nl80211 does not exist on this system. // The genetlink Conn should be closed to avoid leaking file descriptors. return nil, genltest.Error(int(syscall.ENOENT)) }) if _, err := initClient(c); err == nil { t.Fatal("no error occurred, but expected one") } } const familyID = 26 func testClient(t *testing.T, fn genltest.Func) *client { family := genetlink.Family{ ID: familyID, Name: unix.NL80211_GENL_NAME, Version: 1, } c := genltest.Dial(genltest.ServeFamily(family, func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { // If this function is invoked, we are calling a nl80211 function. if diff := cmp.Diff(int(family.ID), int(nreq.Header.Type)); diff != "" { t.Fatalf("unexpected generic netlink family ID (-want +got):\n%s", diff) } if diff := cmp.Diff(family.Version, greq.Header.Version); diff != "" { t.Fatalf("unexpected generic netlink family version (-want +got):\n%s", diff) } msgs, err := fn(greq, nreq) if err != nil { return nil, err } // Do a favor for the caller by planting the correct version in each message // header, as long as no version is supplied. for i := range msgs { if msgs[i].Header.Version == 0 { msgs[i].Header.Version = family.Version } } return msgs, nil })) client, err := initClient(c) if err != nil { t.Fatalf("failed to initialize test client: %v", err) } return client } // diffNetlinkAttributes compares two []netlink.Attributes after zeroing their // length fields that make equality checks in testing difficult. func diffNetlinkAttributes(want, got []netlink.Attribute) string { // If different lengths, diff immediately for better error output. if len(want) != len(got) { return cmp.Diff(want, got) } for i := range want { want[i].Length = 0 got[i].Length = 0 } return cmp.Diff(want, got) } // Helper functions for converting types back into their raw attribute formats func marshalIEs(ies []ie) []byte { buf := bytes.NewBuffer(nil) for _, ie := range ies { buf.WriteByte(ie.ID) buf.WriteByte(uint8(len(ie.Data))) buf.Write(ie.Data) } return buf.Bytes() } func mustMarshalAttributes(attrs []netlink.Attribute) []byte { b, err := netlink.MarshalAttributes(attrs) if err != nil { panic(fmt.Sprintf("failed to marshal attributes: %v", err)) } return b } type attributeser interface { attributes() []netlink.Attribute } var ( _ attributeser = &Interface{} _ attributeser = &BSS{} _ attributeser = &StationInfo{} ) func (ifi *Interface) attributes() []netlink.Attribute { return []netlink.Attribute{ {Type: unix.NL80211_ATTR_IFINDEX, Data: nlenc.Uint32Bytes(uint32(ifi.Index))}, {Type: unix.NL80211_ATTR_IFNAME, Data: nlenc.Bytes(ifi.Name)}, {Type: unix.NL80211_ATTR_MAC, Data: ifi.HardwareAddr}, {Type: unix.NL80211_ATTR_WIPHY, Data: nlenc.Uint32Bytes(uint32(ifi.PHY))}, {Type: unix.NL80211_ATTR_IFTYPE, Data: nlenc.Uint32Bytes(uint32(ifi.Type))}, {Type: unix.NL80211_ATTR_WDEV, Data: nlenc.Uint64Bytes(uint64(ifi.Device))}, {Type: unix.NL80211_ATTR_WIPHY_FREQ, Data: nlenc.Uint32Bytes(uint32(ifi.Frequency))}, } } func (b *BSS) attributes() []netlink.Attribute { return []netlink.Attribute{ // TODO(mdlayher): return more attributes for validation? { Type: unix.NL80211_ATTR_BSS, Data: mustMarshalAttributes([]netlink.Attribute{ {Type: unix.NL80211_BSS_BSSID, Data: b.BSSID}, {Type: unix.NL80211_BSS_FREQUENCY, Data: nlenc.Uint32Bytes(uint32(b.Frequency))}, {Type: unix.NL80211_BSS_BEACON_INTERVAL, Data: nlenc.Uint16Bytes(uint16(b.BeaconInterval / 1024 / time.Microsecond))}, {Type: unix.NL80211_BSS_SEEN_MS_AGO, Data: nlenc.Uint32Bytes(uint32(b.LastSeen / time.Millisecond))}, {Type: unix.NL80211_BSS_STATUS, Data: nlenc.Uint32Bytes(uint32(b.Status))}, { Type: unix.NL80211_BSS_INFORMATION_ELEMENTS, Data: marshalIEs([]ie{{ ID: ieSSID, Data: []byte(b.SSID), }}), }, }), }, } } func (s *StationInfo) attributes() []netlink.Attribute { return []netlink.Attribute{ // TODO(mdlayher): return more attributes for validation? { Type: unix.NL80211_ATTR_MAC, Data: s.HardwareAddr, }, { Type: unix.NL80211_ATTR_STA_INFO, Data: mustMarshalAttributes([]netlink.Attribute{ {Type: unix.NL80211_STA_INFO_CONNECTED_TIME, Data: nlenc.Uint32Bytes(uint32(s.Connected.Seconds()))}, {Type: unix.NL80211_STA_INFO_INACTIVE_TIME, Data: nlenc.Uint32Bytes(uint32(s.Inactive.Seconds() * 1000))}, {Type: unix.NL80211_STA_INFO_RX_BYTES, Data: nlenc.Uint32Bytes(uint32(s.ReceivedBytes))}, {Type: unix.NL80211_STA_INFO_RX_BYTES64, Data: nlenc.Uint64Bytes(uint64(s.ReceivedBytes))}, {Type: unix.NL80211_STA_INFO_TX_BYTES, Data: nlenc.Uint32Bytes(uint32(s.TransmittedBytes))}, {Type: unix.NL80211_STA_INFO_TX_BYTES64, Data: nlenc.Uint64Bytes(uint64(s.TransmittedBytes))}, {Type: unix.NL80211_STA_INFO_SIGNAL, Data: []byte{byte(int8(s.Signal))}}, {Type: unix.NL80211_STA_INFO_RX_PACKETS, Data: nlenc.Uint32Bytes(uint32(s.ReceivedPackets))}, {Type: unix.NL80211_STA_INFO_TX_PACKETS, Data: nlenc.Uint32Bytes(uint32(s.TransmittedPackets))}, {Type: unix.NL80211_STA_INFO_TX_RETRIES, Data: nlenc.Uint32Bytes(uint32(s.TransmitRetries))}, {Type: unix.NL80211_STA_INFO_TX_FAILED, Data: nlenc.Uint32Bytes(uint32(s.TransmitFailed))}, {Type: unix.NL80211_STA_INFO_BEACON_LOSS, Data: nlenc.Uint32Bytes(uint32(s.BeaconLoss))}, { Type: unix.NL80211_STA_INFO_RX_BITRATE, Data: mustMarshalAttributes([]netlink.Attribute{ {Type: unix.NL80211_RATE_INFO_BITRATE, Data: nlenc.Uint16Bytes(uint16(bitrateAttr(s.ReceiveBitrate)))}, {Type: unix.NL80211_RATE_INFO_BITRATE32, Data: nlenc.Uint32Bytes(bitrateAttr(s.ReceiveBitrate))}, }), }, { Type: unix.NL80211_STA_INFO_TX_BITRATE, Data: mustMarshalAttributes([]netlink.Attribute{ {Type: unix.NL80211_RATE_INFO_BITRATE, Data: nlenc.Uint16Bytes(uint16(bitrateAttr(s.TransmitBitrate)))}, {Type: unix.NL80211_RATE_INFO_BITRATE32, Data: nlenc.Uint32Bytes(bitrateAttr(s.TransmitBitrate))}, }), }, }), }, } } func bitrateAttr(bitrate int) uint32 { return uint32(bitrate / 100 / 1000) } func mustMessages(t *testing.T, command uint8, want interface{}) genltest.Func { var as []attributeser switch xs := want.(type) { case []*Interface: for _, x := range xs { as = append(as, x) } case *BSS: as = append(as, xs) case []*StationInfo: for _, x := range xs { as = append(as, x) } default: t.Fatalf("cannot make messages for type: %T", xs) } msgs := make([]genetlink.Message, 0, len(as)) for _, a := range as { msgs = append(msgs, genetlink.Message{ Header: genetlink.Header{ Command: command, }, Data: mustMarshalAttributes(a.attributes()), }) } return func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { return msgs, nil } } golang-github-mdlayher-wifi-0.1.0/client_others.go000066400000000000000000000021071444233265200221740ustar00rootroot00000000000000//go:build !linux // +build !linux package wifi import ( "fmt" "runtime" ) // errUnimplemented is returned by all functions on platforms that // do not have package wifi implemented. var errUnimplemented = fmt.Errorf("wifi: not implemented on %s", runtime.GOOS) // A conn is the no-op implementation of a netlink sockets connection. type client struct{} func newClient() (*client, error) { return nil, errUnimplemented } func (*client) Close() error { return errUnimplemented } 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) Connect(_ *Interface, _ string) error { return errUnimplemented } func (*client) Disconnect(_ *Interface) error { return errUnimplemented } func (*client) ConnectWPAPSK(_ *Interface, _, _ string) error { return errUnimplemented } golang-github-mdlayher-wifi-0.1.0/doc.go000066400000000000000000000001561444233265200201010ustar00rootroot00000000000000// Package wifi provides access to IEEE 802.11 WiFi device operations on Linux // using nl80211. package wifi golang-github-mdlayher-wifi-0.1.0/go.mod000066400000000000000000000006171444233265200201150ustar00rootroot00000000000000module github.com/mdlayher/wifi go 1.20 require ( github.com/google/go-cmp v0.5.9 github.com/mdlayher/genetlink v1.3.2 github.com/mdlayher/netlink v1.7.2 golang.org/x/crypto v0.10.0 golang.org/x/sys v0.9.0 ) require ( github.com/josharian/native v1.1.0 // indirect github.com/mdlayher/socket v0.4.1 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sync v0.1.0 // indirect ) golang-github-mdlayher-wifi-0.1.0/go.sum000066400000000000000000000115311444233265200201370ustar00rootroot00000000000000github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang-github-mdlayher-wifi-0.1.0/wifi.go000066400000000000000000000151071444233265200202740ustar00rootroot00000000000000package wifi import ( "errors" "fmt" "net" "time" ) // errInvalidIE is returned when one or more IEs are malformed. var errInvalidIE = errors.New("invalid 802.11 information element") // An InterfaceType is the operating mode of an Interface. type InterfaceType int const ( // InterfaceTypeUnspecified indicates that an interface's type is unspecified // and the driver determines its function. InterfaceTypeUnspecified InterfaceType = iota // InterfaceTypeAdHoc indicates that an interface is part of an independent // basic service set (BSS) of client devices without a controlling access // point. InterfaceTypeAdHoc // InterfaceTypeStation indicates that an interface is part of a managed // basic service set (BSS) of client devices with a controlling access point. InterfaceTypeStation // InterfaceTypeAP indicates that an interface is an access point. InterfaceTypeAP // InterfaceTypeAPVLAN indicates that an interface is a VLAN interface // associated with an access point. InterfaceTypeAPVLAN // InterfaceTypeWDS indicates that an interface is a wireless distribution // interface, used as part of a network of multiple access points. InterfaceTypeWDS // InterfaceTypeMonitor indicates that an interface is a monitor interface, // receiving all frames from all clients in a given network. InterfaceTypeMonitor // InterfaceTypeMeshPoint indicates that an interface is part of a wireless // mesh network. InterfaceTypeMeshPoint // InterfaceTypeP2PClient indicates that an interface is a client within // a peer-to-peer network. InterfaceTypeP2PClient // InterfaceTypeP2PGroupOwner indicates that an interface is the group // owner within a peer-to-peer network. InterfaceTypeP2PGroupOwner // InterfaceTypeP2PDevice indicates that an interface is a device within // a peer-to-peer client network. InterfaceTypeP2PDevice // InterfaceTypeOCB indicates that an interface is outside the context // of a basic service set (BSS). InterfaceTypeOCB // InterfaceTypeNAN indicates that an interface is part of a near-me // area network (NAN). InterfaceTypeNAN ) // String returns the string representation of an InterfaceType. func (t InterfaceType) String() string { switch t { case InterfaceTypeUnspecified: return "unspecified" case InterfaceTypeAdHoc: return "ad-hoc" case InterfaceTypeStation: return "station" case InterfaceTypeAP: return "access point" case InterfaceTypeWDS: return "wireless distribution" case InterfaceTypeMonitor: return "monitor" case InterfaceTypeMeshPoint: return "mesh point" case InterfaceTypeP2PClient: return "P2P client" case InterfaceTypeP2PGroupOwner: return "P2P group owner" case InterfaceTypeP2PDevice: return "P2P device" case InterfaceTypeOCB: return "outside context of BSS" case InterfaceTypeNAN: return "near-me area network" default: return fmt.Sprintf("unknown(%d)", t) } } // An Interface is a WiFi network interface. type Interface struct { // The index of the interface. Index int // The name of the interface. Name string // The hardware address of the interface. HardwareAddr net.HardwareAddr // The physical device that this interface belongs to. PHY int // The virtual device number of this interface within a PHY. Device int // The operating mode of the interface. Type InterfaceType // The interface's wireless frequency in MHz. Frequency int } // StationInfo contains statistics about a WiFi interface operating in // station mode. type StationInfo struct { // The hardware address of the station. HardwareAddr net.HardwareAddr // The time since the station last connected. Connected time.Duration // The time since wireless activity last occurred. Inactive time.Duration // The number of bytes received by this station. ReceivedBytes int // The number of bytes transmitted by this station. TransmittedBytes int // The number of packets received by this station. ReceivedPackets int // The number of packets transmitted by this station. TransmittedPackets int // The current data receive bitrate, in bits/second. ReceiveBitrate int // The current data transmit bitrate, in bits/second. TransmitBitrate int // The signal strength of this station's connection, in dBm. Signal int // The number of times the station has had to retry while sending a packet. TransmitRetries int // The number of times a packet transmission failed. TransmitFailed int // The number of times a beacon loss was detected. BeaconLoss int } // A BSS is an 802.11 basic service set. It contains information about a wireless // network associated with an Interface. type BSS struct { // The service set identifier, or "network name" of the BSS. SSID string // The BSS service set identifier. In infrastructure mode, this is the // hardware address of the wireless access point that a client is associated // with. BSSID net.HardwareAddr // The frequency used by the BSS, in MHz. Frequency int // The interval between beacon transmissions for this BSS. BeaconInterval time.Duration // The time since the client last scanned this BSS's information. LastSeen time.Duration // The status of the client within the BSS. Status BSSStatus } // A BSSStatus indicates the current status of client within a BSS. type BSSStatus int const ( // BSSStatusAuthenticated indicates that a client is authenticated with a BSS. BSSStatusAuthenticated BSSStatus = iota // BSSStatusAssociated indicates that a client is associated with a BSS. BSSStatusAssociated // BSSStatusIBSSJoined indicates that a client has joined an independent BSS. BSSStatusIBSSJoined ) // String returns the string representation of a BSSStatus. func (s BSSStatus) String() string { switch s { case BSSStatusAuthenticated: return "authenticated" case BSSStatusAssociated: return "associated" case BSSStatusIBSSJoined: return "IBSS joined" default: return fmt.Sprintf("unknown(%d)", s) } } // List of 802.11 Information Element types. const ( ieSSID = 0 ) // An ie is an 802.11 information element. type ie struct { ID uint8 // Length field implied by length of data Data []byte } // parseIEs parses zero or more ies from a byte slice. // Reference: // https://www.safaribooksonline.com/library/view/80211-wireless-networks/0596100523/ch04.html#wireless802dot112-CHP-4-FIG-31 func parseIEs(b []byte) ([]ie, error) { var ies []ie var i int for { if len(b[i:]) == 0 { break } if len(b[i:]) < 2 { return nil, errInvalidIE } id := b[i] i++ l := int(b[i]) i++ if len(b[i:]) < l { return nil, errInvalidIE } ies = append(ies, ie{ ID: id, Data: b[i : i+l], }) i += l } return ies, nil } golang-github-mdlayher-wifi-0.1.0/wifi_test.go000066400000000000000000000055331444233265200213350ustar00rootroot00000000000000package wifi import ( "reflect" "testing" ) func TestInterfaceTypeString(t *testing.T) { tests := []struct { t InterfaceType s string }{ { t: InterfaceTypeUnspecified, s: "unspecified", }, { t: InterfaceTypeAdHoc, s: "ad-hoc", }, { t: InterfaceTypeStation, s: "station", }, { t: InterfaceTypeAP, s: "access point", }, { t: InterfaceTypeWDS, s: "wireless distribution", }, { t: InterfaceTypeMonitor, s: "monitor", }, { t: InterfaceTypeMeshPoint, s: "mesh point", }, { t: InterfaceTypeP2PClient, s: "P2P client", }, { t: InterfaceTypeP2PGroupOwner, s: "P2P group owner", }, { t: InterfaceTypeP2PDevice, s: "P2P device", }, { t: InterfaceTypeOCB, s: "outside context of BSS", }, { t: InterfaceTypeNAN, s: "near-me area network", }, { t: InterfaceTypeNAN + 1, s: "unknown(13)", }, } for _, tt := range tests { t.Run(tt.s, func(t *testing.T) { if want, got := tt.s, tt.t.String(); want != got { t.Fatalf("unexpected interface type string:\n- want: %q\n- got: %q", want, got) } }) } } func TestBSSStatusString(t *testing.T) { tests := []struct { t BSSStatus s string }{ { t: BSSStatusAuthenticated, s: "authenticated", }, { t: BSSStatusAssociated, s: "associated", }, { t: BSSStatusIBSSJoined, s: "IBSS joined", }, { t: 3, s: "unknown(3)", }, } for _, tt := range tests { t.Run(tt.s, func(t *testing.T) { if want, got := tt.s, tt.t.String(); want != got { t.Fatalf("unexpected BSS status string:\n- want: %q\n- got: %q", want, got) } }) } } func Test_parseIEs(t *testing.T) { tests := []struct { name string b []byte ies []ie err error }{ { name: "empty", }, { name: "too short", b: []byte{0x00}, err: errInvalidIE, }, { name: "length too long", b: []byte{0x00, 0xff, 0x00}, err: errInvalidIE, }, { name: "OK one", b: []byte{0x00, 0x03, 'f', 'o', 'o'}, ies: []ie{{ ID: 0, Data: []byte("foo"), }}, }, { name: "OK three", b: []byte{ 0x00, 0x03, 'f', 'o', 'o', 0x01, 0x00, 0x02, 0x06, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, }, ies: []ie{ { ID: 0, Data: []byte("foo"), }, { ID: 1, Data: []byte{}, }, { ID: 2, Data: []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ies, err := parseIEs(tt.b) if want, got := tt.err, err; want != got { t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) } if err != nil { t.Logf("err: %v", err) return } if want, got := tt.ies, ies; !reflect.DeepEqual(want, got) { t.Fatalf("unexpected ies:\n- want: %v\n- got: %v", want, got) } }) } }