blob: cb3cab74e04e4fe33187d3244d674a6f96b4f5d3 [file] [log] [blame]
// 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 wiredhostapd
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"golang.org/x/sys/unix"
"chromiumos/tast/common/crypto/certificate"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/local/network/ip"
"chromiumos/tast/testing"
)
// STA status definitions (i.e., expectation values for Server.ExpectSTAStatus).
const (
STAStatusAuthSuccess = "dot1xAuthAuthSuccessesWhileAuthenticating"
STAStatusAuthLogoff = "dot1xAuthAuthEapLogoffWhileAuthenticated"
)
// EAPConf holds configuration information for creating hostapd's internal EAP server.
type EAPConf struct {
OuterAuth string
InnerAuth string
Identity string
Password string
// Cert holds certificate information for hostapd's EAP server.
Cert *certificate.CertStore
}
// Server holds information about a started hostapd server, primarily for the 'driver=wired' variant.
type Server struct {
// Iface is the name of the network interface which hostapd should manage.
Iface string
// OutDir is the path to which output logs should be written.
OutDir string
EAP *EAPConf
// ctrlIface is the path to the domain socket for controlling hostapd (e.g., with hostapd_cli).
ctrlIface string
// tmpDir is the path where temporary files should be stashed. (Note: different than OutDir, where test
// artifacts should be stashed.)
tmpDir string
cmd *testexec.Cmd
// logIndex is an internal counter, to ensure we write out unique status files.
logIndex int
}
// Stop cleans up any resources and kills the hostapd server.
func (s *Server) Stop(ctx context.Context) error {
if err := s.cmd.Signal(unix.SIGTERM); err != nil {
return errors.Wrap(err, "failed to kill hostapd")
}
// Wait will always fail; ignore errors.
s.cmd.Wait()
if err := os.RemoveAll(s.tmpDir); err != nil {
return errors.Wrapf(err, "failed to clean up tmp dir: %s", s.tmpDir)
}
return nil
}
// cliCmd runs a hostapd command via hostapd_cli. Returns stdout/stderr for success or error.
func (s *Server) cliCmd(ctx context.Context, args ...string) (stdout, stderr string, err error) {
cliArgs := append([]string{"-p", s.ctrlIface, "-i", s.Iface}, args...)
// Don't intermix stdout/stderr for parsing, so we have to capture them separately.
o, e, err := testexec.CommandContext(ctx, "hostapd_cli", cliArgs...).SeparatedOutput()
if err != nil {
err = errors.Wrapf(err, "hostapd_cli failed, args: %v", args)
}
return string(o), string(e), err
}
// ExpectSTAStatus retrieves the status for the station at 'staAddr', logs it to a file in OutDir, and
// verifies if the expected status 'key=val' entry is found.
func (s *Server) ExpectSTAStatus(ctx context.Context, staAddr, key, val string) error {
// Log hostapd status to file with increasing index.
defer func() {
s.logIndex++
}()
var err error
var stdout, stderr string
stdout, stderr, err = s.cliCmd(ctx, "sta", staAddr)
out := stdout + "\nstderr:\n" + stderr
if err != nil {
testing.ContextLog(ctx, "hostapd_cli output: ", out)
return errors.Wrapf(err, "failed to query STA %q", staAddr)
}
// Stash output for analysis.
path := filepath.Join(s.OutDir, fmt.Sprintf("hostapd_auth_%d.txt", s.logIndex))
if err := ioutil.WriteFile(path, []byte(out), 0644); err != nil {
return errors.Wrapf(err, "failed to write file %q", path)
}
expect := key + "=" + val
for _, line := range strings.Split(stdout, "\n") {
if line == expect {
return nil
}
}
return errors.Errorf("hostapd auth status %q not found", expect)
}
// prepareConfigs is a helper to format and write out the config files, certificates, etc., needed by hostapd.
// Returns the path to which hostapd.conf was written.
func (s *Server) prepareConfigs(ctx context.Context) (string, error) {
// NB: 'eap_user' format is not well documented. The '[2]' indicates
// phase 2 (i.e., inner).
eapUser := fmt.Sprintf(`* %s
"%s" %s "%s" [2]
`, s.EAP.OuterAuth, s.EAP.Identity, s.EAP.InnerAuth, s.EAP.Password)
serverCertPath := filepath.Join(s.tmpDir, "cert")
privateKeyPath := filepath.Join(s.tmpDir, "private_key")
eapUserFilePath := filepath.Join(s.tmpDir, "eap_user")
caCertPath := filepath.Join(s.tmpDir, "ca_cert")
confPath := filepath.Join(s.tmpDir, "hostapd.conf")
s.ctrlIface = filepath.Join(s.tmpDir, "hostapd.ctrl")
confContents := fmt.Sprintf(`driver=wired
interface=%s
ctrl_interface=%s
server_cert=%s
private_key=%s
eap_user_file=%s
ca_cert=%s
eap_server=1
ieee8021x=1
eapol_version=2
`, s.Iface, s.ctrlIface, serverCertPath, privateKeyPath, eapUserFilePath, caCertPath)
for _, p := range []struct {
path string
contents string
}{
{confPath, confContents},
{serverCertPath, s.EAP.Cert.ServerCred.Cert},
{privateKeyPath, s.EAP.Cert.ServerCred.PrivateKey},
{eapUserFilePath, eapUser},
{caCertPath, s.EAP.Cert.CACred.Cert},
} {
if err := ioutil.WriteFile(p.path, []byte(p.contents), 0644); err != nil {
return "", errors.Wrapf(err, "failed to write file %q", p.path)
}
}
return confPath, nil
}
// Start starts up a hostapd instance, for wired authentication. The caller should call
// Server.Stop() when finished.
func (s *Server) Start(ctx context.Context) (retErr error) {
var err error
if s.tmpDir, err = ioutil.TempDir("", ""); err != nil {
return errors.Wrap(err, "failed to create a temporary directory")
}
defer func() {
if retErr != nil {
if err := os.RemoveAll(s.tmpDir); err != nil {
testing.ContextLogf(ctx, "Failed to clean up dir %s, %v", s.tmpDir, err)
}
}
}()
confPath, err := s.prepareConfigs(ctx)
if err != nil {
return err
}
// Bring up the hostapd link.
ipr := ip.NewLocalRunner()
if err := ipr.SetLinkUp(ctx, s.Iface); err != nil {
return errors.Wrap(err, "could not bring up hostapd veth")
}
logPath := filepath.Join(s.OutDir, "hostapd.log")
s.cmd = testexec.CommandContext(ctx, "hostapd", "-dd", "-t", "-f", logPath, confPath)
s.cmd.Env = append(os.Environ(), "OPENSSL_CONF=/etc/ssl/openssl.cnf.compat")
if err := s.cmd.Start(); err != nil {
return errors.Wrap(err, "failed to start hostapd")
}
return nil
}
// StartEAPOL tells the Server to initiate EAPOL with nearby links (multicast).
func (s *Server) StartEAPOL(ctx context.Context) error {
// The default group MAC address to which EAP challenges are sent, absent any prior
// knowledge of a specific client on the link -- part of the Link Layer Discovery Protocol
// (LLDP), IEEE 802.1AB.
const nearestMAC = "01:80:c2:00:00:03"
var out string
// Poll because we didn't guarantee the hostapd server has finished starting up (e.g.,
// establishing the control socket).
if err := testing.Poll(ctx, func(ctx context.Context) error {
stdout, stderr, err := s.cliCmd(ctx, "new_sta", nearestMAC)
out = stdout + "\nstderr:\n" + stderr
return err
}, &testing.PollOptions{Timeout: 3 * time.Second}); err != nil {
return errors.Wrapf(err, "new_sta failed, output %q", out)
}
return nil
}