pax_global_header00006660000000000000000000000064150327146750014524gustar00rootroot0000000000000052 comment=ca0bd3cfdc0b3d6eb9a35d9958f073274fc0174d golang-github-mdlayher-wifi-0.6.0/000077500000000000000000000000001503271467500170155ustar00rootroot00000000000000golang-github-mdlayher-wifi-0.6.0/.github/000077500000000000000000000000001503271467500203555ustar00rootroot00000000000000golang-github-mdlayher-wifi-0.6.0/.github/dependabot.yml000066400000000000000000000003211503271467500232010ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "monthly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" golang-github-mdlayher-wifi-0.6.0/.github/workflows/000077500000000000000000000000001503271467500224125ustar00rootroot00000000000000golang-github-mdlayher-wifi-0.6.0/.github/workflows/static-analysis.yml000066400000000000000000000015251503271467500262500ustar00rootroot00000000000000name: Static Analysis on: push: paths: - "go.sum" - "go.mod" - "**.go" - ".github/workflows/static-analysis.yml" - ".golangci.yml" pull_request: permissions: contents: read jobs: build: strategy: matrix: go-version: - "1.23" - "1.24" runs-on: ubuntu-latest steps: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 with: args: --verbose version: v2.2.1 golang-github-mdlayher-wifi-0.6.0/.github/workflows/test.yml000066400000000000000000000014431503271467500241160ustar00rootroot00000000000000name: Test on: push: branches: - '*' pull_request: branches: - '*' jobs: build: strategy: fail-fast: false matrix: go-version: - "1.23" - "1.24" os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # 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.6.0/.golangci.yml000066400000000000000000000005501503271467500214010ustar00rootroot00000000000000version: "2" linters: enable: - revive exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - third_party$ - builtin$ - examples$ formatters: exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ golang-github-mdlayher-wifi-0.6.0/LICENSE.md000066400000000000000000000020631503271467500204220ustar00rootroot00000000000000# 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.6.0/README.md000066400000000000000000000007511503271467500202770ustar00rootroot00000000000000# 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.6.0/client.go000066400000000000000000000047671503271467500206400ustar00rootroot00000000000000package wifi import ( "context" "time" ) // 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) } // AccessPoints retrieves the currently known BSS around the specified Interface. func (c *Client) AccessPoints(ifi *Interface) ([]*BSS, error) { return c.c.AccessPoints(ifi) } // Scan requests the wifi interface to scan for new access points. // // Use context.WithDeadline to set a timeout. func (c *Client) Scan(ctx context.Context, ifi *Interface) error { return c.c.Scan(ctx, ifi) } // StationInfo retrieves all station statistics about a WiFi interface. // // Since v0.2.0: if there are no stations, an empty slice is returned instead // of an error. 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) } // SetReadDeadline sets the read deadline associated with the connection. func (c *Client) SetReadDeadline(t time.Time) error { return c.c.SetReadDeadline(t) } // SetWriteDeadline sets the write deadline associated with the connection. func (c *Client) SetWriteDeadline(t time.Time) error { return c.c.SetWriteDeadline(t) } golang-github-mdlayher-wifi-0.6.0/client_linux.go000066400000000000000000000706041503271467500220500ustar00rootroot00000000000000//go:build linux // +build linux package wifi import ( "bytes" "context" "crypto/sha1" "encoding/binary" "errors" "net" "os" "sync" "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" ) var ( ErrNotSupported = errors.New("not supported") ErrScanGroupNotFound = errors.New("scan multicast group unavailable") ErrScanAborted = errors.New("scan aborted by the kernel") ErrScanValidation = errors.New("scan validation failed") ) // 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 // scan is used to synchronize access to the Scan method. scan sync.Mutex } // 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, scan: sync.Mutex{}, }, 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 { support, err := c.checkExtFeature(ifi, unix.NL80211_EXT_FEATURE_4WAY_HANDSHAKE_STA_PSK) if err != nil { return err } if !support { return ErrNotSupported } // 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) } // AccessPoints requests that nl80211 return all currently known BSS // from the specified Interface. func (c *client) AccessPoints(ifi *Interface) ([]*BSS, error) { msgs, err := c.get( unix.NL80211_CMD_GET_SCAN, netlink.Dump, ifi, nil, ) if err != nil { return nil, err } return parseGetScanResult(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 } 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 } // 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 } // Scan requests that nl80211 perform a scan for new access points using // the specified Interface. This process is long running and uses // a separate connection to nl80211. // // Use context.WithDeadline to set a timeout. // // If a scan is already in progress, this function will return a syscall.EBUSY // error. If the response cannot be validated, the returned error // will include ErrScanValidation. // // Use func AccessPoints to retrieve the results. func (c *client) Scan(ctx context.Context, ifi *Interface) error { c.scan.Lock() defer c.scan.Unlock() // use secondary connection for multicast receives conn, err := genetlink.Dial(&netlink.Config{Strict: true}) if err != nil { return err } defer conn.Close() if deadline, ok := ctx.Deadline(); ok { err := conn.SetDeadline(deadline) if err != nil { return err } } family, err := conn.GetFamily(unix.NL80211_GENL_NAME) if err != nil { return err } var id uint32 for _, group := range family.Groups { if group.Name == unix.NL80211_MULTICAST_GROUP_SCAN { err = conn.JoinGroup(group.ID) if err != nil { return err } id = group.ID break } } if id == 0 { return ErrScanGroupNotFound } // Leave group on exit. Err is non-actionable defer func() { _ = conn.LeaveGroup(id) }() enc := netlink.NewAttributeEncoder() enc.Nested(unix.NL80211_ATTR_SCAN_SSIDS, func(ae *netlink.AttributeEncoder) error { ae.Bytes(unix.NL80211_SCHED_SCAN_MATCH_ATTR_SSID, nlenc.Bytes("")) return nil }) ifi.encode(enc) data, err := enc.Encode() if err != nil { return err } req := genetlink.Message{ Header: genetlink.Header{ Command: unix.NL80211_CMD_TRIGGER_SCAN, Version: c.familyVersion, }, Data: data, } ctx, cancel := context.WithCancel(ctx) defer cancel() result := make(chan error, 1) go func(ctx context.Context, conn *genetlink.Conn, ifiIndex int, familyVersion uint8, result chan<- error) { defer close(result) result <- listenNewScanResults(ctx, conn, ifiIndex, familyVersion) }(ctx, conn, ifi.Index, c.familyVersion, result) flags := netlink.Request | netlink.Acknowledge _, err = conn.Send(req, family.ID, flags) if err != nil { cancel() } err2 := <-result return errors.Join(err, err2) } // SetDeadline sets the read and write deadlines associated with the connection. func (c *client) SetDeadline(t time.Time) error { return c.c.SetDeadline(t) } // SetReadDeadline sets the read deadline associated with the connection. func (c *client) SetReadDeadline(t time.Time) error { return c.c.SetReadDeadline(t) } // SetWriteDeadline sets the write deadline associated with the connection. func (c *client) SetWriteDeadline(t time.Time) error { return c.c.SetWriteDeadline(t) } // 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, ) } // listenNewScanResults listens for new scan results or scan abort messages // from the netlink connection. It processes the messages associated with the // specified interface index and family version, verifying attributes and // handling context cancellations. // // The caller should not receive on the given connection and is responsible // for closing it. func listenNewScanResults(ctx context.Context, conn *genetlink.Conn, ifiIndex int, familyVersion uint8) error { for ctx.Err() == nil { msgs, _, err := conn.Receive() if err != nil { return err } // test for context cancellation and abandon work if so if ctx.Err() != nil { return err } for _, msg := range msgs { if msg.Header.Version != familyVersion { break } switch msg.Header.Command { case unix.NL80211_CMD_SCAN_ABORTED: return ErrScanAborted case unix.NL80211_CMD_NEW_SCAN_RESULTS: // attempt to verify the interface attrs, err := netlink.UnmarshalAttributes(msg.Data) if err != nil { return errors.Join(ErrScanValidation, err) } var intf Interface if err := (&intf).parseAttributes(attrs); err != nil { return errors.Join(ErrScanValidation, err) } if ifiIndex != intf.Index { continue } return nil default: continue } } } return ctx.Err() } // parseGetScanResult parses all the BSS from nl80211 CMD_GET_SCAN response messages. func parseGetScanResult(msgs []genetlink.Message) ([]*BSS, error) { // reimplementing https://github.com/mdlayher/wifi/pull/79 bsss := make([]*BSS, 0, len(msgs)) for _, m := range msgs { attrs, err := netlink.UnmarshalAttributes(m.Data) if err != nil { return nil, err } var bss BSS for _, a := range attrs { if a.Type != unix.NL80211_ATTR_BSS { continue } nattrs, err := netlink.UnmarshalAttributes(a.Data) if err != nil { return nil, err } if !attrsContain(nattrs, unix.NL80211_BSS_STATUS) { bss.Status = BSSStatusNotAssociated } if err := (&bss).parseAttributes(nattrs); err != nil { continue } } bsss = append(bsss, &bss) } return bsss, 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 } // 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) case ieBSSLoad: Bssload, err := decodeBSSLoad(ie.Data) if err != nil { continue // This IE is malformed } b.Load = *Bssload case ieRSN: rsnInfo, err := decodeRSN(ie.Data) if err != nil { continue // This IE is malformed } b.RSN = *rsnInfo } } } } 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_IFINDEX: info.InterfaceIndex = int(nlenc.Uint32(a.Data)) 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_SIGNAL_AVG: info.SignalAverage = 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 } // 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_IFINDEX: info.InterfaceIndex = int(nlenc.Uint32(a.Data)) 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 { 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() } // decodeBSSLoad Decodes the BSSLoad IE. Supports Version 1 and Version 2 // values according to https://raw.githubusercontent.com/wireshark/wireshark/master/epan/dissectors/packet-ieee80211.c // See also source code of iw (v5.19) scan.c Line 1634ff // BSS Load ELement (with length 5) is defined by chapter 9.4.2.27 (page 1066) of the current IEEE 802.11-2020 func decodeBSSLoad(b []byte) (*BSSLoad, error) { var load BSSLoad if len(b) == 5 { // Wireshark calls this "802.11e CCA Version" // This is the version defined in IEEE 802.11 (Versions 2007, 2012, 2016 and 2020) load.Version = 2 load.StationCount = binary.LittleEndian.Uint16(b[0:2]) // first 2 bytes load.ChannelUtilization = b[2] // next 1 byte load.AvailableAdmissionCapacity = binary.LittleEndian.Uint16(b[3:5]) // last 2 bytes } else if len(b) == 4 { // Wireshark calls this "Cisco QBSS Version 1 - non CCA" load.Version = 1 load.StationCount = binary.LittleEndian.Uint16(b[0:2]) // first 2 bytes load.ChannelUtilization = b[2] // next 1 byte load.AvailableAdmissionCapacity = uint16(b[3]) // next 1 byte } else { return nil, errInvalidBSSLoad } return &load, nil } // decodeRSN parses IEEE 802.11 Element ID 48 (RSN Information Element). // (RSN = Robust Security Network) // // The RSN IE structure is defined in IEEE 802.11-2020 standard, section 9.4.2.24 (page 1051). func decodeRSN(b []byte) (*RSNInfo, error) { // IEEE 802.11 Information Elements are limited to 255 octets total (ID + Length + Data) // Since we receive only the data portion, maximum size is 253 bytes (255 - 1 - 1) if len(b) > 253 { return &RSNInfo{}, errRSNDataTooLarge } if len(b) < 8 { // minimum: version(2) + group cipher(4) + pairwise count(2) return &RSNInfo{}, errRSNTooShort } var ri RSNInfo ri.Version = binary.LittleEndian.Uint16(b[:2]) // Note: Most implementations use version 1, but be tolerant of future versions // that maintain backward compatibility. Only reject version 0 as invalid. if ri.Version == 0 { return &ri, errRSNInvalidVersion } // Group cipher suite (4 octets) - OUI is stored big-endian in the data groupCipherOUI := binary.BigEndian.Uint32(b[2:6]) ri.GroupCipher = RSNCipher(groupCipherOUI) pos := 6 // Pairwise cipher list if len(b) < pos+2 { return &ri, errRSNTruncatedPairwiseCount } pcCount := int(binary.LittleEndian.Uint16(b[pos : pos+2])) pos += 2 if pcCount > 60 { // (253-10)/4 ≈ 60 (theoretical max with minimal overhead) return &ri, errRSNPairwiseCipherCountTooLarge } if len(b) < pos+4*pcCount { return &ri, errRSNTruncatedPairwiseList } ri.PairwiseCiphers = make([]RSNCipher, 0, pcCount) // Pre-allocate with known capacity for i := 0; i < pcCount; i++ { sel := binary.BigEndian.Uint32(b[pos : pos+4]) ri.PairwiseCiphers = append(ri.PairwiseCiphers, RSNCipher(sel)) pos += 4 } // AKM list if len(b) < pos+2 { return &ri, nil // AKM list is optional, return what we have } akmCount := int(binary.LittleEndian.Uint16(b[pos : pos+2])) pos += 2 if akmCount > 60 { // (253-10)/4 ≈ 60 (theoretical max with minimal overhead) return &ri, errRSNAKMCountTooLarge } if len(b) < pos+4*akmCount { return &ri, errRSNTruncatedAKMList } // Additional validation: check if we have enough space for the current counts // Calculate minimum required space for what we've parsed so far minRequired := 6 + 2 + 4*pcCount + 2 + 4*akmCount // version + group + pairwise_count + pairwise + akm_count + akms if len(b) < minRequired { return &ri, errRSNTooSmallForCounts } ri.AKMs = make([]RSNAKM, 0, akmCount) // Pre-allocate with known capacity for i := 0; i < akmCount; i++ { sel := binary.BigEndian.Uint32(b[pos : pos+4]) ri.AKMs = append(ri.AKMs, RSNAKM(sel)) pos += 4 } // Capabilities (optional) if len(b) >= pos+2 { ri.Capabilities = binary.LittleEndian.Uint16(b[pos : pos+2]) pos += 2 } // PMKID list – skip if present, with proper bounds checking if len(b) >= pos+2 { pmkCount := int(binary.LittleEndian.Uint16(b[pos : pos+2])) pos += 2 if pmkCount > 15 { // (253-10)/16 ≈ 15 (theoretical max with minimal overhead) return &ri, errRSNPMKIDCountTooLarge } // Check if we have enough bytes for all PMKIDs if len(b) < pos+16*pmkCount { return &ri, errRSNTruncatedPMKIDList } pos += 16 * pmkCount } // Group‑management cipher (optional, WPA3/802.11w) if len(b) >= pos+4 { gmCipherOUI := binary.BigEndian.Uint32(b[pos : pos+4]) ri.GroupMgmtCipher = RSNCipher(gmCipherOUI) } return &ri, nil } // checkExtFeature Checks if a physical interface supports a extended feature func (c *client) checkExtFeature(ifi *Interface, feature uint) (bool, error) { msgs, err := c.get( unix.NL80211_CMD_GET_WIPHY, netlink.Dump, ifi, func(ae *netlink.AttributeEncoder) { ae.Flag(unix.NL80211_ATTR_SPLIT_WIPHY_DUMP, true) }, ) if err != nil { return false, err } var features []byte found: for i := range msgs { attrs, err := netlink.UnmarshalAttributes(msgs[i].Data) if err != nil { return false, err } for _, a := range attrs { if a.Type == unix.NL80211_ATTR_EXT_FEATURES { features = a.Data break found } } } if feature/8 >= uint(len(features)) { return false, nil } return (features[feature/8]&(1<<(feature%8)) != 0), nil } golang-github-mdlayher-wifi-0.6.0/client_linux_integration_test.go000066400000000000000000000061731503271467500255120ustar00rootroot00000000000000//go:build linux // +build linux package wifi_test import ( "context" "errors" "fmt" "os" "sync" "testing" "time" "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(differentI int) { defer wg.Done() execN(t, iterations, names, differentI) }(i) } } func execN(t *testing.T, n int, expect []string, workerID int) { c := testClient(t) names := make(map[string]int) for i := 0; i < n; i++ { ifis, err := c.Interfaces() if err != nil { panicf("[worker_id %d; iteration %d] failed to retrieve interfaces: %v", workerID, i, 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("[worker_id %d; iteration %d] failed to retrieve station info for device %s: %v", workerID, i, ifi.Name, err) } } 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", workerID, i, ifi.Name, err) } } names[ifi.Name]++ } } for _, e := range expect { nn, ok := names[e] if !ok { panicf("[worker_id %d] did not find interface %q during test", workerID, e) } if nn != n { panicf("[worker_id %d] wanted to find %q %d times, found %d", workerID, 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...)) } func TestClient_AccessPoints(t *testing.T) { if os.Geteuid() != 0 { t.Skipf("skipping, must be run as root") } c, err := wifi.New() if err != nil { t.Fatalf("failed to create client: %v", err) } ifis, err := c.Interfaces() if err != nil { t.Fatalf("failed to retrieve interfaces: %v", err) } for _, ifi := range ifis { if ifi.Name == "" || ifi.Type != wifi.InterfaceTypeStation { continue } ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) defer cancel() err = c.Scan(ctx, ifi) if err != nil { t.Fatalf("failed to scan access points for device %s: %v", ifi.Name, err) } _, err := c.AccessPoints(ifi) if err != nil { t.Fatalf("failed to retrieve access points for device %s: %v", ifi.Name, err) } } } golang-github-mdlayher-wifi-0.6.0/client_linux_test.go000066400000000000000000001014001503271467500230740ustar00rootroot00000000000000//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 }) info, err := c.StationInfo(&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, []*StationInfo{}) { t.Fatalf("expected info to be an empty slice, got %v", info) } } func TestLinux_clientStationInfoOK(t *testing.T) { want := []*StationInfo{ { InterfaceIndex: 1, 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, SignalAverage: -53, TransmitRetries: 5, TransmitFailed: 2, BeaconLoss: 3, ReceiveBitrate: 130000000, TransmitBitrate: 130000000, }, { InterfaceIndex: 1, 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, SignalAverage: -27, 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_IFINDEX, Data: nlenc.Uint32Bytes(uint32(s.InterfaceIndex)), }, { 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_SIGNAL_AVG, Data: []byte{byte(int8(s.SignalAverage))}}, {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 (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_IFINDEX, Data: nlenc.Uint32Bytes(uint32(s.InterfaceIndex)), }, { Type: unix.NL80211_ATTR_SURVEY_INFO, Data: mustMarshalAttributes(attributes), }, } } 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) } case []*SurveyInfo: 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 } } func Test_decodeBSSLoad(t *testing.T) { type args struct { b []byte } tests := []struct { name string args args wantVersion uint16 wantStationCount uint16 wantChannelUtilization uint8 wantAvailableAdmissionCapacity uint16 }{ {name: "Parse BSS Load Normal", args: args{b: []byte{3, 0, 8, 0x8D, 0x5B}}, wantVersion: 2, wantStationCount: 3, wantChannelUtilization: 8, wantAvailableAdmissionCapacity: 23437}, {name: "Parse BSS Load Version 1", args: args{b: []byte{9, 0, 8, 0x8D}}, wantVersion: 1, wantStationCount: 9, wantChannelUtilization: 8, wantAvailableAdmissionCapacity: 141}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bssLoad, _ := decodeBSSLoad(tt.args.b) gotVersion := bssLoad.Version gotStationCount := bssLoad.StationCount gotChannelUtilization := bssLoad.ChannelUtilization gotAvailableAdmissionCapacity := bssLoad.AvailableAdmissionCapacity if uint16(gotVersion) != tt.wantVersion { t.Errorf("decodeBSSLoad() gotVersion = %v, want %v", gotVersion, tt.wantVersion) } if gotStationCount != tt.wantStationCount { t.Errorf("decodeBSSLoad() gotStationCount = %v, want %v", gotStationCount, tt.wantStationCount) } if gotChannelUtilization != tt.wantChannelUtilization { t.Errorf("decodeBSSLoad() gotChannelUtilization = %v, want %v", gotChannelUtilization, tt.wantChannelUtilization) } if gotAvailableAdmissionCapacity != tt.wantAvailableAdmissionCapacity { t.Errorf("decodeBSSLoad() gotAvailableAdmissionCapacity = %v, want %v", gotAvailableAdmissionCapacity, tt.wantAvailableAdmissionCapacity) } }) } } func Test_decodeBSSLoadError(t *testing.T) { t.Parallel() _, err := decodeBSSLoad([]byte{3, 0, 8}) if err == nil { 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{ { InterfaceIndex: 1, 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, }, { InterfaceIndex: 1, 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]) } } } // Test data helpers for decodeRSN tests func buildRSNIE(parts ...[]byte) []byte { var result []byte for _, part := range parts { result = append(result, part...) } return result } var ( rsnVersion1 = []byte{0x01, 0x00} rsnVersion2 = []byte{0x02, 0x00} ccmp128Cipher = []byte{0x00, 0x0F, 0xAC, 0x04} tkipCipher = []byte{0x00, 0x0F, 0xAC, 0x02} bipCmac128Cipher = []byte{0x00, 0x0F, 0xAC, 0x06} pskAKM = []byte{0x00, 0x0F, 0xAC, 0x02} saeAKM = []byte{0x00, 0x0F, 0xAC, 0x08} dot1xAKM = []byte{0x00, 0x0F, 0xAC, 0x01} oneCipherCount = []byte{0x01, 0x00} twoCipherCount = []byte{0x02, 0x00} zeroCount = []byte{0x00, 0x00} pmfCapable = []byte{0x80, 0x00} pmfRequired = []byte{0xC0, 0x00} pmkid16Bytes = []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10} ) // Test valid RSN cases func Test_decodeRSN_ValidCases(t *testing.T) { tests := []struct { name string input []byte expected *RSNInfo }{ { name: "minimal valid RSN", input: buildRSNIE(rsnVersion1, ccmp128Cipher, oneCipherCount, ccmp128Cipher), expected: &RSNInfo{ Version: 1, GroupCipher: RSNCipherCCMP128, PairwiseCiphers: []RSNCipher{RSNCipherCCMP128}, AKMs: []RSNAKM{}, }, }, { name: "complete RSN with AKMs and capabilities", input: buildRSNIE(rsnVersion1, ccmp128Cipher, oneCipherCount, ccmp128Cipher, oneCipherCount, pskAKM, pmfCapable), expected: &RSNInfo{ Version: 1, GroupCipher: RSNCipherCCMP128, PairwiseCiphers: []RSNCipher{RSNCipherCCMP128}, AKMs: []RSNAKM{RSNAkmPSK}, Capabilities: 0x0080, }, }, { name: "multiple pairwise ciphers and AKMs", input: buildRSNIE(rsnVersion1, tkipCipher, twoCipherCount, tkipCipher, ccmp128Cipher, twoCipherCount, dot1xAKM, pskAKM), expected: &RSNInfo{ Version: 1, GroupCipher: RSNCipherTKIP, PairwiseCiphers: []RSNCipher{RSNCipherTKIP, RSNCipherCCMP128}, AKMs: []RSNAKM{RSNAkm8021X, RSNAkmPSK}, }, }, { name: "with group management cipher (WPA3/802.11w)", input: buildRSNIE(rsnVersion1, ccmp128Cipher, oneCipherCount, ccmp128Cipher, oneCipherCount, saeAKM, pmfRequired, zeroCount, bipCmac128Cipher), expected: &RSNInfo{ Version: 1, GroupCipher: RSNCipherCCMP128, PairwiseCiphers: []RSNCipher{RSNCipherCCMP128}, AKMs: []RSNAKM{RSNAkmSAE}, Capabilities: 0x00C0, GroupMgmtCipher: RSNCipherBIPCMAC128, }, }, { name: "with PMKID list", input: buildRSNIE(rsnVersion1, ccmp128Cipher, oneCipherCount, ccmp128Cipher, oneCipherCount, pskAKM, zeroCount, oneCipherCount, pmkid16Bytes), expected: &RSNInfo{ Version: 1, GroupCipher: RSNCipherCCMP128, PairwiseCiphers: []RSNCipher{RSNCipherCCMP128}, AKMs: []RSNAKM{RSNAkmPSK}, }, }, { name: "version 2 (should be accepted)", input: buildRSNIE(rsnVersion2, ccmp128Cipher, oneCipherCount, ccmp128Cipher), expected: &RSNInfo{ Version: 2, GroupCipher: RSNCipherCCMP128, PairwiseCiphers: []RSNCipher{RSNCipherCCMP128}, AKMs: []RSNAKM{}, }, }, { name: "unknown cipher and AKM values", input: buildRSNIE(rsnVersion1, []byte{0xFF, 0xFF, 0xFF, 0xFF}, oneCipherCount, []byte{0xAA, 0xBB, 0xCC, 0xDD}, oneCipherCount, []byte{0x11, 0x22, 0x33, 0x44}), expected: &RSNInfo{ Version: 1, GroupCipher: RSNCipher(0xFFFFFFFF), PairwiseCiphers: []RSNCipher{RSNCipher(0xAABBCCDD)}, AKMs: []RSNAKM{RSNAKM(0x11223344)}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assertValidRSN(t, tt.input, tt.expected) }) } } // Test RSN error cases func Test_decodeRSN_ErrorCases(t *testing.T) { tests := []struct { name string input []byte expected *RSNInfo errMsg string }{ { name: "empty input", input: []byte{}, expected: &RSNInfo{}, errMsg: "RSN IE parsing error: IE too short", }, { name: "too short - less than minimum 8 bytes", input: []byte{0x01, 0x00, 0x00, 0x0F, 0xAC, 0x04, 0x01}, expected: &RSNInfo{}, errMsg: "RSN IE parsing error: IE too short", }, { name: "version 0 (invalid)", input: []byte{0x00, 0x00, 0x00, 0x0F, 0xAC, 0x04, 0x01, 0x00}, expected: &RSNInfo{Version: 0}, errMsg: "RSN IE parsing error: invalid version 0", }, { name: "truncated before pairwise count", input: buildRSNIE(rsnVersion1, ccmp128Cipher), expected: &RSNInfo{}, errMsg: "RSN IE parsing error: IE too short", }, { name: "IE data exceeds maximum size", input: make([]byte, 254), expected: &RSNInfo{}, errMsg: "RSN IE parsing error: data exceeds maximum size of 253 octets", }, } // Initialize the oversized test case tests[4].input[0] = 0x01 // version for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assertRSNError(t, tt.input, tt.expected, tt.errMsg) }) } } // Test RSN truncation errors (streamlined) func Test_decodeRSN_TruncationErrors(t *testing.T) { tests := []struct { name string input []byte expected *RSNInfo errMsg string }{ { name: "truncated in pairwise list", input: buildRSNIE(rsnVersion1, ccmp128Cipher, twoCipherCount, ccmp128Cipher), expected: &RSNInfo{Version: 1, GroupCipher: RSNCipherCCMP128}, errMsg: "RSN IE parsing error: truncated in pairwise list", }, { name: "truncated in AKM list", input: buildRSNIE(rsnVersion1, ccmp128Cipher, oneCipherCount, ccmp128Cipher, twoCipherCount, dot1xAKM), expected: &RSNInfo{Version: 1, GroupCipher: RSNCipherCCMP128, PairwiseCiphers: []RSNCipher{RSNCipherCCMP128}}, errMsg: "RSN IE parsing error: truncated in AKM list", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assertRSNError(t, tt.input, tt.expected, tt.errMsg) }) } } // Test RSN count validation and edge cases func Test_decodeRSN_CountValidation(t *testing.T) { t.Run("count errors", func(t *testing.T) { tests := []struct { name string input []byte expected *RSNInfo errMsg string }{ { name: "pairwise cipher count too large", input: buildRSNIE(rsnVersion1, ccmp128Cipher, []byte{0xFF, 0x00}), expected: &RSNInfo{Version: 1, GroupCipher: RSNCipherCCMP128}, errMsg: "RSN IE parsing error: pairwise cipher count too large", }, { name: "AKM count too large", input: buildRSNIE(rsnVersion1, ccmp128Cipher, oneCipherCount, ccmp128Cipher, []byte{0xFF, 0x00}), expected: &RSNInfo{Version: 1, GroupCipher: RSNCipherCCMP128, PairwiseCiphers: []RSNCipher{RSNCipherCCMP128}}, errMsg: "RSN IE parsing error: AKM count too large", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assertRSNError(t, tt.input, tt.expected, tt.errMsg) }) } }) t.Run("zero counts (valid)", func(t *testing.T) { tests := []struct { name string input []byte }{ {"zero pairwise cipher count", buildRSNIE(rsnVersion1, ccmp128Cipher, zeroCount)}, {"zero AKM count", buildRSNIE(rsnVersion1, ccmp128Cipher, oneCipherCount, ccmp128Cipher, zeroCount)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := decodeRSN(tt.input) if err != nil { t.Errorf("decodeRSN() failed: %v", err) } if got.Version != 1 { t.Errorf("decodeRSN() version = %v, want 1", got.Version) } }) } }) } // compareRSNCipherSlices compares two RSNCipher slices, treating nil and empty slices as equal func compareRSNCipherSlices(a, b []RSNCipher) bool { if len(a) == 0 && len(b) == 0 { return true } return reflect.DeepEqual(a, b) } // compareRSNAKMSlices compares two RSNAKM slices, treating nil and empty slices as equal func compareRSNAKMSlices(a, b []RSNAKM) bool { if len(a) == 0 && len(b) == 0 { return true } return reflect.DeepEqual(a, b) } // Helper assertion functions for RSN tests func assertValidRSN(t *testing.T, input []byte, expected *RSNInfo) { t.Helper() got, err := decodeRSN(input) if err != nil { t.Errorf("decodeRSN() unexpected error = %v", err) return } // Compare individual fields if got.Version != expected.Version { t.Errorf("decodeRSN() version = %v, want %v", got.Version, expected.Version) } if got.GroupCipher != expected.GroupCipher { t.Errorf("decodeRSN() group cipher = %v, want %v", got.GroupCipher, expected.GroupCipher) } if !compareRSNCipherSlices(got.PairwiseCiphers, expected.PairwiseCiphers) { t.Errorf("decodeRSN() pairwise ciphers = %v, want %v", got.PairwiseCiphers, expected.PairwiseCiphers) } if !compareRSNAKMSlices(got.AKMs, expected.AKMs) { t.Errorf("decodeRSN() AKMs = %v, want %v", got.AKMs, expected.AKMs) } if got.Capabilities != expected.Capabilities { t.Errorf("decodeRSN() capabilities = %v, want %v", got.Capabilities, expected.Capabilities) } if got.GroupMgmtCipher != expected.GroupMgmtCipher { t.Errorf("decodeRSN() group mgmt cipher = %v, want %v", got.GroupMgmtCipher, expected.GroupMgmtCipher) } } func assertRSNError(t *testing.T, input []byte, expected *RSNInfo, errMsg string) { t.Helper() got, err := decodeRSN(input) if err == nil { t.Errorf("decodeRSN() expected error but got none") return } if errMsg != "" && err.Error() != errMsg { t.Errorf("decodeRSN() error = %v, want %v", err.Error(), errMsg) } // For error cases, check partial parsing results if got.Version != expected.Version { t.Errorf("decodeRSN() version = %v, want %v", got.Version, expected.Version) } if got.GroupCipher != expected.GroupCipher { t.Errorf("decodeRSN() group cipher = %v, want %v", got.GroupCipher, expected.GroupCipher) } } golang-github-mdlayher-wifi-0.6.0/client_others.go000066400000000000000000000032221503271467500222050ustar00rootroot00000000000000//go:build !linux // +build !linux package wifi import ( "context" "fmt" "runtime" "time" ) // 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) AccessPoints(ifi *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) Scan(ctx context.Context, ifi *Interface) error { return errUnimplemented } func (*client) Connect(_ *Interface, _ string) error { return errUnimplemented } func (*client) Disconnect(_ *Interface) error { return errUnimplemented } func (*client) ConnectWPAPSK(_ *Interface, _, _ string) error { return errUnimplemented } func (*client) SetDeadline(t time.Time) error { return errUnimplemented } func (*client) SetReadDeadline(t time.Time) error { return errUnimplemented } func (*client) SetWriteDeadline(t time.Time) error { return errUnimplemented } golang-github-mdlayher-wifi-0.6.0/doc.go000066400000000000000000000001561503271467500201130ustar00rootroot00000000000000// Package wifi provides access to IEEE 802.11 WiFi device operations on Linux // using nl80211. package wifi golang-github-mdlayher-wifi-0.6.0/go.mod000066400000000000000000000006221503271467500201230ustar00rootroot00000000000000module github.com/mdlayher/wifi go 1.23.0 require ( github.com/google/go-cmp v0.7.0 github.com/mdlayher/genetlink v1.3.2 github.com/mdlayher/netlink v1.7.2 golang.org/x/crypto v0.39.0 golang.org/x/sys v0.33.0 ) require ( github.com/josharian/native v1.1.0 // indirect github.com/mdlayher/socket v0.4.1 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.1.0 // indirect ) golang-github-mdlayher-wifi-0.6.0/go.sum000066400000000000000000000027071503271467500201560ustar00rootroot00000000000000github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang-github-mdlayher-wifi-0.6.0/wifi.go000066400000000000000000000417501503271467500203110ustar00rootroot00000000000000package 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") // errInvalidBSSLoad is returned when BSSLoad IE has wrong length. var errInvalidBSSLoad = errors.New("802.11 information element BSSLoad has wrong length") // RSN (Robust Security Network) Information Element parsing errors var ( // Base error for all RSN parsing errors errRSNParse = errors.New("RSN IE parsing error") // Specific RSN parsing errors that wrap the base error errRSNDataTooLarge = fmt.Errorf("%w: data exceeds maximum size of 253 octets", errRSNParse) errRSNTooShort = fmt.Errorf("%w: IE too short", errRSNParse) errRSNInvalidVersion = fmt.Errorf("%w: invalid version 0", errRSNParse) errRSNTruncatedPairwiseCount = fmt.Errorf("%w: truncated before pairwise count", errRSNParse) errRSNPairwiseCipherCountTooLarge = fmt.Errorf("%w: pairwise cipher count too large", errRSNParse) errRSNTruncatedPairwiseList = fmt.Errorf("%w: truncated in pairwise list", errRSNParse) errRSNAKMCountTooLarge = fmt.Errorf("%w: AKM count too large", errRSNParse) errRSNTruncatedAKMList = fmt.Errorf("%w: truncated in AKM list", errRSNParse) errRSNTooSmallForCounts = fmt.Errorf("%w: too small for declared cipher/AKM counts", errRSNParse) errRSNPMKIDCountTooLarge = fmt.Errorf("%w: PMKID count too large", errRSNParse) errRSNTruncatedPMKIDList = fmt.Errorf("%w: truncated in PMKID list", errRSNParse) ) // 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 interface that this station is associated with. InterfaceIndex int // 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 the last received PPDU, in dBm. Signal int // The average signal strength, in dBm. SignalAverage 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 } // BSSLoad is an Information Element containing measurements of the load on the BSS. type BSSLoad struct { // Version: Indicates the version of the BSS Load Element. Can be 1 or 2. Version int // StationCount: total number of STA currently associated with this BSS. StationCount uint16 // ChannelUtilization: Percentage of time (linearly scaled 0 to 255) that the AP sensed the medium was busy. Calculated only for the primary channel. ChannelUtilization uint8 // AvailableAdmissionCapacity: remaining amount of medium time availible via explicit admission controll in units of 32 us/s. AvailableAdmissionCapacity uint16 } // String returns the string representation of a BSSLoad. func (l BSSLoad) String() string { switch l.Version { case 1: return fmt.Sprintf("BSSLoad Version: %d stationCount: %d channelUtilization: %d/255 availableAdmissionCapacity: %d\n", l.Version, l.StationCount, l.ChannelUtilization, l.AvailableAdmissionCapacity, ) case 2: return fmt.Sprintf("BSSLoad Version: %d stationCount: %d channelUtilization: %d/255 availableAdmissionCapacity: %d [*32us/s]\n", l.Version, l.StationCount, l.ChannelUtilization, l.AvailableAdmissionCapacity, ) } return fmt.Sprintf("invalid BSSLoad Version: %d", l.Version) } // 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 // BSSID: 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 // Frequency: The frequency used by the BSS, in MHz. Frequency int // BeaconInterval: The time interval between beacon transmissions for this BSS. BeaconInterval time.Duration // LastSeen: The time since the client last scanned this BSS's information. LastSeen time.Duration // Status: The status of the client within the BSS. Status BSSStatus // Load: The load element of the BSS (contains StationCount, ChannelUtilization and AvailableAdmissionCapacity). Load BSSLoad // RSN Robust Security Network Information Element (IEEE 802.11 Element ID 48) RSN RSNInfo } // 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 // BSSStatusNotAssociated indicates that a client is not associated with a BSS. BSSStatusNotAssociated // 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 BSSStatusNotAssociated: return "unassociated" case BSSStatusIBSSJoined: return "IBSS joined" default: return fmt.Sprintf("unknown(%d)", s) } } // List of 802.11 Information Element types. const ( ieSSID = 0 ieBSSLoad = 11 ieRSN = 48 // Robust Security Network ) // 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 len(b[i:]) != 0 { 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 } type SurveyInfo struct { // The interface that this station is associated with. InterfaceIndex int // 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 } // RSNCipher represents a cipher suite in RSN IE. // Values correspond to OUIs (00-0F-AC-XX) in the wire format as defined in // IEEE 802.11-2020 standard, section 9.4.2.24.2 (Cipher Suites). type RSNCipher uint32 const ( RSNCipherUseGroup RSNCipher = 0x000FAC00 // Use group cipher suite RSNCipherWEP40 RSNCipher = 0x000FAC01 // WEP-40 (insecure, legacy) RSNCipherTKIP RSNCipher = 0x000FAC02 // TKIP (insecure, deprecated) RSNCipherReserved3 RSNCipher = 0x000FAC03 // Reserved RSNCipherCCMP128 RSNCipher = 0x000FAC04 // CCMP-128 (AES) - WPA2 RSNCipherWEP104 RSNCipher = 0x000FAC05 // WEP-104 (insecure, legacy) RSNCipherBIPCMAC128 RSNCipher = 0x000FAC06 // BIP-CMAC-128 (802.11w MFP/PMF) RSNCipherGroupNotAllowed RSNCipher = 0x000FAC07 // Group addressed traffic not allowed RSNCipherGCMP128 RSNCipher = 0x000FAC08 // GCMP-128 (AES-GCMP) - WPA3 RSNCipherGCMP256 RSNCipher = 0x000FAC09 // GCMP-256 (AES-GCMP) - WPA3-Enterprise RSNCipherCCMP256 RSNCipher = 0x000FAC0A // CCMP-256 (AES, 256-bit key) RSNCipherBIPGMAC128 RSNCipher = 0x000FAC0B // BIP-GMAC-128 RSNCipherBIPGMAC256 RSNCipher = 0x000FAC0C // BIP-GMAC-256 RSNCipherBIPCMAC256 RSNCipher = 0x000FAC0D // BIP-CMAC-256 ) // String returns the human-readable name of the RSN cipher. func (c RSNCipher) String() string { switch c { case RSNCipherUseGroup: return "Use‑group" case RSNCipherWEP40: return "WEP‑40" case RSNCipherTKIP: return "TKIP" case RSNCipherReserved3: return "Reserved‑3" case RSNCipherCCMP128: return "CCMP‑128" case RSNCipherWEP104: return "WEP‑104" case RSNCipherBIPCMAC128: return "BIP‑CMAC‑128" case RSNCipherGroupNotAllowed: return "Group‑not‑allowed" case RSNCipherGCMP128: return "GCMP‑128" case RSNCipherGCMP256: return "GCMP‑256" case RSNCipherCCMP256: return "CCMP‑256" case RSNCipherBIPGMAC128: return "BIP‑GMAC‑128" case RSNCipherBIPGMAC256: return "BIP‑GMAC‑256" case RSNCipherBIPCMAC256: return "BIP‑CMAC‑256" default: return fmt.Sprintf("Unknown-0x%08X", uint32(c)) } } // RSNAKM represents an Authentication and Key Management suite in RSN IE. // Values correspond to OUIs (00-0F-AC-XX) in the wire format as defined in // IEEE 802.11-2020 standard, section 9.4.2.24.3 (AKM Suites). type RSNAKM uint32 // RSN AKM suite constants (Wi-Fi Alliance OUI: 00-0F-AC) const ( RSNAkmReserved0 RSNAKM = 0x000FAC00 // Reserved RSNAkm8021X RSNAKM = 0x000FAC01 // 802.1X (WPA-Enterprise) RSNAkmPSK RSNAKM = 0x000FAC02 // PSK (WPA2-Personal) RSNAkmFT8021X RSNAKM = 0x000FAC03 // FT-802.1X (Fast BSS transition with EAP) RSNAkmFTPSK RSNAKM = 0x000FAC04 // FT-PSK (Fast BSS transition with PSK) RSNAkm8021XSHA256 RSNAKM = 0x000FAC05 // 802.1X-SHA256 (WPA2 with SHA256 auth) RSNAkmPSKSHA256 RSNAKM = 0x000FAC06 // PSK-SHA256 (WPA2-PSK with SHA256) RSNAkmTDLS RSNAKM = 0x000FAC07 // TDLS TPK handshake RSNAkmSAE RSNAKM = 0x000FAC08 // SAE (WPA3-Personal) RSNAkmFTSAE RSNAKM = 0x000FAC09 // FT-SAE (WPA3-Personal with Fast Roaming) RSNAkmAPPeerKey RSNAKM = 0x000FAC0A // APPeerKey Authentication with SHA-256 RSNAkm8021XSuiteB RSNAKM = 0x000FAC0B // 802.1X using Suite B compliant EAP (SHA-256) RSNAkm8021XCNSA RSNAKM = 0x000FAC0C // 802.1X using CNSA Suite compliant EAP (SHA-384) RSNAkmFT8021XSHA384 RSNAKM = 0x000FAC0D // FT-802.1X using SHA-384 RSNAkmFILSSHA256 RSNAKM = 0x000FAC0E // FILS key management using SHA-256 RSNAkmFILSSHA384 RSNAKM = 0x000FAC0F // FILS key management using SHA-384 RSNAkmFTFILSSHA256 RSNAKM = 0x000FAC10 // FT authentication over FILS with SHA-256 RSNAkmFTFILSSHA384 RSNAKM = 0x000FAC11 // FT authentication over FILS with SHA-384 RSNAkmReserved18 RSNAKM = 0x000FAC12 // Reserved RSNAkmFTPSKSHA384 RSNAKM = 0x000FAC13 // FT-PSK using SHA-384 RSNAkmPSKSHA384 RSNAKM = 0x000FAC14 // PSK using SHA-384 ) // String returns the human-readable name of the RSN AKM. func (a RSNAKM) String() string { switch a { case RSNAkmReserved0: return "Reserved‑0" case RSNAkm8021X: return "802.1X" case RSNAkmPSK: return "PSK" case RSNAkmFT8021X: return "FT‑802.1X" case RSNAkmFTPSK: return "FT‑PSK" case RSNAkm8021XSHA256: return "802.1X‑SHA256" case RSNAkmPSKSHA256: return "PSK‑SHA256" case RSNAkmTDLS: return "TDLS" case RSNAkmSAE: return "SAE" case RSNAkmFTSAE: return "FT‑SAE" case RSNAkmAPPeerKey: return "AP‑PeerKey" case RSNAkm8021XSuiteB: return "802.1X‑Suite‑B" case RSNAkm8021XCNSA: return "802.1X‑CNSA" case RSNAkmFT8021XSHA384: return "FT‑802.1X‑SHA384" case RSNAkmFILSSHA256: return "FILS‑SHA256" case RSNAkmFILSSHA384: return "FILS‑SHA384" case RSNAkmFTFILSSHA256: return "FT‑FILS‑SHA256" case RSNAkmFTFILSSHA384: return "FT‑FILS‑SHA384" case RSNAkmReserved18: return "Reserved‑18" case RSNAkmFTPSKSHA384: return "FT‑PSK‑SHA384" case RSNAkmPSKSHA384: return "PSK‑SHA384" default: return fmt.Sprintf("Unknown-0x%08X", uint32(a)) } } // Robust Security Network Information Element // The RSN IE structure is defined in IEEE 802.11-2020 standard, section 9.4.2.24 (page 1051) . type RSNInfo struct { Version uint16 GroupCipher RSNCipher // Group cipher suite PairwiseCiphers []RSNCipher // Pairwise cipher suites AKMs []RSNAKM // Authentication and Key Management suites Capabilities uint16 // RSN capability flags GroupMgmtCipher RSNCipher // Group management cipher (present only with WPA3/802.11w) } func (r RSNInfo) IsInitialized() bool { return r.Version != 0 } func (r RSNInfo) String() string { if !r.IsInitialized() { return "" } // Convert pairwise ciphers to strings pairwiseNames := make([]string, len(r.PairwiseCiphers)) for i, cipher := range r.PairwiseCiphers { pairwiseNames[i] = cipher.String() } // Convert AKMs to strings akmNames := make([]string, len(r.AKMs)) for i, akm := range r.AKMs { akmNames[i] = akm.String() } return fmt.Sprintf( "RSN v%d Group:%s Pairwise:%v AKM:%v", r.Version, r.GroupCipher.String(), pairwiseNames, akmNames) } golang-github-mdlayher-wifi-0.6.0/wifi_test.go000066400000000000000000000224561503271467500213520ustar00rootroot00000000000000package wifi import ( "errors" "reflect" "strings" "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: BSSStatusNotAssociated, s: "unassociated", }, { t: BSSStatusIBSSJoined, s: "IBSS joined", }, { t: 4, s: "unknown(4)", }, } 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) } }) } } func TestRSNCipherString(t *testing.T) { tests := []struct { cipher RSNCipher want string }{ {RSNCipherUseGroup, "Use‑group"}, {RSNCipherWEP40, "WEP‑40"}, {RSNCipherTKIP, "TKIP"}, {RSNCipherReserved3, "Reserved‑3"}, {RSNCipherCCMP128, "CCMP‑128"}, {RSNCipherWEP104, "WEP‑104"}, {RSNCipherBIPCMAC128, "BIP‑CMAC‑128"}, {RSNCipherGroupNotAllowed, "Group‑not‑allowed"}, {RSNCipherGCMP128, "GCMP‑128"}, {RSNCipherGCMP256, "GCMP‑256"}, {RSNCipherCCMP256, "CCMP‑256"}, {RSNCipherBIPGMAC128, "BIP‑GMAC‑128"}, {RSNCipherBIPGMAC256, "BIP‑GMAC‑256"}, {RSNCipherBIPCMAC256, "BIP‑CMAC‑256"}, {RSNCipher(0x000FAC99), "Unknown-0x000FAC99"}, // Unknown cipher } for _, tt := range tests { t.Run(tt.want, func(t *testing.T) { if got := tt.cipher.String(); got != tt.want { t.Errorf("RSNCipher.String() = %v, want %v", got, tt.want) } }) } } func TestRSNAKMString(t *testing.T) { tests := []struct { akm RSNAKM want string }{ {RSNAkmReserved0, "Reserved‑0"}, {RSNAkm8021X, "802.1X"}, {RSNAkmPSK, "PSK"}, {RSNAkmFT8021X, "FT‑802.1X"}, {RSNAkmFTPSK, "FT‑PSK"}, {RSNAkm8021XSHA256, "802.1X‑SHA256"}, {RSNAkmPSKSHA256, "PSK‑SHA256"}, {RSNAkmTDLS, "TDLS"}, {RSNAkmSAE, "SAE"}, {RSNAkmFTSAE, "FT‑SAE"}, {RSNAkmAPPeerKey, "AP‑PeerKey"}, {RSNAkm8021XSuiteB, "802.1X‑Suite‑B"}, {RSNAkm8021XCNSA, "802.1X‑CNSA"}, {RSNAkmFT8021XSHA384, "FT‑802.1X‑SHA384"}, {RSNAkmFILSSHA256, "FILS‑SHA256"}, {RSNAkmFILSSHA384, "FILS‑SHA384"}, {RSNAkmFTFILSSHA256, "FT‑FILS‑SHA256"}, {RSNAkmFTFILSSHA384, "FT‑FILS‑SHA384"}, {RSNAkmReserved18, "Reserved‑18"}, {RSNAkmFTPSKSHA384, "FT‑PSK‑SHA384"}, {RSNAkmPSKSHA384, "PSK‑SHA384"}, {RSNAKM(0x000FAC99), "Unknown-0x000FAC99"}, // Unknown AKM } for _, tt := range tests { t.Run(tt.want, func(t *testing.T) { if got := tt.akm.String(); got != tt.want { t.Errorf("RSNAKM.String() = %v, want %v", got, tt.want) } }) } } func TestRSNInfoIsInitialized(t *testing.T) { tests := []struct { name string rsn RSNInfo want bool }{ { name: "uninitialized", rsn: RSNInfo{}, want: false, }, { name: "initialized_version_1", rsn: RSNInfo{Version: 1}, want: true, }, { name: "initialized_version_2", rsn: RSNInfo{Version: 2}, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.rsn.IsInitialized(); got != tt.want { t.Errorf("RSNInfo.IsInitialized() = %v, want %v", got, tt.want) } }) } } func TestRSNInfoString(t *testing.T) { tests := []struct { name string rsn RSNInfo want string }{ { name: "uninitialized", rsn: RSNInfo{}, want: "", }, { name: "basic_wpa2", rsn: RSNInfo{ Version: 1, GroupCipher: RSNCipherCCMP128, PairwiseCiphers: []RSNCipher{RSNCipherCCMP128}, AKMs: []RSNAKM{RSNAkmPSK}, }, want: "RSN v1 Group:CCMP‑128 Pairwise:[CCMP‑128] AKM:[PSK]", }, { name: "wpa3_multiple_ciphers", rsn: RSNInfo{ Version: 1, GroupCipher: RSNCipherGCMP128, PairwiseCiphers: []RSNCipher{RSNCipherGCMP128, RSNCipherCCMP128}, AKMs: []RSNAKM{RSNAkmSAE, RSNAkmPSK}, }, want: "RSN v1 Group:GCMP‑128 Pairwise:[GCMP‑128 CCMP‑128] AKM:[SAE PSK]", }, { name: "enterprise_with_ft", rsn: RSNInfo{ Version: 1, GroupCipher: RSNCipherCCMP128, PairwiseCiphers: []RSNCipher{RSNCipherCCMP128}, AKMs: []RSNAKM{RSNAkm8021X, RSNAkmFT8021X}, }, want: "RSN v1 Group:CCMP‑128 Pairwise:[CCMP‑128] AKM:[802.1X FT‑802.1X]", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.rsn.String(); got != tt.want { t.Errorf("RSNInfo.String() = %v, want %v", got, tt.want) } }) } } func TestRSNErrorHierarchy(t *testing.T) { // Test that specific errors wrap the base error and basic functionality tests := []struct { name string err error description string }{ {"DataTooLarge", errRSNDataTooLarge, "data exceeds maximum size"}, {"TooShort", errRSNTooShort, "IE too short"}, {"InvalidVersion", errRSNInvalidVersion, "invalid version"}, {"TruncatedPairwiseCount", errRSNTruncatedPairwiseCount, "truncated before pairwise count"}, {"PairwiseCipherCountTooLarge", errRSNPairwiseCipherCountTooLarge, "pairwise cipher count too large"}, {"TruncatedPairwiseList", errRSNTruncatedPairwiseList, "truncated in pairwise list"}, {"AKMCountTooLarge", errRSNAKMCountTooLarge, "AKM count too large"}, {"TruncatedAKMList", errRSNTruncatedAKMList, "truncated in AKM list"}, {"TooSmallForCounts", errRSNTooSmallForCounts, "too small for declared cipher/AKM counts"}, {"PMKIDCountTooLarge", errRSNPMKIDCountTooLarge, "PMKID count too large"}, {"TruncatedPMKIDList", errRSNTruncatedPMKIDList, "truncated in PMKID list"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Test that the specific error wraps the base error (errors.Is functionality) if !errors.Is(tt.err, errRSNParse) { t.Errorf("errors.Is(%v, errRSNParse) = false, want true", tt.err) } // Test that the specific error is still identifiable as itself if !errors.Is(tt.err, tt.err) { t.Errorf("errors.Is(%v, %v) = false, want true", tt.err, tt.err) } // Test that specific errors don't match other specific errors if tt.err != errRSNDataTooLarge && errors.Is(tt.err, errRSNDataTooLarge) { t.Errorf("error %v should not match errRSNDataTooLarge", tt.err) } // Test that RSN errors don't match non-RSN errors if errors.Is(tt.err, errInvalidIE) { t.Errorf("RSN error %v should not match errInvalidIE", tt.err) } // Test that error message is properly formatted errMsg := tt.err.Error() if errMsg == "" { t.Errorf("error message is empty") } // Verify error message contains both base and specific parts if !strings.Contains(errMsg, errRSNParse.Error()) { t.Errorf("error message should contain %q, got: %q", errRSNParse.Error(), errMsg) } if !strings.Contains(errMsg, tt.description) { t.Errorf("error message should contain %q, got: %q", tt.description, errMsg) } }) } // Test that base error doesn't match specific errors t.Run("Base error isolation", func(t *testing.T) { if errors.Is(errRSNParse, errRSNDataTooLarge) { t.Error("base error should not match specific errors") } }) }