// 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" "net/http" "os" "os/exec" "os/signal" "os/user" "path" "runtime" "runtime/pprof" "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 cpuprofile string memprofile string ) 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. 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 // === Set up connection keepalive to client conn.StartupKeepAlive() // goroutine, returns immediately defer conn.ShutdownKeepAlive() if chaffing { conn.StartupChaff() } 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. 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 // === Set up connection keepalive to client conn.StartupKeepAlive() // goroutine, returns immediately defer conn.ShutdownKeepAlive() if chaffing { conn.StartupChaff() } //defer conn.Close() 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. 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) // // 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. // // One drawback of using 'login' is that the remote side // cannot give us back the shell's exit code, since it // exits back to 'login', which usually returns its own // 0 status back to us. // // Note login will drop privs to the intended user for us. // c = exec.Command(xs.GetTool("login"), "-f", "-p", who) //nolint:gosec } else { // Run shell directly (which allows nonzero exit codes back to // the local system upon shell exit, whereas 'login' does not.) // // Note we must drop privs ourselves for the user shell since // we aren't using 'login' on the remote end which would do it // for us. // 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)) _ = 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) conn.Pproc = c.Process.Pid //fmt.Printf("[process %d started]\n", c.Process.Pid) 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 ***") } }() // === Set up connection keepalive to client conn.StartupKeepAlive() // goroutine, returns immediately defer conn.ShutdownKeepAlive() if chaffing { conn.StartupChaff() } // #gv:s/label=\"runShellAs\$4\"/label=\"deferChaffShutdown\"/ defer func() { 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(fmt.Sprintf("*** Main proc has exited (%d) ***", c.ProcessState.ExitCode())) //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 rekeySecs uint var remodSupported bool // true: when rekeying, switch to random cipher/hmac alg var useSystemPasswd bool flag.BoolVar(&vopt, "v", false, "show version") flag.UintVar(&rekeySecs, "r", 300, "rekey interval in `secs`") flag.BoolVar(&remodSupported, "R", false, "Borg Countermeasures (remodulate cipher/hmac alg on each rekey)") flag.StringVar(&laddr, "l", ":2000", "interface[:port] to listen") //nolint:gomnd,lll 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.StringVar(&cpuprofile, "cpuprofile", "", "write cpu profile to <`file`>") flag.StringVar(&memprofile, "memprofile", "", "write memory profile to <`file`>") 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.") } } // === Profiling instrumentation if cpuprofile != "" { f, err := os.Create(cpuprofile) if err != nil { log.Fatal("could not create CPU profile: ", err) } defer f.Close() fmt.Println("StartCPUProfile()") if err := pprof.StartCPUProfile(f); err != nil { log.Fatal("could not start CPU profile: ", err) //nolint:gocritic } else { defer pprof.StopCPUProfile() } go func() { http.ListenAndServe("localhost:6060", nil) }() //nolint:errcheck,gosec } // 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 { case syscall.SIGTERM: //"terminated": logger.LogNotice(fmt.Sprintf("[Got signal: %s]", sig.String())) //nolint:errcheck signal.Reset() syscall.Kill(0, syscall.SIGTERM) //nolint:errcheck case syscall.SIGINT: //"interrupt": logger.LogNotice(fmt.Sprintf("[Got signal: %s]", sig.String())) //nolint:errcheck signal.Reset() syscall.Kill(0, syscall.SIGINT) //nolint:errcheck case syscall.SIGHUP: //"hangup": logger.LogNotice(fmt.Sprintf("[Got signal: %s - nop]", sig.String())) //nolint:errcheck if cpuprofile != "" || memprofile != "" { dumpProf() } default: logger.LogNotice(fmt.Sprintf("[Got signal: %s - ignored]", sig.String())) //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() //logger.LogDebug(fmt.Sprintf("l.Accept()\n")) 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") // Only enable cipher alg changes on re-key if we were told // to support it (launching xsd with -R), *and* the client // proposes to use it. if !remodSupported { if (conn.Opts() & xsnet.CORemodulateShields) != 0 { logger.LogDebug("[client proposed cipher/hmac remod, but we don't support it.]") conn.Close() continue } } else { if conn.Opts()&xsnet.CORemodulateShields != 0 { logger.LogDebug("[cipher/hmac remodulation active]") } else { logger.LogDebug("[cipher/hmac remodulation inactive]") } } conn.RekeyHelper(rekeySecs) // 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.ShutdownRekey() 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 ~/%s", token, xsnet.XS_ID_AUTHTOKFILE) 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 } // 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 } func dumpProf() { if cpuprofile != "" { pprof.StopCPUProfile() } if memprofile != "" { f, err := os.Create(memprofile) if err != nil { log.Fatal("could not create memory profile: ", err) } defer f.Close() runtime.GC() // get up-to-date statistics if err := pprof.WriteHeapProfile(f); err != nil { log.Fatal("could not write memory profile: ", err) //nolint:gocritic } } //os.Exit(status) }