Changed passing of init data through stdin, added support for user groups to be set properly, cleanup torbrowser profile

master
xSmurf 9 years ago
parent c4a773822b
commit 3f466e9d8e

@ -21,6 +21,7 @@ type Config struct {
AllowRootShell bool `json:"allow_root_shell" desc:"Allow entering a sandbox shell as root"`
LogXpra bool `json:"log_xpra" desc:"Log output of Xpra"`
EnvironmentVars []string `json:"environment_vars" desc:"Default environment variables passed to sandboxes"`
DefaultGroups []string `json:"default_groups" desc:"List of default group names that can be used inside the sandbox"`
}
const OzVersion = "0.0.1"
@ -28,20 +29,23 @@ const DefaultConfigPath = "/etc/oz/oz.conf"
func NewDefaultConfig() *Config {
return &Config{
ProfileDir: "/var/lib/oz/cells.d",
ShellPath: "/bin/bash",
PrefixPath: "/usr/local",
SandboxPath: "/srv/oz",
NMIgnoreFile: "/etc/NetworkManager/conf.d/oz.conf",
BridgeMACAddr: "6A:A8:2E:56:E8:9C",
DivertSuffix: "unsafe",
UseFullDev: false,
AllowRootShell: false,
LogXpra: false,
ProfileDir: "/var/lib/oz/cells.d",
ShellPath: "/bin/bash",
PrefixPath: "/usr/local",
SandboxPath: "/srv/oz",
NMIgnoreFile: "/etc/NetworkManager/conf.d/oz.conf",
BridgeMACAddr: "6A:A8:2E:56:E8:9C",
DivertSuffix: "unsafe",
UseFullDev: false,
AllowRootShell: false,
LogXpra: false,
EnvironmentVars: []string{
"USER", "USERNAME", "LOGNAME",
"LANG", "LANGUAGE", "_",
},
DefaultGroups: []string{
"audio", "video",
},
}
}

@ -53,20 +53,28 @@ func ListSandboxes() ([]SandboxInfo, error) {
return body.Sandboxes, nil
}
func Launch(arg, cpath string, args, env []string, noexec bool) error {
func Launch(arg, cpath string, args []string, noexec bool) error {
idx, name, err := parseProfileArg(arg)
if err != nil {
return err
}
pwd, _ := os.Getwd()
groups, _ := os.Getgroups()
gg := []uint32{}
if len(groups) > 0 {
gg = make([]uint32, len(groups))
for i, v := range groups {
gg[i] = uint32(v)
}
}
resp, err := clientSend(&LaunchMsg{
Index: idx,
Name: name,
Path: cpath,
Pwd: pwd,
Gids: gg,
Args: args,
Env: env,
Env: os.Environ(),
Noexec: noexec,
})
if err != nil {

@ -1,11 +1,13 @@
package daemon
import (
"bufio"
"encoding/json"
"fmt"
"os"
"os/signal"
"path"
"strconv"
"strings"
"syscall"
@ -16,16 +18,23 @@ import (
"github.com/op/go-logging"
)
type groupEntry struct {
Name string
Gid uint32
Members []string
}
type daemonState struct {
log *logging.Logger
config *oz.Config
profiles oz.Profiles
sandboxes []*Sandbox
nextSboxId int
nextDisplay int
memBackend *logging.ChannelMemoryBackend
backends []logging.Backend
network *network.HostNetwork
log *logging.Logger
config *oz.Config
profiles oz.Profiles
sandboxes []*Sandbox
nextSboxId int
nextDisplay int
memBackend *logging.ChannelMemoryBackend
backends []logging.Backend
network *network.HostNetwork
systemGroups map[string]groupEntry
}
func Main() {
@ -66,6 +75,9 @@ func initialize() *daemonState {
os.Exit(1)
}
d.profiles = ps
if err := d.cacheSystemGroups(); err != nil {
d.log.Fatalf("Unable to cache list of system groups: %v", err)
}
oz.ReapChildProcs(d.log, d.handleChildExit)
d.nextSboxId = 1
d.nextDisplay = 100
@ -80,9 +92,7 @@ func initialize() *daemonState {
}
d.network = htn
//network.NetPrint(d.log)
break
}
}
@ -140,6 +150,39 @@ func (d *daemonState) processSignals(c <-chan os.Signal) {
}
}
func (d *daemonState) cacheSystemGroups() error {
fg, err := os.Open("/etc/group")
if err != nil {
return err
}
defer fg.Close()
sg := bufio.NewScanner(fg)
newGroups := make(map[string]groupEntry)
for sg.Scan() {
gd := strings.Split(sg.Text(), ":")
if len(gd) < 4 {
continue
}
gid, err := strconv.ParseUint(gd[2], 10, 32)
if err != nil {
continue
}
newGroups[gd[0]] = groupEntry{
Name: gd[0],
Gid: uint32(gid),
Members: strings.Split(gd[3], ","),
}
}
if err := sg.Err(); err != nil {
return err
}
d.systemGroups = newGroups
return nil
}
func (d *daemonState) handleChildExit(pid int, wstatus syscall.WaitStatus) {
d.Debug("Child process pid=%d exited with status %d", pid, wstatus.ExitStatus())

@ -2,8 +2,10 @@ package daemon
import (
"bufio"
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
@ -52,7 +54,7 @@ func createSocketPath(base string) (string, error) {
return path.Join(base, fmt.Sprintf("oz-init-control-%s", hex.EncodeToString(bs))), nil
}
func createInitCommand(initPath, name string, socketPath string, env []string, uid uint32, display int, stn *network.SandboxNetwork) *exec.Cmd {
func createInitCommand(initPath string, cloneNet bool) *exec.Cmd {
cmd := exec.Command(initPath)
cmd.Dir = "/"
@ -61,7 +63,7 @@ func createInitCommand(initPath, name string, socketPath string, env []string, u
cloneFlags |= syscall.CLONE_NEWPID
cloneFlags |= syscall.CLONE_NEWUTS
if stn.Nettype != network.TYPE_HOST {
if cloneNet {
cloneFlags |= syscall.CLONE_NEWNET
}
@ -69,25 +71,7 @@ func createInitCommand(initPath, name string, socketPath string, env []string, u
//Chroot: chroot,
Cloneflags: cloneFlags,
}
cmd.Env = []string{
"INIT_PROFILE=" + name,
"INIT_SOCKET=" + socketPath,
fmt.Sprintf("INIT_UID=%d", uid),
}
if stn.Ip != "" {
cmd.Env = append(cmd.Env, "INIT_ADDR="+stn.Ip)
cmd.Env = append(cmd.Env, "INIT_VHOST="+stn.VethHost)
cmd.Env = append(cmd.Env, "INIT_VGUEST="+stn.VethGuest)
cmd.Env = append(cmd.Env, "INIT_GATEWAY="+stn.Gateway.String()+"/"+stn.Class)
}
cmd.Env = append(cmd.Env, fmt.Sprintf("INIT_DISPLAY=%d", display))
for _, e := range env {
cmd.Env = append(cmd.Env, ozinit.EnvPrefix+e)
}
return cmd
}
@ -107,8 +91,11 @@ func (d *daemonState) launch(p *oz.Profile, msg *LaunchMsg, uid, gid uint32, log
*/
u, err := user.LookupId(strconv.FormatUint(uint64(uid), 10))
if err != nil {
log.Error("Failed to look up user with uid=%ld: %v", uid, err)
os.Exit(1)
return nil, fmt.Errorf("Failed to look up user with uid=%ld: %v", uid, err)
}
groups, err := d.sanitizeGroups(p, u.Username, msg.Gids)
if err != nil {
return nil, fmt.Errorf("Unable to sanitize user groups: %v", err)
}
display := 0
@ -131,20 +118,40 @@ func (d *daemonState) launch(p *oz.Profile, msg *LaunchMsg, uid, gid uint32, log
return nil, fmt.Errorf("Failed to create random socket path: %v", err)
}
initPath := path.Join(d.config.PrefixPath, "bin", "oz-init")
cmd := createInitCommand(initPath, p.Name, socketPath, msg.Env, uid, display, stn)
log.Debug("Command environment: %+v", cmd.Env)
cmd := createInitCommand(initPath, (stn.Nettype != network.TYPE_HOST))
pp, err := cmd.StderrPipe()
if err != nil {
//fs.Cleanup()
return nil, fmt.Errorf("error creating stderr pipe for init process: %v", err)
}
pi, err := cmd.StdinPipe()
if err != nil {
//fs.Cleanup()
return nil, fmt.Errorf("error creating stdin pipe for init process: %v", err)
}
jdata, err := json.Marshal(ozinit.InitData{
Display: display,
User: *u,
Uid: uid,
Gid: gid,
Gids: groups,
Network: *stn,
Profile: *p,
Config: *d.config,
Sockaddr: socketPath,
LaunchEnv: msg.Env,
})
if err != nil {
return nil, fmt.Errorf("Unable to marshal init state: %+v", err)
}
io.Copy(pi, bytes.NewBuffer(jdata))
pi.Close()
if err := cmd.Start(); err != nil {
//fs.Cleanup()
return nil, fmt.Errorf("Unable to start process: %+v", err)
}
//rootfs := path.Join(d.config.SandboxPath, "rootfs")
sbox := &Sandbox{
daemon: d,
@ -152,7 +159,7 @@ func (d *daemonState) launch(p *oz.Profile, msg *LaunchMsg, uid, gid uint32, log
display: display,
profile: p,
init: cmd,
cred: &syscall.Credential{Uid: uid, Gid: gid},
cred: &syscall.Credential{Uid: uid, Gid: gid, Groups: msg.Gids},
user: u,
fs: fs.NewFilesystem(d.config, log),
//addr: path.Join(rootfs, ozinit.SocketAddress),
@ -205,6 +212,38 @@ func (d *daemonState) launch(p *oz.Profile, msg *LaunchMsg, uid, gid uint32, log
return sbox, nil
}
func (d *daemonState) sanitizeGroups(p *oz.Profile, username string, gids []uint32) (map[string]uint32, error) {
allowedGroups := d.config.DefaultGroups
allowedGroups = append(allowedGroups, p.AllowedGroups...)
if len(d.systemGroups) == 0 {
if err := d.cacheSystemGroups(); err != nil {
return nil, err
}
}
groups := map[string]uint32{}
for _, sg := range d.systemGroups {
for _, gg := range allowedGroups {
if sg.Name == gg {
found := false
for _, uname := range sg.Members {
if uname == username {
found = true
break
}
}
if !found {
continue
}
d.log.Debug("Allowing user: %s (%d)", gg, sg.Gid)
groups[sg.Name] = sg.Gid
break
}
}
}
return groups, nil
}
func (sbox *Sandbox) launchProgram(binpath, cpath, pwd string, args []string, log *logging.Logger) {
if sbox.profile.AllowFiles {
sbox.whitelistArgumentFiles(binpath, pwd, args, log)

@ -39,6 +39,7 @@ type LaunchMsg struct {
Path string
Name string
Pwd string
Gids []uint32
Args []string
Env []string
Noexec bool

@ -2,10 +2,10 @@ package ozinit
import (
"bufio"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"os/exec"
"os/signal"
@ -26,8 +26,6 @@ import (
"github.com/op/go-logging"
)
const EnvPrefix = "INIT_ENV_"
type initState struct {
log *logging.Logger
profile *oz.Profile
@ -36,8 +34,9 @@ type initState struct {
launchEnv []string
lock sync.Mutex
children map[int]*exec.Cmd
uid int
gid int
uid uint32
gid uint32
gids map[string]uint32
user *user.User
display int
fs *fs.Filesystem
@ -47,6 +46,19 @@ type initState struct {
network *network.SandboxNetwork
}
type InitData struct {
Profile oz.Profile
Config oz.Config
Sockaddr string
LaunchEnv []string
Uid uint32
Gid uint32
Gids map[string]uint32
User user.User
Network network.SandboxNetwork
Display int
}
// By convention oz-init writes log messages to stderr with a single character
// prefix indicating the logging level. These messages are read one line at a time
// over a pipe by oz-daemon and translated into appropriate log events.
@ -76,108 +88,40 @@ func parseArgs() *initState {
os.Exit(1)
}
getvar := func(name string) string {
val := os.Getenv(name)
if val == "" {
log.Error("Error: missing required '%s' argument", name)
os.Exit(1)
}
return val
}
pname := getvar("INIT_PROFILE")
sockaddr := getvar("INIT_SOCKET")
uidval := getvar("INIT_UID")
dispval := os.Getenv("INIT_DISPLAY")
stnip := os.Getenv("INIT_ADDR")
stnvhost := os.Getenv("INIT_VHOST")
stnvguest := os.Getenv("INIT_VGUEST")
stngateway := os.Getenv("INIT_GATEWAY")
var config *oz.Config
config, err := oz.LoadConfig(oz.DefaultConfigPath)
if err != nil {
if os.IsNotExist(err) {
log.Info("Configuration file (%s) is missing, using defaults.", oz.DefaultConfigPath)
config = oz.NewDefaultConfig()
} else {
log.Error("Could not load configuration: %s", oz.DefaultConfigPath, err)
os.Exit(1)
}
}
p, err := loadProfile(config.ProfileDir, pname)
if err != nil {
log.Error("Could not load profile %s: %v", pname, err)
os.Exit(1)
}
uid, err := strconv.Atoi(uidval)
if err != nil {
log.Error("Could not parse INIT_UID argument (%s) into an integer: %v", uidval, err)
os.Exit(1)
}
u, err := user.LookupId(uidval)
if err != nil {
log.Error("Failed to look up user with uid=%s: %v", uidval, err)
os.Exit(1)
}
gid, err := strconv.Atoi(u.Gid)
if err != nil {
log.Error("Failed to parse gid value (%s) from user struct: %v", u.Gid, err)
initData := new(InitData)
if err := json.NewDecoder(os.Stdin).Decode(&initData); err != nil {
log.Error("unable to decode init data: %v", err)
os.Exit(1)
}
display := 0
if dispval != "" {
d, err := strconv.Atoi(dispval)
if err != nil {
log.Error("Unable to parse display (%s) into an integer: %v", dispval, err)
os.Exit(1)
}
display = d
}
stn := new(network.SandboxNetwork)
if stnip != "" {
gateway, _, err := net.ParseCIDR(stngateway)
if err != nil {
log.Error("Unable to parse network configuration gateway (%s): %v", stngateway, err)
os.Exit(1)
}
log.Debug("Init state: %+v", initData)
stn.Ip = stnip
stn.VethHost = stnvhost
stn.VethGuest = stnvguest
stn.Gateway = gateway
if (initData.User.Uid != strconv.Itoa(int(initData.Uid))) || (initData.Uid == 0) {
log.Error("invalid uid or user passed to init.")
os.Exit(1)
}
env := []string{}
for _, e := range os.Environ() {
if strings.HasPrefix(e, EnvPrefix) {
e = e[len(EnvPrefix):]
log.Debug("Adding (%s) to launch environment", e)
env = append(env, e)
}
}
env = append(env, initData.LaunchEnv...)
env = append(env, "PATH=/usr/bin:/bin")
if p.XServer.Enabled {
env = append(env, "DISPLAY=:"+strconv.Itoa(display))
if initData.Profile.XServer.Enabled {
env = append(env, "DISPLAY=:"+strconv.Itoa(initData.Display))
}
return &initState{
log: log,
config: config,
sockaddr: sockaddr,
config: &initData.Config,
sockaddr: initData.Sockaddr,
launchEnv: env,
profile: p,
profile: &initData.Profile,
children: make(map[int]*exec.Cmd),
uid: uid,
gid: gid,
user: u,
display: display,
fs: fs.NewFilesystem(config, log),
network: stn,
uid: initData.Uid,
gid: initData.Gid,
gids: initData.Gids,
user: &initData.User,
display: initData.Display,
fs: fs.NewFilesystem(&initData.Config, log),
network: &initData.Network,
}
}
@ -196,7 +140,7 @@ func (st *initState) runInit() {
os.Exit(1)
}
if err := os.Chown(st.sockaddr, st.uid, st.gid); err != nil {
if err := os.Chown(st.sockaddr, int(st.uid), int(st.gid)); err != nil {
st.log.Warning("Failed to chown oz-init control socket: %v", err)
}
@ -238,6 +182,7 @@ func (st *initState) runInit() {
fsbx := path.Join("/tmp", "oz-sandbox")
err = ioutil.WriteFile(fsbx, []byte(st.profile.Name), 0644)
// Signal the daemon we are ready
os.Stderr.WriteString("OK\n")
go st.processSignals(sigs, s)
@ -267,10 +212,22 @@ func (st *initState) startXpraServer() {
xpra.Process.Env = []string{
"HOME=" + st.user.HomeDir,
}
groups := append([]uint32{}, st.gid)
if gid, gexists := st.gids["video"]; gexists {
groups = append(groups, gid)
}
if st.profile.XServer.AudioMode != "" && st.profile.XServer.AudioMode != oz.PROFILE_AUDIO_NONE {
if gid, gexists := st.gids["audio"]; gexists {
groups = append(groups, gid)
}
}
xpra.Process.SysProcAttr = &syscall.SysProcAttr{}
xpra.Process.SysProcAttr.Credential = &syscall.Credential{
Uid: uint32(st.uid),
Gid: uint32(st.gid),
Uid: st.uid,
Gid: st.gid,
Groups: groups,
}
st.log.Info("Starting xpra server")
if err := xpra.Process.Start(); err != nil {
@ -325,10 +282,15 @@ func (st *initState) launchApplication(cpath, pwd string, cmdArgs []string) (*ex
st.log.Warning("Failed to create stderr pipe: %v", err)
return nil, err
}
groups := append([]uint32{}, st.gid)
for _, gid := range st.gids {
groups = append(groups, gid)
}
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: uint32(st.uid),
Gid: uint32(st.gid),
Uid: st.uid,
Gid: st.gid,
Groups: groups,
}
cmd.Env = append(cmd.Env, st.launchEnv...)
@ -338,6 +300,9 @@ func (st *initState) launchApplication(cpath, pwd string, cmdArgs []string) (*ex
cmd.Args = append(cmd.Args, cmdArgs...)
if pwd == "" {
pwd = st.user.HomeDir
}
if _, err := os.Stat(pwd); err == nil {
cmd.Dir = pwd
}
@ -398,12 +363,19 @@ func (st *initState) handleRunShell(rs *RunShellMsg, msg *ipc.Message) error {
if (msg.Ucred.Uid == 0 || msg.Ucred.Gid == 0) && st.config.AllowRootShell != true {
return msg.Respond(&ErrorMsg{"Cannot open shell because allowRootShell is disabled"})
}
groups := append([]uint32{}, st.gid)
if (msg.Ucred.Uid != 0 && msg.Ucred.Gid != 0) {
for _, gid := range st.gids {
groups = append(groups, gid)
}
}
st.log.Info("Starting shell with uid = %d, gid = %d", msg.Ucred.Uid, msg.Ucred.Gid)
cmd := exec.Command(st.config.ShellPath, "-i")
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: msg.Ucred.Uid,
Gid: msg.Ucred.Gid,
Uid: msg.Ucred.Uid,
Gid: msg.Ucred.Gid,
Groups: groups,
}
cmd.Env = append(cmd.Env, st.launchEnv...)
if rs.Term != "" {

@ -49,7 +49,7 @@ func runSandboxed() {
os.Exit(1)
}
}
if err := daemon.Launch("0", apath, os.Args[1:], os.Environ(), false); err != nil {
if err := daemon.Launch("0", apath, os.Args[1:], false); err != nil {
fmt.Fprintf(os.Stderr, "launch command failed: %v.\n", err)
os.Exit(1)
}
@ -128,7 +128,7 @@ func handleLaunch(c *cli.Context) {
fmt.Println("Argument needed to launch command")
os.Exit(1)
}
err := daemon.Launch(c.Args()[0], "", c.Args()[1:], os.Environ(), noexec)
err := daemon.Launch(c.Args()[0], "", c.Args()[1:], noexec)
if err != nil {
fmt.Printf("launch command failed: %v\n", err)
os.Exit(1)
@ -177,7 +177,7 @@ func handleShell(c *cli.Context) {
fmt.Printf("start shell command failed: %v\n", err)
os.Exit(1)
}
fmt.Println("Entering interactive shell?\n")
fmt.Printf("Entering interactive shell in `%s`\n\n", sb.Profile)
st, err := SetRawTerminal(0)
HandleResize(fd)
f := os.NewFile(uintptr(fd), "")

@ -33,6 +33,7 @@ type Profile struct {
NoDefaults bool
// Allow bind mounting of files passed as arguments inside the sandbox
AllowFiles bool `json:"allow_files"`
AllowedGroups []string `json:"allowed_groups"`
// List of paths to bind mount inside jail
Whitelist []WhitelistItem
// List of paths to blacklist inside jail
@ -118,6 +119,7 @@ func NewDefaultProfile() *Profile {
return &Profile{
Multi: false,
AllowFiles: false,
AllowedGroups: []string{},
XServer: XServerConf{
Enabled: true,
EnableTray: false,

@ -1,6 +1,7 @@
{
"path": "/usr/bin/torbrowser-launcher"
, "watchdog": "start-tor-browser"
, "allowed_groups": ["debian-tor"]
, "xserver": {
"enabled": true
, "enable_tray": true
@ -21,8 +22,12 @@
, "blacklist": [
]
, "environment": [
{"name":"TOR_SKIP_LAUNCH"}
, {"name":"TOR_SOCKS_HOST"}
, {"name":"TOR_SOCKS_PORT"}
{"name":"TOR_SKIP_LAUNCH"}
, {"name":"TOR_SOCKS_HOST"}
, {"name":"TOR_SOCKS_PORT"}
, {"name":"TOR_CONTROL_PORT"}
, {"name":"TOR_CONTROL_PASSWD"}
, {"name":"TOR_CONTROL_AUTHENTICATE"}
, {"name":"TOR_CONTROL_COOKIE_AUTH_FILE"}
]
}

Loading…
Cancel
Save