Files
zeekayandHanzo Dev a386271c80 isolation is a value the org sets, and an empty one is not runc
Every sandbox was created privileged under the daemon's default runtime, and
the setting that would have changed it defaulted to an empty string that
silently meant runc. For a service whose whole premise is running code
somebody else wrote, that is not a boundary.

An org sets its isolation and the create request carries it, so a sandbox
records what it was made with: changing the policy moves new sandboxes and
leaves running ones alone. gvisor is the default. firecracker is delivered
through kata's shim, which is why it is one name rather than two. runc is
refused unless the runner names that org, and an empty list names nobody.

Absent means the default, unknown is a refusal, and a runtime the daemon does
not have is a refusal. Nothing substitutes quietly.

The runner took an organization in a field called UserId with a note beside
it saying so. Deciding who may have runc through a field named after
something else is not reviewable.

Privileged is replaced by dropping everything and granting the eight the
daemon uses. Nothing the sandbox does broke; what stopped working is mount,
mknod, raw sockets, chroot, writing to proc, and seeing the host's disks.
AUDIT_WRITE is among them because without it sudo works and complains every
time.

The creation histogram's lowest bucket was a tenth of a second, so the metric
could not observe the claim the product makes about its own speed. It can
now, and it carries the isolation, so comparing them is a query.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-11 13:47:31 -07:00

258 lines
5.9 KiB
Go

// Copyright 2025 Hanzo Industries Inc.
// SPDX-License-Identifier: AGPL-3.0
package ssh
import (
"fmt"
"io"
"os"
"os/exec"
"github.com/hanzoai/runtime/apps/daemon/pkg/common"
"github.com/hanzoai/runtime/apps/daemon/pkg/ssh/config"
"github.com/gliderlabs/ssh"
"github.com/pkg/sftp"
"golang.org/x/sys/unix"
log "github.com/sirupsen/logrus"
)
type Server struct {
ProjectDir string
DefaultProjectDir string
}
func (s *Server) Start() error {
forwardedTCPHandler := &ssh.ForwardedTCPHandler{}
unixForwardHandler := newForwardedUnixHandler()
sshServer := ssh.Server{
Addr: fmt.Sprintf(":%d", config.SSH_PORT),
Handler: func(session ssh.Session) {
switch ss := session.Subsystem(); ss {
case "":
case "sftp":
s.sftpHandler(session)
return
default:
log.Errorf("Subsystem %s not supported\n", ss)
session.Exit(1)
return
}
ptyReq, winCh, isPty := session.Pty()
if session.RawCommand() == "" && isPty {
s.handlePty(session, ptyReq, winCh)
} else {
s.handleNonPty(session)
}
},
ChannelHandlers: map[string]ssh.ChannelHandler{
"session": ssh.DefaultSessionHandler,
"direct-tcpip": ssh.DirectTCPIPHandler,
"[email protected]": directStreamLocalHandler,
},
RequestHandlers: map[string]ssh.RequestHandler{
"tcpip-forward": forwardedTCPHandler.HandleSSHRequest,
"cancel-tcpip-forward": forwardedTCPHandler.HandleSSHRequest,
"[email protected]": unixForwardHandler.HandleSSHRequest,
"[email protected]": unixForwardHandler.HandleSSHRequest,
},
SubsystemHandlers: map[string]ssh.SubsystemHandler{
"sftp": s.sftpHandler,
},
LocalPortForwardingCallback: ssh.LocalPortForwardingCallback(func(ctx ssh.Context, dhost string, dport uint32) bool {
return true
}),
ReversePortForwardingCallback: ssh.ReversePortForwardingCallback(func(ctx ssh.Context, host string, port uint32) bool {
return true
}),
SessionRequestCallback: func(sess ssh.Session, requestType string) bool {
return true
},
}
log.Printf("Starting ssh server on port %d...\n", config.SSH_PORT)
return sshServer.ListenAndServe()
}
func (s *Server) handlePty(session ssh.Session, ptyReq ssh.Pty, winCh <-chan ssh.Window) {
dir := s.ProjectDir
if _, err := os.Stat(s.ProjectDir); os.IsNotExist(err) {
dir = s.DefaultProjectDir
}
env := []string{}
if ssh.AgentRequested(session) {
l, err := ssh.NewAgentListener()
if err != nil {
log.Errorf("Failed to start agent listener: %v", err)
return
}
defer l.Close()
go ssh.ForwardAgentConnections(l, session)
env = append(env, fmt.Sprintf("%s=%s", "SSH_AUTH_SOCK", l.Addr().String()))
}
sizeCh := make(chan common.TTYSize)
go func() {
for win := range winCh {
sizeCh <- common.TTYSize{
Height: win.Height,
Width: win.Width,
}
}
}()
err := common.SpawnTTY(common.SpawnTTYOptions{
Dir: dir,
StdIn: session,
StdOut: session,
Term: ptyReq.Term,
Env: env,
SizeCh: sizeCh,
})
if err != nil {
// Debug log here because this gets called on each ssh "exit"
// TODO: Find a better way to handle this
log.Debugf("Failed to spawn tty: %v", err)
return
}
}
func (s *Server) handleNonPty(session ssh.Session) {
args := []string{}
if len(session.Command()) > 0 {
args = append([]string{"-c"}, session.RawCommand())
}
cmd := exec.Command("/bin/sh", args...)
cmd.Env = append(cmd.Env, os.Environ()...)
if ssh.AgentRequested(session) {
l, err := ssh.NewAgentListener()
if err != nil {
log.Errorf("Failed to start agent listener: %v", err)
return
}
defer l.Close()
go ssh.ForwardAgentConnections(l, session)
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", "SSH_AUTH_SOCK", l.Addr().String()))
}
cmd.Dir = s.ProjectDir
if _, err := os.Stat(s.ProjectDir); os.IsNotExist(err) {
cmd.Dir = s.DefaultProjectDir
}
cmd.Stdout = session
cmd.Stderr = session.Stderr()
stdinPipe, err := cmd.StdinPipe()
if err != nil {
log.Errorf("Unable to setup stdin for session: %v", err)
return
}
go func() {
_, err := io.Copy(stdinPipe, session)
if err != nil {
log.Errorf("Unable to read from session: %v", err)
return
}
_ = stdinPipe.Close()
}()
err = cmd.Start()
if err != nil {
log.Errorf("Unable to start command: %v", err)
return
}
sigs := make(chan ssh.Signal, 1)
session.Signals(sigs)
defer func() {
session.Signals(nil)
close(sigs)
}()
go func() {
for sig := range sigs {
signal := s.osSignalFrom(sig)
err := cmd.Process.Signal(signal)
if err != nil {
log.Warnf("Unable to send signal to process: %v", err)
}
}
}()
err = cmd.Wait()
if err != nil {
log.Println(session.RawCommand(), " ", err)
session.Exit(127)
return
}
err = session.Exit(0)
if err != nil {
log.Warnf("Unable to exit session: %v", err)
}
}
func (s *Server) osSignalFrom(sig ssh.Signal) os.Signal {
switch sig {
case ssh.SIGABRT:
return unix.SIGABRT
case ssh.SIGALRM:
return unix.SIGALRM
case ssh.SIGFPE:
return unix.SIGFPE
case ssh.SIGHUP:
return unix.SIGHUP
case ssh.SIGILL:
return unix.SIGILL
case ssh.SIGINT:
return unix.SIGINT
case ssh.SIGKILL:
return unix.SIGKILL
case ssh.SIGPIPE:
return unix.SIGPIPE
case ssh.SIGQUIT:
return unix.SIGQUIT
case ssh.SIGSEGV:
return unix.SIGSEGV
case ssh.SIGTERM:
return unix.SIGTERM
case ssh.SIGUSR1:
return unix.SIGUSR1
case ssh.SIGUSR2:
return unix.SIGUSR2
// Unhandled, use sane fallback.
default:
return unix.SIGKILL
}
}
func (s *Server) sftpHandler(session ssh.Session) {
debugStream := io.Discard
serverOptions := []sftp.ServerOption{
sftp.WithDebug(debugStream),
}
server, err := sftp.NewServer(
session,
serverOptions...,
)
if err != nil {
log.Errorf("sftp server init error: %s\n", err)
return
}
if err := server.Serve(); err == io.EOF {
server.Close()
} else if err != nil {
log.Errorf("sftp server completed with error: %s\n", err)
}
}