| // Copyright 2021 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package wifi |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/binary" |
| "encoding/hex" |
| "fmt" |
| "io" |
| "net" |
| "strconv" |
| "time" |
| |
| "github.com/golang/protobuf/ptypes/empty" |
| "github.com/google/gopacket/layers" |
| "go.chromium.org/tast-tests/cros/common/tbdep" |
| |
| tdreq "go.chromium.org/tast-tests/cros/common/testdevicerequirements" |
| "go.chromium.org/tast-tests/cros/common/wifi/ieee80211" |
| "go.chromium.org/tast-tests/cros/common/wifi/security/wpa" |
| "go.chromium.org/tast-tests/cros/common/wifi/wpacli" |
| "go.chromium.org/tast-tests/cros/remote/bundles/cros/wifi/wifiutil" |
| "go.chromium.org/tast-tests/cros/remote/wificell" |
| "go.chromium.org/tast-tests/cros/remote/wificell/hostapd" |
| "go.chromium.org/tast-tests/cros/remote/wificell/pcap" |
| "go.chromium.org/tast-tests/cros/services/cros/wifi" |
| "go.chromium.org/tast/core/ctxutil" |
| "go.chromium.org/tast/core/errors" |
| "go.chromium.org/tast/core/testing" |
| ) |
| |
| func init() { |
| testing.AddTest(&testing.Test{ |
| Func: RRMBeaconReport, |
| Desc: "Verifies that the DUT responds properly to beacon report requests", |
| Contacts: []string{ |
| "chromeos-wifi-champs@google.com", // WiFi oncall rotation |
| "matthewmwang@chromium.org", // Test author |
| }, |
| BugComponent: "b:893827", // ChromeOS > Platform > Connectivity > WiFi |
| Attr: []string{"group:wificell", "wificell_func", "wificell_unstable"}, |
| TestBedDeps: []string{tbdep.Wificell, tbdep.WifiStateNormal, tbdep.BluetoothStateNormal, tbdep.PeripheralWifiStateWorking}, |
| ServiceDeps: []string{wificell.ShillServiceName}, |
| Fixture: wificell.FixtureID(wificell.TFFeaturesRouters), |
| Requirements: []string{tdreq.WiFiGenSupportMBO}, |
| VariantCategory: `{"name": "WifiBtChipset_Soc_Kernel"}`, |
| }) |
| } |
| |
| func RRMBeaconReport(ctx context.Context, s *testing.State) { |
| /* |
| In this test, we verify that a DUT responds properly to beacon report |
| requests from an AP. Beacon requests are part of the 802.11k Radio |
| Resource Management (RRM) standard and are a way for the AP to |
| ascertain information about the DUT's environment. In particular, a |
| beacon request will ask a DUT about APs that it sees on specified |
| SSIDs and/or channels and a DUT will either scan or use cached scan |
| results to fulfill that request. |
| |
| This test sets up 3 distinct SSIDs on the 2.4GHz phy of the router AP, |
| 3 duplicate SSIDs on the 2.4GHz phy of the pcap AP (i.e. there are 3 |
| separate networks, and each network has 2 APs). It then connects to |
| the first AP on the first SSID and sends a series of beacon requests. |
| We collect a pcap for each of the responses and compare it to the |
| expected response. |
| */ |
| type reportBSS struct { |
| BSSID net.HardwareAddr |
| Channel uint8 |
| IEs []layers.Dot11InformationElementID |
| } |
| type actionHeader struct { |
| Category uint8 |
| _ [2]uint8 |
| } |
| type beaconRepHeader struct { |
| _ uint8 |
| Len uint8 |
| } |
| type beaconRepInfo struct { |
| _ [4]uint8 |
| Channel uint8 |
| _ [13]uint8 |
| BSSID [6]uint8 |
| _ [5]uint8 |
| } |
| type elemHeader struct { |
| ID uint8 |
| Len uint8 |
| } |
| const ( |
| RRMCategoryCode = 0x05 |
| extraFirstReportBytes = 12 |
| beaconReqScanTimeout = 10 * time.Second |
| ) |
| var ( |
| bcastAddr = []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} |
| testIEsNoMD = []layers.Dot11InformationElementID{ |
| layers.Dot11InformationElementIDSSID, |
| layers.Dot11InformationElementIDVendor, |
| } |
| testIEs = append(testIEsNoMD, layers.Dot11InformationElementIDMobilityDomain) |
| beaconRepInfoSz = binary.Size(beaconRepInfo{}) |
| ) |
| tf := s.FixtValue().(*wificell.TestFixture) |
| ctx, restoreBgAndFg, err := tf.WifiClient().TurnOffBgAndFgscan(ctx) |
| if err != nil { |
| s.Fatal("Failed to turn off the background and/or foreground scan: ", err) |
| } |
| defer func() { |
| if err := restoreBgAndFg(); err != nil { |
| s.Error("Failed to restore the background and/or foreground scan config: ", err) |
| } |
| }() |
| |
| // The MBO certification test plan specified that we set up the AP with |
| // FT. Note that we don't actually test the FT feature here, other than |
| // the initial connection. |
| ftResp, err := tf.WifiClient().GetGlobalFTProperty(ctx, &empty.Empty{}) |
| if err != nil { |
| s.Fatal("Failed to get the global FT property: ", err) |
| } |
| defer func(ctx context.Context) { |
| if _, err := tf.WifiClient().SetGlobalFTProperty(ctx, &wifi.SetGlobalFTPropertyRequest{Enabled: ftResp.Enabled}); err != nil { |
| s.Errorf("Failed to set global FT property back to %v: %v", ftResp.Enabled, err) |
| } |
| }(ctx) |
| ctx, cancel := ctxutil.Shorten(ctx, time.Second) |
| defer cancel() |
| |
| if _, err := tf.WifiClient().SetGlobalFTProperty(ctx, &wifi.SetGlobalFTPropertyRequest{Enabled: true}); err != nil { |
| s.Fatal("Failed to turn on the global FT property: ", err) |
| } |
| |
| // Generate 6 different MACs for the 6 different APs |
| var macs [6]net.HardwareAddr |
| for i := 0; i < len(macs); i++ { |
| mac, err := hostapd.RandomMAC() |
| if err != nil { |
| s.Fatal("Failed to get a random mac address: ", err) |
| } |
| macs[i] = mac |
| } |
| var ( |
| id0 = hex.EncodeToString(macs[0]) |
| id1 = hex.EncodeToString(macs[3]) |
| ) |
| const ( |
| key0 = "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100" |
| key1 = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" |
| mdID = "a1b2" |
| ) |
| |
| secConfFac := wpa.NewConfigFactory("chromeos", wpa.Mode(wpa.ModePureWPA2), wpa.Ciphers2(wpa.CipherCCMP), wpa.FTMode(wpa.FTModeMixed)) |
| var ssids [3]string |
| for i := 0; i < len(ssids); i++ { |
| ssids[i] = hostapd.RandomSSID("TAST_BEACON_REP_") |
| } |
| |
| // Note that we choose 2.4GHz channels here because several of our |
| // devices are "no IR" for all 5GHz channels |
| ap0Ops := []hostapd.Option{ |
| hostapd.Channel(6), hostapd.Mode(hostapd.Mode80211acMixed), hostapd.BSSID(macs[0].String()), |
| hostapd.MobilityDomain(mdID), hostapd.NASIdentifier(id0), hostapd.R1KeyHolder(id0), |
| hostapd.R0KHs(fmt.Sprintf("%s %s %s", macs[3], id1, key0)), |
| hostapd.R1KHs(fmt.Sprintf("%s %s %s", macs[3], macs[3], key1)), |
| hostapd.SSID(ssids[0]), hostapd.HTCaps(hostapd.HTCapHT40), hostapd.VHTChWidth(hostapd.VHTChWidth20Or40), |
| hostapd.RRMBeaconReport(), |
| hostapd.AdditionalBSSs( |
| hostapd.AdditionalBSS{IfaceName: "beaconRepDev1", SSID: ssids[1], BSSID: macs[1].String()}, |
| hostapd.AdditionalBSS{IfaceName: "beaconRepDev2", SSID: ssids[2], BSSID: macs[2].String()}, |
| ), |
| } |
| ap1Ops := []hostapd.Option{ |
| hostapd.Channel(1), hostapd.Mode(hostapd.Mode80211acMixed), hostapd.BSSID(macs[3].String()), |
| hostapd.MobilityDomain(mdID), hostapd.NASIdentifier(id1), hostapd.R1KeyHolder(id1), |
| hostapd.R0KHs(fmt.Sprintf("%s %s %s", macs[0], id0, key1)), |
| hostapd.R1KHs(fmt.Sprintf("%s %s %s", macs[0], macs[0], key0)), |
| hostapd.SSID(ssids[0]), hostapd.HTCaps(hostapd.HTCapHT40), hostapd.VHTChWidth(hostapd.VHTChWidth20Or40), |
| hostapd.RRMBeaconReport(), |
| hostapd.AdditionalBSSs( |
| hostapd.AdditionalBSS{IfaceName: "beaconRepDev3", SSID: ssids[1], BSSID: macs[4].String()}, |
| hostapd.AdditionalBSS{IfaceName: "beaconRepDev4", SSID: ssids[2], BSSID: macs[5].String()}, |
| ), |
| } |
| |
| s.Log("Starting the first AP") |
| ap0, _, deconfig0 := wifiutil.ConfigureAP(ctx, s, ap0Ops, 0, secConfFac) |
| defer func(ctx context.Context) { |
| if ap0 != nil { |
| deconfig0(ctx, ap0) |
| ap0 = nil |
| } |
| }(ctx) |
| ctx, cancel = tf.ReserveForDeconfigAP(ctx, ap0) |
| defer cancel() |
| ap0Chan := uint8(ap0.Config().Channel) |
| |
| disconnect := wifiutil.ConnectAP(ctx, s, ap0, 0) |
| defer disconnect(ctx) |
| ctx, cancel = tf.ReserveForDisconnect(ctx) |
| defer cancel() |
| |
| if err := tf.VerifyConnection(ctx, ap0); err != nil { |
| s.Fatal("Failed to verify connection: ", err) |
| } |
| |
| s.Log("Starting the second AP") |
| ap1, _, deconfig1 := wifiutil.ConfigureAP(ctx, s, ap1Ops, 1, secConfFac) |
| defer func(ctx context.Context) { |
| if ap1 != nil { |
| deconfig1(ctx, ap1) |
| ap1 = nil |
| } |
| }(ctx) |
| ctx, cancel = tf.ReserveForDeconfigAP(ctx, ap1) |
| defer cancel() |
| ap1Chan := uint8(ap1.Config().Channel) |
| |
| router, err := tf.StandardRouter() |
| if err != nil { |
| s.Fatal("Unable to get legacy router: ", err) |
| } |
| freqOpts, err := ap0.Config().PcapFreqOptions() |
| if err != nil { |
| s.Fatal("Failed to get pcap frequency options: ", err) |
| } |
| |
| clientMAC, err := tf.ClientHardwareAddr(ctx) |
| if err != nil { |
| s.Fatal("Unable to get DUT MAC address: ", err) |
| } |
| clientMACBytes, err := net.ParseMAC(clientMAC) |
| if err != nil { |
| s.Fatal("Unable to parse MAC address: ", err) |
| } |
| runOnce := func(ctx context.Context, params hostapd.BeaconReqParams, expected []reportBSS, name string) error { |
| SendBeaconRequest := func(ctx context.Context) error { |
| var wpaMonitor *wpacli.WPAMonitor |
| if params.Mode != hostapd.ModeTable { |
| var stop func() |
| wpaMonitor, stop, ctx, err = tf.StartWPAMonitor(ctx, wificell.DefaultDUT) |
| if err != nil { |
| return errors.Wrap(err, "failed to start wpa monitor") |
| } |
| defer stop() |
| } |
| if err := ap0.SendBeaconRequest(ctx, clientMAC, params); err != nil { |
| return errors.Wrap(err, "failed to send beacon request") |
| } |
| if wpaMonitor != nil { |
| if err := testing.Poll(ctx, func(ctx context.Context) error { |
| event, err := wpaMonitor.WaitForEvent(ctx) |
| if err != nil { |
| return testing.PollBreak(errors.Wrap(err, "failed to wait for scan event")) |
| } |
| if event == nil { |
| return testing.PollBreak(errors.New("timed out waiting for scan event")) |
| } |
| if _, scanSuccess := event.(*wpacli.ScanResultsEvent); scanSuccess { |
| return nil |
| } |
| return errors.New("no scan event found") |
| }, &testing.PollOptions{Timeout: beaconReqScanTimeout}); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| pcapPath, err := wifiutil.CollectPcapForAction(ctx, router, name, int(ap0Chan), false /*is6GHz*/, freqOpts, SendBeaconRequest) |
| if err != nil { |
| return err |
| } |
| |
| filters := []pcap.Filter{ |
| pcap.Dot11FCSValid(), |
| pcap.TransmitterAddress(clientMACBytes), |
| pcap.TypeFilter(layers.LayerTypeDot11MgmtAction, nil), |
| } |
| packets, err := pcap.ReadPackets(pcapPath, filters...) |
| if err != nil { |
| return err |
| } |
| foundRRM := false |
| for _, p := range packets { |
| layer := p.Layer(layers.LayerTypeDot11MgmtAction) |
| if layer == nil { |
| return errors.New("found packet without Action layer") |
| } |
| actual := make(map[string]reportBSS) |
| action := layer.(*layers.Dot11MgmtAction) |
| var header actionHeader |
| r := bytes.NewReader(action.Contents) |
| if err := binary.Read(r, binary.LittleEndian, &header); err != nil { |
| return err |
| } |
| if header.Category != RRMCategoryCode { |
| continue |
| } |
| foundRRM = true |
| lastReport := false |
| // The packet should contain a list of reports, each of |
| // which corresponds to a specific BSSID. Each report |
| // will contain a list of subelements. The information |
| // subelement will contain a list of IEs. Do a triply- |
| // nested loop here to parse out the IEs for each AP. |
| for r.Len() > 0 { |
| if lastReport { |
| return errors.New("last report indication true for non-last report") |
| } |
| var beaconHdr beaconRepHeader |
| var beaconInfo beaconRepInfo |
| if err := binary.Read(r, binary.LittleEndian, &beaconHdr); err != nil { |
| return err |
| } |
| if err := binary.Read(r, binary.LittleEndian, &beaconInfo); err != nil { |
| return err |
| } |
| //reportLen := action.Contents[i+1] |
| bssid := net.HardwareAddr(beaconInfo.BSSID[:]) |
| |
| // A report for a BSSID can be fragmented. Check |
| // the map first in case we've already seen the |
| // BSSID |
| bss, reportExists := actual[bssid.String()] |
| if !reportExists { |
| bss.Channel = beaconInfo.Channel |
| bss.BSSID = bssid |
| } |
| subelems := make([]byte, int(beaconHdr.Len)-beaconRepInfoSz) |
| if _, err := io.ReadFull(r, subelems); err != nil { |
| return err |
| } |
| subelemR := bytes.NewReader(subelems) |
| for subelemR.Len() > 0 { |
| var subelemHeader elemHeader |
| if err := binary.Read(subelemR, binary.LittleEndian, &subelemHeader); err != nil { |
| return err |
| } |
| if subelemHeader.ID == uint8(hostapd.SubelemInfo) { |
| iesLen := int(subelemHeader.Len) |
| if !reportExists { |
| // The first report of each BSSID includes 12 extra bytes of information before the list of IEs. Skip them. |
| if _, err := subelemR.Seek(extraFirstReportBytes, io.SeekCurrent); err != nil { |
| return err |
| } |
| iesLen -= extraFirstReportBytes |
| } |
| ies := make([]byte, iesLen) |
| if _, err := io.ReadFull(subelemR, ies); err != nil { |
| return err |
| } |
| ieR := bytes.NewReader(ies) |
| // Append the IEs. |
| for ieR.Len() > 0 { |
| var ieHeader elemHeader |
| if err := binary.Read(ieR, binary.LittleEndian, &ieHeader); err != nil { |
| return err |
| } |
| bss.IEs = append(bss.IEs, layers.Dot11InformationElementID(ieHeader.ID)) |
| if _, err := ieR.Seek(int64(ieHeader.Len), io.SeekCurrent); err != nil { |
| return err |
| } |
| } |
| } else if subelemHeader.ID == uint8(hostapd.SubelemLastIndication) { |
| // Only the last report should have this subelement if we've requested it. |
| if !params.LastFrame { |
| return errors.New("last indication reported but not requested") |
| } |
| var isLast uint8 |
| if err := binary.Read(subelemR, binary.LittleEndian, &isLast); err != nil { |
| return err |
| } |
| lastReport = isLast == 1 |
| } else { |
| if _, err := subelemR.Seek(int64(subelemHeader.Len), io.SeekCurrent); err != nil { |
| return err |
| } |
| } |
| } |
| actual[bss.BSSID.String()] = bss |
| } |
| // Expect an exact match in BSSID entries except when we request all IEs in the reporting detail. |
| if len(expected) != len(actual) { |
| return errors.Errorf("Number of BSS entries doesn't match. Got %v, want %v", len(actual), len(expected)) |
| } |
| for _, expectedBSS := range expected { |
| actualBSS, ok := actual[expectedBSS.BSSID.String()] |
| if !ok { |
| return errors.Errorf("BSSID %s not found in report", expectedBSS.BSSID) |
| } |
| if !bytes.Equal(actualBSS.BSSID, expectedBSS.BSSID) { |
| return errors.Errorf("BSSIDs did not match. Got %s, want %s", actualBSS.BSSID, expectedBSS.BSSID) |
| } |
| if actualBSS.Channel != expectedBSS.Channel { |
| return errors.Errorf("Channel for BSSID %s did not match. Got %v, want %v", actualBSS.BSSID, actualBSS.Channel, expectedBSS.Channel) |
| } |
| actualIEs := make(map[layers.Dot11InformationElementID]struct{}) |
| for _, ie := range actualBSS.IEs { |
| actualIEs[ie] = struct{}{} |
| } |
| if params.ReportingDetail == hostapd.DetailAllFields && len(actualIEs) == 0 { |
| return errors.New("Requested all IEs but got none") |
| } |
| if params.ReportingDetail != hostapd.DetailAllFields && len(expectedBSS.IEs) != len(actualIEs) { |
| return errors.Errorf("Number of IEs doesn't match. Got %v, want %v", len(actualIEs), len(expectedBSS.IEs)) |
| } |
| for _, ie := range expectedBSS.IEs { |
| if _, ok := actualIEs[ie]; !ok { |
| return errors.Errorf("IEs for BSSID %s did not include all expected IEs. Got %v, want %v", actualBSS.BSSID, actualBSS.IEs, expectedBSS.IEs) |
| } |
| } |
| } |
| } |
| if !foundRRM { |
| return errors.New("no RRM found") |
| } |
| return nil |
| } |
| testcases := []struct { |
| Request hostapd.BeaconReqParams |
| Report []reportBSS |
| }{{ |
| // Scan for all channels with SSID 0, and include specified IEs. |
| // Expect two entries corresponding to the two APs on SSID 0 |
| // with the IEs requested. |
| hostapd.BeaconReqParams{ |
| OpClass: ieee80211.OpClass2GHz, |
| Channel: 0, |
| Duration: 20, |
| Mode: hostapd.ModeActive, |
| SSID: ssids[0], |
| BSSID: bcastAddr, |
| ReportingDetail: hostapd.DetailRequestedOnly, |
| Request: testIEs, |
| }, |
| []reportBSS{{ |
| BSSID: macs[0], |
| Channel: ap0Chan, |
| IEs: testIEs, |
| }, { |
| BSSID: macs[3], |
| Channel: ap1Chan, |
| IEs: testIEs, |
| }, |
| }, |
| }, { |
| // Scan for both channels in the report channels element. |
| // Expect all 6 APs to show up with all requested IEs for the |
| // two APs that support FT, and all requested IEs less the |
| // Mobility Domain IE for the APs that don't. |
| hostapd.BeaconReqParams{ |
| OpClass: ieee80211.OpClass2GHz, |
| Channel: 255, |
| Duration: 50, |
| Mode: hostapd.ModeActive, |
| BSSID: bcastAddr, |
| ReportingDetail: hostapd.DetailRequestedOnly, |
| ReportChannels: []byte{ap0Chan, ap1Chan}, |
| Request: testIEs, |
| LastFrame: true, |
| }, |
| []reportBSS{{ |
| BSSID: macs[0], |
| Channel: ap0Chan, |
| IEs: testIEs, |
| }, { |
| BSSID: macs[1], |
| Channel: ap0Chan, |
| IEs: testIEsNoMD, |
| }, { |
| BSSID: macs[2], |
| Channel: ap0Chan, |
| IEs: testIEsNoMD, |
| }, { |
| BSSID: macs[3], |
| Channel: ap1Chan, |
| IEs: testIEs, |
| }, { |
| BSSID: macs[4], |
| Channel: ap1Chan, |
| IEs: testIEsNoMD, |
| }, { |
| BSSID: macs[5], |
| Channel: ap1Chan, |
| IEs: testIEsNoMD, |
| }, |
| }, |
| }, { |
| // Use cached scan results to get APs on SSID 0 without any IEs. |
| // Expect the two APs on SSID 0 with no IEs included. |
| hostapd.BeaconReqParams{ |
| OpClass: ieee80211.OpClass2GHz, |
| Channel: 0, |
| Duration: 20, |
| Mode: hostapd.ModeTable, |
| SSID: ssids[0], |
| BSSID: bcastAddr, |
| ReportingDetail: hostapd.DetailNone, |
| LastFrame: true, |
| }, |
| []reportBSS{{ |
| BSSID: macs[0], |
| Channel: ap0Chan, |
| }, { |
| BSSID: macs[3], |
| Channel: ap1Chan, |
| }, |
| }, |
| }, { |
| // Passive scan on the second router's channel and include all IEs. |
| // Expect the three APs on the second router with all IEs. |
| hostapd.BeaconReqParams{ |
| OpClass: ieee80211.OpClass2GHz, |
| Channel: ap1Chan, |
| Duration: 112, |
| Mode: hostapd.ModePassive, |
| BSSID: bcastAddr, |
| ReportingDetail: hostapd.DetailAllFields, |
| LastFrame: true, |
| }, |
| []reportBSS{{ |
| BSSID: macs[3], |
| Channel: ap1Chan, |
| }, { |
| BSSID: macs[4], |
| Channel: ap1Chan, |
| }, { |
| BSSID: macs[5], |
| Channel: ap1Chan, |
| }, |
| }, |
| }, { |
| // Scan on the second router's channel for a specific BSSID with specific IEs. |
| // Expect the AP with that BSSID with the specified IEs. |
| hostapd.BeaconReqParams{ |
| OpClass: ieee80211.OpClass2GHz, |
| Channel: ap1Chan, |
| Duration: 20, |
| Mode: hostapd.ModeActive, |
| BSSID: macs[3], |
| ReportingDetail: hostapd.DetailRequestedOnly, |
| Request: testIEs, |
| LastFrame: true, |
| }, |
| []reportBSS{{ |
| BSSID: macs[3], |
| Channel: ap1Chan, |
| IEs: testIEs, |
| }, |
| }, |
| }, { |
| // Scan on the two specified channels for SSID 0 with the specified IEs. |
| // Expect the two APs on SSID 0 with the specified IEs. |
| hostapd.BeaconReqParams{ |
| OpClass: ieee80211.OpClass2GHz, |
| Channel: 255, |
| Duration: 50, |
| Mode: hostapd.ModeActive, |
| SSID: ssids[0], |
| BSSID: bcastAddr, |
| ReportingDetail: hostapd.DetailRequestedOnly, |
| ReportChannels: []byte{ap0Chan, ap1Chan}, |
| Request: testIEs, |
| LastFrame: true, |
| }, |
| []reportBSS{{ |
| BSSID: macs[0], |
| Channel: ap0Chan, |
| IEs: testIEs, |
| }, { |
| BSSID: macs[3], |
| Channel: ap1Chan, |
| IEs: testIEs, |
| }, |
| }, |
| }} |
| for i, tc := range testcases { |
| s.Log("Running test case: ", i) |
| if err = runOnce(ctx, tc.Request, tc.Report, strconv.Itoa(i)); err != nil { |
| s.Fatalf("Run %d failed: %v", i, err) |
| } |
| } |
| } |