commit 6221d0595a2fa8e115ea5adddc5d9413d01b93f9 Author: brl Date: Mon Jun 1 20:02:32 2015 -0400 initial commit of oz-ng diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8368d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +oz.iml +.idea/ diff --git a/child.go b/child.go new file mode 100644 index 0000000..dc91409 --- /dev/null +++ b/child.go @@ -0,0 +1,45 @@ +package oz + +import ( + "syscall" + "github.com/op/go-logging" + "os" + "os/signal" +) + +func ReapChildProcs(log *logging.Logger, callback func(int, syscall.WaitStatus)) chan os.Signal { + sigs := make(chan os.Signal, 3) + signal.Notify(sigs, syscall.SIGCHLD) + go func() { + for { + <-sigs + handleSIGCHLD(log, callback) + } + }() + return sigs +} + +func handleSIGCHLD(log *logging.Logger, callback func(int, syscall.WaitStatus)) { + var wstatus syscall.WaitStatus + for { + pid,err := syscall.Wait4(-1, &wstatus, syscall.WNOHANG, nil) + switch err { + case syscall.ECHILD: + return + + case syscall.EINTR: + + case nil: + if pid == 0 { + return + } + callback(pid, wstatus) + + default: + if log != nil { + log.Warning("syscall.Wait4() returned error: %v", err) + } + return + } + } +} diff --git a/cmd/oz-daemon/main.go b/cmd/oz-daemon/main.go new file mode 100644 index 0000000..d487ade --- /dev/null +++ b/cmd/oz-daemon/main.go @@ -0,0 +1,8 @@ +package main +import ( + "github.com/subgraph/oz/oz-daemon" +) + +func main() { + daemon.Main() +} diff --git a/cmd/oz-init/main.go b/cmd/oz-init/main.go new file mode 100644 index 0000000..f92153b --- /dev/null +++ b/cmd/oz-init/main.go @@ -0,0 +1,7 @@ +package main +import "github.com/subgraph/oz/oz-init" + +func main() { + ozinit.Main() +} + diff --git a/fs/cleanup.go b/fs/cleanup.go new file mode 100644 index 0000000..7cf270c --- /dev/null +++ b/fs/cleanup.go @@ -0,0 +1,114 @@ +package fs + +import ( + "io/ioutil" + "sort" + "strings" + "syscall" + "errors" + "github.com/op/go-logging" +) + +func (fs *Filesystem) Cleanup() error { + if fs.base == "" { + msg := "cannot Cleanup() filesystem, fs.base is empty" + fs.log.Warning(msg) + return errors.New(msg) + } + + for { + mnts,err := getMountsBelow(fs.base) + if err != nil { + return err + } + if len(mnts) == 0 { + return nil + } + atLeastOne, lastErr := mnts.unmountAll(fs.log) + if !atLeastOne { + return lastErr + } + } +} + +func (mnts mountEntries) unmountAll(log *logging.Logger) (bool, error) { + reterr := error(nil) + atLeastOne := false + for _,m := range mnts { + if err := syscall.Unmount(m.dir, 0); err != nil { + log.Warning("Failed to unmount mountpoint %s: %v", m.dir, err) + reterr = err + } else { + atLeastOne = true + } + } + return atLeastOne, reterr +} + +type mountEntry struct { + src string + dir string + fs string + options string +} + +type mountEntries []*mountEntry + +func (m mountEntries) Len() int { return len(m) } +func (m mountEntries) Swap(i, j int) { m[i], m[j] = m[j], m[i] } +func (m mountEntries) Less(i, j int) bool { return m[i].depth() > m[j].depth() } + +func (me mountEntry) depth() int { return strings.Count(me.dir, "/") } + +func getMountsBelow(base string) (mountEntries, error) { + mnts, err := getProcMounts() + if err != nil { + return nil, err + } + sort.Sort(mnts) + var filtered mountEntries + for _, m := range mnts { + if strings.HasPrefix(m.dir, base) { + filtered = append(filtered, m) + } + } + return filtered, nil +} + +func (m mountEntries) contains(dir string) bool { + for _, mnt := range m { + if dir == mnt.dir { + return true + } + } + return false +} + +func getProcMounts() (mountEntries, error) { + lines, err := readProcMounts() + if err != nil { + return nil, err + } + var entries mountEntries + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) >= 4 { + entries = append(entries, &mountEntry{ + src: parts[0], + dir: parts[1], + fs: parts[2], + options: parts[3], + }) + } + + } + return entries, nil +} + +func readProcMounts() ([]string, error) { + content, err := ioutil.ReadFile("/proc/mounts") + if err != nil { + return nil, err + } + return strings.Split(string(content), "\n"), nil +} diff --git a/fs/fs.go b/fs/fs.go new file mode 100644 index 0000000..5f004f2 --- /dev/null +++ b/fs/fs.go @@ -0,0 +1,207 @@ +package fs + +import ( + "fmt" + "os" + "os/user" + "path" + "strconv" + "strings" + "syscall" + + "github.com/op/go-logging" + "github.com/subgraph/oz" +) + +type directory struct { + path string + empty bool +} + +type Filesystem struct { + log *logging.Logger + home string + base string + root string + userID string + noDefaults bool + noSysAndProc bool + whitelist []*mountItem + blacklist []*mountItem +} + +func (fs *Filesystem) Root() string { + return fs.root +} + +func (fs *Filesystem) addWhitelist(path, target string, readonly bool) error { + item, err := fs.newItem(path, target, readonly) + if err != nil { + return err + } + fs.whitelist = append(fs.whitelist, item) + return nil +} + +func (fs *Filesystem) addBlacklist(path string) error { + item, err := fs.newItem(path, "", false) + if err != nil { + return err + } + fs.blacklist = append(fs.blacklist, item) + return nil +} + +func (fs *Filesystem) newItem(path, target string, readonly bool) (*mountItem, error) { + p, err := fs.resolveVars(path) + if err != nil { + return nil, err + } + return &mountItem{ + path: p, + target: target, + //readonly: readonly, + fs: fs, + }, nil +} + +func NewFromProfile(profile *oz.Profile, log *logging.Logger) *Filesystem { + fs := NewFilesystem(profile.Name, log) + for _,wl := range profile.Whitelist { + fs.addWhitelist(wl.Path, wl.Path, wl.ReadOnly) + } + for _,bl := range profile.Blacklist { + fs.addBlacklist(bl.Path) + } + fs.noDefaults = profile.NoDefaults + fs.noSysAndProc = profile.NoSysProc + return fs +} + +func NewFilesystem(name string, log *logging.Logger) *Filesystem { + + fs := new(Filesystem) + fs.log = log + if log == nil { + fs.log = logging.MustGetLogger("oz") + } + fs.base = path.Join("/srv/oz", name) + fs.root = path.Join(fs.base, "rootfs") + + u, err := user.Current() + if err != nil { + panic("Failed to look up current user: " + err.Error()) + } + fs.home = u.HomeDir + fs.userID = strconv.Itoa(os.Getuid()) + + return fs +} + +/* +func xcreateEmptyDirectories(base string, paths []string) error { + for _, p := range paths { + target := path.Join(base, p) + if err := createEmptyDir(p, target); err != nil { + return err + } + } + return nil +} + +func createEmptyDir(source, target string) error { + return nil +} + +func createSubdirs(base string, subdirs []string) error { + for _, sdir := range subdirs { + path := path.Join(base, sdir) + if err := createDirTree(path); err != nil { + return err + } + } + return nil +} + +func createDirTree(path string) error { + st, err := os.Stat(path) + if err == nil { + if !st.IsDir() { + return fmt.Errorf("cannot create directory %s because path already exists and is not directory", path) + } + return nil + } + if !os.IsNotExist(err) { + return fmt.Errorf("unexpected error attempting Stat() on path %s: %v", path, err) + } + if err := os.MkdirAll(path, 0755); err != nil { + return fmt.Errorf("error creating directory tree %s: %v", path, err) + } + return nil +} +*/ + +// bindMount performs a bind mount of the source path item so that it is visible +// at the target path. By default the mount is flagged MS_NOSUID and MS_NODEV +// but additional flags can be passed in extraFlags. If the readonly flag is +// set the bind mount is remounted as MS_RDONLY. +func bindMount(source, target string, readonly bool, extraFlags uintptr) error { + flags := syscall.MS_BIND | syscall.MS_NOSUID | syscall.MS_NODEV | extraFlags + if err := syscall.Mount(source, target, "", flags, ""); err != nil { + return fmt.Errorf("failed to bind mount %s to %s: %v", source, target, err) + } + if readonly { + flags |= syscall.MS_RDONLY | syscall.MS_REMOUNT + if err := syscall.Mount("", target, "", flags, ""); err != nil { + return fmt.Errorf("failed to remount %s as RDONLY: %v", target, err) + } + } + return nil +} + +func createEmptyFile(name string, mode os.FileMode) error { + if err := os.MkdirAll(path.Dir(name), 0750); err != nil { + return err + } + fd, err := os.Create(name) + if err != nil { + return err + } + if err := fd.Close(); err != nil { + return err + } + if err := os.Chmod(name, mode); err != nil { + return err + } + return nil +} + +func copyPathPermissions(root, src string) error { + current := "/" + for _, part := range strings.Split(src, "/") { + if part == "" { + continue + } + current = path.Join(current, part) + target := path.Join(root, current) + if err := copyFilePermissions(current, target); err != nil { + return err + } + } + return nil +} + +func copyFilePermissions(src, target string) error { + fi, err := os.Stat(src) + if err != nil { + return err + } + return copyFileInfo(fi, target) +} + +func copyFileInfo(info os.FileInfo, target string) error { + st := info.Sys().(*syscall.Stat_t) + os.Chown(target, int(st.Uid), int(st.Gid)) + os.Chmod(target, info.Mode().Perm()) + return nil +} diff --git a/fs/item.go b/fs/item.go new file mode 100644 index 0000000..bcbf1e8 --- /dev/null +++ b/fs/item.go @@ -0,0 +1,183 @@ +package fs + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + "syscall" + "github.com/op/go-logging" +) + +type MountFlag int + +func (mf MountFlag) isSet(f MountFlag) bool { + return mf&f == f +} + +const ( + MountReadOnly MountFlag = 1 << iota + MountCreateIfAbsent +) + +type mountItem struct { + path string + target string + flags MountFlag + fs *Filesystem +} + +func (mi *mountItem) targetPath() string { + root := mi.fs.root + if mi.target != "" { + return path.Join(root, mi.target) + } + return path.Join(root, mi.path) +} + +func (mi *mountItem) bind() error { + if strings.Contains(mi.path, "*") { + return mi.bindGlobbed() + } + return mi.bindItem() +} + +func (mi *mountItem) bindGlobbed() error { + if mi.target != "" { + mi.fs.log.Warning("Ignoring target directory (%s) for mount item containing glob character: (%s)", mi.target, mi.path) + mi.target = "" + } + globbed, err := filepath.Glob(mi.path) + if err != nil { + return err + } + savedPath := mi.path + for _, p := range globbed { + if strings.Contains(p, "*") { + // XXX + continue + } + mi.path = p + if err := mi.bind(); err != nil { + // XXX + mi.path = savedPath + return err + } + } + mi.path = savedPath + return nil +} + +func (mi *mountItem) readSourceInfo(src string) (os.FileInfo, error) { + if fi, err := os.Stat(src); err == nil { + return fi, nil + } else if !os.IsNotExist(err) { + return nil, err + } + + if !mi.flags.isSet(MountCreateIfAbsent) { + return nil, fmt.Errorf("source path (%s) does not exist", src) + } + + if !strings.HasPrefix(src, mi.fs.home) { + return nil, fmt.Errorf("mount item (%s) has flag MountCreateIfAbsent, but is not child of home directory (%s)", src, mi.fs.home) + } + + if err := os.MkdirAll(src, 0750); err != nil { + return nil, err + } + + pinfo, err := os.Stat(path.Dir(src)) + if err != nil { + return nil, err + } + + if err := copyFileInfo(pinfo, src); err != nil { + return nil, err + } + + return os.Stat(src) +} + +func (mi *mountItem) bindItem() error { + src, err := filepath.EvalSymlinks(mi.path) + if err != nil { + return fmt.Errorf("error resolving symlinks for path (%s): %v", mi.path, err) + } + + sinfo, err := mi.readSourceInfo(src) + if err != nil { + // XXX + return err + } + + target := mi.targetPath() + _, err = os.Stat(target) + if err == nil || !os.IsNotExist(err) { + mi.fs.log.Warning("Target (%s) already exists, ignoring", target) + return nil + } + if sinfo.IsDir() { + if err := os.MkdirAll(target, sinfo.Mode().Perm()); err != nil { + return err + } + } else { + if err := createEmptyFile(target, 0750); err != nil { + return err + } + } + + if err := copyPathPermissions(mi.fs.root, src); err != nil { + return fmt.Errorf("failed to copy path permissions for (%s): %v", src, err) + } + return bindMount(src, target, mi.flags.isSet(MountReadOnly), 0) +} + +func (mi *mountItem) blacklist() error { + if strings.Contains(mi.path, "*") { + return mi.blacklistGlobbed() + } + return blacklistItem(mi.path, mi.fs.log) +} + +func (mi *mountItem) blacklistGlobbed() error { + globbed, err := filepath.Glob(mi.path) + if err != nil { + // XXX + } + for _, p := range globbed { + if err := blacklistItem(p, mi.fs.log); err != nil { + return err + } + } + return nil +} + +func blacklistItem(path string, log *logging.Logger) error { + p, err := filepath.EvalSymlinks(path) + if err != nil { + log.Warning("Symlink evaluation failed for path: %s", path) + return err + } + fi, err := os.Stat(p) + if err != nil { + if os.IsNotExist(err) { + log.Info("Blacklist item (%s) does not exist", p) + return nil + } + return err + } + + src := emptyFilePath + if fi.IsDir() { + src = emptyDirPath + } + if err := syscall.Mount(src, p, "none", syscall.MS_BIND, "mode=400,gid=0"); err != nil { + // XXX warning + return err + } + // XXX log success + + return nil +} diff --git a/fs/ozinit.go b/fs/ozinit.go new file mode 100644 index 0000000..3e4350d --- /dev/null +++ b/fs/ozinit.go @@ -0,0 +1,134 @@ +package fs + +import ( + "os" + "path" + "syscall" +) + +// OzInit is run from the oz-init process and performs post chroot filesystem initialization +func (fs *Filesystem) OzInit() error { + if err := fs.ozinitMountDev(); err != nil { + return err + } + if err := fs.ozinitMountSysProc(); err != nil { + return err + } + if err := fs.ozinitCreateSymlinks(); err != nil { + return err + } + if err := fs.ozinitBlacklistItems(); err != nil { + return err + } + return nil +} + +func (fs *Filesystem) ozinitMountDev() error { + flags := uintptr(syscall.MS_NOSUID | syscall.MS_REC | syscall.MS_NOEXEC) + if err := syscall.Mount("none", "/dev", "devtmpfs", flags, ""); err != nil { + fs.log.Warning("Failed to mount devtmpfs: %v", err) + return err + } + + if err := mountSpecial("/dev/shm", "tmpfs"); err != nil { + fs.log.Warning("Failed to mount shm directory: %v", err) + return err + } + if err := mountSpecial("/dev/pts", "devpts"); err != nil { + fs.log.Warning("Failed to mount pts directory: %v", err) + return err + } + return nil +} + +func mountSpecial(path, mtype string) error { + flags := uintptr(syscall.MS_NOSUID | syscall.MS_REC | syscall.MS_NOEXEC) + if err := os.MkdirAll(path, 0755); err != nil { + return err + } + return syscall.Mount(path, path, mtype, flags, "") +} + +func (fs *Filesystem) ozinitMountSysProc() error { + if fs.noSysAndProc { + return nil + } + flags := uintptr(syscall.MS_NOSUID | syscall.MS_REC | syscall.MS_NOEXEC | syscall.MS_NODEV) + proc := "/proc" + if err := syscall.Mount("proc", proc, "proc", flags, ""); err != nil { + fs.log.Warning("Failed to mount /proc: %v", err) + return err + } + roMounts := []string{ + "sysrq-trigger", + "bus", + "irq", + "sys/kernel/hotplug", + } + for _, rom := range roMounts { + p := path.Join(proc, rom) + if err := bindMount(p, p, true, 0); err != nil { + fs.log.Warning("Failed to RO mount %s: %v", p, err) + return err + } + } + + if err := syscall.Mount("sysfs", "/sys", "sysfs", syscall.MS_RDONLY|flags, ""); err != nil { + fs.log.Warning("Failed to mount /sys: %v", err) + return err + } + + return nil +} + +func (fs *Filesystem) ozinitCreateSymlinks() error { + for _, sl := range basicSymlinks { + if err := syscall.Symlink(sl[0], sl[1]); err != nil { + return err + } + } + return nil +} + +func (fs *Filesystem) ozinitBlacklistItems() error { + if err := createBlacklistDir(emptyDirPath); err != nil { + return err + } + if err := createBlacklistFile(emptyFilePath); err != nil { + return err + } + for _, item := range fs.blacklist { + if err := item.blacklist(); err != nil { + return err + } + } + return nil +} + +func createBlacklistDir(path string) error { + if err := os.MkdirAll(path, 0000); err != nil { + return err + } + return setBlacklistPerms(path, 0500) +} + +func createBlacklistFile(path string) error { + fd, err := os.Create(path) + if err != nil { + return err + } + if err := fd.Close(); err != nil { + return err + } + return setBlacklistPerms(path, 0400) +} + +func setBlacklistPerms(path string, mode os.FileMode) error { + if err := os.Chown(path, 0, 0); err != nil { + return err + } + if err := os.Chmod(path, mode); err != nil { + return err + } + return nil +} diff --git a/fs/resolve.go b/fs/resolve.go new file mode 100644 index 0000000..ba2c697 --- /dev/null +++ b/fs/resolve.go @@ -0,0 +1,50 @@ +package fs + +import ( + "fmt" + "os/exec" + "path" + "path/filepath" + "strings" +) + +func (fs *Filesystem) resolvePath(p string) ([]string, error) { + p, err := fs.resolveVars(p) + if err != nil { + return nil, err + } + return fs.resolveGlob(p) +} + +func (fs *Filesystem) resolveVars(p string) (string, error) { + const pathVar = "${PATH}/" + const homeVar = "${HOME}" + const uidVar = "${UID}" + + switch { + case strings.HasPrefix(p, pathVar): + resolved, err := exec.LookPath(p[len(pathVar):]) + if err != nil { + return "", fmt.Errorf("failed to resolve %s", p) + } + return resolved, nil + + case strings.HasPrefix(p, homeVar): + return path.Join(fs.home, p[len(homeVar):]), nil + + case strings.HasPrefix(p, uidVar): + return strings.Replace(p, uidVar, fs.userID, -1), nil + } + return p, nil +} + +func (fs *Filesystem) resolveGlob(p string) ([]string, error) { + if !strings.Contains(p, "*") { + return []string{p}, nil + } + list, err := filepath.Glob(p) + if err != nil { + return nil, fmt.Errorf("failed to glob resolve %s: %v", p, err) + } + return list, nil +} diff --git a/fs/setup.go b/fs/setup.go new file mode 100644 index 0000000..41e777b --- /dev/null +++ b/fs/setup.go @@ -0,0 +1,144 @@ +package fs + +import ( + "fmt" + "os" + "path" + "syscall" +) + +var basicBindDirs = []string{ + "/bin", "/lib", "/lib64", "/usr", "/etc", "/var/lib/oz", +} + +var basicEmptyDirs = []string{ + "/sbin", "/var", "/var/lib", + "/var/cache", "/home", "/boot", + "/tmp", "/run", "/run/user", + "/run/lock", "/root", "/opt", + "/srv", "/dev", "/proc", "/sys", + "/mnt", "/media", +} + +var basicBlacklist = []string{ + "/usr/sbin", "/sbin", "${PATH}/su", + "${PATH}/sudo", "${PATH}/fusermount", + "${PATH}/xinput", "${PATH}/strace", + "${PATH}/mount", "${PATH}/umount", +} + +const emptyFilePath = "/tmp/oz.ro.file" +const emptyDirPath = "/tmp/oz.ro.dir" + +var basicSymlinks = [][2]string{ + {"/run", "/var/run"}, + {"/tmp", "/var/tmp"}, + {"/run/lock", "/var/lock"}, +} + +func (fs *Filesystem) Setup() error { + if err := fs.setupRootfs(); err != nil { + return err + } + if err := fs.setupChroot(); err != nil { + return err + } + return fs.setupMountItems() +} + +func (fs *Filesystem) setupRootfs() error { + if err := os.MkdirAll(fs.base, 0755); err != nil { + return fmt.Errorf("unable to create directory (%s): %v", fs.base, err) + } + flags := uintptr(syscall.MS_NOSUID | syscall.MS_NOEXEC | syscall.MS_NODEV) + data := "mode=755,gid=0" + if err := syscall.Mount(fs.base, fs.base, "tmpfs", flags, data); err != nil { + return fmt.Errorf("failed to create base tmpfs at %s: %v", fs.base, err) + } + // create extra directories + extra := []string{"sockets"} + for _, sub := range extra { + d := path.Join(fs.base, sub) + if err := os.Mkdir(d, 0755); err != nil { + return fmt.Errorf("unable to create directory (%s): %v", d, err) + } + } + return nil +} + +func (fs *Filesystem) setupChroot() error { + var err error + if fs.noDefaults { + err = createEmptyDirectories(fs.root, basicBindDirs) + } else { + err = bindBasicDirectories(fs.root, basicBindDirs) + } + if err != nil { + return err + } + err = createEmptyDirectories(fs.root, basicEmptyDirs) + if err != nil { + return err + } + return setupTmp(fs.root) +} + +func bindBasicDirectories(root string, dirs []string) error { + for _, src := range dirs { + st, err := os.Lstat(src) + if err != nil { + return err + } + mode := st.Mode() + target := path.Join(root, src) + if err := os.MkdirAll(target, mode.Perm()); err != nil { + return err + } + if err := bindMount(src, target, true, 0); err != nil { + return err + } + } + return nil +} + +func createEmptyDirectories(root string, dirs []string) error { + for _, p := range dirs { + target := path.Join(root, p) + if err := createEmptyDirectory(p, target); err != nil { + return err + } + } + return nil +} + +func createEmptyDirectory(source, target string) error { + fi, err := os.Stat(source) + if err != nil { + return err + } + mode := fi.Mode() + if err := os.MkdirAll(target, mode.Perm()); err != nil { + return err + } + if err := copyFileInfo(fi, target); err != nil { + return err + } + return nil +} + +func setupTmp(root string) error { + target := path.Join(root, "tmp") + if err := os.Chmod(target, 0777); err != nil { + return err + } + return bindMount(target, target, false, syscall.MS_NOEXEC) +} + +func (fs *Filesystem) setupMountItems() error { + for _, item := range fs.whitelist { + if err := item.bind(); err != nil { + // XXX + } + } + return nil +} diff --git a/ipc/handlers.go b/ipc/handlers.go new file mode 100644 index 0000000..e317300 --- /dev/null +++ b/ipc/handlers.go @@ -0,0 +1,82 @@ +package ipc +import ( + "reflect" + "errors" + "fmt" +) + +type handlerMap map[string]reflect.Value + +func (handlers handlerMap) dispatch(m *Message) error { + h,ok := handlers[m.Type] + if !ok { + return errors.New("no handler found for message type:"+ m.Type) + } + return executeHandler(h, m) +} + +func executeHandler(h reflect.Value, m *Message) error { + var args [2]reflect.Value + args[0] = reflect.ValueOf(m.Body) + args[1]= reflect.ValueOf(m) + + rs := h.Call(args[:]) + if len(rs) != 1 { + return errors.New("handler function did not return expected single result value") + } + if rs[0].IsNil() { + return nil + } + return rs[0].Interface().(error) +} + +func (handlers handlerMap) addHandler(h interface{}) error { + msgType, err := typeCheckHandler(h) + if err != nil { + return err + } + if _,ok := handlers[msgType]; ok{ + return fmt.Errorf("duplicate handler registered for message type '%s'", msgType) + } + handlers[msgType] = reflect.ValueOf(h) + return nil +} + +var errType = reflect.TypeOf((*error)(nil)).Elem() +var messageType = reflect.TypeOf((*Message)(nil)) + +func typeCheckHandler(h interface{}) (string, error) { + t := reflect.TypeOf(h) + if t.Kind() != reflect.Func { + return "", fmt.Errorf("handler %v is not a function", t) + } + if t.NumIn() != 2 { + return "", fmt.Errorf("handler %v has incorrect number of input arguments, got %d", t, t.NumIn()) + } + if t.NumOut() != 1 { + return "", fmt.Errorf("handler %v has incorrect number of return values %d", t, t.NumOut()) + } + if t.In(0).Kind() != reflect.Ptr { + return "", errors.New("first argument of handler is not a pointer") + } + in0 := t.In(0).Elem() + if in0.Kind() != reflect.Struct { + return "", fmt.Errorf("first argument of handler is not a pointer to struct") + } + if in1 := t.In(1); !in1.AssignableTo(messageType) { + return "", fmt.Errorf("second argument of handler must have type *Message") + } + if out := t.Out(0); !out.AssignableTo(errType) { + return "", fmt.Errorf("return type of handler must be error") + } + + if in0.NumField() == 0 { + return "", fmt.Errorf("first argument structure has no fields") + } + if len(in0.Field(0).Tag) == 0 { + return "", fmt.Errorf("first argument structure, first field has no tag") + } + return string(in0.Field(0).Tag), nil +} + + diff --git a/ipc/handlers_test.go b/ipc/handlers_test.go new file mode 100644 index 0000000..1b891a3 --- /dev/null +++ b/ipc/handlers_test.go @@ -0,0 +1,86 @@ +package ipc +import ( + "testing" + "reflect" + "errors" +) + +func TestTypeCheckHandler(t *testing.T) { + type testStruct struct {} + + cases := []interface{} { + "foo", + func(){}, + func(a,b int){}, + func(a *testStruct, b *Message) {}, + func(a,b *int) error{return nil}, + func(a *testStruct, b int) error {return nil}, + func(a *testStruct, b Message) error {return nil}, + func(a *testStruct, b *Message) int{return 0}, + func(a *testStruct, b *Message, c int) error {return nil}, + } + + for i,h := range cases { + if _,err := typeCheckHandler(h); err == nil { + t.Errorf("typeCheckHandler should return an error for case %d", i) + } + } +} + +func TestAddHandler(t *testing.T) { + hmap := handlerMap(make(map[string]reflect.Value)) + type testStruct struct{ + t int "tst" + } + legit := func(ts *testStruct, m *Message) error { return nil } + + if err := hmap.addHandler("bar"); err == nil { + t.Error("attempt to register string as handler function did not fail as expected") + } + + if err := hmap.addHandler(legit); err != nil { + t.Error("registration of good handler function failed:", err) + } + + if err := hmap.addHandler(legit); err == nil { + t.Error("registration of duplicate handler function did not fail") + } +} + +func TestDispatch(t *testing.T) { + type testStruct struct{ t int "tester"} + type testStruct2 struct{ t int "tester2"} + count := 0 + h1 := func(ts *testStruct, m *Message) error { + count += 1 + return nil + } + + h2 := func(ts *testStruct2, m *Message) error { + count += 1 + return errors.New("...") + } + + hmap := handlerMap(make(map[string]reflect.Value)) + if err := hmap.addHandler(h1); err != nil { + t.Errorf("unexpected failure to register handler: %v", err) + } + if err := hmap.addHandler(h2); err != nil { + t.Errorf("unexpected failure to register handler: %v", err) + } + m := new(Message) + m.Type = "tester" + m.Body = new(testStruct) + if err := hmap.dispatch(m); err != nil { + t.Error("unexpected error calling dispatch():", err) + } + m.Type = "tester2" + m.Body = new(testStruct2) + if err := hmap.dispatch(m); err == nil { + t.Errorf("dispatch() did not return error as expected") + + } + if count != 2 { + t.Errorf("count was not incremented to 2 as expected. count = %d", count) + } +} diff --git a/ipc/ipc.go b/ipc/ipc.go new file mode 100644 index 0000000..863ca32 --- /dev/null +++ b/ipc/ipc.go @@ -0,0 +1,288 @@ +package ipc + +import ( + "encoding/json" + "errors" + "net" + "syscall" + + "github.com/op/go-logging" + "reflect" + "crypto/rand" + "encoding/hex" + "fmt" +) + +const maxFdCount = 3 + +var log = logging.MustGetLogger("oz") + +type MsgConn struct { + msgs chan *Message + addr *net.UnixAddr + conn *net.UnixConn + buf [1024]byte + oob []byte + handlers handlerMap + factory MsgFactory + isClosed bool + done chan bool + idGen <-chan int + respMan *responseManager +} + +func NewMsgConn(factory MsgFactory, address string) *MsgConn { + mc := new(MsgConn) + mc.addr = &net.UnixAddr{address, "unixgram"} + mc.oob = createOobBuffer() + mc.msgs = make(chan *Message) + mc.handlers = make(map[string]reflect.Value) + mc.factory = factory + mc.done = make(chan bool) + mc.idGen = newIdGen(mc.done) + mc.respMan = newResponseManager() + return mc +} + +func newIdGen(done <-chan bool) <-chan int { + ch := make(chan int) + go idGenLoop(done, ch) + return ch +} + +func idGenLoop(done <-chan bool, out chan <- int) { + current := int(1) + for { + select { + case out <- current: + current += 1 + case <-done: + return + } + } +} + +func (mc *MsgConn) Listen() error { + if mc.conn != nil { + return errors.New("cannot Listen(), already connected") + } + conn, err := net.ListenUnixgram("unixgram", mc.addr) + if err != nil { + return err + } + if err := setPassCred(conn); err != nil { + return err + } + mc.conn = conn + return nil +} + +func (mc *MsgConn) Connect() error { + if mc.conn != nil { + return errors.New("cannot Connect(), already connected") + } + clientAddr,err := CreateRandomAddress("@oz-") + if err != nil { + return err + } + conn, err := net.DialUnix("unixgram", &net.UnixAddr{clientAddr, "unixgram"}, nil) + if err != nil { + return err + } + mc.conn = conn + go mc.readLoop() + return nil +} + +func CreateRandomAddress(prefix string) (string,error) { + var bs [16]byte + n,err := rand.Read(bs[:]) + if n != len(bs) { + return "", errors.New("incomplete read of random bytes for client name") + } + if err != nil { + return "", errors.New("error reading random bytes for client name: "+ err.Error()) + } + return prefix+ hex.EncodeToString(bs[:]),nil +} + +func (mc *MsgConn) Run() error { + go mc.readLoop() + for m := range mc.msgs { + if err := mc.handlers.dispatch(m); err != nil { + return fmt.Errorf("error dispatching message: %v", err) + } + } + return nil +} + +func (mc *MsgConn) readLoop() { + for { + if mc.processOneMessage() { + return + } + } +} + +func (mc *MsgConn) processOneMessage() bool { + m,err := mc.readMessage() + if err != nil { + close(mc.msgs) + if !mc.isClosed { + log.Warning("error on MsgConn.readMessage(): %v", err) + } + return true + } + if !mc.respMan.handle(m) { + mc.msgs <- m + } + return false +} + +func (mc *MsgConn) Close() error { + mc.isClosed = true + close(mc.done) + return mc.conn.Close() +} + +func createOobBuffer() []byte { + oobSize := syscall.CmsgSpace(syscall.SizeofUcred) + syscall.CmsgSpace(4*maxFdCount) + return make([]byte, oobSize) +} + +func (mc *MsgConn) readMessage() (*Message, error) { + n, oobn, _, a, err := mc.conn.ReadMsgUnix(mc.buf[:], mc.oob) + if err != nil { + return nil, err + } + m, err := mc.parseMessage(mc.buf[:n]) + if err != nil { + return nil, err + } + m.mconn = mc + m.Peer = a + + if oobn > 0 { + err := m.parseControlData(mc.oob[:oobn]) + if err != nil { + } + } + return m, nil +} + +// AddHandlers registers a list of message handling functions with a MsgConn instance. +// Each handler function must have two arguments and return a single error value. The +// first argument must be pointer to a message structure type. A message structure type +// is a structure that must have a struct tag on the first field: +// +// type FooMsg struct { +// Stuff string "Foo" // <------ struct tag +// // etc... +// } +// +// type SimpleMsg struct { +// dummy int "Simple" // struct has no fields, so add an unexported dummy field just for the tag +// } +// +// The second argument to a handler function must have type *ipc.Message. After a handler function +// has been registered, received messages matching the first argument will be dispatched to the corresponding +// handler function. +// +// func fooHandler(foo *FooMsg, msg *ipc.Message) error { /* ... */ } +// func simpleHandler(simple *SimpleMsg, msg *ipc.Message) error { /* ... */ } +// +// /* register fooHandler() to handle incoming FooMsg and SimpleHandler to handle SimpleMsg */ +// conn.AddHandlers(fooHandler, simpleHandler) +// + + +func (mc *MsgConn) AddHandlers(args ...interface{}) error { + for len(args) > 0 { + if err := mc.handlers.addHandler(args[0]); err != nil { + return err + } + args = args[1:] + } + return nil +} + +func (mc *MsgConn) SendMsg(msg interface{}, fds... int) error { + return mc.sendMessage(msg, <-mc.idGen, mc.addr, fds...) +} + +func (mc *MsgConn) ExchangeMsg(msg interface{}, fds... int) (ResponseReader, error) { + id := <-mc.idGen + rr := mc.respMan.register(id) + + if err := mc.sendMessage(msg, id, mc.addr, fds...); err != nil { + rr.Done() + return nil, err + } + return rr,nil +} + +func (mc *MsgConn) sendMessage(msg interface{}, msgID int, dst *net.UnixAddr, fds... int) error { + msgType, err := getMessageType(msg) + if err != nil { + return err + } + base, err := mc.newBaseMessage(msgType, msgID, msg) + if err != nil { + return err + } + raw, err := json.Marshal(base) + if err != nil { + return err + } + return mc.sendRaw(raw, dst, fds...) +} + +func getMessageType(msg interface{}) (string, error) { + t := reflect.TypeOf(msg) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return "", fmt.Errorf("sendMessage() msg (%T) is not a struct", msg) + } + if t.NumField() == 0 || len(t.Field(0).Tag) == 0 { + return "", fmt.Errorf("sendMessage() msg struct (%T) does not have tag on first field") + } + return string(t.Field(0).Tag), nil +} + + +func (mc *MsgConn) newBaseMessage(msgType string, msgID int, body interface{}) (*BaseMsg, error) { + bodyBytes,err := json.Marshal(body) + if err != nil { + return nil, err + } + base := new(BaseMsg) + base.Type = msgType + base.MsgID = msgID + base.Body = bodyBytes + return base, nil +} + +func (mc *MsgConn) sendRaw(data []byte, dst *net.UnixAddr, fds ...int) error { + if len(fds) > 0 { + return mc.sendWithFds(data, dst, fds) + } + return mc.write(data, dst) +} + +func (mc *MsgConn) write(data []byte, dst *net.UnixAddr) error { + if dst != nil { + _,err := mc.conn.WriteToUnix(data, dst) + return err + } + _,err := mc.conn.Write(data) + return err +} + +func (mc *MsgConn) sendWithFds(data []byte, dst *net.UnixAddr, fds []int) error { + oob := syscall.UnixRights(fds...) + _,_,err := mc.conn.WriteMsgUnix(data, oob, dst) + return err +} + diff --git a/ipc/ipc_test.go b/ipc/ipc_test.go new file mode 100644 index 0000000..34f83c7 --- /dev/null +++ b/ipc/ipc_test.go @@ -0,0 +1,100 @@ +package ipc +import ( + "testing" + "sync" + "os" +) + +type TestMsg struct { + t int "Test" +} + +type testConnection struct { + server *MsgConn + client *MsgConn + wg sync.WaitGroup + called bool +} + +type testServer struct { + conn *MsgConn + wg sync.WaitGroup +} +const testSocket = "@test" +var testFactory = NewMsgFactory(new(TestMsg)) + +func testConnect(handler func(*TestMsg, *Message) error) (*testConnection, error) { + s := NewMsgConn(testFactory, testSocket) + c := NewMsgConn(testFactory, testSocket) + tc := &testConnection{ + server: s, + client: c, + } + wrapper := func(tm *TestMsg, msg *Message) error { + err := handler(tm, msg) + tc.called = true + tc.wg.Done() + return err + } + if err := s.AddHandlers(wrapper); err != nil { + return nil, err + } + if err := s.Listen(); err != nil { + return nil, err + } + if err := c.Connect(); err != nil { + return nil, err + } + tc.wg.Add(1) + go tc.server.Run() + return tc, nil +} + +func runTest(t *testing.T, handler func(*TestMsg, *Message) error, tester func(*testConnection)) { + tc,err := testConnect(handler) + if err != nil { + t.Error("error setting up test connection:", err) + } + tester(tc) + tc.wait() + if !tc.called { + t.Error("handler function not called") + } +} + +func (tc *testConnection) wait() { + tc.wg.Wait() + tc.client.Close() + tc.server.Close() +} + +func TestUcred(t *testing.T) { + handler := func(tm *TestMsg, msg *Message) error { + uid := uint32(os.Getuid()) + gid := uint32(os.Getgid()) + pid := int32(os.Getpid()) + u := msg.Ucred + if u.Uid != uid || u.Gid != gid || u.Pid != pid { + t.Errorf("ucred (%d/%d/%d) does not match process (%d/%d/%d)", u.Uid, u.Gid, u.Pid, uid, gid, pid) + } + return nil + } + runTest(t, handler, func(tc *testConnection) { + tc.client.SendMsg(&TestMsg{}) + }) + +} + +func TestPassFDs(t *testing.T) { + fds := []int{1,2} + handler := func(tm *TestMsg, msg *Message) error { + if len(msg.Fds) != len(fds) { + t.Errorf("Expecting %d descriptors, got %d", len(fds), len(msg.Fds)) + } + return nil + } + runTest(t, handler, func(tc *testConnection) { + tc.client.SendMsg(&TestMsg{}, fds...) + + }) +} \ No newline at end of file diff --git a/ipc/message.go b/ipc/message.go new file mode 100644 index 0000000..6320bbc --- /dev/null +++ b/ipc/message.go @@ -0,0 +1,124 @@ +package ipc + +import ( + "encoding/json" + "net" + "syscall" + "fmt" + "reflect" + "errors" +) + + + +func NewMsgFactory(msgTypes ...interface{}) MsgFactory { + mf := (MsgFactory)(make(map[string]func() interface{})) + for _, mt := range msgTypes { + if err := mf.register(mt); err != nil { + log.Fatalf("failed adding (%T) in NewMsgFactory: %v", mt, err) + return nil + } + } + return mf +} + +type MsgFactory map[string](func() interface{}) + +func (mf MsgFactory) create(msgType string) (interface{}, error) { + f,ok := mf[msgType] + if !ok { + return nil, fmt.Errorf("cannot create msg type: %s", msgType) + } + return f(), nil +} + +func (mf MsgFactory) register(mt interface{}) error { + t := reflect.TypeOf(mt) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return errors.New("not a structure") + } + if t.NumField() == 0 || len(t.Field(0).Tag) == 0 { + return errors.New("no tag on first field of structure") + } + tag := string(t.Field(0).Tag) + + mf[tag] = func() interface{} { + v := reflect.New(t) + return v.Interface() + } + return nil +} + +type Message struct { + Type string + MsgID int + Body interface{} + Peer *net.UnixAddr + Ucred *syscall.Ucred + Fds []int + mconn *MsgConn +} + +type BaseMsg struct { + Type string + MsgID int + IsResponse bool + Body json.RawMessage +} + +func (mc *MsgConn) parseMessage(data []byte) (*Message, error) { + var base BaseMsg + if err := json.Unmarshal(data, &base); err != nil { + return nil, err + } + body,err := mc.factory.create(base.Type) + if err != nil { + return nil, err + } + if err := json.Unmarshal(base.Body, body); err != nil { + return nil, err + } + m := new(Message) + m.Type = base.Type + m.MsgID = base.MsgID + m.Body = body + return m,nil +} + +func (m *Message) Free() { + for _,fd := range m.Fds { + syscall.Close(fd) + } + m.Fds = nil +} + +func (m *Message) parseControlData(data []byte) error { + cmsgs, err := syscall.ParseSocketControlMessage(data) + if err != nil { + return err + } + for _, cmsg := range cmsgs { + switch cmsg.Header.Type { + case syscall.SCM_CREDENTIALS: + cred, err := syscall.ParseUnixCredentials(&cmsg) + if err != nil { + return err + } + m.Ucred = cred + case syscall.SCM_RIGHTS: + fds, err := syscall.ParseUnixRights(&cmsg) + if err != nil { + return err + } + m.Fds = fds + } + } + return nil +} + +func (m *Message) Respond(msg interface{}, fds... int) error { + return m.mconn.sendMessage(msg, m.MsgID, m.Peer, fds...) +} diff --git a/ipc/response.go b/ipc/response.go new file mode 100644 index 0000000..d859228 --- /dev/null +++ b/ipc/response.go @@ -0,0 +1,78 @@ +package ipc +import ( + "time" + "sync" +) + +type ResponseReader interface { + Chan() <-chan *Message + Done() +} + +type responseWaiter struct { + rm *responseManager + id int + timeout time.Time + ch chan *Message +} + +func (rw *responseWaiter) Chan() <-chan *Message { + return rw.ch +} + +func (rw *responseWaiter) Done() { + rw.rm.lock.Lock() + defer rw.rm.lock.Unlock() + close(rw.ch) + delete(rw.rm.responseMap, rw.id) +} + +type responseManager struct { + lock sync.Locker + responseMap map[int]*responseWaiter +} + +func newResponseManager() *responseManager { + rm := new(responseManager) + rm.lock = new(sync.Mutex) + rm.responseMap = make(map[int]*responseWaiter) + return rm +} + +func (rm *responseManager) register(id int) ResponseReader { + ch := make(chan *Message) + rm.lock.Lock() + defer rm.lock.Unlock() + rm.removeById(id, true) + rw := &responseWaiter{ + rm: rm, + id: id, + ch: ch, + } + rm.responseMap[id] = rw + return rw +} + +func (rm *responseManager) handle(m *Message) bool { + rm.lock.Lock() + defer rm.lock.Unlock() + rw := rm.responseMap[m.MsgID] + if rw == nil { + return false + } + rw.ch <- m + return true +} + +func (rm *responseManager) removeById(id int, klose bool) *responseWaiter{ + rw := rm.responseMap[id] + if rw == nil { + return nil + } + delete(rm.responseMap, id) + if klose { + close(rw.ch) + } + return rw +} + diff --git a/ipc/response_test.go b/ipc/response_test.go new file mode 100644 index 0000000..0b5c6df --- /dev/null +++ b/ipc/response_test.go @@ -0,0 +1,58 @@ +package ipc +import ( + "testing" +) + +func TestRegister(t *testing.T) { + rm := newResponseManager() + if len(rm.responseMap) != 0 { + t.Error("responseMap should be empty") + } + rm.register(1) + rm.register(2) + rm.register(1) + if len(rm.responseMap) != 2 { + t.Errorf("responseMap should have 2 items, not %d", len(rm.responseMap)) + } +} + +func TestRemoveById(t *testing.T) { + rm := newResponseManager() + rm.register(1) + rm.register(2) + rm.register(3) + + rm.removeById(2, true) + rm.removeById(2, true) + + if len(rm.responseMap) != 2 { + t.Errorf("responseMap should have 2 items, not %d", len(rm.responseMap)) + } + rm.removeById(1, true) + rm.removeById(3, true) + if len(rm.responseMap) != 0 { + t.Errorf("responseMap should have 0 items, not %d", len(rm.responseMap)) + } +} + +func TestHandle(t *testing.T) { + m := new(Message) + rm := newResponseManager() + rr := rm.register(1) + rm.register(2) + m.MsgID = 3 + if rm.handle(m) { + t.Errorf("handle() should have returned false") + } + go func() { + <-rr.Chan() + }() + m.MsgID = 1 + if !rm.handle(m) { + t.Errorf("handle() should have returned true") + } + if len(rm.responseMap) != 2 { + t.Errorf("responseMap should have 2 items after handling message") + + } +} diff --git a/ipc/util.go b/ipc/util.go new file mode 100644 index 0000000..f056a90 --- /dev/null +++ b/ipc/util.go @@ -0,0 +1,26 @@ +package ipc + +import ( + "net" + "reflect" + "syscall" +) + +func setPassCred(c net.Conn) error { + fd := reflectFD(c) + return syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_PASSCRED, 1) +} + +func reflectFD(c net.Conn) int { + sysfd := extractField(c, "fd", "sysfd") + return int(sysfd.Int()) +} + +func extractField(ob interface{}, fieldNames ...string) reflect.Value { + v := reflect.Indirect(reflect.ValueOf(ob)) + for _, fn := range fieldNames { + field := v.FieldByName(fn) + v = reflect.Indirect(field) + } + return v +} diff --git a/oz-daemon/client.go b/oz-daemon/client.go new file mode 100644 index 0000000..3df665a --- /dev/null +++ b/oz-daemon/client.go @@ -0,0 +1,141 @@ +package daemon +import ( + "github.com/subgraph/oz/ipc" + "errors" + "strconv" + "fmt" +) + +func clientConnect() (*ipc.MsgConn, error) { + c := ipc.NewMsgConn(messageFactory, SocketName) + if err := c.Connect(); err != nil { + return nil, err + } + return c, nil +} + +func clientSend(msg interface{}) (*ipc.Message, error) { + c,err := clientConnect() + if err != nil { + return nil, err + } + rr, err := c.ExchangeMsg(msg) + resp := <- rr.Chan() + rr.Done() + c.Close() + if err != nil { + return nil, err + } + return resp,nil +} + +func ListProfiles() ([]Profile, error) { + resp,err := clientSend(new(ListProfilesMsg)) + if err != nil { + return nil, err + } + body,ok := resp.Body.(*ListProfilesResp) + if !ok { + return nil, errors.New("ListProfiles response was not expected type") + } + return body.Profiles, nil +} + +func ListSandboxes() ([]SandboxInfo, error) { + resp,err := clientSend(&ListSandboxesMsg{}) + if err != nil { + return nil, err + } + body,ok := resp.Body.(*ListSandboxesResp) + if !ok { + return nil, errors.New("ListSandboxes response was not expected type") + } + return body.Sandboxes, nil +} + +func Launch(arg string) error { + idx,name,err := parseProfileArg(arg) + if err != nil { + return err + } + resp,err := clientSend(&LaunchMsg{ + Index: idx, + Name: name, + }) + if err != nil { + return err + } + switch body := resp.Body.(type) { + case *ErrorMsg: + fmt.Printf("error was %s\n", body.Msg) + case *OkMsg: + fmt.Println("ok received") + default: + fmt.Printf("Unexpected message received %+v", body) + } + return nil +} + +func Clean(arg string) error { + idx,name,err := parseProfileArg(arg) + if err != nil { + return err + } + resp,err := clientSend(&CleanMsg{ + Index: idx, + Name: name, + }) + if err != nil { + return err + } + // TODO collapse this logic into a function like clientSend + switch body := resp.Body.(type) { + case *ErrorMsg: + return errors.New(body.Msg) + case *OkMsg: + return nil + default: + return fmt.Errorf("Unexpected message received %+v", body) + } +} + +func parseProfileArg(arg string) (int, string, error) { + if len(arg) == 0 { + return 0, "", errors.New("profile argument needed") + } + if n,err := strconv.Atoi(arg); err == nil { + return n, "", nil + } + return 0, arg, nil +} + +func Logs(count int, follow bool) (chan string, error) { + c,err := clientConnect() + if err != nil { + return nil, err + } + rr,err := c.ExchangeMsg(&LogsMsg{Count: count, Follow: follow}) + if err != nil { + return nil, err + } + out := make(chan string) + go dumpLogs(out, rr) + return out, nil +} + +func dumpLogs(out chan<- string, rr ipc.ResponseReader) { + for resp := range rr.Chan() { + switch body := resp.Body.(type) { + case *OkMsg: + rr.Done() + close(out) + return + case *LogData: + for _, ll := range body.Lines { + out <- ll + } + default: + out <- fmt.Sprintf("Unexpected response type (%T)", body) + } + } +} diff --git a/oz-daemon/config.go b/oz-daemon/config.go new file mode 100644 index 0000000..a55f49f --- /dev/null +++ b/oz-daemon/config.go @@ -0,0 +1,6 @@ +package daemon + + +type Config struct { + profileDir string `json:"profile_dir"` +} \ No newline at end of file diff --git a/oz-daemon/daemon.go b/oz-daemon/daemon.go new file mode 100644 index 0000000..54dfb7c --- /dev/null +++ b/oz-daemon/daemon.go @@ -0,0 +1,158 @@ +package daemon + +import ( + "fmt" + + "github.com/op/go-logging" + "github.com/subgraph/oz" + "github.com/subgraph/oz/ipc" + "syscall" + "github.com/subgraph/oz/fs" +) + +type daemonState struct { + log *logging.Logger + profiles oz.Profiles + sandboxes []*Sandbox + nextSboxId int + memBackend *logging.ChannelMemoryBackend + backends []logging.Backend +} + +func Main() { + d := initialize() + + err := runServer( + d.handlePing, + d.handleListProfiles, + d.handleLaunch, + d.handleListSandboxes, + d.handleClean, + d.handleLogs, + ) + if err != nil { + d.log.Warning("Error running server: %v", err) + } +} + +func initialize() *daemonState { + d := &daemonState{} + d.initializeLogging() + ps,err := oz.LoadProfiles("/var/lib/oz/cells.d") + if err != nil { + d.log.Fatalf("Failed to load profiles: %v", err) + } + d.Debug("%d profiles loaded", len(ps)) + d.profiles = ps + oz.ReapChildProcs(d.log, d.handleChildExit) + d.nextSboxId = 1 + return d +} + + +func (d *daemonState) handleChildExit(pid int, wstatus syscall.WaitStatus) { + d.Debug("Child process pid=%d exited with status %d", pid, wstatus.ExitStatus()) + for _,sbox := range d.sandboxes { + if sbox.init.Process.Pid == pid { + sbox.fs.Cleanup() + } + } +} + +func runServer(args ...interface{}) error { + serv := ipc.NewMsgConn(messageFactory, SocketName) + if err := serv.AddHandlers(args...); err != nil { + return err + } + if err := serv.Listen(); err != nil { + return err + } + return serv.Run() +} + +func (d * daemonState) handlePing(msg *PingMsg, m *ipc.Message) error { + d.Debug("received ping with data [%s]", msg.Data) + return m.Respond(&PingMsg{msg.Data}) +} + +func (d * daemonState) handleListProfiles(msg *ListProfilesMsg, m *ipc.Message) error { + r := new(ListProfilesResp) + index := 1 + for _,p := range d.profiles { + r.Profiles = append(r.Profiles, Profile{Index: index, Name: p.Name, Path: p.Path}) + index += 1 + } + return m.Respond(r) +} + +func (d *daemonState) handleLaunch(msg *LaunchMsg, m *ipc.Message) error { + d.Debug("Launch message received: %+v", msg) + p,err := d.getProfileByIdxOrName(msg.Index, msg.Name) + if err != nil { + return m.Respond(&ErrorMsg{err.Error()}) + } + d.Debug("Would launch %s", p.Name) + + _,err = d.launch(p) + if err != nil { + d.Warning("launch of %s failed: %v", p.Name, err) + return m.Respond(&ErrorMsg{err.Error()}) + } + return m.Respond(&OkMsg{}) +} + +func (d *daemonState) getProfileByIdxOrName(index int, name string) (*oz.Profile, error) { + if len(name) == 0 { + if index < 1 || index > len(d.profiles) { + return nil, fmt.Errorf("not a valid profile index (%d)", index) + } + return d.profiles[index-1], nil + } + + for _,p := range d.profiles { + if p.Name == name { + return p,nil + } + } + return nil, fmt.Errorf("could not find profile name '%s'", name) +} + +func (d *daemonState) handleListSandboxes(list *ListSandboxesMsg, msg *ipc.Message) error { + r := new(ListSandboxesResp) + for _, sb := range d.sandboxes { + r.Sandboxes = append(r.Sandboxes, SandboxInfo{Id: sb.id, Address: sb.addr, Profile: sb.profile.Name}) + } + return msg.Respond(r) +} + +func (d *daemonState) handleClean(clean *CleanMsg, msg *ipc.Message) error { + p,err := d.getProfileByIdxOrName(clean.Index, clean.Name) + if err != nil { + return msg.Respond(&ErrorMsg{err.Error()}) + } + for _, sb := range d.sandboxes { + if sb.profile.Name == p.Name { + errmsg := fmt.Sprintf("Cannot clean profile '%s' because there are sandboxes running for this profile", p.Name) + return msg.Respond(&ErrorMsg{errmsg}) + } + } + fs := fs.NewFromProfile(p, d.log) + if err := fs.Cleanup(); err != nil { + return msg.Respond(&ErrorMsg{err.Error()}) + } + return msg.Respond(&OkMsg{}) +} + +func (d *daemonState) handleLogs(logs *LogsMsg, msg *ipc.Message) error { + for n := d.memBackend.Head(); n != nil; n = n.Next() { + s := n.Record.Formatted(0) + msg.Respond(&LogData{Lines: []string{s}}) + } + if logs.Follow { + d.followLogs(msg) + return nil + } + msg.Respond(&OkMsg{}) + return nil +} + diff --git a/oz-daemon/launch.go b/oz-daemon/launch.go new file mode 100644 index 0000000..04a0a73 --- /dev/null +++ b/oz-daemon/launch.go @@ -0,0 +1,128 @@ +package daemon +import ( + "github.com/subgraph/oz" + "github.com/subgraph/oz/fs" + "os/exec" + "github.com/subgraph/oz/ipc" + "syscall" + "fmt" + "io" + "bufio" +) + +const initPath = "/usr/local/bin/oz-init" + + +type Sandbox struct { + daemon *daemonState + id int + profile *oz.Profile + init *exec.Cmd + fs *fs.Filesystem + stderr io.ReadCloser + addr string +} + +/* +func findSandbox(id int) *Sandbox { + for _, sb := range sandboxes { + if sb.id == id { + return sb + } + } + return nil +} +*/ +const initCloneFlags = syscall.CLONE_NEWNS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUTS + +func createInitCommand(addr, name, chroot string) *exec.Cmd { + cmd := exec.Command(initPath) + cmd.Dir = "/" + cmd.SysProcAttr = &syscall.SysProcAttr{ + Chroot: chroot, + Cloneflags: initCloneFlags, + } + cmd.Env = []string{ + "INIT_ADDRESS="+addr, + "INIT_PROFILE="+name, + } + return cmd +} + +func (d *daemonState) launch(p *oz.Profile) (*Sandbox, error) { + fs := fs.NewFromProfile(p, d.log) + if err := fs.Setup(); err != nil { + return nil, err + } + addr,err := ipc.CreateRandomAddress("@oz-init-") + if err != nil { + return nil, err + } + cmd := createInitCommand(addr, p.Name, fs.Root()) + pp,err := cmd.StderrPipe() + if err != nil { + fs.Cleanup() + return nil, fmt.Errorf("error creating stderr pipe for init process: %v", err) + + } + if err := cmd.Start(); err != nil { + fs.Cleanup() + return nil, err + } + sbox := &Sandbox{ + daemon: d, + id: d.nextSboxId, + profile: p, + init: cmd, + fs: fs, + addr: addr, + stderr: pp, + } + go sbox.logMessages() + d.nextSboxId += 1 + d.sandboxes = append(d.sandboxes, sbox) + return sbox,nil +} + +func (sbox *Sandbox) logMessages() { + scanner := bufio.NewScanner(sbox.stderr) + for scanner.Scan() { + line := scanner.Text() + if len(line) > 1 { + sbox.logLine(line) + } + } + sbox.stderr.Close() +} + +func (sbox *Sandbox) logLine(line string) { + if len(line) < 2 { + return + } + f := sbox.getLogFunc(line[0]) + msg := line[2:] + if f != nil { + f("[%s] %s", sbox.profile.Name, msg) + } else { + sbox.daemon.log.Info("[%s] %s", sbox.profile.Name, line) + } +} + +func (sbox *Sandbox) getLogFunc(c byte) func(string, ...interface{}) { + log := sbox.daemon.log + switch(c) { + case 'D': + return log.Debug + case 'I': + return log.Info + case 'N': + return log.Notice + case 'W': + return log.Warning + case 'E': + return log.Error + case 'C': + return log.Critical + } + return nil +} diff --git a/oz-daemon/logging.go b/oz-daemon/logging.go new file mode 100644 index 0000000..6f79355 --- /dev/null +++ b/oz-daemon/logging.go @@ -0,0 +1,90 @@ +package daemon +import ( + "github.com/op/go-logging" + "log" + "os" + "github.com/subgraph/oz/ipc" +) + + +func (d *daemonState) Debug(format string, args ...interface{}) { + d.log.Debug(format, args...) +} +func (d *daemonState) Info(format string, args ...interface{}) { + d.log.Info(format, args...) +} +func (d *daemonState) Notice(format string, args ...interface{}) { + d.log.Notice(format, args...) +} +func (d *daemonState) Warning(format string, args ...interface{}) { + d.log.Warning(format, args...) +} +func (d *daemonState) Error(format string, args ...interface{}) { + d.log.Error(format, args...) +} +func (d *daemonState) Critical(format string, args ...interface{}) { + d.log.Critical(format, args...) +} + +func (d *daemonState) initializeLogging() { + d.log = logging.MustGetLogger("oz") + be := logging.NewChannelMemoryBackend(100) + fbe := logging.NewBackendFormatter(be, format) + d.memBackend = be + stderr := logging.NewLogBackend(os.Stderr, "", log.LstdFlags) + d.backends = []logging.Backend{ + stderr, + fbe, + } + d.installBackends() +} +var format = logging.MustStringFormatter( + "%{color}%{time:15:04:05} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}", +) +func (d *daemonState) addBackend(be logging.Backend) { + d.backends = append(d.backends, be) + d.installBackends() +} + +func (d *daemonState) removeBackend(be logging.Backend) { + newBackends := []logging.Backend{} + for _,b := range d.backends { + if b != be { + newBackends = append(newBackends, b) + } + } + d.backends = newBackends + d.installBackends() +} + +func (d *daemonState) installBackends() { + if len(d.backends) == 1 { + d.log.SetBackend(logging.AddModuleLevel(d.backends[0])) + return + } + d.log.SetBackend(logging.MultiLogger(d.backends...)) +} + +type logFollower struct { + daemon *daemonState + wrapper logging.Backend + m *ipc.Message +} + +func (lf *logFollower) Log(level logging.Level, calldepth int, rec *logging.Record) error { + s := rec.Formatted(calldepth) + if err := lf.m.Respond(&LogData{[]string{s}}); err != nil { + lf.remove() + } + return nil +} + +func (lf *logFollower) remove() { + lf.daemon.removeBackend(lf.wrapper) +} + +func (d *daemonState) followLogs(m *ipc.Message) { + be := &logFollower{m:m, daemon: d} + be.wrapper = logging.NewBackendFormatter(be, format) + d.addBackend(be.wrapper) +} diff --git a/oz-daemon/protocol.go b/oz-daemon/protocol.go new file mode 100644 index 0000000..ff16e26 --- /dev/null +++ b/oz-daemon/protocol.go @@ -0,0 +1,78 @@ +package daemon +import "github.com/subgraph/oz/ipc" + +const SocketName = "@oz-control" + +type OkMsg struct { + _ string "Ok" +} + +type ErrorMsg struct { + Msg string "Error" +} + +type PingMsg struct { + Data string "Ping" +} + +type ListProfilesMsg struct { + _ string "ListProfiles" +} + +type Profile struct { + Index int + Name string + Path string +} + +type ListProfilesResp struct { + Profiles []Profile "ListProfilesResp" +} + +type LaunchMsg struct { + Index int "Launch" + Name string +} + +type ListSandboxesMsg struct { + _ string "ListSandboxes" +} + +type SandboxInfo struct { + Id int + Address string + Profile string +} + +type ListSandboxesResp struct { + Sandboxes []SandboxInfo "ListSandboxesResp" +} + +type CleanMsg struct { + Index int "Clean" + Name string +} + +type LogsMsg struct { + Count int "Logs" + Follow bool +} + +type LogData struct { + Lines []string "LogData" +} + +var messageFactory = ipc.NewMsgFactory( + new(PingMsg), + new(OkMsg), + new(ErrorMsg), + new(ListProfilesMsg), + new(ListProfilesResp), + new(LaunchMsg), + new(ListSandboxesMsg), + new(ListSandboxesResp), + new(CleanMsg), + new(LogsMsg), + new(LogData), +) + diff --git a/oz-daemon/sandbox.go b/oz-daemon/sandbox.go new file mode 100644 index 0000000..9d30720 --- /dev/null +++ b/oz-daemon/sandbox.go @@ -0,0 +1 @@ +package daemon diff --git a/oz-init/client.go b/oz-init/client.go new file mode 100644 index 0000000..5c6dc0f --- /dev/null +++ b/oz-init/client.go @@ -0,0 +1,72 @@ +package ozinit +import ( + "github.com/subgraph/oz/ipc" + "errors" + "fmt" +) + +func clientConnect(addr string) (*ipc.MsgConn, error) { + c := ipc.NewMsgConn(messageFactory, addr) + if err := c.Connect(); err != nil { + return nil, err + } + return c, nil +} + +func clientSend(addr string, msg interface{}) (*ipc.Message, error) { + c,err := clientConnect(addr) + if err != nil { + return nil, err + } + rr, err := c.ExchangeMsg(msg) + resp := <- rr.Chan() + rr.Done() + + c.Close() + if err != nil { + return nil, err + } + return resp, nil +} + +func Ping(addr string) error { + resp,err := clientSend(addr, new(PingMsg)) + if err != nil { + return err + } + switch body := resp.Body.(type) { + case *PingMsg: + return nil + case *ErrorMsg: + return errors.New(body.Msg) + default: + return fmt.Errorf("Unexpected message received: %+v", body) + } +} + +func RunShell(addr, term string) (int, error) { + c,err := clientConnect(addr) + if err != nil { + return 0, err + } + rr,err := c.ExchangeMsg(&RunShellMsg{Term: term}) + resp := <- rr.Chan() + rr.Done() + c.Close() + if err != nil { + return 0, err + } + switch body := resp.Body.(type) { + case *ErrorMsg: + return 0,errors.New(body.Msg) + case *OkMsg: + if len(resp.Fds) == 0 { + return 0, errors.New("RunShell message returned Ok, but no file descriptor received") + } + return resp.Fds[0], nil + default: + return 0, fmt.Errorf("Unexpected message type received: %+v", body) + } +} + + diff --git a/oz-init/init.go b/oz-init/init.go new file mode 100644 index 0000000..d96bffb --- /dev/null +++ b/oz-init/init.go @@ -0,0 +1,160 @@ +package ozinit + +import ( + "os" + "github.com/codegangsta/cli" + "github.com/subgraph/oz" + "github.com/subgraph/oz/fs" + "github.com/subgraph/oz/ipc" + "os/exec" + "syscall" + "github.com/op/go-logging" + "github.com/kr/pty" + "fmt" +) + +const profileDirectory = "/var/lib/oz/cells.d" + +var log = createLogger() + +func createLogger() *logging.Logger { + l := logging.MustGetLogger("oz-init") + be := logging.NewLogBackend(os.Stderr, "", 0) + f := logging.MustStringFormatter("%{level:.1s} %{message}") + fbe := logging.NewBackendFormatter(be,f) + logging.SetBackend(fbe) + return l +} + +var allowRootShell = false +var profileName = "none" + +func Main() { + app := cli.NewApp() + app.Name = "oz-init" + app.Action = runInit + app.Flags = []cli.Flag { + cli.StringFlag { + Name: "address", + Usage: "unix socket address to listen for commands on", + EnvVar: "INIT_ADDRESS", + }, + cli.StringFlag{ + Name: "profile", + Usage: "name of profile to launch", + EnvVar: "INIT_PROFILE", + }, + + } + app.Run(os.Args) +} + +func runInit(c *cli.Context) { + address := c.String("address") + profile := c.String("profile") + if address == "" { + log.Error("Error: missing required 'address' argument") + os.Exit(1) + } + if profile == "" { + log.Error("Error: missing required 'profile' argument") + os.Exit(1) + } + profileName = profile + log.Info("Starting oz-init for profile: %s", profile) + log.Info("Socket address: %s", address) + p,err := loadProfile(profile) + if err != nil { + log.Error("Could not load profile %s: %v", profile, err) + os.Exit(1) + } + + fs := fs.NewFromProfile(p, log) + if err := fs.OzInit(); err != nil { + log.Error("Error: setting up filesystem failed: %v\n", err) + os.Exit(1) + } + + oz.ReapChildProcs(log, handleChildExit) + + serv := ipc.NewMsgConn(messageFactory, address) + serv.AddHandlers( + handlePing, + handleRunShell, + ) + serv.Listen() + serv.Run() + log.Info("oz-init exiting...") +} + +func loadProfile(name string) (*oz.Profile, error) { + ps,err := oz.LoadProfiles(profileDirectory) + if err != nil { + return nil, err + } + for _,p := range ps { + if name == p.Name { + return p, nil + } + } + return nil, fmt.Errorf("no profile named '%s'", name) +} + +func handlePing(ping *PingMsg, msg *ipc.Message) error { + return msg.Respond(&PingMsg{Data: ping.Data}) +} + +func handleRunShell(rs *RunShellMsg, msg *ipc.Message) error { + if msg.Ucred == nil { + return msg.Respond(&ErrorMsg{"No credentials received for RunShell command"}) + } + if msg.Ucred.Uid == 0 || msg.Ucred.Gid == 0 && !allowRootShell { + return msg.Respond(&ErrorMsg{"Cannot open shell because allowRootShell is disabled"}) + } + log.Info("Starting shell with uid = %d, gid = %d", msg.Ucred.Uid, msg.Ucred.Gid) + cmd := exec.Command("/bin/sh", "-i") + cmd.SysProcAttr = &syscall.SysProcAttr{} + cmd.SysProcAttr.Credential = &syscall.Credential{ + Uid: msg.Ucred.Uid, + Gid: msg.Ucred.Gid, + } + if rs.Term != "" { + cmd.Env = append(cmd.Env, "TERM="+rs.Term) + } + cmd.Env = append(cmd.Env, "PATH=/usr/bin:/bin") + cmd.Env = append(cmd.Env, fmt.Sprintf("PS1=[%s] $ ", profileName)) + log.Info("Executing shell...") + f,err := ptyStart(cmd) + defer f.Close() + if err != nil { + return msg.Respond(&ErrorMsg{err.Error()}) + } + err = msg.Respond(&OkMsg{}, int(f.Fd())) + return err +} + +func ptyStart(c *exec.Cmd) (ptty *os.File, err error) { + ptty,tty, err := pty.Open() + if err != nil { + return nil, err + } + defer tty.Close() + c.Stdin = tty + c.Stdout = tty + c.Stderr = tty + if c.SysProcAttr == nil { + c.SysProcAttr = &syscall.SysProcAttr{} + } + c.SysProcAttr.Setctty = true + c.SysProcAttr.Setsid = true + if err := c.Start(); err != nil { + ptty.Close() + return nil, err + } + return ptty, nil +} + +func handleChildExit(pid int, wstatus syscall.WaitStatus) { + log.Debug("Child process pid=%d exited with status %d", pid, wstatus.ExitStatus()) +} + diff --git a/oz-init/protocol.go b/oz-init/protocol.go new file mode 100644 index 0000000..cb285ed --- /dev/null +++ b/oz-init/protocol.go @@ -0,0 +1,26 @@ +package ozinit +import "github.com/subgraph/oz/ipc" + +type OkMsg struct { + _ string "Ok" +} + +type ErrorMsg struct { + Msg string "Error" +} + +type PingMsg struct { + Data string "Ping" +} + +type RunShellMsg struct { + Term string "RunShell" +} + +var messageFactory = ipc.NewMsgFactory( + new(OkMsg), + new(ErrorMsg), + new(PingMsg), + new(RunShellMsg), +) + diff --git a/oz/main.go b/oz/main.go new file mode 100644 index 0000000..839dc0a --- /dev/null +++ b/oz/main.go @@ -0,0 +1,168 @@ +package main + +import ( + "fmt" + "github.com/codegangsta/cli" + "os" + "github.com/subgraph/oz/oz-daemon" + "strconv" + "io" + "github.com/subgraph/oz/oz-init" +) + +func main() { + app := cli.NewApp() + + app.Name = "oz" + app.Usage = "command line interface to Oz sandboxes" + app.Author = "Subgraph" + app.Email = "info@subgraph.com" + app.Commands = []cli.Command { + { + Name: "profiles", + Usage: "list available application profiles", + Action: handleProfiles, + }, + { + Name: "launch", + Usage: "launch an application profile", + Action: handleLaunch, + }, + { + Name: "list", + Usage: "list running sandboxes", + Action: handleList, + }, + { + Name: "shell", + Usage: "start a shell in a running container", + Action: handleShell, + }, + { + Name: "clean", + Action: handleClean, + + }, + { + Name: "logs", + Action: handleLogs, + Flags: []cli.Flag { + cli.BoolFlag{ + Name: "f", + }, + + }, + }, + } + app.Run(os.Args) +} + +func handleProfiles(c *cli.Context) { + ps,err := daemon.ListProfiles() + if err != nil { + fmt.Printf("Error listing profiles: %v\n", err) + os.Exit(1) + } + for i,p := range ps { + fmt.Printf("%2d) %-30s %s\n", i+1, p.Name, p.Path) + } +} + +func handleLaunch(c *cli.Context) { + if len(c.Args()) == 0 { + fmt.Println("Argument needed to launch command") + os.Exit(1) + } + err := daemon.Launch(c.Args()[0]) + if err != nil { + fmt.Printf("launch command failed: %v\n", err) + + } +} + +func handleList(c *cli.Context) { + sboxes,err := daemon.ListSandboxes() + if err != nil { + fmt.Printf("Error listing running containers: %v\n", err) + os.Exit(1) + } + if len(sboxes) == 0 { + fmt.Println("No running containers") + return + } + for _, sb := range sboxes { + fmt.Printf("%2d) %s\n", sb.Id, sb.Profile) + + } +} + +func handleShell(c *cli.Context) { + if len(c.Args()) == 0 { + fmt.Println("Sandbox id argument needed") + os.Exit(1) + } + id,err := strconv.Atoi(c.Args()[0]) + if err != nil { + fmt.Println("Sandbox id argument must be an integer") + os.Exit(1) + } + + sb,err := getSandboxById(id) + if err != nil { + fmt.Printf("Error retrieving sandbox list: %v\n", err) + } + if sb == nil { + fmt.Printf("No sandbox found with id = %d\n", id) + } + term := os.Getenv("TERM") + fd,err := ozinit.RunShell(sb.Address, term) + if err != nil { + fmt.Printf("start shell command failed: %v\n", err) + os.Exit(1) + } + fmt.Println("Entering interactive shell?\n") + st,err := SetRawTerminal(0) + HandleResize(fd) + f := os.NewFile(uintptr(fd), "") + go io.Copy(f, os.Stdin) + io.Copy(os.Stdout, f) + RestoreTerminal(0, st) + fmt.Println("done..") +} + + +func getSandboxById(id int) (*daemon.SandboxInfo, error) { + sboxes,err := daemon.ListSandboxes() + if err != nil { + return nil, err + } + for _, sb := range sboxes { + if id == sb.Id { + return &sb,nil + } + } + return nil, nil +} + +func handleClean(c *cli.Context) { + if len(c.Args()) == 0 { + fmt.Println("Need a profile to clean") + os.Exit(1) + } + err := daemon.Clean(c.Args()[0]) + if err != nil { + fmt.Println("Clean failed:", err) + } +} + +func handleLogs(c *cli.Context) { + follow := c.Bool("f") + ch,err := daemon.Logs(0,follow) + if err != nil { + fmt.Println("Logs failed", err) + os.Exit(1) + } + for ll := range ch { + fmt.Println(ll) + } +} diff --git a/oz/term.go b/oz/term.go new file mode 100644 index 0000000..2243b44 --- /dev/null +++ b/oz/term.go @@ -0,0 +1,112 @@ +package main +import ( + "syscall" + "unsafe" + "os" + "os/signal" + "fmt" +) + +type winsize struct { + Height uint16 + Width uint16 + x uint16 + y uint16 +} +type State struct { + termios Termios +} + +const ( + getTermios = syscall.TCGETS + setTermios = syscall.TCSETS +) + +type Termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Cc [20]byte + Ispeed uint32 + Ospeed uint32 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd uintptr) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, getTermios, uintptr(unsafe.Pointer(&oldState.termios))); err != 0 { + return nil, err + } + + newState := oldState.termios + + newState.Iflag &^= (syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON) + newState.Oflag &^= syscall.OPOST + newState.Lflag &^= (syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN) + newState.Cflag &^= (syscall.CSIZE | syscall.PARENB) + newState.Cflag |= syscall.CS8 + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, setTermios, uintptr(unsafe.Pointer(&newState))); err != 0 { + return nil, err + } + return &oldState, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func RestoreTerminal(fd uintptr, state *State) error { + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(setTermios), uintptr(unsafe.Pointer(&state.termios))) + return err +} + +func SetRawTerminal(fd uintptr) (*State, error) { + oldState, err := MakeRaw(fd) + if err != nil { + return nil, err + } + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + _ = <-c + RestoreTerminal(fd, oldState) + os.Exit(0) + }() + return oldState, err +} + +func GetWinsize(fd uintptr) (*winsize, syscall.Errno) { + ws := &winsize{} + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(ws))) + return ws, err +} + +func SetWinsize(fd uintptr, ws *winsize) syscall.Errno { + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws))) + return err +} + +func HandleResize(fd int) { + sigs := make(chan os.Signal) + signal.Notify(sigs, syscall.SIGWINCH) + go func() { + for { + <-sigs + handleSIGWINCH(fd) + } + }() + handleSIGWINCH(fd) +} + +func handleSIGWINCH(fd int) { + ws,errno := GetWinsize(0) + if errno != 0 { + fmt.Printf("error reading winsize: %v\n", errno.Error()) + return + } + if errno := SetWinsize(uintptr(fd), ws); errno != 0 { + fmt.Printf("error setting winsize: %v", errno.Error()) + } +} \ No newline at end of file diff --git a/profile.go b/profile.go new file mode 100644 index 0000000..095fe02 --- /dev/null +++ b/profile.go @@ -0,0 +1,118 @@ +package oz + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "path" +) + +type Profile struct { + // Name of this profile + Name string + // Path to binary to launch + Path string + // Optional path of binary to watch for watchdog purposes if different than Path + Watchdog string + // Optional wrapper binary to use when launching command (ex: tsocks) + Wrapper string + // If true launch one container per instance, otherwise run all instances in same container + Multi bool + // Disable mounting of sys and proc inside the container + NoSysProc bool + // Disable bind mounting of default directories (etc,usr,bin,lib,lib64) + // Also disables default blacklist items (/sbin, /usr/sbin, /usr/bin/sudo) + // Normally not used + NoDefaults bool + // List of paths to bind mount inside jail + Whitelist []WhitelistItem + // List of paths to blacklist inside jail + Blacklist []BlacklistItem + // Optional XServer config + XServer XServerConf + // List of environment variables + Environment []EnvVar +} + +type XServerConf struct { + Enabled bool + TrayIcon string `json:"tray_icon"` + WindowIcon string `json:"window_icon"` + EnableTray bool `json:"enable_tray"` + UseDBUS bool `json:"use_dbus"` + UsePulseAudio bool `json:"use_pulse_audio"` + DisableClipboard bool `json:"disable_clipboard"` + DisableAudio bool `json:"disable_audio"` + WorkDir string `json:"work_dir"` +} + +type WhitelistItem struct { + Path string + ReadOnly bool +} + +type BlacklistItem struct { + Path string +} + +type EnvVar struct { + Name string + Value string +} + +const defaultProfileDirectory = "/var/lib/oz/cells.d" + +var loadedProfiles []*Profile + +type Profiles []*Profile + +func (ps Profiles) GetProfileByName(name string) (*Profile,error) { + if loadedProfiles == nil { + ps ,err := LoadProfiles(defaultProfileDirectory) + if err != nil { + return nil, err + } + loadedProfiles = ps + } + + for _,p := range loadedProfiles { + if p.Name == name { + return p,nil + } + } + return nil, nil +} + +func LoadProfiles(dir string) (Profiles, error) { + fs, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + ps := []*Profile{} + for _, f := range fs { + if !f.IsDir() { + name := path.Join(dir, f.Name()) + p, err := loadProfileFile(name) + if err != nil { + return nil, fmt.Errorf("error loading '%s': %v", f.Name(), err) + } + ps = append(ps, p) + } + } + return ps, nil +} + +func loadProfileFile(file string) (*Profile, error) { + bs, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + p := new(Profile) + if err := json.Unmarshal(bs, p); err != nil { + return nil, err + } + if p.Name == "" { + p.Name = path.Base(p.Path) + } + return p, nil +} diff --git a/profiles/evince.json b/profiles/evince.json new file mode 100644 index 0000000..ad56912 --- /dev/null +++ b/profiles/evince.json @@ -0,0 +1,17 @@ +{ +"path": "/usr/bin/evince" +, "xserver": { + "enabled": true + , "enable_tray": true + , "tray_icon":"/usr/share/icons/hicolor/48x48/apps/evince.png" +} +, "network":{ + "nettype":"empty" +} +, "whitelist": [ +] +, "blacklist": [ +] +, "environment": [ +] +} diff --git a/profiles/gajim.json b/profiles/gajim.json new file mode 100644 index 0000000..758a011 --- /dev/null +++ b/profiles/gajim.json @@ -0,0 +1,33 @@ +{ +"path": "/usr/bin/gajim" +, "xserver": { + "enabled": true + , "enable_tray": true + , "tray_icon":"/usr/share/icons/gnome-colors-common/scalable/apps/gajim.svg" + , "disable_audio":true +} +, "network":{ + "nettype":"empty" + , "sockets": [ + {"nettype":"client", "proto":"tcp", "port":9050} + , {"nettype":"client", "destination": "xmpp.lalonde.me", "proto":"tcp", "port":5222} + , {"nettype":"client", "destination": "xmpp.lalonde.me", "proto":"tcp", "port":5269} + , {"nettype":"client", "destination": "xmpp.lalonde.me", "proto":"tcp", "port":5000} + , {"nettype":"client", "destination": "xmpp.lalonde.me", "proto":"tcp", "port":5280} + , {"nettype":"client", "destination": "xmpp.lalonde.me", "proto":"tcp", "port":5281} + ] +} +, "whitelist": [ + {"source":"/run/resolvconf"} + , {"source":"/run/user/${UID}/keyring-*"} + , {"source":"${HOME}/.local/share/gajim"} + , {"source":"${HOME}/.cache/gajim"} + , {"source":"${HOME}/.config/gajim"} + , {"source":"${HOME}/.local/share/keyrings"} +] +, "blacklist": [ + {"source":"/run/user/${UID}/keyring-*/ssh"} + , {"source":"/run/user/${UID}/keyring-*/pkcs11"} + , {"source":"/run/user/${UID}/keyring-*/gpg"} +] +} diff --git a/profiles/icedove.json b/profiles/icedove.json new file mode 100644 index 0000000..242cbfd --- /dev/null +++ b/profiles/icedove.json @@ -0,0 +1,34 @@ +{ +"path": "/usr/bin/icedove" +, "xserver": { + "enabled": true + , "enable_tray": true + , "tray_icon": "/usr/share/icons/hicolor/scalable/apps/icedove.svg" + , "disable_audio": true +} +, "network":{ + "nettype":"bridge" + , "sockets": [ + {"nettype":"client", "proto":"tcp", "port":9050} + ] +} +, "whitelist": [ + {"source":"/run/resolvconf"} + , {"source":"/run/user/${UID}/keyring"} + , {"source":"/tmp/gpg-*"} + , {"source":"${HOME}/.cache/icedove"} + , {"source":"${HOME}/.gnupg"} + , {"source":"${HOME}/..thunderbird"} + , {"source":"${HOME}/.icedove"} + + , {"source":"${HOME}/.config/gtk-3.0"} + , {"source":"${HOME}/.config/gtk-2.0"} +] +, "_blacklist": [ +] +, "environment": [ + {"name":"GPG_AGENT_INFO"} + , {"name":"GNOME_KEYRING_CONTROL"} + , {"name":"GNOME_KEYRING_PID", "value":"1"} +] +} diff --git a/profiles/iceweasel.json b/profiles/iceweasel.json new file mode 100644 index 0000000..6e818ec --- /dev/null +++ b/profiles/iceweasel.json @@ -0,0 +1,27 @@ +{ +"path": "/usr/bin/iceweasel" +, "xserver": { + "enabled": true + , "enable_tray": true + , "tray_icon":"/usr/share/icons/hicolor/scalable/apps/iceweasel.svg" +} +, "network":{ + "nettype":"bridge" + , "sockets": [ + {"nettype":"client", "proto":"tcp", "port":9050} + ] +} +, "whitelist": [ + {"source":"/run/resolvconf"} + , {"source":"${HOME}/.mozilla"} + , {"source":"${HOME}/.cache/mozilla"} + , {"source":"${HOME}/Downloads/"} + + , {"source":"${HOME}/.config/gtk-3.0"} + , {"source":"${HOME}/.config/gtk-2.0"} +] +, "blacklist": [ +] +, "environment": [ +] +} diff --git a/profiles/liferea.json b/profiles/liferea.json new file mode 100644 index 0000000..97929dc --- /dev/null +++ b/profiles/liferea.json @@ -0,0 +1,34 @@ +{ +"path": "/usr/bin/liferea" +, "_wrapper": "/usr/bin/torify" +, "xserver": { + "enabled": true + , "enable_tray": true + , "tray_icon":"/usr/share/icons/hicolor/scalable/apps/liferea.svg" + , "_tray_icon":"/usr/share/icons/gnome-colors-common/scalable/apps/liferea.svg" +} +, "network":{ + "nettype":"bridge" + , "sockets": [ + {"nettype":"client", "proto":"tcp", "port":9050} + ] +} +, "whitelist": [ + {"source":"/run/resolvconf"} + + , {"source":"${HOME}/.local/share/liferea"} + , {"source":"${HOME}/.cache/liferea"} + , {"source":"${HOME}/.config/liferea"} + + , {"source":"${HOME}/.config/gtk-3.0"} + , {"source":"${HOME}/.config/gtk-2.0"} + + , {"source":"${HOME}/.config/dconf"} + , {"source":"${HOME}/.cache/dconf"} + , {"source":"/run/user/${UID}/dconf"} +] +, "blacklist": [ +] +, "_environment": [ +] +} diff --git a/profiles/pidgin.json b/profiles/pidgin.json new file mode 100644 index 0000000..3db5035 --- /dev/null +++ b/profiles/pidgin.json @@ -0,0 +1,18 @@ +{ +"path": "/usr/bin/pidgin" +, "xserver": { + "enabled": true + , "enable_tray": true + , "tray_icon":"/usr/share/icons/gnome-colors-common/scalable/apps/pidgin-menu.svg" +} +, "network":{ + "nettype":"bridge" +} +, "whitelist": [ + {"source":"${HOME}/.purple"} +] +, "blacklist": [ +] +, "environment": [ +] +} diff --git a/profiles/pond.json b/profiles/pond.json new file mode 100644 index 0000000..50df8ef --- /dev/null +++ b/profiles/pond.json @@ -0,0 +1,29 @@ +{ +"path": "/usr/local/bin/pond" +, "xserver": { + "enabled": true + , "enable_tray": true + , "tray_icon":"/usr/share/icons/gnome-colors-common/scalable/apps/office-mail.svg" + , "window_icon":"/usr/share/icons/gnome-colors-common/scalable/apps/office-mail.svg" + , "disable_audio":true +} +, "network":{ + "nettype":"empty" + , "sockets": [ + {"nettype":"client", "proto":"tcp", "port":9050} + , {"nettype":"client", "proto":"tcp", "port":30003} + ] +} +, "whitelist": [ + {"source":"${HOME}/.pond"} + , {"source":"/opt/usr/share/gopkgs/pond"} +] +, "blacklist": [ +] +, "environment": [ + {"name":"GOPATH", "value":"/opt/usr/share/gopkgs/pond"} + , {"name":"TOR_SKIP_LAUNCH"} + , {"name":"TOR_SOCKS_HOST"} + , {"name":"TOR_SOCKS_PORT"} +] +} diff --git a/profiles/torbrowser-launcher.json b/profiles/torbrowser-launcher.json new file mode 100644 index 0000000..7d919fd --- /dev/null +++ b/profiles/torbrowser-launcher.json @@ -0,0 +1,30 @@ +{ +"path": "/usr/bin/torbrowser-launcher" +, "watchdog": "start-tor-browser" +, "xserver": { + "enabled": true + , "enable_tray": true + , "disable_audio": true + , "disable_clipboard": false + , "tray_icon":"/usr/share/pixmaps/torbrowser80.xpm" +} +, "network":{ + "nettype":"empty" + , "sockets": [ + {"nettype":"client", "proto":"tcp", "port":9050} + ] +} +, "whitelist": [ + {"source":"${HOME}/.local/share/torbrowser"} + , {"source":"${HOME}/.cache/torbrowser"} + , {"source":"${HOME}/.config/torbrowser"} + , {"source":"${HOME}/Downloads/TorBrowser"} +] +, "blacklist": [ +] +, "environment": [ + {"name":"TOR_SKIP_LAUNCH"} + , {"name":"TOR_SOCKS_HOST"} + , {"name":"TOR_SOCKS_PORT"} +] +} diff --git a/profiles/xchat.json b/profiles/xchat.json new file mode 100644 index 0000000..ed80311 --- /dev/null +++ b/profiles/xchat.json @@ -0,0 +1,24 @@ +{ +"path": "/usr/bin/xchat" +, "xserver": { + "enabled": true + , "enable_tray": true + , "tray_icon": "/usr/share/icons/gnome-colors-common/scalable/apps/xchat.svg" + , "disable_audio": true +} +, "network":{ + "nettype":"empty" + , "sockets": [ + {"nettype":"client", "destination": "10.10.0.90", "proto":"tcp", "port":57000} + ] +} +, "whitelist": [ + {"source":"${HOME}/.xpra/xchat"} + , {"source":"${HOME}/.xchat2"} + + , {"source":"${HOME}/.config/gtk-3.0"} + , {"source":"${HOME}/.config/gtk-2.0"} +] +, "blacklist": [ +] +}