Oz setup utility, handling of launching from diverted binary, handling of launching in running sandbox

master
xSmurf 9 years ago
parent 00d1aabc25
commit 8ff505f2e3

@ -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)
}
}

@ -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,

@ -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 {

@ -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 {

@ -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 {

@ -33,6 +33,7 @@ type ListProfilesResp struct {
type LaunchMsg struct {
Index int "Launch"
Name string
Args []string
Env []string
}

@ -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 {

@ -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"})

@ -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),
)

@ -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)
}
}

@ -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 {

Loading…
Cancel
Save