blob: e48fef7db4050d5c6cdfe1682dbe9ebe5e504c7f [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The following enables go generate to generate the doc.go file.
//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go . -help
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"time"
"v.io/x/lib/cmdline"
"v.io/x/lib/metadata"
"v.io/x/ref"
vsignals "v.io/x/ref/lib/signals"
"v.io/x/ref/services/agent/internal/constants"
"v.io/x/ref/services/agent/internal/launcher"
"v.io/x/ref/services/agent/internal/lock"
"v.io/x/ref/services/agent/internal/version"
"v.io/x/ref/services/agent/server"
)
func init() {
metadata.Insert("v23agentd.VersionMin", version.Supported.Min.String())
metadata.Insert("v23agentd.VersionMax", version.Supported.Max.String())
}
var (
versionToUse = version.Supported.Max
idleGrace time.Duration
daemon bool
stop bool
credentials string
createCreds bool
)
func main() {
cmdAgentD.Flags.StringVar(&credentials, constants.CredentialsFlag, os.Getenv(ref.EnvCredentials), fmt.Sprintf("Credentials directory. Defaults to the %s environment variable.", ref.EnvCredentials))
cmdAgentD.Flags.BoolVar(&createCreds, "create", false, "Whether to create the credentials if missing.")
cmdAgentD.Flags.Var(&versionToUse, constants.VersionFlag, "Version that the agent should use. Will fail if the version is not in the range of supported versions (obtained from the --metadata flag)")
cmdAgentD.Flags.BoolVar(&daemon, constants.DaemonFlag, true, "Run the agent as a daemon (returns right away but leaves the agent running in the background)")
cmdAgentD.Flags.DurationVar(&idleGrace, constants.TimeoutFlag, 0, "How long the agent stays alive without any client connections. Zero implies no timeout.")
cmdline.HideGlobalFlagsExcept()
cmdline.Main(cmdAgentD)
}
var cmdAgentD = &cmdline.Command{
Name: "v23agentd",
Short: "Holds a private key in memory and makes it available to other processes",
Long: `
Command v23agentd manages the security agent daemon, which holds the private
key, blessings and recognized roots of a principal in memory and makes the
principal available to other processes.
Other processes can access the agent credentials when V23_AGENT_PATH is set to
<credential dir>/agent/sock.
Without arguments, v23agentd starts the agent to exclusively serve the specified
credentials.
Exits right away if another agent is already serving the credentials.
Exits when there are no processes accessing the credentials (after a grace
period).
Example:
$ v23agentd --credentials=$HOME/.credentials
$ V23_AGENT_PATH=$HOME/.credentials/agent/sock principal dump
`,
Children: []*cmdline.Command{cmdStop},
}
type runner func(*cmdline.Env, []string) error
func (r runner) Run(env *cmdline.Env, args []string) error {
if credentials == "" {
return env.UsageErrorf("--%s must specify an existing credentials directory", constants.CredentialsFlag)
}
return r(env, args)
}
func init() {
cmdAgentD.Runner = runner(runAgentD)
}
func flagsFor(cmd *cmdline.Command) (ret []string) {
cmdAgentD.ParsedFlags.Visit(func(f *flag.Flag) {
switch f.Name {
case constants.DaemonFlag:
case constants.CredentialsFlag:
default:
ret = append(ret, fmt.Sprintf("--%s=%s", f.Name, f.Value))
}
})
return
}
func runAgentD(env *cmdline.Env, args []string) error {
if !version.Supported.Contains(versionToUse) {
return fmt.Errorf("version %v not in the supported range %v", versionToUse, version.Supported)
}
switch _, err := os.Stat(credentials); {
case os.IsNotExist(err):
if createCreds {
if err := os.MkdirAll(credentials, 0700); err != nil {
return fmt.Errorf("failed to create credentials dir \"%s\": %v", credentials, err)
}
} else {
return fmt.Errorf("credentials dir \"%s\" does not exist", credentials)
}
case err != nil:
return fmt.Errorf("cannot access credentials dir \"%s\": %v", credentials, err)
}
if daemon {
return launcher.LaunchAgent(credentials, os.Args[0], true, flagsFor(cmdAgentD)...)
}
notifyParent, detachIO, err := setupNotifyParent(env)
if err != nil {
return err
}
cleanup, commandChannels, ipc, err := initialize(env, credentials)
switch err {
case nil, errAlreadyRunning:
notifyParent(constants.ServingMsg)
if os.Getenv(constants.EnvAgentNoPrintCredsEnv) == "" {
fmt.Fprintf(env.Stdout, "%v=%v\n", ref.EnvAgentPath, constants.SocketPath(credentials))
}
if err == errAlreadyRunning {
return nil
}
default:
return err
}
defer cleanup()
if detachIO {
if err := detachStdInOutErr(constants.AgentDir(credentials)); err != nil {
return err
}
// TODO(caprita): Consider ignoring SIGHUP.
}
noConnections := make(chan struct{})
go idleWatch(env, ipc, noConnections, commandChannels)
select {
case sig := <-vsignals.ShutdownOnSignals(nil):
fmt.Fprintln(env.Stderr, "Received signal", sig)
case <-noConnections:
fmt.Fprintln(env.Stderr, "Idle timeout")
case <-commandChannels.exit:
fmt.Fprintln(env.Stderr, "Received exit command")
}
return nil
}
var errAlreadyRunning = errors.New("already running")
// initialize sets up the service to serve the principal. Upon success, the
// agent lock is locked and a cleanup function is returned (which includes
// unlocking the agent lock). Otherwise, an error is returned.
func initialize(env *cmdline.Env, credentials string) (func(), commandChannels, server.IPCState, error) {
agentDir := constants.AgentDir(credentials)
// Lock the credentials dir and then try to grab the agent lock. We
// need to first lock the credentials dir before the agent lock in order
// to avoid the following race: the old agent is about to exit; it stops
// serving, but before it releases the agent lock, a client tries
// connecting to the socket and fails; the client spawns a new agent
// that comes in and tries to grab the agent lock; it can't, and then
// exits; the old agent then also exits; this leaves no agent running.
credsLock := lock.NewDirLock(credentials).Must()
agentLock := lock.NewDirLock(agentDir).Must()
credsLock.Lock()
cleanup := credsLock.Unlock
// In case we return early from initialize.
defer func() {
if cleanup != nil {
cleanup()
}
}()
if err := os.MkdirAll(agentDir, 0700); err != nil {
return nil, commandChannels{}, nil, err
}
if !agentLock.TryLock() {
// Another agent is already serving the credentials.
return nil, commandChannels{}, nil, errAlreadyRunning
}
cleanup = push(cleanup, agentLock.Unlock)
p, err := server.LoadPrincipal(credentials, createCreds)
if err != nil {
return nil, commandChannels{}, nil, fmt.Errorf("failed to create new principal from dir(%s): %v", credentials, err)
}
ipc, err := server.Serve(p, constants.SocketPath(credentials))
if err != nil {
return nil, commandChannels{}, nil, fmt.Errorf("Serve failed: %v", err)
}
cleanup = push(cleanup, ipc.Close)
handlers, commandCh := setupCommandHandlers(ipc)
closeCommands, err := setupCommandSocket(agentDir, handlers)
if err != nil {
return nil, commandChannels{}, nil, fmt.Errorf("setupCommandSocket failed: %v", err)
}
cleanup = push(cleanup, func() {
if err := closeCommands(); err != nil {
fmt.Fprintf(env.Stderr, "closeCommands failed: %v\n", err)
}
})
cleanup = push(cleanup, credsLock.Lock)
// Disable running cleanup at the end of initialize.
defer func() {
cleanup = nil
}()
credsLock.Unlock()
return cleanup, commandCh, ipc, nil
}
func push(a, b func()) func() {
return func() {
b()
a()
}
}
// setupNotifyParent checks if the agent was started by a parent process that
// configured a pipe over which the agent is supposed to send a status message.
// If so, it returns a function that should be called to send the status. It
// also returns whether the agent should detach from stdout, stderr, and stdin
// following its initialization.
func setupNotifyParent(env *cmdline.Env) (func(string), bool, error) {
parentPipeFD := os.Getenv(constants.EnvAgentParentPipeFD)
if parentPipeFD == "" {
return func(string) {}, false, nil
}
fd, err := strconv.Atoi(parentPipeFD)
if err != nil {
return nil, false, err
}
parentPipe := os.NewFile(uintptr(fd), "parent-pipe")
return func(message string) {
defer parentPipe.Close()
if n, err := fmt.Fprintln(parentPipe, message); n != len(message)+1 || err != nil {
// No need to stop the agent if we fail to write back to
// the parent. The agent is otherwise healthy.
fmt.Fprintf(env.Stderr, "Failed to write %v to parent: (%d, %v)\n", message, n, err)
}
}, true, nil
}
func idleWatch(env *cmdline.Env, ipc server.IPCState, noConnections chan struct{}, channels commandChannels) {
defer close(noConnections)
grace := idleGrace
var idleStartOverride time.Time
idleDuration := func() time.Duration {
idleStart := ipc.IdleStartTime()
if idleStart.IsZero() {
return 0
}
if idleStart.Before(idleStartOverride) {
idleStart = idleStartOverride
}
return time.Now().Sub(idleStart)
}
for {
var sleepCh <-chan time.Time
if grace > 0 {
idle := idleDuration()
if idle > grace {
fmt.Fprintln(env.Stderr, "IDLE for", idle, "exiting.")
return
}
sleepFor := grace - idle
if sleepFor < time.Millisecond {
sleepFor = time.Millisecond
}
sleepCh = time.After(sleepFor)
}
select {
case <-sleepCh:
case newGrace := <-channels.graceChange:
grace = newGrace
case graceReport := <-channels.graceQuery:
graceReport <- grace
case <-channels.idleChange:
idleStartOverride = time.Now()
case idleReport := <-channels.idleQuery:
idleReport <- idleDuration()
}
}
}
var cmdStop = &cmdline.Command{
Name: "stop",
Short: "Stops the agent",
Long: `
If an agent serving the specified credentials is running, stops the agent. If
none is running, does nothing.
`,
Runner: runner(runStop),
}
func runStop(env *cmdline.Env, args []string) error {
commandsSock := filepath.Join(constants.AgentDir(credentials), "commands")
switch _, err := os.Stat(commandsSock); {
case os.IsNotExist(err):
fmt.Fprintf(env.Stdout, "No agent appears to be running for \"%s\".\n", credentials)
return nil
case err != nil:
return err
}
cmds, err := net.Dial("unix", commandsSock)
if err != nil {
return err
}
defer cmds.Close()
if _, err := cmds.Write([]byte("EXIT\n")); err != nil {
return err
}
cmdsRead := bufio.NewScanner(cmds)
if cmdsRead.Scan() && cmdsRead.Text() != "OK" {
return fmt.Errorf("unexpected reply for EXIT command: %v", cmdsRead.Text())
}
return cmdsRead.Err()
}