github.com/jlmucb/cloudproxy@v0.0.0-20170830161738-b5aa0b619bc4/go/tao/kvm_coreos_factory.go (about)

     1  // Copyright (c) 2014, Google Inc.  All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package tao
    16  
    17  import (
    18  	"archive/tar"
    19  	"bufio"
    20  	"bytes"
    21  	"compress/gzip"
    22  	"crypto/rand"
    23  	"crypto/rsa"
    24  	"crypto/sha256"
    25  	"encoding/base64"
    26  	"encoding/hex"
    27  	"errors"
    28  	"fmt"
    29  	"io"
    30  	"io/ioutil"
    31  	"net"
    32  	"os"
    33  	"os/exec"
    34  	"path"
    35  	"strconv"
    36  	"syscall"
    37  	"time"
    38  
    39  	"github.com/golang/glog"
    40  	"github.com/jlmucb/cloudproxy/go/tao/auth"
    41  	"github.com/jlmucb/cloudproxy/go/util"
    42  
    43  	// "github.com/golang/crypto/ssh"
    44  	"golang.org/x/crypto/ssh"
    45  )
    46  
    47  // A CoreOSConfig contains the details needed to start a new CoreOS VM.
    48  type CoreOSConfig struct {
    49  	Name       string
    50  	ImageFile  string
    51  	Memory     int
    52  	RulesPath  string
    53  	SSHKeysCfg string
    54  	SocketPath string
    55  }
    56  
    57  // A KvmCoreOSContainer represents a hosted program running as a CoreOS image on
    58  // KVM. It uses os/exec.Cmd to send commands to QEMU/KVM to start CoreOS then
    59  // uses SSH to connect to CoreOS to start the LinuxHost there with a
    60  // virtio-serial connection for its communication with the Tao running on Linux
    61  // in the guest. This use of os/exec is to avoid having to rewrite or hook into
    62  // libvirt for now.
    63  type KvmCoreOSContainer struct {
    64  
    65  	// The spec from which this vm was created.
    66  	spec HostedProgramSpec
    67  
    68  	// TODO(kwalsh) A secured, private copy of the image.
    69  	// Temppath string
    70  
    71  	// TODO(kwalsh) A temporary directory for the config drive.
    72  	Tempdir string
    73  
    74  	// Hash of the CoreOS image.
    75  	Hash []byte
    76  
    77  	// Hash of the factory's KVM image.
    78  	// TODO(kwalsh) Move this to LinuxKVMCoreOSFactory. and don't recompute?
    79  	FactoryHash []byte
    80  
    81  	// The factory responsible for the vm.
    82  	Factory *LinuxKVMCoreOSFactory
    83  
    84  	// Configuration details for CoreOS, mostly obtained from the factory.
    85  	// TODO(kwalsh) what is a good description for this?
    86  	Cfg *CoreOSConfig
    87  
    88  	// The underlying vm process.
    89  	QCmd *exec.Cmd
    90  
    91  	// Path to linux host.
    92  	// TODO(kwalsh) is this description correct?
    93  	LHPath string
    94  
    95  	// A channel to be signaled when the vm is done.
    96  	Done chan bool
    97  }
    98  
    99  // WaitChan returns a chan that will be signaled when the hosted vm is done.
   100  func (kcc *KvmCoreOSContainer) WaitChan() <-chan bool {
   101  	return kcc.Done
   102  }
   103  
   104  // Kill sends a SIGKILL signal to a QEMU instance.
   105  func (kcc *KvmCoreOSContainer) Kill() error {
   106  	// Kill the qemu command directly.
   107  	// TODO(tmroeder): rewrite this using qemu's communication/management
   108  	// system; sending SIGKILL is definitely not the right way to do this.
   109  	return kcc.QCmd.Process.Kill()
   110  }
   111  
   112  // Start starts a QEMU/KVM CoreOS container using the command line.
   113  func (kcc *KvmCoreOSContainer) startVM() error {
   114  	// Create a temporary directory for the config drive.
   115  	td, err := ioutil.TempDir("", "coreos")
   116  	kcc.Tempdir = td
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	// Create a temporary directory for the linux_host image. Note that the
   122  	// args were validated in Start before this call.
   123  	kcc.LHPath = kcc.spec.Args[1]
   124  
   125  	// Expand the host file into the directory.
   126  	linuxHostFile, err := os.Open(kcc.spec.Path)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	zipReader, err := gzip.NewReader(linuxHostFile)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	defer zipReader.Close()
   136  
   137  	unzippedImage, err := ioutil.ReadAll(zipReader)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	unzippedReader := bytes.NewReader(unzippedImage)
   142  	tarReader := tar.NewReader(unzippedReader)
   143  	for {
   144  		hdr, err := tarReader.Next()
   145  		if err == io.EOF {
   146  			break
   147  		}
   148  		if err != nil {
   149  			return err
   150  		}
   151  
   152  		fi := hdr.FileInfo()
   153  		outputName := path.Join(kcc.LHPath, hdr.Name)
   154  		if fi.IsDir() {
   155  			if err := os.Mkdir(outputName, fi.Mode()); err != nil {
   156  				return err
   157  			}
   158  		} else {
   159  
   160  			outputFile, err := os.OpenFile(outputName, os.O_CREATE|os.O_TRUNC|os.O_RDWR, fi.Mode())
   161  			if err != nil {
   162  				return err
   163  			}
   164  
   165  			if _, err := io.Copy(outputFile, tarReader); err != nil {
   166  				outputFile.Close()
   167  				return err
   168  			}
   169  			outputFile.Close()
   170  		}
   171  	}
   172  
   173  	latestDir := path.Join(td, "openstack/latest")
   174  	if err := os.MkdirAll(latestDir, 0700); err != nil {
   175  		return err
   176  	}
   177  
   178  	cfg := kcc.Cfg
   179  	userData := path.Join(latestDir, "user_data")
   180  	if err := ioutil.WriteFile(userData, []byte(cfg.SSHKeysCfg), 0700); err != nil {
   181  		return err
   182  	}
   183  
   184  	// Copy the rules into the mirrored filesystem for use by the Linux host
   185  	// on CoreOS.
   186  	if cfg.RulesPath != "" {
   187  		rules, err := ioutil.ReadFile(cfg.RulesPath)
   188  		if err != nil {
   189  			return err
   190  		}
   191  		rulesFile := path.Join(kcc.LHPath, path.Base(cfg.RulesPath))
   192  		if err := ioutil.WriteFile(rulesFile, []byte(rules), 0700); err != nil {
   193  			return err
   194  		}
   195  	}
   196  
   197  	qemuProg := "qemu-system-x86_64"
   198  	qemuArgs := []string{"-name", cfg.Name,
   199  		"-m", strconv.Itoa(cfg.Memory),
   200  		"-machine", "accel=kvm:tcg",
   201  		// Networking.
   202  		"-net", "nic,vlan=0,model=virtio",
   203  		"-net", "user,vlan=0,hostfwd=tcp::" + kcc.spec.Args[2] + "-:22,hostname=" + cfg.Name,
   204  		// Tao communications through virtio-serial. With this
   205  		// configuration, QEMU waits for a server on cfg.SocketPath,
   206  		// then connects to it.
   207  		"-chardev", "socket,path=" + cfg.SocketPath + ",id=port0-char",
   208  		"-device", "virtio-serial",
   209  		"-device", "virtserialport,id=port1,name=tao,chardev=port0-char",
   210  		// The CoreOS image to boot from.
   211  		"-drive", "if=virtio,file=" + cfg.ImageFile,
   212  		// A Plan9P filesystem for SSH configuration (and our rules).
   213  		"-fsdev", "local,id=conf,security_model=none,readonly,path=" + td,
   214  		"-device", "virtio-9p-pci,fsdev=conf,mount_tag=config-2",
   215  		// Another Plan9P filesystem for the linux_host files.
   216  		"-fsdev", "local,id=tao,security_model=none,path=" + kcc.LHPath,
   217  		"-device", "virtio-9p-pci,fsdev=tao,mount_tag=tao",
   218  		// Machine config.
   219  		"-cpu", "host",
   220  		"-smp", "4",
   221  		"-nographic"} // for now, we add -nographic explicitly.
   222  	// TODO(tmroeder): append args later.
   223  	//qemuArgs = append(qemuArgs, kcc.spec.Args...)
   224  
   225  	kcc.QCmd = exec.Command(qemuProg, qemuArgs...)
   226  	// Don't connect QEMU/KVM to any of the current input/output channels,
   227  	// since we'll connect over SSH.
   228  	//kcc.QCmd.Stdin = os.Stdin
   229  	//kcc.QCmd.Stdout = os.Stdout
   230  	//kcc.QCmd.Stderr = os.Stderr
   231  	// TODO(kwalsh) set up env, dir, and uid/gid.
   232  	return kcc.QCmd.Start()
   233  }
   234  
   235  // Stop sends a SIGSTOP signal to a docker container.
   236  func (kcc *KvmCoreOSContainer) Stop() error {
   237  	// Stop the QEMU/KVM process with SIGSTOP.
   238  	// TODO(tmroeder): rewrite this using qemu's communication/management
   239  	// system; sending SIGSTOP is definitely not the right way to do this.
   240  	return kcc.QCmd.Process.Signal(syscall.SIGSTOP)
   241  }
   242  
   243  // Pid returns a numeric ID for this container.
   244  func (kcc *KvmCoreOSContainer) Pid() int {
   245  	return kcc.QCmd.Process.Pid
   246  }
   247  
   248  // ExitStatus returns an exit code for the container.
   249  func (kcc *KvmCoreOSContainer) ExitStatus() (int, error) {
   250  	s := kcc.QCmd.ProcessState
   251  	if s == nil {
   252  		return -1, fmt.Errorf("Child has not exited")
   253  	}
   254  	if code, ok := (*s).Sys().(syscall.WaitStatus); ok {
   255  		return int(code), nil
   256  	}
   257  	return -1, fmt.Errorf("Couldn't get exit status\n")
   258  }
   259  
   260  // A LinuxKVMCoreOSFactory manages hosted programs started as QEMU/KVM
   261  // instances over a given CoreOS image.
   262  type LinuxKVMCoreOSFactory struct {
   263  	Cfg        *CoreOSConfig
   264  	SocketPath string
   265  	PublicKey  string
   266  	PrivateKey ssh.Signer
   267  }
   268  
   269  // NewLinuxKVMCoreOSFactory returns a new HostedProgramFactory that can
   270  // create docker containers to wrap programs.
   271  // TODO(kwalsh) fix comment.
   272  func NewLinuxKVMCoreOSFactory(sockPath string, cfg *CoreOSConfig) (HostedProgramFactory, error) {
   273  
   274  	// Create a key to use to connect to the instance and set up LinuxHost
   275  	// there.
   276  	priv, err := rsa.GenerateKey(rand.Reader, 2048)
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  	sshpk, err := ssh.NewPublicKey(&priv.PublicKey)
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  	pkstr := "ssh-rsa " + base64.StdEncoding.EncodeToString(sshpk.Marshal()) + " linux_host"
   285  
   286  	sshpriv, err := ssh.NewSignerFromKey(priv)
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	return &LinuxKVMCoreOSFactory{
   292  		Cfg:        cfg,
   293  		SocketPath: sockPath,
   294  		PublicKey:  pkstr,
   295  		PrivateKey: sshpriv,
   296  	}, nil
   297  }
   298  
   299  // CloudConfigFromSSHKeys converts an ssh authorized-keys file into a format
   300  // that can be used by CoreOS to authorize incoming SSH connections over the
   301  // Plan9P-mounted filesystem it uses. This also adds the SSH key used by the
   302  // factory to configure the virtual machine.
   303  func CloudConfigFromSSHKeys(keysFile string) (string, error) {
   304  	sshKeys := "#cloud-config\nssh_authorized_keys:"
   305  	sshFile, err := os.Open(keysFile)
   306  	if err != nil {
   307  		return "", err
   308  	}
   309  	scanner := bufio.NewScanner(sshFile)
   310  	for scanner.Scan() {
   311  		sshKeys += "\n - " + scanner.Text()
   312  	}
   313  
   314  	return sshKeys, nil
   315  }
   316  
   317  // MakeSubprin computes the hash of a QEMU/KVM CoreOS image to get a
   318  // subprincipal for authorization purposes.
   319  func (lkcf *LinuxKVMCoreOSFactory) NewHostedProgram(spec HostedProgramSpec) (child HostedProgram, err error) {
   320  	// (id uint, image string, uid, gid int) (auth.SubPrin, string, error) {
   321  	// TODO(tmroeder): the combination of TeeReader and ReadAll doesn't seem
   322  	// to copy the entire image, so we're going to hash in place for now.
   323  	// This needs to be fixed to copy the image so we can avoid a TOCTTOU
   324  	// attack.
   325  	// TODO(kwalsh) why is this recomputed for each hosted program?
   326  	b, err := ioutil.ReadFile(lkcf.Cfg.ImageFile)
   327  	if err != nil {
   328  		return
   329  	}
   330  	h := sha256.Sum256(b)
   331  
   332  	bb, err := ioutil.ReadFile(spec.Path)
   333  	if err != nil {
   334  		return
   335  	}
   336  	hh := sha256.Sum256(bb)
   337  
   338  	// vet things
   339  
   340  	child = &KvmCoreOSContainer{
   341  		spec:        spec,
   342  		FactoryHash: h[:],
   343  		Hash:        hh[:],
   344  		Factory:     lkcf,
   345  		Done:        make(chan bool, 1),
   346  	}
   347  	return
   348  }
   349  
   350  // Subprin returns the subprincipal representing the hosted vm.
   351  func (kcc *KvmCoreOSContainer) Subprin() auth.SubPrin {
   352  	subprin := FormatCoreOSSubprin(kcc.spec.Id, kcc.FactoryHash)
   353  	lhSubprin := FormatLinuxHostSubprin(kcc.spec.Id, kcc.Hash)
   354  	return append(subprin, lhSubprin...)
   355  }
   356  
   357  // FormatLinuxHostSubprin produces a string that represents a subprincipal with
   358  // the given ID and hash.
   359  func FormatLinuxHostSubprin(id uint, hash []byte) auth.SubPrin {
   360  	var args []auth.Term
   361  	if id != 0 {
   362  		args = append(args, auth.Int(id))
   363  	}
   364  	args = append(args, auth.Bytes(hash))
   365  	return auth.SubPrin{auth.PrinExt{Name: "LinuxHost", Arg: args}}
   366  }
   367  
   368  // FormatCoreOSSubprin produces a string that represents a subprincipal with the
   369  // given ID and hash.
   370  func FormatCoreOSSubprin(id uint, hash []byte) auth.SubPrin {
   371  	var args []auth.Term
   372  	if id != 0 {
   373  		args = append(args, auth.Int(id))
   374  	}
   375  	args = append(args, auth.Bytes(hash))
   376  	return auth.SubPrin{auth.PrinExt{Name: "CoreOS", Arg: args}}
   377  }
   378  
   379  func getRandomFileName(n int) string {
   380  	// Get a random name for the socket.
   381  	nameBytes := make([]byte, n)
   382  	if _, err := rand.Read(nameBytes); err != nil {
   383  		return ""
   384  	}
   385  	return hex.EncodeToString(nameBytes)
   386  }
   387  
   388  // Spec returns the specification used to start the hosted vm.
   389  func (kcc *KvmCoreOSContainer) Spec() HostedProgramSpec {
   390  	return kcc.spec
   391  }
   392  
   393  var nameLen = 10
   394  
   395  // Start launches a QEMU/KVM CoreOS instance, connects to it with SSH to start
   396  // the LinuxHost on it, and returns the socket connection to that host.
   397  func (kcc *KvmCoreOSContainer) Start() (channel io.ReadWriteCloser, err error) {
   398  
   399  	// The args must contain the directory to write the linux_host into, as
   400  	// well as the port to use for SSH.
   401  	if len(kcc.spec.Args) != 3 {
   402  		glog.Errorf("Expected %d args, but got %d", 3, len(kcc.spec.Args))
   403  		for i, a := range kcc.spec.Args {
   404  			glog.Errorf("Arg %d: %s", i, a)
   405  		}
   406  		err = errors.New("KVM/CoreOS guest Tao requires args: <linux_host image> <temp directory for linux_host> <SSH port>")
   407  		return
   408  	}
   409  	// Build the new Config and start it. Make sure it has a random name so
   410  	// it doesn't conflict with other virtual machines.
   411  	sockName := getRandomFileName(nameLen)
   412  	sockPath := path.Join(kcc.Factory.SocketPath, sockName)
   413  	sshCfg := kcc.Factory.Cfg.SSHKeysCfg + "\n - " + string(kcc.Factory.PublicKey)
   414  
   415  	// Create a new docker image from the filesystem tarball, and use it to
   416  	// build a container and launch it.
   417  	kcc.Cfg = &CoreOSConfig{
   418  		Name:       getRandomFileName(nameLen),
   419  		ImageFile:  kcc.Factory.Cfg.ImageFile, // the VM image
   420  		Memory:     kcc.Factory.Cfg.Memory,
   421  		RulesPath:  kcc.Factory.Cfg.RulesPath,
   422  		SSHKeysCfg: sshCfg,
   423  		SocketPath: sockPath,
   424  	}
   425  
   426  	// Create the listening server before starting the connection. This lets
   427  	// QEMU start right away. See the comments in Start, above, for why this
   428  	// is.
   429  	channel = util.NewUnixSingleReadWriteCloser(kcc.Cfg.SocketPath)
   430  	defer func() {
   431  		if err != nil {
   432  			channel.Close()
   433  			channel = nil
   434  		}
   435  	}()
   436  	if err = kcc.startVM(); err != nil {
   437  		return
   438  	}
   439  	// TODO(kwalsh) reap and clenaup when vm dies; see linux_process_factory.go
   440  
   441  	// We need some way to wait for the socket to open before we can connect
   442  	// to it and return the ReadWriteCloser for communication. Also we need
   443  	// to connect by SSH to the instance once it comes up properly. For now,
   444  	// we just wait for a timeout before trying to connect and listen.
   445  	tc := time.After(10 * time.Second)
   446  
   447  	// Set up an ssh client config to use to connect to CoreOS.
   448  	conf := &ssh.ClientConfig{
   449  		// The CoreOS user for the SSH keys is currently always 'core'
   450  		// on the virtual machine.
   451  		User: "core",
   452  		Auth: []ssh.AuthMethod{ssh.PublicKeys(kcc.Factory.PrivateKey)},
   453  	}
   454  
   455  	glog.Info("Waiting for at most 10 seconds before trying to connect")
   456  	<-tc
   457  
   458  	hostPort := net.JoinHostPort("localhost", kcc.spec.Args[2])
   459  	client, err := ssh.Dial("tcp", hostPort, conf)
   460  	if err != nil {
   461  		err = fmt.Errorf("couldn't dial '%s': %s", hostPort, err)
   462  		return
   463  	}
   464  
   465  	// We need to run a set of commands to set up the LinuxHost on the
   466  	// remote system.
   467  	// Mount the filesystem.
   468  	mount, err := client.NewSession()
   469  	mount.Stdin = kcc.spec.Stdin
   470  	mount.Stdout = kcc.spec.Stdout
   471  	mount.Stderr = kcc.spec.Stderr
   472  	if err != nil {
   473  		err = fmt.Errorf("couldn't establish a mount session on SSH: %s", err)
   474  		return
   475  	}
   476  	if err = mount.Run("sudo mkdir /media/tao && sudo mount -t 9p -o trans=virtio,version=9p2000.L tao /media/tao && sudo chmod -R 755 /media/tao"); err != nil {
   477  		err = fmt.Errorf("couldn't mount the tao filesystem on the guest: %s", err)
   478  		return
   479  	}
   480  	mount.Close()
   481  
   482  	// Start the linux_host on the container.
   483  	start, err := client.NewSession()
   484  	start.Stdin = kcc.spec.Stdin
   485  	start.Stdout = kcc.spec.Stdout
   486  	start.Stderr = kcc.spec.Stderr
   487  	if err != nil {
   488  		err = fmt.Errorf("couldn't establish a start session on SSH: %s", err)
   489  		return
   490  	}
   491  	if err = start.Start("sudo /media/tao/linux_host start -stacked -parent_type file -parent_spec 'tao::RPC+tao::FileMessageChannel(/dev/virtio-ports/tao)' -tao_domain /media/tao -host /media/tao/linux_tao_host"); err != nil {
   492  		err = fmt.Errorf("couldn't start linux_host on the guest: %s", err)
   493  		return
   494  	}
   495  	start.Close()
   496  
   497  	return
   498  }
   499  
   500  func (kcc *KvmCoreOSContainer) Cleanup() error {
   501  	// TODO(kwalsh) maybe also kill vm if still running?
   502  	os.RemoveAll(kcc.Tempdir)
   503  	os.RemoveAll(kcc.LHPath)
   504  	return nil
   505  }