@@ -5,11 +5,13 @@ package wifi
55
66import (
77 "bytes"
8+ "context"
89 "crypto/sha1"
910 "encoding/binary"
1011 "errors"
1112 "net"
1213 "os"
14+ "sync"
1315 "time"
1416 "unicode/utf8"
1517
@@ -20,8 +22,12 @@ import (
2022 "golang.org/x/sys/unix"
2123)
2224
23- // errNotSupported is returned when an operation is not supported
24- var ErrNotSupported = errors .New ("not supported" )
25+ var (
26+ ErrNotSupported = errors .New ("not supported" )
27+ ErrScanGroupNotFound = errors .New ("scan multicast group unavailable" )
28+ ErrScanAborted = errors .New ("scan aborted by the kernel" )
29+ ErrScanValidation = errors .New ("scan validation failed" )
30+ )
2531
2632// A client is the Linux implementation of osClient, which makes use of
2733// netlink, generic netlink, and nl80211 to provide access to WiFi device
@@ -30,6 +36,9 @@ type client struct {
3036 c * genetlink.Conn
3137 familyID uint16
3238 familyVersion uint8
39+
40+ // scan is used to synchronize access to the Scan method.
41+ scan sync.Mutex
3342}
3443
3544// newClient dials a generic netlink connection and verifies that nl80211
@@ -67,6 +76,8 @@ func initClient(c *genetlink.Conn) (*client, error) {
6776 c : c ,
6877 familyID : family .ID ,
6978 familyVersion : family .Version ,
79+
80+ scan : sync.Mutex {},
7081 }, nil
7182}
7283
@@ -180,6 +191,21 @@ func (c *client) BSS(ifi *Interface) (*BSS, error) {
180191 return parseBSS (msgs )
181192}
182193
194+ // AccessPoints requests that nl80211 return all currently known BSS
195+ // from the specified Interface.
196+ func (c * client ) AccessPoints (ifi * Interface ) ([]* BSS , error ) {
197+ msgs , err := c .get (
198+ unix .NL80211_CMD_GET_SCAN ,
199+ netlink .Dump ,
200+ ifi ,
201+ nil ,
202+ )
203+ if err != nil {
204+ return nil , err
205+ }
206+ return parseGetScanResult (msgs )
207+ }
208+
183209// StationInfo requests that nl80211 return all station info for the specified
184210// Interface.
185211func (c * client ) StationInfo (ifi * Interface ) ([]* StationInfo , error ) {
@@ -233,6 +259,105 @@ func (c *client) SurveyInfo(ifi *Interface) ([]*SurveyInfo, error) {
233259 return surveys , nil
234260}
235261
262+ // Scan requests that nl80211 perform a scan for new access points using
263+ // the specified Interface. This process is long running and uses
264+ // a separate connection to nl80211.
265+ //
266+ // Use context.WithDeadline to set a timeout.
267+ //
268+ // If a scan is already in progress, this function will return a syscall.EBUSY
269+ // error. If the response cannot be validated, the returned error
270+ // will include ErrScanValidation.
271+ //
272+ // Use func AccessPoints to retrieve the results.
273+ func (c * client ) Scan (ctx context.Context , ifi * Interface ) error {
274+ c .scan .Lock ()
275+ defer c .scan .Unlock ()
276+
277+ // use secondary connection for multicast receives
278+ conn , err := genetlink .Dial (& netlink.Config {Strict : true })
279+ if err != nil {
280+ return err
281+ }
282+
283+ defer conn .Close ()
284+
285+ if deadline , ok := ctx .Deadline (); ok {
286+ err := conn .SetDeadline (deadline )
287+ if err != nil {
288+ return err
289+ }
290+ }
291+
292+ family , err := conn .GetFamily (unix .NL80211_GENL_NAME )
293+ if err != nil {
294+ return err
295+ }
296+
297+ var id uint32
298+ for _ , group := range family .Groups {
299+ if group .Name == unix .NL80211_MULTICAST_GROUP_SCAN {
300+ err = conn .JoinGroup (group .ID )
301+ if err != nil {
302+ return err
303+ }
304+
305+ id = group .ID
306+ break
307+ }
308+ }
309+
310+ if id == 0 {
311+ return ErrScanGroupNotFound
312+ }
313+
314+ // Leave group on exit. Err is non-actionable
315+ defer func () { _ = conn .LeaveGroup (id ) }()
316+
317+ enc := netlink .NewAttributeEncoder ()
318+ enc .Nested (unix .NL80211_ATTR_SCAN_SSIDS , func (ae * netlink.AttributeEncoder ) error {
319+ ae .Bytes (unix .NL80211_SCHED_SCAN_MATCH_ATTR_SSID , nlenc .Bytes ("" ))
320+ return nil
321+ })
322+
323+ ifi .encode (enc )
324+
325+ data , err := enc .Encode ()
326+ if err != nil {
327+ return err
328+ }
329+
330+ req := genetlink.Message {
331+ Header : genetlink.Header {
332+ Command : unix .NL80211_CMD_TRIGGER_SCAN ,
333+ Version : c .familyVersion ,
334+ },
335+ Data : data ,
336+ }
337+
338+ ctx , cancel := context .WithCancel (ctx )
339+ defer cancel ()
340+
341+ result := make (chan error , 1 )
342+ go func (ctx context.Context , conn * genetlink.Conn , ifiIndex int , familyVersion uint8 , result chan <- error ) {
343+
344+ defer close (result )
345+ result <- listenNewScanResults (ctx , conn , ifiIndex , familyVersion )
346+
347+ }(ctx , conn , ifi .Index , c .familyVersion , result )
348+
349+ flags := netlink .Request | netlink .Acknowledge
350+
351+ _ , err = conn .Send (req , family .ID , flags )
352+ if err != nil {
353+ cancel ()
354+ }
355+
356+ err2 := <- result
357+
358+ return errors .Join (err , err2 )
359+ }
360+
236361// SetDeadline sets the read and write deadlines associated with the connection.
237362func (c * client ) SetDeadline (t time.Time ) error {
238363 return c .c .SetDeadline (t )
@@ -295,6 +420,94 @@ func (c *client) execute(
295420 )
296421}
297422
423+ // listenNewScanResults listens for new scan results or scan abort messages
424+ // from the netlink connection. It processes the messages associated with the
425+ // specified interface index and family version, verifying attributes and
426+ // handling context cancellations.
427+ //
428+ // The caller should not receive on the given connection and is responsible
429+ // for closing it.
430+ func listenNewScanResults (ctx context.Context , conn * genetlink.Conn , ifiIndex int , familyVersion uint8 ) error {
431+ for ctx .Err () == nil {
432+ msgs , _ , err := conn .Receive ()
433+ if err != nil {
434+ return err
435+ }
436+
437+ // test for context cancellation and abandon work if so
438+ if ctx .Err () != nil {
439+ return err
440+ }
441+
442+ for _ , msg := range msgs {
443+ if msg .Header .Version != familyVersion {
444+ break
445+ }
446+
447+ switch msg .Header .Command {
448+ case unix .NL80211_CMD_SCAN_ABORTED :
449+ return ErrScanAborted
450+ case unix .NL80211_CMD_NEW_SCAN_RESULTS :
451+ // attempt to verify the interface
452+ attrs , err := netlink .UnmarshalAttributes (msg .Data )
453+ if err != nil {
454+ return errors .Join (ErrScanValidation , err )
455+ }
456+
457+ var intf Interface
458+ if err := (& intf ).parseAttributes (attrs ); err != nil {
459+ return errors .Join (ErrScanValidation , err )
460+ }
461+
462+ if ifiIndex != intf .Index {
463+ continue
464+ }
465+
466+ return nil
467+ default :
468+ continue
469+ }
470+
471+ }
472+ }
473+
474+ return ctx .Err ()
475+ }
476+
477+ // parseGetScanResult parses all the BSS from nl80211 CMD_GET_SCAN response messages.
478+ func parseGetScanResult (msgs []genetlink.Message ) ([]* BSS , error ) {
479+ // reimplementing https://github.com/mdlayher/wifi/pull/79
480+ bsss := make ([]* BSS , 0 , len (msgs ))
481+ for _ , m := range msgs {
482+ attrs , err := netlink .UnmarshalAttributes (m .Data )
483+ if err != nil {
484+ return nil , err
485+ }
486+
487+ var bss BSS
488+ for _ , a := range attrs {
489+ if a .Type != unix .NL80211_ATTR_BSS {
490+ continue
491+ }
492+
493+ nattrs , err := netlink .UnmarshalAttributes (a .Data )
494+ if err != nil {
495+ return nil , err
496+ }
497+
498+ if ! attrsContain (nattrs , unix .NL80211_BSS_STATUS ) {
499+ bss .Status = BSSStatusNotAssociated
500+ }
501+
502+ if err := (& bss ).parseAttributes (nattrs ); err != nil {
503+ continue
504+ }
505+ }
506+ bsss = append (bsss , & bss )
507+ }
508+ return bsss , nil
509+ }
510+
298511// parseInterfaces parses zero or more Interfaces from nl80211 interface
299512// messages.
300513func parseInterfaces (msgs []genetlink.Message ) ([]* Interface , error ) {
0 commit comments