Add basic documentation & tests

Add a couple of basic tests for the SCAN_RESULTS parser, and document
usage.
This commit is contained in:
Dave Pifke 2017-05-25 12:41:20 -07:00
parent 8a251ea4ef
commit 2928b06b23
No known key found for this signature in database
GPG Key ID: 3685742996D9D533
5 changed files with 429 additions and 25 deletions

26
LICENSE.txt Normal file
View File

@ -0,0 +1,26 @@
Copyright (c) 2017 Dave Pifke.
Redistribution and use in source and binary forms, with or without
modification, is permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

70
README.md Normal file
View File

@ -0,0 +1,70 @@
# pifke.org/wpasupplicant
[![GoDoc](https://godoc.org/pifke.org/wpasupplicant?status.svg)](https://godoc.org/pifke.org/wpasupplicant)
[![Build Status](https://api.travis-ci.org/dpifke/golang-wpasupplicant.svg)](https://travis-ci.org/dpifke/golang-wpasupplicant)
[![Test Coverage](https://coveralls.io/repos/github/dpifke/golang-wpasupplicant/badge.svg)](https://coveralls.io/github/dpifke/golang-wpasupplicant)
Golang interface for talking to wpa_supplicant.
At the moment, this simply provides an interface for fetching wifi scan
results. More functionality (probably) coming soon.
## Example
```
import (
"fmt"
"pifke.org/wpasupplicant"
)
// Prints the BSSID (MAC address) and SSID of each access point in range:
w := wpasupplicant.Unixgram("wlan0")
for _, bss := range w.ScanResults() {
fmt.Fprintf("%s\t%s\n", bss.BSSID(), bss.SSID())
}
```
## Downloading
If you use this library in your own code, please use the canonical URL in your
Go code, instead of Github:
```
go get pifke.org/wpasupplicant
```
Or (until I finish setting up the self-hosted repository):
```
# From the root of your project:
git submodule add https://github.com/dpifke/golang-wpasupplicant vendor/pifke.org/wpasupplicant
```
Then:
```
import (
"pifke.org/wpasupplicant"
)
```
As opposed to the pifke.org URL, I make no guarantee this Github repository
will exist or be up-to-date in the future.
## Documentation
Available on [godoc.org](https://godoc.org/pifke.org/wpasupplicant).
## License
Three-clause BSD. See LICENSE.txt.
Contact me if you want to use this code under different terms.
## Author
Dave Pifke. My email address is my first name "at" my last name "dot org."
I'm [@dpifke](https://twitter.com/dpifke) on Twitter. My PGP key
is available on [Keybase](https://keybase.io/dpifke).

View File

@ -1,10 +1,38 @@
// Copyright (c) 2017 Dave Pifke.
//
// Redistribution and use in source and binary forms, with or without
// modification, is permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
package wpasupplicant package wpasupplicant
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net" "net"
"os" "os"
@ -14,18 +42,30 @@ import (
"syscall" "syscall"
) )
// message is a queued response (or read error) from the wpa_supplicant
// daemon. Messages may be either solicited or unsolicited.
type message struct { type message struct {
priority int priority int
data []byte data []byte
err error err error
} }
// unixgramConn is the implementation of Conn for the AF_UNIX SOCK_DGRAM
// control interface.
//
// See https://w1.fi/wpa_supplicant/devel/ctrl_iface_page.html.
type unixgramConn struct { type unixgramConn struct {
c *net.UnixConn c *net.UnixConn
fd uintptr fd uintptr
solicited, unsolicited chan message solicited, unsolicited chan message
} }
// socketPath is where to find the the AF_UNIX sockets for each interface. It
// can be overridden for testing.
var socketPath = "/run/wpa_supplicant"
// Unixgram returns a connection to wpa_supplicant for the specified
// interface, using the socket-based control interface.
func Unixgram(ifName string) (Conn, error) { func Unixgram(ifName string) (Conn, error) {
var err error var err error
uc := &unixgramConn{} uc := &unixgramConn{}
@ -38,7 +78,7 @@ func Unixgram(ifName string) (Conn, error) {
uc.c, err = net.DialUnix("unixgram", uc.c, err = net.DialUnix("unixgram",
&net.UnixAddr{Name: local.Name(), Net: "unixgram"}, &net.UnixAddr{Name: local.Name(), Net: "unixgram"},
&net.UnixAddr{Name: path.Join("/run/wpa_supplicant", ifName), Net: "unixgram"}) &net.UnixAddr{Name: path.Join(socketPath, ifName), Net: "unixgram"})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -54,13 +94,32 @@ func Unixgram(ifName string) (Conn, error) {
go uc.readLoop() go uc.readLoop()
// TODO: issue an ACCEPT command so as to receive unsolicited
// messages. (We don't do this yet, since we don't yet have any way
// to consume them.)
return uc, nil return uc, nil
} }
// readLoop is spawned after we connect. It receives messages from the
// socket, and routes them to the appropriate channel based on whether they
// are solicited (in response to a request) or unsolicited.
func (uc *unixgramConn) readLoop() { func (uc *unixgramConn) readLoop() {
for { for {
// The syscall below will block until a datagram is received.
// It uses a zero-length buffer to look at the datagram
// without discarding it (MSG_PEEK), returning the actual
// datagram size (MSG_TRUNC). See the recvfrom(2) man page.
//
// The actual read occurs using UnixConn.Read(), once we've
// allocated an appropriately-sized buffer.
n, _, err := syscall.Recvfrom(int(uc.fd), []byte{}, syscall.MSG_PEEK|syscall.MSG_TRUNC) n, _, err := syscall.Recvfrom(int(uc.fd), []byte{}, syscall.MSG_PEEK|syscall.MSG_TRUNC)
if err != nil { if err != nil {
// Treat read errors as a response to whatever command
// was last issued.
uc.solicited <- message{
err: err,
}
continue continue
} }
@ -70,8 +129,13 @@ func (uc *unixgramConn) readLoop() {
uc.solicited <- message{ uc.solicited <- message{
err: err, err: err,
} }
continue
} }
// Unsolicited messages are preceded by a priority
// specification, e.g. "<1>message". If there's no priority,
// default to 2 (info) and assume it's the response to
// whatever command was last issued.
var p int var p int
var c chan message var c chan message
if len(buf) >= 3 && buf[0] == '<' && buf[2] == '>' { if len(buf) >= 3 && buf[0] == '<' && buf[2] == '>' {
@ -96,6 +160,7 @@ func (uc *unixgramConn) readLoop() {
} }
} }
// cmd executes a command and waits for a reply.
func (uc *unixgramConn) cmd(cmd string) ([]byte, error) { func (uc *unixgramConn) cmd(cmd string) ([]byte, error) {
// TODO: block if any other commands are running // TODO: block if any other commands are running
@ -108,6 +173,32 @@ func (uc *unixgramConn) cmd(cmd string) ([]byte, error) {
return msg.data, msg.err return msg.data, msg.err
} }
// ParseError is returned when we can't parse the wpa_supplicant response.
// Some functions may return multiple ParseErrors.
type ParseError struct {
// Line is the line of output from wpa_supplicant which we couldn't
// parse.
Line string
// Err is any nested error.
Err error
}
func (err *ParseError) Error() string {
b := &bytes.Buffer{}
b.WriteString("failed to parse wpa_supplicant response")
if err.Line != "" {
fmt.Fprintf(b, ": %q", err.Line)
}
if err.Err != nil {
fmt.Fprintf(b, ": %s", err.Err.Error())
}
return b.String()
}
func (uc *unixgramConn) Ping() error { func (uc *unixgramConn) Ping() error {
resp, err := uc.cmd("PING") resp, err := uc.cmd("PING")
if err != nil { if err != nil {
@ -117,20 +208,30 @@ func (uc *unixgramConn) Ping() error {
if bytes.Compare(resp, []byte("PONG\n")) == 0 { if bytes.Compare(resp, []byte("PONG\n")) == 0 {
return nil return nil
} }
return fmt.Errorf("expected %q, got %q", "PONG", resp) return &ParseError{Line: string(resp)}
} }
func (uc *unixgramConn) ScanResults() ([]ScanResult, error) { func (uc *unixgramConn) ScanResults() ([]ScanResult, []error) {
resp, err := uc.cmd("SCAN_RESULTS") resp, err := uc.cmd("SCAN_RESULTS")
if err != nil { if err != nil {
return nil, err return nil, []error{err}
} }
s := bufio.NewScanner(bytes.NewBuffer(resp)) return parseScanResults(bytes.NewBuffer(resp))
if !s.Scan() {
return nil, errors.New("failed to parse scan results")
} }
var bssidCol, freqCol, rssiCol, flagsCol, ssidCol, maxCol int
// parseScanResults parses the SCAN_RESULTS output from wpa_supplicant. This
// is split out from ScanResults() to make testing easier.
func parseScanResults(resp io.Reader) (res []ScanResult, errs []error) {
// In an attempt to make our parser more resilient, we start by
// parsing the header line and using that to determine the column
// order.
s := bufio.NewScanner(resp)
if !s.Scan() {
errs = append(errs, &ParseError{})
return
}
bssidCol, freqCol, rssiCol, flagsCol, ssidCol, maxCol := -1, -1, -1, -1, -1, -1
for n, col := range strings.Split(s.Text(), " / ") { for n, col := range strings.Split(s.Text(), " / ") {
switch col { switch col {
case "bssid": case "bssid":
@ -147,31 +248,49 @@ func (uc *unixgramConn) ScanResults() ([]ScanResult, error) {
maxCol = n maxCol = n
} }
var res []ScanResult var err error
for s.Scan() { for s.Scan() {
fields := strings.Split(s.Text(), "\t") ln := s.Text()
fields := strings.Split(ln, "\t")
if len(fields) < maxCol { if len(fields) < maxCol {
continue // TODO: log error errs = append(errs, &ParseError{Line: ln})
continue
} }
bssid, err := net.ParseMAC(fields[bssidCol]) var bssid net.HardwareAddr
if err != nil { if bssidCol != -1 {
continue // TODO: log error if bssid, err = net.ParseMAC(fields[bssidCol]); err != nil {
errs = append(errs, &ParseError{Line: ln, Err: err})
continue
}
} }
freq, err := strconv.Atoi(fields[freqCol]) var freq int
if err != nil { if freqCol != -1 {
continue // TODO: log error if freq, err = strconv.Atoi(fields[freqCol]); err != nil {
errs = append(errs, &ParseError{Line: ln, Err: err})
continue
}
} }
rssi, err := strconv.Atoi(fields[rssiCol]) var rssi int
if err != nil { if rssiCol != -1 {
continue // TODO: log error if rssi, err = strconv.Atoi(fields[rssiCol]); err != nil {
errs = append(errs, &ParseError{Line: ln, Err: err})
continue
}
} }
var flags []string var flags []string
if len(fields[flagsCol]) >= 2 && fields[flagsCol][0] != '[' && fields[flagsCol][len(fields[flagsCol])-1] != ']' { if flagsCol != -1 {
flags = strings.Split(fields[flagsCol][1:len(fields[flagsCol])-2], "][") if len(fields[flagsCol]) >= 2 && fields[flagsCol][0] == '[' && fields[flagsCol][len(fields[flagsCol])-1] == ']' {
flags = strings.Split(fields[flagsCol][1:len(fields[flagsCol])-1], "][")
}
}
var ssid string
if ssidCol != -1 {
ssid = fields[ssidCol]
} }
res = append(res, &scanResult{ res = append(res, &scanResult{
@ -179,9 +298,9 @@ func (uc *unixgramConn) ScanResults() ([]ScanResult, error) {
frequency: freq, frequency: freq,
rssi: rssi, rssi: rssi,
flags: flags, flags: flags,
ssid: fields[ssidCol], ssid: ssid,
}) })
} }
return res, nil return
} }

133
unixgram_test.go Normal file
View File

@ -0,0 +1,133 @@
// Copyright (c) 2017 Dave Pifke.
//
// Redistribution and use in source and binary forms, with or without
// modification, is permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
package wpasupplicant
import (
"bytes"
"net"
"testing"
)
var parseScanResultTests = []struct {
input string
expect []*scanResult
}{
{
// actual output from wpa_supplicant 2.4-0ubuntu6
input: "bssid / frequency / signal level / flags / ssid\n" +
"8a:15:14:8a:46:51\t5560\t-58\t[WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][ESS]\tWIP-Backoffice\n" +
"8a:15:14:8a:46:50\t5560\t-58\t[WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][ESS]\tWorkInProgressMember\n",
expect: []*scanResult{
&scanResult{
bssid: net.HardwareAddr{0x8a, 0x15, 0x14, 0x8a, 0x46, 0x51},
frequency: 5560,
rssi: -58,
flags: []string{"WPA-PSK-CCMP+TKIP", "WPA2-PSK-CCMP+TKIP", "ESS"},
ssid: "WIP-Backoffice",
},
&scanResult{
bssid: net.HardwareAddr{0x8a, 0x15, 0x14, 0x8a, 0x46, 0x50},
frequency: 5560,
rssi: -58,
flags: []string{"WPA-PSK-CCMP+TKIP", "WPA2-PSK-CCMP+TKIP", "ESS"},
ssid: "WorkInProgressMember",
},
},
}, {
// reordered/added/missing columns from some theoretical
// future version of wpa_supplicant
input: "frequency / bssid / foobar / ssid\n" +
"5560\t8a:15:14:8a:46:51\thello\tWIP-Backoffice\n" +
"5560\t8a:15:14:8a:46:50\tgoodbye\tWorkInProgressMember\n",
expect: []*scanResult{
&scanResult{
bssid: net.HardwareAddr{0x8a, 0x15, 0x14, 0x8a, 0x46, 0x51},
frequency: 5560,
ssid: "WIP-Backoffice",
},
&scanResult{
bssid: net.HardwareAddr{0x8a, 0x15, 0x14, 0x8a, 0x46, 0x50},
frequency: 5560,
ssid: "WorkInProgressMember",
},
},
},
}
func TestParseScanResults(t *testing.T) {
for _, test := range parseScanResultTests {
output, errs := parseScanResults(bytes.NewBufferString(test.input))
if len(errs) > 0 {
t.Error("errors parsing scan results")
}
if len(output) != len(test.expect) {
t.Errorf("wrong number of results (got %d, expect %d)", len(output), len(test.expect))
}
for i := range output {
if test.expect[i].bssid != nil {
if bytes.Compare(output[i].BSSID(), test.expect[i].bssid) != 0 {
t.Errorf("wrong bssid (got %q, expect %q)", output[i].BSSID(), test.expect[i].bssid)
}
}
if test.expect[i].frequency != 0 {
if output[i].Frequency() != test.expect[i].frequency {
t.Errorf("wrong frequency (got %d, expect %d)", output[i].Frequency(), test.expect[i].frequency)
}
}
if test.expect[i].rssi != 0 {
if output[i].RSSI() != test.expect[i].rssi {
t.Errorf("wrong rssi (got %d, expect %d)", output[i].RSSI(), test.expect[i].rssi)
}
}
if test.expect[i].ssid != "" {
if output[i].SSID() != test.expect[i].ssid {
t.Errorf("wrong rssi (got %s, expect %s)", output[i].SSID(), test.expect[i].ssid)
}
}
flags := output[i].Flags()
flagsMatch := true
if len(test.expect[i].flags) != len(flags) {
flagsMatch = false
} else {
for j := range test.expect[i].flags {
if flags[j] != test.expect[i].flags[j] {
flagsMatch = false
break
}
}
}
if !flagsMatch {
t.Errorf("got flags %q, expected %q", flags, test.expect[i].flags)
}
}
}
}

View File

@ -1,9 +1,43 @@
// Copyright (c) 2017 Dave Pifke.
//
// Redistribution and use in source and binary forms, with or without
// modification, is permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
// Package wpasupplicant provides an interface for talking to the
// wpa_supplicant daemon.
//
// At the moment, this simply provides an interface for fetching wifi scan
// results. More functionality is (probably) coming soon.
package wpasupplicant package wpasupplicant
import ( import (
"net" "net"
) )
// Cipher is one of the WPA_CIPHER constants from the wpa_supplicant source.
type Cipher int type Cipher int
const ( const (
@ -24,6 +58,8 @@ const (
GTK_NOT_USED GTK_NOT_USED
) )
// KeyMgmt is one of the WPA_KEY_MGMT constants from the wpa_supplicant
// source.
type KeyMgmt int type KeyMgmt int
const ( const (
@ -49,14 +85,27 @@ const (
type Algorithm int type Algorithm int
// ScanResult is a scanned BSS.
type ScanResult interface { type ScanResult interface {
// BSSID is the MAC address of the BSS.
BSSID() net.HardwareAddr BSSID() net.HardwareAddr
// SSID is the SSID of the BSS.
SSID() string SSID() string
// Frequency is the frequency, in Mhz, of the BSS.
Frequency() int Frequency() int
// RSSI is the received signal strength, in dB, of the BSS.
RSSI() int RSSI() int
// Flags is an array of flags, in string format, returned by the
// wpa_supplicant SCAN_RESULTS command. Future versions of this code
// will parse these into something more meaningful.
Flags() []string Flags() []string
} }
// scanResult is a package-private implementation of ScanResult.
type scanResult struct { type scanResult struct {
bssid net.HardwareAddr bssid net.HardwareAddr
ssid string ssid string
@ -71,8 +120,15 @@ func (r *scanResult) Frequency() int { return r.frequency }
func (r *scanResult) RSSI() int { return r.rssi } func (r *scanResult) RSSI() int { return r.rssi }
func (r *scanResult) Flags() []string { return r.flags } func (r *scanResult) Flags() []string { return r.flags }
// Conn is a connection to wpa_supplicant over one of its communication
// channels.
type Conn interface { type Conn interface {
// Ping tests the connection. It returns nil if wpa_supplicant is
// responding.
Ping() error Ping() error
ScanResults() ([]ScanResult, error) // ScanResult returns the latest scanning results. It returns a slice
// of scanned BSSs, and/or a slice of errors representing problems
// communicating with wpa_supplicant or parsing its output.
ScanResults() ([]ScanResult, []error)
} }