mirror of https://github.com/xSmurf/oz.git
commit
6221d0595a
@ -0,0 +1,2 @@
|
|||||||
|
oz.iml
|
||||||
|
.idea/
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package main
|
||||||
|
import (
|
||||||
|
"github.com/subgraph/oz/oz-daemon"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
daemon.Main()
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
import "github.com/subgraph/oz/oz-init"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ozinit.Main()
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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...)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
@ -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...)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
profileDir string `json:"profile_dir"`
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
package daemon
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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": [
|
||||||
|
]
|
||||||
|
}
|
@ -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"}
|
||||||
|
]
|
||||||
|
}
|
@ -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"}
|
||||||
|
]
|
||||||
|
}
|
@ -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": [
|
||||||
|
]
|
||||||
|
}
|
@ -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": [
|
||||||
|
]
|
||||||
|
}
|
@ -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": [
|
||||||
|
]
|
||||||
|
}
|
@ -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"}
|
||||||
|
]
|
||||||
|
}
|
@ -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"}
|
||||||
|
]
|
||||||
|
}
|
@ -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": [
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in new issue