diff --git a/cmd/oz-setup/main.go b/cmd/oz-setup/main.go new file mode 100644 index 0000000..1f35805 --- /dev/null +++ b/cmd/oz-setup/main.go @@ -0,0 +1,311 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + "syscall" + + "github.com/subgraph/oz" + + "github.com/codegangsta/cli" +) + +var PathDpkgDivert string +var PathDpkg string +var OzConfig *oz.Config +var OzProfiles *oz.Profiles +var OzProfile *oz.Profile + +func init() { + checkRoot() + PathDpkgDivert = checkDpkgDivert() + PathDpkg = checkDpkg() +} + +func main() { + app := cli.NewApp() + + app.Name = "oz-utils" + app.Usage = "command line interface to install, remove, and create Oz sandboxes\nYou can specify a package name, a binary path, or a Oz profile file " + app.Author = "Subgraph" + app.Email = "info@subgraph.com" + app.Version = "0.0.1" + app.EnableBashCompletion = true + + flagsHookMode := []cli.Flag{ + cli.BoolFlag{ + Name: "hook", + Usage: "Run in hook mode, not normally used by the end user", + }, + } + + app.Commands = []cli.Command{ + { + Name: "config", + Usage: "check and show Oz configurations", + Subcommands: []cli.Command{ + { + Name: "check", + Usage: "check oz configuration and profiles for errors", + Action: handleConfigcheck, + }, + { + Name: "show", + Usage: "prints ouf oz configuration", + Action: handleConfigshow, + }, + }, + }, + { + Name: "install", + Usage: "install binary diversion for a program", + Action: handleInstall, + Flags: flagsHookMode, + }, + { + Name: "remove", + Usage: "remove a binary diversion for a program", + Action: handleRemove, + Flags: flagsHookMode, + }, + { + Name: "status", + Usage: "show the status of a binary diversion for a program", + Action: handleStatus, + }, + { + Name: "create", + Usage: "create a new sandbox profile", + Action: handleCreate, + }, + } + + app.Run(os.Args) +} + +func handleConfigcheck(c *cli.Context) { + fmt.Println("Here be dragons!") + os.Exit(1) +} + +func handleConfigshow(c *cli.Context) { + handleConfigcheck(c) +} + +func handleInstall(c *cli.Context) { + OzConfig = loadConfig() + pname := c.Args()[0] + OzProfile, err := loadProfile(pname, OzConfig.ProfileDir) + if err != nil || OzProfile == nil { + installExit(c.Bool("hook"), fmt.Errorf("Unable to load profiles for %s.\n", pname)) + return // For clarity + } + + if OzConfig.DivertSuffix == "" { + installExit(c.Bool("hook"), fmt.Errorf("Divert requires a suffix to be set.\n")) + return // For clarity + } + + isInstalled, err := isDivertInstalled(OzProfile.Path) + if err != nil { + fmt.Fprintf(os.Stderr, "Unknown error: %+v\n", err) + os.Exit(1) + } + if isInstalled == true { + fmt.Println("Divert already installed for ", OzProfile.Path) + os.Exit(0) + } + + dpkgArgs := []string{ + "--add", + "--package", + "oz", + "--rename", + "--divert", + getBinaryPath(OzProfile.Path), + OzProfile.Path, + } + + _, err = exec.Command(PathDpkgDivert, dpkgArgs...).Output() + if err != nil { + fmt.Fprintf(os.Stderr, "Dpkg divert command `%s %+s` failed: %s", PathDpkgDivert, dpkgArgs, err) + os.Exit(1) + } + + err = syscall.Symlink(OzConfig.ClientPath, OzProfile.Path) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create symlink %s", err) + os.Exit(1) + } + + fmt.Printf("Successfully installed Oz sandbox for: %s.\n", OzProfile.Path) +} + +func handleRemove(c *cli.Context) { + OzConfig = loadConfig() + pname := c.Args()[0] + OzProfile, err := loadProfile(pname, OzConfig.ProfileDir) + if err != nil || OzProfile == nil { + installExit(c.Bool("hook"), fmt.Errorf("Unable to load profiles for %s.\n", pname)) + return // For clarity + } + + if OzConfig.DivertSuffix == "" { + installExit(c.Bool("hook"), fmt.Errorf("Divert requires a suffix to be set.\n")) + return // For clarity + } + + isInstalled, err := isDivertInstalled(OzProfile.Path) + if err != nil { + fmt.Fprintf(os.Stderr, "Unknown error: %+v\n", err) + os.Exit(1) + } + if isInstalled == false { + fmt.Println("Divert is not installed for ", OzProfile.Path) + os.Exit(0) + } + + os.Remove(OzProfile.Path) + + dpkgArgs := []string{ + "--rename", + "--package", + "oz", + "--remove", + OzProfile.Path, + } + + _, err = exec.Command(PathDpkgDivert, dpkgArgs...).Output() + if err != nil { + fmt.Fprintf(os.Stderr, "Dpkg divert command `%s %+s` failed: %s", PathDpkgDivert, dpkgArgs, err) + os.Exit(1) + } + + fmt.Printf("Successfully remove jail for: %s.\n", OzProfile.Path) +} + +func handleStatus(c *cli.Context) { + OzConfig = loadConfig() + pname := c.Args()[0] + OzProfile, err := loadProfile(pname, OzConfig.ProfileDir) + if err != nil || OzProfile == nil { + fmt.Fprintf(os.Stderr, "Unable to load profiles (%s).\n", err) + os.Exit(1) + } + + if OzConfig.DivertSuffix == "" { + fmt.Fprintf(os.Stderr, "Divert requires a suffix to be set.\n") + os.Exit(1) + } + + isInstalled, err := isDivertInstalled(OzProfile.Path) + if err != nil { + fmt.Fprintf(os.Stderr, "Unknown error: %+v\n", err) + os.Exit(1) + } + if isInstalled { + fmt.Println("Package divert is \033[0;32minstalled\033[0m for: ", OzProfile.Path) + } else { + fmt.Println("Package divert is \033[0;31mnot installed\033[0m for: ", OzProfile.Path) + } + +} + +func handleCreate(c *cli.Context) { + OzConfig = loadConfig() + + fmt.Println("The weasels ran off with this command... please come back later!") + os.Exit(1) +} + +/* +* UTILITIES + */ + +func checkRoot() { + if os.Getuid() != 0 { + fmt.Fprintf(os.Stderr, "%s should be used as root.\n", os.Args[0]) + os.Exit(1) + } +} + +func checkDpkgDivert() string { + ddpath, err := exec.LookPath("dpkg-divert") + if err != nil { + fmt.Fprintln(os.Stderr, "You do not appear to have dpkg-divert, are you not running Debian/Ubuntu?") + os.Exit(1) + } + return ddpath +} + +func checkDpkg() string { + dpath, err := exec.LookPath("dpkg") + if err != nil { + fmt.Fprintln(os.Stderr, "You do not appear to have dpkg, are you not running Debian/Ubuntu?") + os.Exit(1) + } + return dpath +} + +func isDivertInstalled(bpath string) (bool, error) { + outp, err := exec.Command(PathDpkgDivert, "--truename", bpath).Output() + if err != nil { + return false, err + } + dpath := strings.TrimSpace(string(outp)) + + isInstalled := (dpath == getBinaryPath(string(bpath))) + if isInstalled { + _, err := os.Readlink(bpath) + if err != nil { + return false, fmt.Errorf("`%s` appears to be diverted but is not installed", dpath) + } + } + return isInstalled, nil +} + +func getBinaryPath(bpath string) string { + bpath = strings.TrimSpace(string(bpath)) + + if strings.HasSuffix(bpath, "."+OzConfig.DivertSuffix) == false { + bpath += "." + OzConfig.DivertSuffix + } + + return bpath +} + +func loadConfig() *oz.Config { + config, err := oz.LoadConfig(oz.DefaultConfigPath) + if err != nil { + if os.IsNotExist(err) { + fmt.Fprintln(os.Stderr, "Configuration file (%s) is missing, using defaults.", oz.DefaultConfigPath) + config = oz.NewDefaultConfig() + } else { + fmt.Fprintln(os.Stderr, "Could not load configuration: %s", oz.DefaultConfigPath, err) + os.Exit(1) + } + } + + return config +} + +func loadProfile(name, profileDir string) (*oz.Profile, error) { + ps, err := oz.LoadProfiles(profileDir) + if err != nil { + return nil, err + } + + return ps.GetProfileByName(name) + +} + +func installExit(hook bool, err error) { + if hook { + os.Exit(0) + } else { + fmt.Fprintf(os.Stderr, "%s", err) + os.Exit(1) + } +} diff --git a/config.go b/config.go index 4a51cd5..32af5f9 100644 --- a/config.go +++ b/config.go @@ -12,8 +12,11 @@ import ( type Config struct { ProfileDir string `json:"profile_dir"` ShellPath string `json:"shell_path"` + InitPath string `json:"init_path"` + ClientPath string `json:"client_path"` SandboxPath string `json:"sandbox_path"` BridgeMACAddr string `json:"bridge_mac"` + DivertSuffix string `json:"divert_suffix"` NMIgnoreFile string `json:"nm_ignore_file"` UseFullDev bool `json:"use_full_dev"` AllowRootShell bool `json:"allow_root_shell"` @@ -27,9 +30,12 @@ func NewDefaultConfig() *Config { return &Config{ ProfileDir: "/var/lib/oz/cells.d", ShellPath: "/bin/bash", + InitPath: "/usr/local/bin/oz-init", + ClientPath: "/usr/local/bin/oz", SandboxPath: "/srv/oz", NMIgnoreFile: "/etc/NetworkManager/conf.d/oz.conf", BridgeMACAddr: "6A:A8:2E:56:E8:9C", + DivertSuffix: "unsafe", UseFullDev: false, AllowRootShell: false, LogXpra: false, diff --git a/oz-daemon/client.go b/oz-daemon/client.go index bad0e45..41a4e38 100644 --- a/oz-daemon/client.go +++ b/oz-daemon/client.go @@ -51,7 +51,7 @@ func ListSandboxes() ([]SandboxInfo, error) { return body.Sandboxes, nil } -func Launch(arg string, env []string) error { +func Launch(arg string, args, env []string) error { idx, name, err := parseProfileArg(arg) if err != nil { return err @@ -59,6 +59,7 @@ func Launch(arg string, env []string) error { resp, err := clientSend(&LaunchMsg{ Index: idx, Name: name, + Args: args, Env: env, }) if err != nil { diff --git a/oz-daemon/daemon.go b/oz-daemon/daemon.go index c357102..d29105f 100644 --- a/oz-daemon/daemon.go +++ b/oz-daemon/daemon.go @@ -132,12 +132,17 @@ func (d *daemonState) handleLaunch(msg *LaunchMsg, m *ipc.Message) error { if err != nil { return m.Respond(&ErrorMsg{err.Error()}) } - d.Debug("Would launch %s", p.Name) - env := d.sanitizeEnvironment(p, msg.Env) - _, err = d.launch(p, env, m.Ucred.Uid, m.Ucred.Gid, d.log) - if err != nil { - d.Warning("launch of %s failed: %v", p.Name, err) - return m.Respond(&ErrorMsg{err.Error()}) + if sbox := d.getRunningSandboxByName(p.Name); sbox != nil { + d.Info("Found running sandbox for `%s`, running program there", p.Name) + sbox.launchProgram(msg.Args, d.log) + } else { + d.Debug("Would launch %s", p.Name) + env := d.sanitizeEnvironment(p, msg.Env) + _, err = d.launch(p, msg.Args, env, m.Ucred.Uid, m.Ucred.Gid, d.log) + if err != nil { + d.Warning("Launch of %s failed: %v", p.Name, err) + return m.Respond(&ErrorMsg{err.Error()}) + } } return m.Respond(&OkMsg{}) } @@ -213,6 +218,16 @@ func (d *daemonState) getProfileByIdxOrName(index int, name string) (*oz.Profile return nil, fmt.Errorf("could not find profile name '%s'", name) } +func (d *daemonState) getRunningSandboxByName(name string) *Sandbox { + for _, sb := range d.sandboxes { + if sb.profile.Name == name { + return sb + } + } + + return nil +} + func (d *daemonState) handleListSandboxes(list *ListSandboxesMsg, msg *ipc.Message) error { r := new(ListSandboxesResp) for _, sb := range d.sandboxes { diff --git a/oz-daemon/launch.go b/oz-daemon/launch.go index 4b97e07..3990f49 100644 --- a/oz-daemon/launch.go +++ b/oz-daemon/launch.go @@ -19,8 +19,6 @@ import ( "github.com/subgraph/oz/oz-init" ) -const initPath = "/usr/local/bin/oz-init" - type Sandbox struct { daemon *daemonState id int @@ -36,7 +34,7 @@ type Sandbox struct { network *network.SandboxNetwork } -func createInitCommand(name, chroot string, env []string, uid uint32, display int, stn *network.SandboxNetwork, nettype string) *exec.Cmd { +func createInitCommand(initPath, name, chroot string, env []string, uid uint32, display int, stn *network.SandboxNetwork, nettype string) *exec.Cmd { cmd := exec.Command(initPath) cmd.Dir = "/" @@ -74,7 +72,7 @@ func createInitCommand(name, chroot string, env []string, uid uint32, display in return cmd } -func (d *daemonState) launch(p *oz.Profile, env []string, uid, gid uint32, log *logging.Logger) (*Sandbox, error) { +func (d *daemonState) launch(p *oz.Profile, args, env []string, uid, gid uint32, log *logging.Logger) (*Sandbox, error) { u, err := user.LookupId(fmt.Sprintf("%d", uid)) if err != nil { return nil, fmt.Errorf("failed to lookup user for uid=%d: %v", uid, err) @@ -97,7 +95,7 @@ func (d *daemonState) launch(p *oz.Profile, env []string, uid, gid uint32, log * } } - cmd := createInitCommand(p.Name, fs.Root(), env, uid, display, stn, p.Networking.Nettype) + cmd := createInitCommand(d.config.InitPath, p.Name, fs.Root(), env, uid, display, stn, p.Networking.Nettype) log.Debug("Command environment: %+v", cmd.Env) pp, err := cmd.StderrPipe() if err != nil { @@ -131,9 +129,15 @@ func (d *daemonState) launch(p *oz.Profile, env []string, uid, gid uint32, log * return nil, fmt.Errorf("Unable to create veth networking: %+v", err) } } - + sbox.ready.Add(1) go sbox.logMessages() + + go func () { + sbox.ready.Wait() + go sbox.launchProgram(args, log) + }() + if sbox.profile.XServer.Enabled { go func() { sbox.ready.Wait() @@ -145,6 +149,13 @@ func (d *daemonState) launch(p *oz.Profile, env []string, uid, gid uint32, log * return sbox, nil } +func (sbox *Sandbox) launchProgram(args []string, log *logging.Logger) { + err := ozinit.RunProgram(sbox.addr, args) + if err != nil { + log.Error("start shell command failed: %v", err) + } +} + func (sbox *Sandbox) remove(log *logging.Logger) { sboxes := []*Sandbox{} for _, sb := range sbox.daemon.sandboxes { diff --git a/oz-daemon/protocol.go b/oz-daemon/protocol.go index ac88a41..6328ad1 100644 --- a/oz-daemon/protocol.go +++ b/oz-daemon/protocol.go @@ -33,6 +33,7 @@ type ListProfilesResp struct { type LaunchMsg struct { Index int "Launch" Name string + Args []string Env []string } diff --git a/oz-init/client.go b/oz-init/client.go index a2f9928..eae556d 100644 --- a/oz-init/client.go +++ b/oz-init/client.go @@ -41,6 +41,28 @@ func Ping(addr string) error { } } +func RunProgram(addr string, args []string) error { + c, err := clientConnect(addr) + if err != nil { + return err + } + rr, err := c.ExchangeMsg(&RunProgramMsg{Args: args}) + resp := <-rr.Chan() + rr.Done() + c.Close() + if err != nil { + return err + } + switch body := resp.Body.(type) { + case *ErrorMsg: + return errors.New(body.Msg) + case *OkMsg: + return nil + default: + return fmt.Errorf("Unexpected message type received: %+v", body) + } +} + func RunShell(addr, term string) (int, error) { c, err := clientConnect(addr) if err != nil { diff --git a/oz-init/init.go b/oz-init/init.go index dc7c751..58df85d 100644 --- a/oz-init/init.go +++ b/oz-init/init.go @@ -181,7 +181,10 @@ func (st *initState) runInit() { if syscall.Sethostname([]byte(st.profile.Name)) != nil { st.log.Error("Failed to set hostname to (%s)", st.profile.Name) } - st.log.Info("Hostname set to (%s)", st.profile.Name) + if syscall.Setdomainname([]byte("local")) != nil { + st.log.Error("Failed to set domainname") + } + st.log.Info("Hostname set to (%s.local)", st.profile.Name) if err := st.fs.OzInit(); err != nil { st.log.Error("Error: setting up filesystem failed: %v\n", err) @@ -194,10 +197,10 @@ func (st *initState) runInit() { st.startXpraServer() } st.xpraReady.Wait() - st.launchApplication() s, err := ipc.NewServer(SocketAddress, messageFactory, st.log, handlePing, + st.handleRunProgram, st.handleRunShell, ) if err != nil { @@ -268,17 +271,21 @@ func (st *initState) readXpraOutput(r io.ReadCloser) { } } -func (st *initState) launchApplication() { - cmd := exec.Command(st.profile.Path) +func (st *initState) launchApplication(cmdArgs []string) (*exec.Cmd, error) { + suffix := "" + if st.config.DivertSuffix != "" { + suffix = "."+st.config.DivertSuffix + } + cmd := exec.Command(st.profile.Path+suffix) stdout, err := cmd.StdoutPipe() if err != nil { st.log.Warning("Failed to create stdout pipe: %v", err) - return + return nil, err } stderr, err := cmd.StderrPipe() if err != nil { st.log.Warning("Failed to create stderr pipe: %v", err) - return + return nil, err } cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr.Credential = &syscall.Credential{ @@ -286,14 +293,17 @@ func (st *initState) launchApplication() { Gid: uint32(st.gid), } cmd.Env = append(cmd.Env, st.launchEnv...) + cmd.Args = append(cmd.Args, cmdArgs...) if err := cmd.Start(); err != nil { st.log.Warning("Failed to start application (%s): %v", st.profile.Path, err) - return + return nil, err } st.addChildProcess(cmd) go st.readApplicationOutput(stdout, "stdout") go st.readApplicationOutput(stderr, "stderr") + + return cmd, nil } func (st *initState) readApplicationOutput(r io.ReadCloser, label string) { @@ -321,6 +331,18 @@ func handlePing(ping *PingMsg, msg *ipc.Message) error { return msg.Respond(&PingMsg{Data: ping.Data}) } +func (st *initState) handleRunProgram(rp *RunProgramMsg, msg *ipc.Message) error { + st.log.Info("Run program message received: %+v", rp) + _, err := st.launchApplication(rp.Args) + if err != nil { + err := msg.Respond(&ErrorMsg{Msg: err.Error()}) + return err + } else { + err := msg.Respond(&OkMsg{}) + return err + } +} + func (st *initState) handleRunShell(rs *RunShellMsg, msg *ipc.Message) error { if msg.Ucred == nil { return msg.Respond(&ErrorMsg{"No credentials received for RunShell command"}) diff --git a/oz-init/protocol.go b/oz-init/protocol.go index 842cf5f..b53177b 100644 --- a/oz-init/protocol.go +++ b/oz-init/protocol.go @@ -18,9 +18,14 @@ type RunShellMsg struct { Term string "RunShell" } +type RunProgramMsg struct { + Args []string "RunProgram" +} + var messageFactory = ipc.NewMsgFactory( new(OkMsg), new(ErrorMsg), new(PingMsg), new(RunShellMsg), + new(RunProgramMsg), ) diff --git a/oz/main.go b/oz/main.go index 9f1e3df..e28a794 100644 --- a/oz/main.go +++ b/oz/main.go @@ -2,15 +2,53 @@ package main import ( "fmt" - "github.com/codegangsta/cli" - "github.com/subgraph/oz/oz-daemon" - "github.com/subgraph/oz/oz-init" "io" "os" + "path" "strconv" + + "github.com/subgraph/oz/oz-daemon" + "github.com/subgraph/oz/oz-init" + + "github.com/codegangsta/cli" ) +type fnRunType func() + +var runFunc fnRunType +var runBasename string + +func init() { + runBasename = path.Base(os.Args[0]) + + switch runBasename { + case "oz": + runFunc = runApplication + default: + // TODO: Exit if already inside sandbox should only happen + runFunc = runSandbox + } +} + func main() { + runFunc() +} + +func runSandbox() { + hostname, _ := os.Hostname() + if runBasename == hostname { + fmt.Fprintf(os.Stderr, "Cannot launch from inside a sandbox.\n") + os.Exit(1) + } + + err := daemon.Launch(runBasename, os.Args[1:], os.Environ()) + if err != nil { + fmt.Fprintf(os.Stderr, "launch command failed: %v.\n", err) + os.Exit(1) + } +} + +func runApplication() { app := cli.NewApp() app.Name = "oz" @@ -75,9 +113,10 @@ func handleLaunch(c *cli.Context) { fmt.Println("Argument needed to launch command") os.Exit(1) } - err := daemon.Launch(c.Args()[0], os.Environ()) + err := daemon.Launch(c.Args()[0], c.Args()[1:], os.Environ()) if err != nil { fmt.Printf("launch command failed: %v\n", err) + os.Exit(1) } } diff --git a/profile.go b/profile.go index 0a17bc8..d861756 100644 --- a/profile.go +++ b/profile.go @@ -97,6 +97,23 @@ func (ps Profiles) GetProfileByName(name string) (*Profile, error) { return nil, nil } +func (ps Profiles) GetProfileByPath(bpath string) (*Profile, error) { + if loadedProfiles == nil { + ps, err := LoadProfiles(defaultProfileDirectory) + if err != nil { + return nil, err + } + loadedProfiles = ps + } + + for _, p := range loadedProfiles { + if p.Path == bpath { + return p, nil + } + } + return nil, nil +} + func LoadProfiles(dir string) (Profiles, error) { fs, err := ioutil.ReadDir(dir) if err != nil {