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 }