| // Copyright 2020 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package wifi |
| |
| import ( |
| "context" |
| "encoding/hex" |
| "fmt" |
| "net" |
| "strconv" |
| "time" |
| |
| "github.com/golang/protobuf/ptypes/empty" |
| |
| "chromiumos/tast/common/crypto/certificate" |
| "chromiumos/tast/common/shillconst" |
| "chromiumos/tast/common/wifi/security" |
| "chromiumos/tast/common/wifi/security/wpa" |
| "chromiumos/tast/common/wifi/security/wpaeap" |
| "chromiumos/tast/ctxutil" |
| "chromiumos/tast/remote/network/iw" |
| "chromiumos/tast/remote/wificell" |
| "chromiumos/tast/remote/wificell/dutcfg" |
| "chromiumos/tast/remote/wificell/hostapd" |
| "chromiumos/tast/services/cros/wifi" |
| "chromiumos/tast/testing" |
| ) |
| |
| type roamFTparam struct { |
| secConfFac security.ConfigFactory |
| mixed bool |
| } |
| |
| var ( |
| roamFTCert1 = certificate.TestCert1() |
| roamFTCert2 = certificate.TestCert2() |
| ) |
| |
| func init() { |
| testing.AddTest(&testing.Test{ |
| Func: RoamFT, |
| Desc: "Verifies that DUT can roam with FT auth suites", |
| Contacts: []string{ |
| "chharry@google.com", // Test author |
| "chromeos-wifi-champs@google.com", // WiFi oncall rotation; or http://b/new?component=893827 |
| }, |
| Attr: []string{"group:wificell", "wificell_func"}, |
| ServiceDeps: []string{wificell.TFServiceName}, |
| Fixture: "wificellFixt", |
| Params: []testing.Param{{ |
| Name: "psk", |
| Val: roamFTparam{ |
| secConfFac: wpa.NewConfigFactory("chromeos", wpa.Mode(wpa.ModePureWPA2), wpa.Ciphers2(wpa.CipherCCMP), wpa.FTMode(wpa.FTModePure)), |
| }, |
| }, { |
| Name: "mixed_psk", |
| Val: roamFTparam{ |
| secConfFac: wpa.NewConfigFactory("chromeos", wpa.Mode(wpa.ModePureWPA2), wpa.Ciphers2(wpa.CipherCCMP), wpa.FTMode(wpa.FTModeMixed)), |
| mixed: true, |
| }, |
| }, { |
| Name: "eap", |
| Val: roamFTparam{ |
| secConfFac: wpaeap.NewConfigFactory( |
| roamFTCert1.CACred.Cert, roamFTCert1.ServerCred, |
| wpaeap.ClientCACert(roamFTCert1.CACred.Cert), wpaeap.ClientCred(roamFTCert1.ClientCred), |
| wpaeap.Mode(wpa.ModePureWPA2), wpaeap.FTMode(wpa.FTModePure), |
| ), |
| }, |
| }, { |
| Name: "mixed_eap", |
| Val: roamFTparam{ |
| secConfFac: wpaeap.NewConfigFactory( |
| roamFTCert1.CACred.Cert, roamFTCert1.ServerCred, |
| wpaeap.ClientCACert(roamFTCert1.CACred.Cert), wpaeap.ClientCred(roamFTCert1.ClientCred), |
| wpaeap.Mode(wpa.ModePureWPA2), wpaeap.FTMode(wpa.FTModeMixed), |
| ), |
| mixed: true, |
| }, |
| }}, |
| }) |
| } |
| |
| func RoamFT(ctx context.Context, s *testing.State) { |
| /* |
| Roaming using FT is different from standard roaming in that there |
| is a special key exchange protocol that needs to occur between the |
| APs prior to a successful roam. In order for this communication to |
| work, we need to construct a specific interface architecture as |
| shown below: |
| _________ _________ |
| | | | | |
| | br0 | | br1 | |
| |_________| |_________| |
| ____| |____ ____| |____ |
| _____|____ ____|____ ____|____ ____|_____ |
| | | | | | | | | |
| | managed0 | | veth0 | <---> | veth1 | | managed1 | |
| |__________| |_________| |_________| |__________| |
| |
| The managed0 and managed1 interfaces cannot communicate with each |
| other without a bridge. However, the same bridge cannot be used |
| to bridge the two interfaces either (as soon as managed0 is bound |
| to a bridge, hostapd would notice and would configure the same MAC |
| address as managed0 onto the bridge, and send/recv the L2 packet |
| with the bridge). Thus, we create a virtual ethernet interface with |
| one peer on either bridge to allow the bridges to forward traffic |
| between managed0 and managed1. |
| */ |
| tf := s.FixtValue().(*wificell.TestFixture) |
| |
| router, err := tf.StandardRouterWithBridgeAndVethSupport() |
| if err != nil { |
| s.Fatal("Failed to get router: ", err) |
| } |
| |
| // Shorten a second for releasing each network device. |
| reserveForRelease := func(ctx context.Context) (context.Context, func()) { |
| return ctxutil.Shorten(ctx, time.Second) |
| } |
| |
| apID := 0 |
| uniqueAPName := func() string { |
| id := strconv.Itoa(apID) |
| apID++ |
| return id |
| } |
| |
| clientMAC, err := tf.ClientHardwareAddr(ctx) |
| if err != nil { |
| s.Fatal("Unable to get DUT MAC address: ", err) |
| } |
| |
| // runOnce sets up the network environment as mentioned above, and verifies the DUT is able to roam between the APs iff expectedFailure is not set. |
| runOnce := func(ctx context.Context, secConfFac security.ConfigFactory, expectedFailure bool) { |
| var cancel context.CancelFunc |
| var err error |
| var br [2]string |
| for i := 0; i < 2; i++ { |
| br[i], err = router.NewBridge(ctx) |
| if err != nil { |
| s.Fatal("Failed to get a bridge: ", err) |
| } |
| defer func(ctx context.Context, b string) { |
| if err := router.ReleaseBridge(ctx, b); err != nil { |
| s.Error("Failed to release bridge: ", err) |
| } |
| }(ctx, br[i]) |
| ctx, cancel = reserveForRelease(ctx) |
| defer cancel() |
| } |
| |
| var veth [2]string |
| veth[0], veth[1], err = router.NewVethPair(ctx) |
| if err != nil { |
| s.Fatal("Failed to get a veth pair: ", err) |
| } |
| defer func(ctx context.Context) { |
| if err := router.ReleaseVethPair(ctx, veth[0]); err != nil { |
| s.Error("Failed to release veth: ", err) |
| } |
| }(ctx) |
| ctx, cancel = reserveForRelease(ctx) |
| defer cancel() |
| |
| // Bind the two ends of the veth to the two bridges. |
| for i := 0; i < 2; i++ { |
| if err := router.BindVethToBridge(ctx, veth[i], br[i]); err != nil { |
| s.Fatalf("Failed to bind the veth %q to bridge %q: %v", veth[i], br[i], err) |
| } |
| defer func(ctx context.Context, ve string) { |
| if err := router.UnbindVeth(ctx, ve); err != nil { |
| s.Errorf("Failed to unbind %q: %v", ve, err) |
| } |
| }(ctx, veth[i]) |
| ctx, cancel = reserveForRelease(ctx) |
| defer cancel() |
| } |
| |
| s.Logf("Network environment setup is done: %s <= %s----%s => %s", br[0], veth[0], veth[1], br[1]) |
| |
| mac0, err := hostapd.RandomMAC() |
| if err != nil { |
| s.Fatal("Failed to get a random mac address: ", err) |
| } |
| mac1, err := hostapd.RandomMAC() |
| if err != nil { |
| s.Fatal("Failed to get a random mac address: ", err) |
| } |
| var ( |
| id0 = hex.EncodeToString(mac0) |
| id1 = hex.EncodeToString(mac1) |
| ) |
| const ( |
| key0 = "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100" |
| key1 = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" |
| mdID = "a1b2" |
| ) |
| |
| ap0SecConf, err := secConfFac.Gen() |
| if err != nil { |
| s.Fatal("Failed to generate security config: ", err) |
| } |
| ap0Ops := []hostapd.Option{ |
| hostapd.Channel(1), hostapd.Mode(hostapd.Mode80211g), hostapd.BSSID(mac0.String()), |
| hostapd.MobilityDomain(mdID), hostapd.NASIdentifier(id0), hostapd.R1KeyHolder(id0), |
| hostapd.R0KHs(fmt.Sprintf("%s %s %s", mac1, id1, key0)), |
| hostapd.R1KHs(fmt.Sprintf("%s %s %s", mac1, mac1, key1)), |
| hostapd.Bridge(br[0]), hostapd.SecurityConfig(ap0SecConf), |
| } |
| ap0Conf, err := hostapd.NewConfig(ap0Ops...) |
| if err != nil { |
| s.Fatal("Failed to generate the hostapd config for AP0: ", err) |
| } |
| ap1SecConf, err := secConfFac.Gen() |
| if err != nil { |
| s.Fatal("Failed to generate security config: ", err) |
| } |
| ap1Ops := []hostapd.Option{ |
| hostapd.SSID(ap0Conf.SSID), hostapd.Channel(157), hostapd.Mode(hostapd.Mode80211acPure), hostapd.BSSID(mac1.String()), |
| hostapd.HTCaps(hostapd.HTCapHT40Plus), hostapd.VHTCaps(hostapd.VHTCapSGI80), hostapd.VHTChWidth(hostapd.VHTChWidth80), hostapd.VHTCenterChannel(155), |
| hostapd.MobilityDomain(mdID), hostapd.NASIdentifier(id1), hostapd.R1KeyHolder(id1), |
| hostapd.R0KHs(fmt.Sprintf("%s %s %s", mac0, id0, key1)), |
| hostapd.R1KHs(fmt.Sprintf("%s %s %s", mac0, mac0, key0)), |
| hostapd.Bridge(br[1]), hostapd.SecurityConfig(ap1SecConf), |
| } |
| ap1Conf, err := hostapd.NewConfig(ap1Ops...) |
| if err != nil { |
| s.Fatal("Failed to generate the hostapd config for AP1: ", err) |
| } |
| |
| s.Log("Starting the first AP on ", br[0]) |
| ap0Name := uniqueAPName() |
| ap0, err := router.StartHostapd(ctx, ap0Name, ap0Conf) |
| if err != nil { |
| s.Fatal("Failed to start the hostapd server on the first AP: ", err) |
| } |
| defer func(ctx context.Context) { |
| if err := router.StopHostapd(ctx, ap0); err != nil { |
| s.Error("Failed to stop the hostapd server on the first AP: ", err) |
| } |
| }(ctx) |
| ctx, cancel = ap0.ReserveForClose(ctx) |
| defer cancel() |
| |
| var ( |
| serverIP = net.IPv4(192, 168, 0, 254) |
| startIP = net.IPv4(192, 168, 0, 1) |
| endIP = net.IPv4(192, 168, 0, 128) |
| broadcastIP = net.IPv4(192, 168, 0, 255) |
| mask = net.IPv4Mask(255, 255, 255, 0) |
| ) |
| s.Logf("Starting the DHCP server on %s, serverIP=%s", br[0], serverIP) |
| ds, err := router.StartDHCP(ctx, ap0Name, br[0], startIP, endIP, serverIP, broadcastIP, mask) |
| if err != nil { |
| s.Fatal("Failed to start the DHCP server: ", err) |
| } |
| defer func(ctx context.Context) { |
| if err := router.StopDHCP(ctx, ds); err != nil { |
| s.Error("Failed to stop the DHCP server: ", err) |
| } |
| }(ctx) |
| ctx, cancel = ds.ReserveForClose(ctx) |
| defer cancel() |
| |
| connResp, err := tf.ConnectWifi(ctx, ap0.Config().SSID, dutcfg.ConnSecurity(ap0SecConf)) |
| if err != nil { |
| if expectedFailure { |
| s.Log("Failed to connect to the AP as expected; Tearing down") |
| return |
| } |
| s.Fatal("Failed to connect to the AP: ", err) |
| } |
| if expectedFailure { |
| s.Fatal("Expected failure but succeeded") |
| } |
| roamSucceeded := false |
| defer func(ctx context.Context) { |
| if roamSucceeded { |
| return |
| } |
| if err := tf.CleanDisconnectWifi(ctx); err != nil { |
| s.Error("Failed to disconnect from the AP: ", err) |
| } |
| }(ctx) |
| ctx, cancel = tf.ReserveForDisconnect(ctx) |
| defer cancel() |
| |
| if err := tf.PingFromDUT(ctx, serverIP.String()); err != nil { |
| s.Fatal("Failed to ping from the DUT: ", err) |
| } |
| |
| s.Log("Connected to the first AP; Start roaming") |
| props := []*wificell.ShillProperty{{ |
| Property: shillconst.ServicePropertyWiFiRoamState, |
| ExpectedValues: []interface{}{shillconst.RoamStateConfiguration}, |
| Method: wifi.ExpectShillPropertyRequest_ON_CHANGE, |
| }, { |
| Property: shillconst.ServicePropertyWiFiRoamState, |
| ExpectedValues: []interface{}{shillconst.RoamStateReady}, |
| Method: wifi.ExpectShillPropertyRequest_ON_CHANGE, |
| }, { |
| Property: shillconst.ServicePropertyWiFiRoamState, |
| ExpectedValues: []interface{}{shillconst.RoamStateIdle}, |
| Method: wifi.ExpectShillPropertyRequest_ON_CHANGE, |
| }, { |
| Property: shillconst.ServicePropertyWiFiBSSID, |
| ExpectedValues: []interface{}{mac1.String()}, |
| Method: wifi.ExpectShillPropertyRequest_CHECK_ONLY, |
| }} |
| waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second) |
| defer cancel() |
| waitForProps, err := tf.WifiClient().ExpectShillProperty(waitCtx, connResp.ServicePath, props, []string{shillconst.ServicePropertyIsConnected}) |
| if err != nil { |
| s.Fatal("Failed to create a property watcher: ", err) |
| } |
| |
| s.Log("Starting the second AP on ", br[1]) |
| ap1Name := uniqueAPName() |
| ap1, err := router.StartHostapd(ctx, ap1Name, ap1Conf) |
| if err != nil { |
| s.Fatal("Failed to start the hostapd server on the second AP: ", err) |
| } |
| defer func(ctx context.Context) { |
| if err := router.StopHostapd(ctx, ap1); err != nil { |
| s.Error("Failed to stop the hostapd server on the second AP: ", err) |
| } |
| }(ctx) |
| ctx, cancel = ap1.ReserveForClose(ctx) |
| defer cancel() |
| |
| s.Logf("Sending BSS TM Request from AP %s to DUT %s", mac0, clientMAC) |
| req := hostapd.BSSTMReqParams{Neighbors: []string{mac1.String()}} |
| if err := ap0.SendBSSTMRequest(ctx, clientMAC, req); err != nil { |
| s.Fatal("Failed to send BSS TM Request: ", err) |
| } |
| |
| monitorResult, err := waitForProps() |
| if err != nil { |
| s.Fatal("Failed to wait for the properties: ", err) |
| } |
| roamSucceeded = true |
| defer func(ctx context.Context) { |
| if roamSucceeded { |
| return |
| } |
| if err := tf.CleanDisconnectWifi(ctx); err != nil { |
| s.Error("Failed to disconnect from the AP: ", err) |
| } |
| }(ctx) |
| // Check that we don't disconnect along the way here, in case we're ping-ponging around APs -- |
| // and after the first (failed) roam, the second re-connection will not be testing FT at all. |
| for _, ph := range monitorResult { |
| if ph.Name == shillconst.ServicePropertyIsConnected { |
| if !ph.Value.(bool) { |
| s.Error("Failed to stay connected during the roaming process") |
| } |
| } |
| } |
| |
| // Verify the L3 connectivity and make sure that the DUT stays connected to the second AP. |
| if err := tf.PingFromDUT(ctx, serverIP.String()); err != nil { |
| s.Fatal("Failed to verify connection: ", err) |
| } |
| dutState, err := tf.WifiClient().QueryService(ctx) |
| if err != nil { |
| s.Fatal("Failed to query service: ", err) |
| } |
| if dutState.Wifi.Bssid != mac1.String() { |
| s.Fatalf("Unexpected BSSID: got %s, want %s", dutState.Wifi.Bssid, mac1) |
| } |
| } |
| hasFTSupport := func(ctx context.Context) bool { |
| phys, _, err := iw.NewRemoteRunner(s.DUT().Conn()).ListPhys(ctx) |
| if err != nil { |
| s.Fatal("Failed to check SME capability: ", err) |
| } |
| for _, p := range phys { |
| for _, c := range p.Commands { |
| // A DUT which has SME capability should support FT. |
| if c == "authenticate" { |
| return true |
| } |
| // A full-mac driver that supports update_ft_ies functions also supports FT. |
| if c == "update_ft_ies" { |
| return true |
| } |
| } |
| } |
| return false |
| } |
| |
| ctx, restoreBg, err := tf.WifiClient().TurnOffBgscan(ctx) |
| if err != nil { |
| s.Fatal("Failed to turn off the background scan: ", err) |
| } |
| defer func() { |
| if err := restoreBg(); err != nil { |
| s.Error("Failed to restore the background scan config: ", err) |
| } |
| }() |
| |
| allowRoamResp, err := tf.WifiClient().GetScanAllowRoamProperty(ctx, &empty.Empty{}) |
| if err != nil { |
| s.Fatal("Failed to get the ScanAllowRoam property: ", err) |
| } |
| if allowRoamResp.Allow { |
| if _, err := tf.WifiClient().SetScanAllowRoamProperty(ctx, &wifi.SetScanAllowRoamPropertyRequest{Allow: false}); err != nil { |
| s.Error("Failed to set ScanAllowRoam property to false: ", err) |
| } |
| defer func(ctx context.Context) { |
| if _, err := tf.WifiClient().SetScanAllowRoamProperty(ctx, &wifi.SetScanAllowRoamPropertyRequest{Allow: allowRoamResp.Allow}); err != nil { |
| s.Errorf("Failed to set ScanAllowRoam property back to %v: %v", allowRoamResp.Allow, err) |
| } |
| }(ctx) |
| } |
| |
| 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() |
| |
| param := s.Param().(roamFTparam) |
| // Turn on the global FT and test once. |
| if _, err := tf.WifiClient().SetGlobalFTProperty(ctx, &wifi.SetGlobalFTPropertyRequest{Enabled: true}); err != nil { |
| s.Fatal("Failed to turn on the global FT property: ", err) |
| } |
| // Expect failure if we are running pure FT test and the DUT is not supporting SME. |
| runOnce(ctx, param.secConfFac, !param.mixed && !hasFTSupport(ctx)) |
| // Run the test without global FT. It should pass iff we configured the AP in mixed mode. |
| if _, err := tf.WifiClient().SetGlobalFTProperty(ctx, &wifi.SetGlobalFTPropertyRequest{Enabled: false}); err != nil { |
| s.Fatal("Failed to turn off the global FT property: ", err) |
| } |
| runOnce(ctx, param.secConfFac, !param.mixed) |
| } |