mirror of https://gogs.blitter.com/RLabs/xs
892 lines
29 KiB
Go
Executable File
892 lines
29 KiB
Go
Executable File
// xsd server
|
|
//
|
|
// Copyright (c) 2017-2020 Russell Magee
|
|
// Licensed under the terms of the MIT license (see LICENSE.mit in this
|
|
// distribution)
|
|
//
|
|
// golang implementation by Russ Magee (rmagee_at_gmail.com)
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"os/user"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"blitter.com/go/goutmp"
|
|
xs "blitter.com/go/xs"
|
|
"blitter.com/go/xs/logger"
|
|
"blitter.com/go/xs/xsnet"
|
|
"github.com/creack/pty"
|
|
)
|
|
|
|
var (
|
|
version string
|
|
gitCommit string // set in -ldflags by build
|
|
|
|
useSysLogin bool
|
|
kcpMode string // set to a valid KCP BlockCrypt alg tag to use rather than TCP
|
|
|
|
// Log - syslog output (with no -d)
|
|
Log *logger.Writer
|
|
)
|
|
|
|
const (
|
|
AuthTokenLen = 64
|
|
LoginTimeoutSecs = 30
|
|
)
|
|
|
|
func ioctl(fd, request, argp uintptr) error {
|
|
if _, _, e := syscall.Syscall6(syscall.SYS_IOCTL, fd, request, argp, 0, 0, 0); e != 0 {
|
|
return e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ptsName(fd uintptr) (string, error) {
|
|
var n uintptr
|
|
err := ioctl(fd, syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("/dev/pts/%d", n), nil
|
|
}
|
|
|
|
/* -------------------------------------------------------------- */
|
|
// Perform a client->server copy
|
|
func runClientToServerCopyAs(who, ttype string, conn *xsnet.Conn, fpath string, chaffing bool) (exitStatus uint32, err error) {
|
|
u, _ := user.Lookup(who)
|
|
var uid, gid uint32
|
|
fmt.Sscanf(u.Uid, "%d", &uid)
|
|
fmt.Sscanf(u.Gid, "%d", &gid)
|
|
log.Println("uid:", uid, "gid:", gid)
|
|
|
|
// Need to clear server's env and set key vars of the
|
|
// target user. This isn't perfect (TERM doesn't seem to
|
|
// work 100%; ANSI/xterm colour isn't working even
|
|
// if we set "xterm" or "ansi" here; and line count
|
|
// reported by 'stty -a' defaults to 24 regardless
|
|
// of client shell window used to run client.
|
|
// Investigate -- rlm 2018-01-26)
|
|
os.Clearenv()
|
|
os.Setenv("HOME", u.HomeDir)
|
|
os.Setenv("TERM", ttype)
|
|
os.Setenv("XS_SESSION", "1")
|
|
|
|
var c *exec.Cmd
|
|
cmdName := xs.GetTool("tar")
|
|
|
|
var destDir string
|
|
if path.IsAbs(fpath) {
|
|
destDir = fpath
|
|
} else {
|
|
destDir = path.Join(u.HomeDir, fpath)
|
|
}
|
|
|
|
cmdArgs := []string{"-xz", "-C", destDir}
|
|
|
|
// NOTE the lack of quotes around --xform option's sed expression.
|
|
// When args are passed in exec() format, no quoting is required
|
|
// (as this isn't input from a shell) (right? -rlm 20180823)
|
|
//cmdArgs := []string{"-x", "-C", destDir, `--xform=s#.*/\(.*\)#\1#`}
|
|
fmt.Println(cmdName, cmdArgs)
|
|
c = exec.Command(cmdName, cmdArgs...)
|
|
|
|
c.Dir = destDir
|
|
|
|
//If os.Clearenv() isn't called by server above these will be seen in the
|
|
//client's session env.
|
|
//c.Env = []string{"HOME=" + u.HomeDir, "SUDO_GID=", "SUDO_UID=", "SUDO_USER=", "SUDO_COMMAND=", "MAIL=", "LOGNAME="+who}
|
|
//c.Dir = u.HomeDir
|
|
c.SysProcAttr = &syscall.SysProcAttr{}
|
|
c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid}
|
|
c.Stdin = conn
|
|
c.Stdout = os.Stdout
|
|
c.Stderr = os.Stderr
|
|
|
|
if chaffing {
|
|
conn.EnableChaff()
|
|
}
|
|
defer conn.DisableChaff()
|
|
defer conn.ShutdownChaff()
|
|
|
|
// Start the command (no pty)
|
|
log.Printf("[%v %v]\n", cmdName, cmdArgs)
|
|
err = c.Start() // returns immediately
|
|
/////////////
|
|
// NOTE: There is, apparently, a bug in Go stdlib here. Start()
|
|
// can actually return immediately, on a command which *does*
|
|
// start but exits quickly, with c.Wait() error
|
|
// "c.Wait status: exec: not started".
|
|
// As in this example, attempting a client->server copy to
|
|
// a nonexistent remote dir (it's tar exiting right away, exitStatus
|
|
// 2, stderr
|
|
// /bin/tar -xz -C /home/someuser/nosuchdir
|
|
// stderr: fork/exec /bin/tar: no such file or directory
|
|
//
|
|
// In this case, c.Wait() won't give us the real
|
|
// exit status (is it lost?).
|
|
/////////////
|
|
if err != nil {
|
|
log.Println("cmd exited immediately. Cannot get cmd.Wait().ExitStatus()")
|
|
err = errors.New("cmd exited prematurely")
|
|
//exitStatus = uint32(254)
|
|
exitStatus = xsnet.CSEExecFail
|
|
} else {
|
|
if err := c.Wait(); err != nil {
|
|
//fmt.Println("*** c.Wait() done ***")
|
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
|
// The program has exited with an exit code != 0
|
|
|
|
// This works on both Unix and Windows. Although package
|
|
// syscall is generally platform dependent, WaitStatus is
|
|
// defined for both Unix and Windows and in both cases has
|
|
// an ExitStatus() method with the same signature.
|
|
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
|
exitStatus = uint32(status.ExitStatus())
|
|
//err = errors.New("cmd returned nonzero status")
|
|
log.Printf("Exit Status: %d\n", exitStatus)
|
|
}
|
|
}
|
|
}
|
|
log.Println("*** client->server cp finished ***")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Perform a server->client copy
|
|
func runServerToClientCopyAs(who, ttype string, conn *xsnet.Conn, srcPath string, chaffing bool) (exitStatus uint32, err error) {
|
|
u, err := user.Lookup(who)
|
|
if err != nil {
|
|
exitStatus = 1
|
|
return
|
|
}
|
|
var uid, gid uint32
|
|
_, _ = fmt.Sscanf(u.Uid, "%d", &uid)
|
|
_, _ = fmt.Sscanf(u.Gid, "%d", &gid)
|
|
log.Println("uid:", uid, "gid:", gid)
|
|
|
|
// Need to clear server's env and set key vars of the
|
|
// target user. This isn't perfect (TERM doesn't seem to
|
|
// work 100%; ANSI/xterm colour isn't working even
|
|
// if we set "xterm" or "ansi" here; and line count
|
|
// reported by 'stty -a' defaults to 24 regardless
|
|
// of client shell window used to run client.
|
|
// Investigate -- rlm 2018-01-26)
|
|
os.Clearenv()
|
|
_ = os.Setenv("HOME", u.HomeDir)
|
|
_ = os.Setenv("TERM", ttype)
|
|
_ = os.Setenv("XS_SESSION", "1")
|
|
|
|
var c *exec.Cmd
|
|
cmdName := xs.GetTool("tar")
|
|
if !path.IsAbs(srcPath) {
|
|
srcPath = fmt.Sprintf("%s%c%s", u.HomeDir, os.PathSeparator, srcPath)
|
|
}
|
|
|
|
srcDir, srcBase := path.Split(srcPath)
|
|
cmdArgs := []string{"-cz", "-C", srcDir, "-f", "-", srcBase}
|
|
|
|
c = exec.Command(cmdName, cmdArgs...)
|
|
|
|
//If os.Clearenv() isn't called by server above these will be seen in the
|
|
//client's session env.
|
|
//c.Env = []string{"HOME=" + u.HomeDir, "SUDO_GID=", "SUDO_UID=", "SUDO_USER=", "SUDO_COMMAND=", "MAIL=", "LOGNAME="+who}
|
|
c.Dir = u.HomeDir
|
|
c.SysProcAttr = &syscall.SysProcAttr{}
|
|
c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid}
|
|
c.Stdout = conn
|
|
// Stderr sinkholing (or buffering to something other than stdout)
|
|
// is important. Any extraneous output to tarpipe messes up remote
|
|
// side as it's expecting pure tar data.
|
|
// (For example, if user specifies abs paths, tar outputs
|
|
// "Removing leading '/' from path names")
|
|
stdErrBuffer := new(bytes.Buffer)
|
|
c.Stderr = stdErrBuffer
|
|
//c.Stderr = nil
|
|
|
|
if chaffing {
|
|
conn.EnableChaff()
|
|
}
|
|
//defer conn.Close()
|
|
defer conn.DisableChaff()
|
|
defer conn.ShutdownChaff()
|
|
|
|
// Start the command (no pty)
|
|
log.Printf("[%v %v]\n", cmdName, cmdArgs)
|
|
err = c.Start() // returns immediately
|
|
if err != nil {
|
|
log.Printf("Command finished with error: %v", err)
|
|
return xsnet.CSEExecFail, err // !?
|
|
}
|
|
if err := c.Wait(); err != nil {
|
|
//fmt.Println("*** c.Wait() done ***")
|
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
|
// The program has exited with an exit code != 0
|
|
|
|
// This works on both Unix and Windows. Although package
|
|
// syscall is generally platform dependent, WaitStatus is
|
|
// defined for both Unix and Windows and in both cases has
|
|
// an ExitStatus() method with the same signature.
|
|
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
|
exitStatus = uint32(status.ExitStatus())
|
|
if len(stdErrBuffer.Bytes()) > 0 {
|
|
log.Print(stdErrBuffer)
|
|
}
|
|
log.Printf("Exit Status: %d", exitStatus)
|
|
}
|
|
}
|
|
}
|
|
//fmt.Println("*** server->client cp finished ***")
|
|
return
|
|
}
|
|
|
|
// Run a command (via default shell) as a specific user. Uses
|
|
// ptys to support commands which expect a terminal. //nolint:gofmt
|
|
func runShellAs(who, hname, ttype, cmd string, interactive bool, //nolint:funlen
|
|
conn *xsnet.Conn, chaffing bool) (exitStatus uint32, err error) {
|
|
var wg sync.WaitGroup
|
|
u, err := user.Lookup(who)
|
|
if err != nil {
|
|
exitStatus = 1
|
|
return
|
|
}
|
|
var uid, gid uint32
|
|
_, _ = fmt.Sscanf(u.Uid, "%d", &uid)
|
|
_, _ = fmt.Sscanf(u.Gid, "%d", &gid)
|
|
log.Println("uid:", uid, "gid:", gid)
|
|
|
|
// Need to clear server's env and set key vars of the
|
|
// target user. This isn't perfect (TERM doesn't seem to
|
|
// work 100%; ANSI/xterm colour isn't working even
|
|
// if we set "xterm" or "ansi" here; and line count
|
|
// reported by 'stty -a' defaults to 24 regardless
|
|
// of client shell window used to run client.
|
|
// Investigate -- rlm 2018-01-26)
|
|
os.Clearenv()
|
|
_ = os.Setenv("HOME", u.HomeDir)
|
|
_ = os.Setenv("TERM", ttype)
|
|
_ = os.Setenv("XS_SESSION", "1")
|
|
|
|
var c *exec.Cmd
|
|
|
|
if interactive {
|
|
if useSysLogin {
|
|
// Use the server's login binary (post-auth, which
|
|
// is still done via our own bcrypt file)
|
|
//
|
|
// Note login will drop privs to the intended user for us
|
|
//
|
|
// Things UNIX login does, like print the 'motd',
|
|
// and use the shell specified by /etc/passwd, will be done
|
|
// automagically, at the cost of another external tool
|
|
// dependency.
|
|
//
|
|
c = exec.Command(xs.GetTool("login"), "-f", "-p", who) //nolint:gosec
|
|
} else {
|
|
// Using our separate login via local passwd file
|
|
//
|
|
// Note we must drop privs ourselves for the user shell
|
|
//
|
|
c = exec.Command(xs.GetTool("bash"), "-i", "-l") //nolint:gosec
|
|
c.SysProcAttr = &syscall.SysProcAttr{}
|
|
c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid}
|
|
}
|
|
} else {
|
|
c = exec.Command(xs.GetTool("bash"), "-c", cmd) //nolint:gosec
|
|
c.SysProcAttr = &syscall.SysProcAttr{}
|
|
c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid}
|
|
}
|
|
//If os.Clearenv() isn't called by server above these will be seen in the
|
|
//client's session env.
|
|
//c.Env = []string{"HOME=" + u.HomeDir, "SUDO_GID=", "SUDO_UID=", "SUDO_USER=", "SUDO_COMMAND=", "MAIL=", "LOGNAME="+who}
|
|
c.Dir = u.HomeDir
|
|
|
|
// Start the command with a pty.
|
|
ptmx, err := pty.Start(c) // returns immediately with ptmx file
|
|
if err != nil {
|
|
log.Println(err)
|
|
return xsnet.CSEPtyExecFail, err
|
|
}
|
|
// Make sure to close the pty at the end.
|
|
// #gv:s/label=\"runShellAs\$1\"/label=\"deferPtmxClose\"/
|
|
defer func() {
|
|
//logger.LogDebug(fmt.Sprintf("[Exited process was %d]", c.Process.Pid))
|
|
|
|
//Ensure socket has sent all data to client prior to closing
|
|
//NOTE: This is not ideal, as it would be better to somehow
|
|
//determine if there is any pending outgoing (write) data to the
|
|
//underlying socket (TCP/KCP) prior to closing; however Go's net pkg
|
|
//completely hides lower-level stuff. net.Conn.Close() according to
|
|
//docs sends written data in the background, so how best to determine
|
|
//all data has been sent? -rlm 2022-10-04
|
|
|
|
time.Sleep(100 * time.Millisecond) //Empirically determined :^/
|
|
_ = ptmx.Close()
|
|
}()
|
|
|
|
// get pty info for system accounting (who, lastlog)
|
|
pts, pe := ptsName(ptmx.Fd())
|
|
if pe != nil {
|
|
return xsnet.CSEPtyGetNameFail, err
|
|
}
|
|
utmpx := goutmp.Put_utmp(who, pts, hname)
|
|
defer func() { goutmp.Unput_utmp(utmpx) }()
|
|
goutmp.Put_lastlog_entry("xs", who, pts, hname)
|
|
|
|
log.Printf("[%s]\n", cmd)
|
|
if err != nil {
|
|
log.Printf("Command finished with error: %v", err)
|
|
} else {
|
|
// Watch for term resizes
|
|
// #gv:s/label=\"runShellAs\$2\"/label=\"termResizeWatcher\"/
|
|
go func() {
|
|
for sz := range conn.WinCh {
|
|
log.Printf("[Setting term size to: %v %v]\n", sz.Rows, sz.Cols)
|
|
pty.Setsize(ptmx, &pty.Winsize{Rows: sz.Rows, Cols: sz.Cols}) //nolint:errcheck
|
|
}
|
|
log.Println("*** WinCh goroutine done ***")
|
|
}()
|
|
|
|
// Copy stdin to the pty.. (bgnd goroutine)
|
|
// #gv:s/label=\"runShellAs\$3\"/label=\"stdinToPtyWorker\"/
|
|
go func() {
|
|
_, e := io.Copy(ptmx, conn)
|
|
if e != nil {
|
|
log.Println("** stdin->pty ended **:", e.Error())
|
|
} else {
|
|
log.Println("*** stdin->pty goroutine done ***")
|
|
}
|
|
}()
|
|
|
|
if chaffing {
|
|
conn.EnableChaff()
|
|
}
|
|
// #gv:s/label=\"runShellAs\$4\"/label=\"deferChaffShutdown\"/
|
|
defer func() {
|
|
conn.DisableChaff()
|
|
conn.ShutdownChaff()
|
|
}()
|
|
|
|
// ..and the pty to stdout.
|
|
// This may take some time exceeding that of the
|
|
// actual command's lifetime, so the c.Wait() below
|
|
// must synchronize with the completion of this goroutine
|
|
// to ensure all stdout data gets to the client before
|
|
// connection is closed.
|
|
wg.Add(1)
|
|
// #gv:s/label=\"runShellAs\$5\"/label=\"ptyToStdoutWorker\"/
|
|
go func() {
|
|
defer wg.Done()
|
|
_, e := io.Copy(conn, ptmx)
|
|
if e != nil {
|
|
log.Println("** pty->stdout ended **:", e.Error())
|
|
} else {
|
|
// The above io.Copy() will exit when the command attached
|
|
// to the pty exits
|
|
log.Println("*** pty->stdout goroutine done ***")
|
|
}
|
|
}()
|
|
|
|
if err := c.Wait(); err != nil {
|
|
//fmt.Println("*** c.Wait() done ***")
|
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
|
// The program has exited with an exit code != 0
|
|
|
|
// This works on both Unix and Windows. Although package
|
|
// syscall is generally platform dependent, WaitStatus is
|
|
// defined for both Unix and Windows and in both cases has
|
|
// an ExitStatus() method with the same signature.
|
|
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
|
exitStatus = uint32(status.ExitStatus())
|
|
log.Printf("Exit Status: %d", exitStatus)
|
|
}
|
|
}
|
|
conn.SetStatus(xsnet.CSOType(exitStatus))
|
|
} else {
|
|
logger.LogDebug("*** Main proc has exited. ***") //nolint:errcheck
|
|
// Background jobs still may be running; close the
|
|
// pty anyway, so the client can return before
|
|
// wg.Wait() below completes (Issue #18)
|
|
if interactive {
|
|
_ = ptmx.Close()
|
|
}
|
|
}
|
|
wg.Wait() // Wait on pty->stdout completion to client
|
|
}
|
|
return
|
|
}
|
|
|
|
// GenAuthToken generates a pseudorandom auth token for a specific
|
|
// user from a specific host to allow non-interactive logins.
|
|
func GenAuthToken(who string, connhost string) string {
|
|
//hname, e := os.Hostname()
|
|
//if e != nil {
|
|
// hname = "#badhost#"
|
|
//}
|
|
hname := connhost
|
|
|
|
token := make([]byte, AuthTokenLen)
|
|
_, _ = rand.Read(token)
|
|
return fmt.Sprintf("%s:%s:%s", hname, who, hex.EncodeToString(token))
|
|
}
|
|
|
|
var (
|
|
aKEXAlgs allowedKEXAlgs
|
|
aCipherAlgs allowedCipherAlgs
|
|
aHMACAlgs allowedHMACAlgs
|
|
)
|
|
|
|
type allowedKEXAlgs []string
|
|
type allowedCipherAlgs []string
|
|
type allowedHMACAlgs []string
|
|
|
|
func (a allowedKEXAlgs) allowed(k xsnet.KEXAlg) bool {
|
|
for i := 0; i < len(a); i++ {
|
|
if a[i] == "KEX_all" || a[i] == k.String() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (a *allowedKEXAlgs) String() string {
|
|
return fmt.Sprintf("allowedKEXAlgs: %v", *a)
|
|
}
|
|
|
|
func (a *allowedKEXAlgs) Set(value string) error {
|
|
*a = append(*a, strings.TrimSpace(value))
|
|
return nil
|
|
}
|
|
|
|
func (a allowedCipherAlgs) allowed(c xsnet.CSCipherAlg) bool {
|
|
for i := 0; i < len(a); i++ {
|
|
if a[i] == "C_all" || a[i] == c.String() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (a *allowedCipherAlgs) String() string {
|
|
return fmt.Sprintf("allowedCipherAlgs: %v", *a)
|
|
}
|
|
|
|
func (a *allowedCipherAlgs) Set(value string) error {
|
|
*a = append(*a, strings.TrimSpace(value))
|
|
return nil
|
|
}
|
|
|
|
func (a allowedHMACAlgs) allowed(h xsnet.CSHmacAlg) bool {
|
|
for i := 0; i < len(a); i++ {
|
|
if a[i] == "H_all" || a[i] == h.String() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (a *allowedHMACAlgs) String() string {
|
|
return fmt.Sprintf("allowedHMACAlgs: %v", *a)
|
|
}
|
|
|
|
func (a *allowedHMACAlgs) Set(value string) error {
|
|
*a = append(*a, strings.TrimSpace(value))
|
|
return nil
|
|
}
|
|
|
|
// Main server that listens and spawns goroutines for each
|
|
// connecting client to serve interactive or file copy sessions
|
|
// and any requested tunnels.
|
|
// Note that this server does not do UNIX forks of itself to give
|
|
// each client its own separate manager process, so if the main
|
|
// daemon dies, all clients will be rudely disconnected.
|
|
// Consider this when planning to restart or upgrade in-place an installation.
|
|
// TODO: reduce gocyclo
|
|
func main() { //nolint:funlen,gocyclo
|
|
var vopt bool
|
|
var chaffEnabled bool
|
|
var chaffFreqMin uint
|
|
var chaffFreqMax uint
|
|
var chaffBytesMax uint
|
|
var dbg bool
|
|
var laddr string
|
|
|
|
var useSystemPasswd bool
|
|
|
|
flag.BoolVar(&vopt, "v", false, "show version")
|
|
flag.StringVar(&laddr, "l", ":2000", "interface[:port] to listen")
|
|
flag.StringVar(&kcpMode, "K", "unused", `set to one of ["KCP_NONE","KCP_AES", "KCP_BLOWFISH", "KCP_CAST5", "KCP_SM4", "KCP_SALSA20", "KCP_SIMPLEXOR", "KCP_TEA", "KCP_3DES", "KCP_TWOFISH", "KCP_XTEA"] to use KCP (github.com/xtaci/kcp-go) reliable UDP instead of TCP`) //nolint:lll
|
|
flag.BoolVar(&useSysLogin, "L", false, "use system login")
|
|
flag.BoolVar(&chaffEnabled, "e", true, "enable chaff pkts")
|
|
flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt freq min (msecs)") //nolint:gomnd
|
|
flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt freq max (msecs)") //nolint:gomnd
|
|
flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt size max (bytes)") //nolint:gomnd
|
|
flag.BoolVar(&useSystemPasswd, "s", true, "use system shadow passwds")
|
|
flag.BoolVar(&dbg, "d", false, "debug logging")
|
|
flag.Var(&aKEXAlgs, "aK", "Allowed KEX `alg`s (eg. '-aK KEXAlgA -aK KEXAlgB ...')"+`
|
|
KEX_all
|
|
KEX_HERRADURA256
|
|
KEX_HERRADURA512
|
|
KEX_HERRADURA1024
|
|
KEX_HERRADURA2048
|
|
KEX_KYBER512
|
|
KEX_KYBER768
|
|
KEX_KYBER1024
|
|
KEX_NEWHOPE
|
|
KEX_NEWHOPE_SIMPLE
|
|
KEX_FRODOKEM_1344AES
|
|
KEX_FRODOKEM_1344SHAKE
|
|
KEX_FRODOKEM_976AES
|
|
KEX_FRODOKEM_976SHAKE`)
|
|
flag.Var(&aCipherAlgs, "aC", "Allowed `cipher`s (eg. '-aC CAlgA -aC CAlgB ...')"+`
|
|
C_all
|
|
C_AES_256
|
|
C_TWOFISH_128
|
|
C_BLOWFISH_64
|
|
C_CRYPTMT1
|
|
C_HOPSCOTCH
|
|
C_CHACHA20_12`)
|
|
flag.Var(&aHMACAlgs, "aH", "Allowed `HMAC`s (eg. '-aH HMACAlgA -aH HMACAlgB ...')"+`
|
|
H_all
|
|
H_SHA256
|
|
H_SHA512`)
|
|
|
|
flag.Parse()
|
|
|
|
if vopt {
|
|
fmt.Printf("version %s (%s)\n", version, gitCommit)
|
|
os.Exit(0)
|
|
}
|
|
|
|
{
|
|
me, e := user.Current()
|
|
if e != nil || me.Uid != "0" {
|
|
log.Fatal("Must run as root.")
|
|
}
|
|
}
|
|
|
|
// Enforce some sane min/max vals on chaff flags
|
|
if chaffFreqMin < 2 { //nolint:gomnd
|
|
chaffFreqMin = 2
|
|
}
|
|
if chaffFreqMax == 0 {
|
|
chaffFreqMax = chaffFreqMin + 1
|
|
}
|
|
if chaffBytesMax == 0 || chaffBytesMax > 4096 {
|
|
chaffBytesMax = 64
|
|
}
|
|
|
|
Log, _ = logger.New(logger.LOG_DAEMON|logger.LOG_DEBUG|logger.LOG_NOTICE|logger.LOG_ERR, "xsd")
|
|
xsnet.Init(dbg, "xsd", logger.LOG_DAEMON|logger.LOG_DEBUG|logger.LOG_NOTICE|logger.LOG_ERR)
|
|
if dbg {
|
|
log.SetOutput(Log)
|
|
} else {
|
|
log.SetOutput(io.Discard)
|
|
}
|
|
|
|
// Set up allowed algs, if specified (default allow all)
|
|
if len(aKEXAlgs) == 0 {
|
|
aKEXAlgs = []string{"none"}
|
|
}
|
|
logger.LogNotice(fmt.Sprintf("Allowed KEXAlgs: %v\n", aKEXAlgs)) //nolint:errcheck
|
|
|
|
if len(aCipherAlgs) == 0 {
|
|
aCipherAlgs = []string{"none"}
|
|
}
|
|
logger.LogNotice(fmt.Sprintf("Allowed CipherAlgs: %v\n", aCipherAlgs)) //nolint:errcheck
|
|
|
|
if len(aHMACAlgs) == 0 {
|
|
aHMACAlgs = []string{"none"}
|
|
}
|
|
logger.LogNotice(fmt.Sprintf("Allowed HMACAlgs: %v\n", aHMACAlgs)) //nolint:errcheck
|
|
|
|
// Set up handler for daemon signalling
|
|
exitCh := make(chan os.Signal, 1)
|
|
signal.Notify(exitCh, os.Signal(syscall.SIGTERM), os.Signal(syscall.SIGINT), os.Signal(syscall.SIGHUP), os.Signal(syscall.SIGUSR1), os.Signal(syscall.SIGUSR2)) //nolint:lll
|
|
go func() {
|
|
for {
|
|
sig := <-exitCh
|
|
switch sig.String() {
|
|
case "terminated":
|
|
logger.LogNotice(fmt.Sprintf("[Got signal: %s]", sig)) //nolint:errcheck
|
|
signal.Reset()
|
|
syscall.Kill(0, syscall.SIGTERM) //nolint:errcheck
|
|
case "interrupt":
|
|
logger.LogNotice(fmt.Sprintf("[Got signal: %s]", sig)) //nolint:errcheck
|
|
signal.Reset()
|
|
syscall.Kill(0, syscall.SIGINT) //nolint:errcheck
|
|
case "hangup":
|
|
logger.LogNotice(fmt.Sprintf("[Got signal: %s - nop]", sig)) //nolint:errcheck
|
|
default:
|
|
logger.LogNotice(fmt.Sprintf("[Got signal: %s - ignored]", sig)) //nolint:errcheck
|
|
}
|
|
}
|
|
}()
|
|
|
|
proto := "tcp"
|
|
if kcpMode != "unused" {
|
|
proto = "kcp"
|
|
}
|
|
l, err := xsnet.Listen(proto, laddr, kcpMode)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer l.Close()
|
|
|
|
log.Println("Serving on", laddr)
|
|
for {
|
|
// Wait for a connection.
|
|
// Then check if client-proposed algs are allowed
|
|
conn, err := l.Accept()
|
|
if err != nil {
|
|
log.Printf("Accept() got error(%v), hanging up.\n", err)
|
|
} else {
|
|
if !aKEXAlgs.allowed(conn.KEX()) {
|
|
log.Printf("Accept() rejected for banned KEX alg %d, hanging up.\n", conn.KEX())
|
|
conn.SetStatus(xsnet.CSEKEXAlgDenied)
|
|
conn.Close()
|
|
} else if !aCipherAlgs.allowed(conn.CAlg()) {
|
|
log.Printf("Accept() rejected for banned Cipher alg %d, hanging up.\n", conn.CAlg())
|
|
conn.SetStatus(xsnet.CSECipherAlgDenied)
|
|
conn.Close()
|
|
} else if !aHMACAlgs.allowed(conn.HAlg()) {
|
|
log.Printf("Accept() rejected for banned HMAC alg %d, hanging up.\n", conn.HAlg())
|
|
conn.SetStatus(xsnet.CSEHMACAlgDenied)
|
|
conn.Close()
|
|
} else {
|
|
log.Println("Accepted client")
|
|
|
|
// Set up chaffing to client
|
|
// Will only start when runShellAs() is called
|
|
// after stdin/stdout are hooked up
|
|
conn.SetupChaff(chaffFreqMin, chaffFreqMax, chaffBytesMax) // configure server->client chaffing
|
|
|
|
// Handle the connection in a new goroutine.
|
|
// The loop then returns to accepting, so that
|
|
// multiple connections may be served concurrently.
|
|
go func(hc *xsnet.Conn) (e error) {
|
|
defer hc.Close()
|
|
|
|
// Start login timeout here and disconnect if user/pass phase stalls
|
|
loginTimeout := time.AfterFunc(LoginTimeoutSecs*time.Second, func() {
|
|
logger.LogNotice(fmt.Sprintln("Login timed out")) //nolint:errcheck
|
|
hc.Write([]byte{0}) //nolint:errcheck
|
|
hc.Close()
|
|
})
|
|
|
|
//We use io.ReadFull() here to guarantee we consume
|
|
//just the data we want for the xs.Session, and no more.
|
|
//Otherwise data will be sitting in the channel that isn't
|
|
//passed down to the command handlers.
|
|
var rec xs.Session
|
|
var len1, len2, len3, len4, len5, len6 uint32
|
|
|
|
n, err := fmt.Fscanf(hc, "%d %d %d %d %d %d\n", &len1, &len2, &len3, &len4, &len5, &len6)
|
|
log.Printf("xs.Session read:%d %d %d %d %d %d\n", len1, len2, len3, len4, len5, len6)
|
|
|
|
if err != nil || n < 6 {
|
|
log.Println("[Bad xs.Session fmt]")
|
|
return err
|
|
}
|
|
|
|
tmp := make([]byte, len1)
|
|
_, err = io.ReadFull(hc, tmp)
|
|
if err != nil {
|
|
log.Println("[Bad xs.Session.Op]")
|
|
return err
|
|
}
|
|
rec.SetOp(tmp)
|
|
|
|
tmp = make([]byte, len2)
|
|
_, err = io.ReadFull(hc, tmp)
|
|
if err != nil {
|
|
log.Println("[Bad xs.Session.Who]")
|
|
return err
|
|
}
|
|
rec.SetWho(tmp)
|
|
|
|
tmp = make([]byte, len3)
|
|
_, err = io.ReadFull(hc, tmp)
|
|
if err != nil {
|
|
log.Println("[Bad xs.Session.ConnHost]")
|
|
return err
|
|
}
|
|
rec.SetConnHost(tmp)
|
|
|
|
tmp = make([]byte, len4)
|
|
_, err = io.ReadFull(hc, tmp)
|
|
if err != nil {
|
|
log.Println("[Bad xs.Session.TermType]")
|
|
return err
|
|
}
|
|
rec.SetTermType(tmp)
|
|
|
|
tmp = make([]byte, len5)
|
|
_, err = io.ReadFull(hc, tmp)
|
|
if err != nil {
|
|
log.Println("[Bad xs.Session.Cmd]")
|
|
return err
|
|
}
|
|
rec.SetCmd(tmp)
|
|
|
|
tmp = make([]byte, len6)
|
|
_, err = io.ReadFull(hc, tmp)
|
|
if err != nil {
|
|
log.Println("[Bad xs.Session.AuthCookie]")
|
|
return err
|
|
}
|
|
rec.SetAuthCookie(tmp)
|
|
|
|
log.Printf("[xs.Session: op:%c who:%s connhost:%s cmd:%s auth:****]\n",
|
|
rec.Op()[0], string(rec.Who()), string(rec.ConnHost()), string(rec.Cmd()))
|
|
|
|
var valid bool
|
|
var allowedCmds string // Currently unused
|
|
if xs.AuthUserByToken(xs.NewAuthCtx(), string(rec.Who()), string(rec.ConnHost()), string(rec.AuthCookie(true))) {
|
|
valid = true
|
|
} else {
|
|
if useSystemPasswd {
|
|
//var passErr error
|
|
valid, _ /*passErr*/ = xs.VerifyPass(xs.NewAuthCtx(), string(rec.Who()), string(rec.AuthCookie(true)))
|
|
} else {
|
|
valid, allowedCmds = xs.AuthUserByPasswd(xs.NewAuthCtx(), string(rec.Who()), string(rec.AuthCookie(true)), "/etc/xs.passwd")
|
|
}
|
|
}
|
|
|
|
_ = loginTimeout.Stop()
|
|
// Security scrub
|
|
rec.ClearAuthCookie()
|
|
|
|
// Tell client if auth was valid
|
|
if valid {
|
|
hc.Write([]byte{1}) //nolint:errcheck
|
|
} else {
|
|
logger.LogNotice(fmt.Sprintln("Invalid user", string(rec.Who()))) //nolint:errcheck
|
|
hc.Write([]byte{0}) //nolint:errcheck
|
|
return
|
|
}
|
|
|
|
log.Printf("[allowedCmds:%s]\n", allowedCmds)
|
|
|
|
if rec.Op()[0] == 'A' {
|
|
// Generate automated login token
|
|
addr := hc.RemoteAddr()
|
|
hname := goutmp.GetHost(addr.String())
|
|
logger.LogNotice(fmt.Sprintf("[Generating autologin token for [%s@%s]]\n", rec.Who(), hname)) //nolint:errcheck
|
|
token := GenAuthToken(string(rec.Who()), string(rec.ConnHost()))
|
|
tokenCmd := fmt.Sprintf("echo %q | tee -a ~/.xs_id", token)
|
|
cmdStatus, runErr := runShellAs(string(rec.Who()), hname, string(rec.TermType()), tokenCmd, false, hc, chaffEnabled)
|
|
// Returned hopefully via an EOF or exit/logout;
|
|
// Clear current op so user can enter next, or EOF
|
|
rec.SetOp([]byte{0})
|
|
if runErr != nil {
|
|
logger.LogErr(fmt.Sprintf("[Error generating autologin token for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
|
|
} else {
|
|
log.Printf("[Autologin token generation completed for %s@%s, status %d]\n", rec.Who(), hname, cmdStatus)
|
|
hc.SetStatus(xsnet.CSOType(cmdStatus))
|
|
}
|
|
} else if rec.Op()[0] == 'c' {
|
|
// Non-interactive command
|
|
addr := hc.RemoteAddr()
|
|
hname := goutmp.GetHost(addr.String())
|
|
logger.LogNotice(fmt.Sprintf("[Running command for [%s@%s]]\n", rec.Who(), hname)) //nolint:errcheck
|
|
cmdStatus, runErr := runShellAs(string(rec.Who()), hname, string(rec.TermType()), string(rec.Cmd()), false, hc, chaffEnabled)
|
|
// Returned hopefully via an EOF or exit/logout;
|
|
// Clear current op so user can enter next, or EOF
|
|
rec.SetOp([]byte{0})
|
|
if runErr != nil {
|
|
logger.LogErr(fmt.Sprintf("[Error spawning cmd for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
|
|
} else {
|
|
logger.LogNotice(fmt.Sprintf("[Command completed for %s@%s, status %d]\n", rec.Who(), hname, cmdStatus)) //nolint:errcheck
|
|
hc.SetStatus(xsnet.CSOType(cmdStatus))
|
|
}
|
|
} else if rec.Op()[0] == 's' {
|
|
// Interactive session
|
|
addr := hc.RemoteAddr()
|
|
hname := goutmp.GetHost(addr.String())
|
|
logger.LogNotice(fmt.Sprintf("[Running shell for [%s@%s]]\n", rec.Who(), hname)) //nolint:errcheck
|
|
|
|
cmdStatus, runErr := runShellAs(string(rec.Who()), hname, string(rec.TermType()), string(rec.Cmd()), true, hc, chaffEnabled)
|
|
// Returned hopefully via an EOF or exit/logout;
|
|
// Clear current op so user can enter next, or EOF
|
|
rec.SetOp([]byte{0})
|
|
if runErr != nil {
|
|
Log.Err(fmt.Sprintf("[Error spawning shell for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
|
|
} else {
|
|
logger.LogNotice(fmt.Sprintf("[Shell completed for %s@%s, status %d]\n", rec.Who(), hname, cmdStatus)) //nolint:errcheck
|
|
hc.SetStatus(xsnet.CSOType(cmdStatus))
|
|
}
|
|
} else if rec.Op()[0] == 'D' {
|
|
// File copy (destination) operation - client copy to server
|
|
log.Printf("[Client->Server copy]\n")
|
|
addr := hc.RemoteAddr()
|
|
hname := goutmp.GetHost(addr.String())
|
|
logger.LogNotice(fmt.Sprintf("[c->s copy for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
|
|
cmdStatus, runErr := runClientToServerCopyAs(string(rec.Who()), string(rec.TermType()), hc, string(rec.Cmd()), chaffEnabled)
|
|
// Returned hopefully via an EOF or exit/logout;
|
|
// Clear current op so user can enter next, or EOF
|
|
rec.SetOp([]byte{0})
|
|
if runErr != nil {
|
|
logger.LogErr(fmt.Sprintf("[c->s copy error for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
|
|
} else {
|
|
logger.LogNotice(fmt.Sprintf("[c->s copy completed for %s@%s, status %d]\n", rec.Who(), hname, cmdStatus)) //nolint:errcheck
|
|
}
|
|
// TODO: Test this with huge files.. see Bug #22 - do we need to
|
|
// sync w/sender (client) that we've gotten all data?
|
|
hc.SetStatus(xsnet.CSOType(cmdStatus))
|
|
|
|
// Send CSOExitStatus *before* client closes channel
|
|
s := make([]byte, 4) //nolint:gomnd
|
|
binary.BigEndian.PutUint32(s, cmdStatus)
|
|
log.Printf("** cp writing closeStat %d at Close()\n", cmdStatus)
|
|
hc.WritePacket(s, xsnet.CSOExitStatus) //nolint:errcheck
|
|
} else if rec.Op()[0] == 'S' {
|
|
// File copy (src) operation - server copy to client
|
|
log.Printf("[Server->Client copy]\n")
|
|
addr := hc.RemoteAddr()
|
|
hname := goutmp.GetHost(addr.String())
|
|
logger.LogNotice(fmt.Sprintf("[s->c copy for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
|
|
cmdStatus, runErr := runServerToClientCopyAs(string(rec.Who()), string(rec.TermType()), hc, string(rec.Cmd()), chaffEnabled)
|
|
if runErr != nil {
|
|
logger.LogErr(fmt.Sprintf("[s->c copy error for %s@%s]\n", rec.Who(), hname)) //nolint:errcheck
|
|
} else {
|
|
// Returned hopefully via an EOF or exit/logout;
|
|
logger.LogNotice(fmt.Sprintf("[s->c copy completed for %s@%s, status %d]\n", rec.Who(), hname, cmdStatus)) //nolint:errcheck
|
|
}
|
|
// HACK: Bug #22: (xc) Need to wait for rcvr to get final data
|
|
// TODO: Await specific msg from client to inform they have gotten all data from the tarpipe
|
|
time.Sleep(900 * time.Millisecond) //nolint:gomnd // Let rcvr set this on setup?
|
|
|
|
// Clear current op so user can enter next, or EOF
|
|
rec.SetOp([]byte{0})
|
|
hc.SetStatus(xsnet.CSOType(cmdStatus))
|
|
//fmt.Println("Waiting for EOF from other end.")
|
|
//_, _ = hc.Read(nil /*ackByte*/)
|
|
//fmt.Println("Got remote end ack.")
|
|
} else {
|
|
logger.LogErr(fmt.Sprintln("[Bad xs.Session]")) //nolint:errcheck
|
|
}
|
|
return
|
|
}(&conn) //nolint:errcheck
|
|
} // algs valid and not blacklisted
|
|
} // Accept() success
|
|
} //endfor
|
|
//logger.LogNotice(fmt.Sprintln("[Exiting]")) //nolint:errcheck
|
|
}
|