Merge branch 'hkexcp-proto'

This commit is contained in:
Russ Magee 2018-08-31 11:47:39 -07:00
commit 45d270b03e
5 changed files with 611 additions and 158 deletions

12
cp.cmd Normal file
View file

@ -0,0 +1,12 @@
## Template for copying files from local to remote site, destdir DEST:
tar -cz -f - testdir/sub1/bar.txt | \
tar -xzv -C DEST --xform="s#.*/\(.*\)#\1#"
# Note the --xform= option will strip leading path components from the file
# on extraction (ie., throw away dirtree info when copying into remote DEST)
#
# Probably need to have a '-r' option ala 'scp -r' to control --xform=
# (in the absence of --xform=.. above, files and dirs will all be extracted
# to remote DEST preserving tree structure.)
tar cf /dev/stdout ../*.txt | tar xf -

View file

@ -202,7 +202,6 @@ func Dial(protocol string, ipport string, extensions ...string) (hc *Conn, err e
hc.r, hc.rm, err = hc.getStream(hc.h.FA()) hc.r, hc.rm, err = hc.getStream(hc.h.FA())
hc.w, hc.wm, err = hc.getStream(hc.h.FA()) hc.w, hc.wm, err = hc.getStream(hc.h.FA())
*hc.closeStat = 99 // open or prematurely-closed status *hc.closeStat = 99 // open or prematurely-closed status
return return
} }
@ -439,7 +438,12 @@ func (hc Conn) Read(b []byte) (n int, err error) {
log.Printf("[TermSize pkt: rows %v cols %v]\n", hc.Rows, hc.Cols) log.Printf("[TermSize pkt: rows %v cols %v]\n", hc.Rows, hc.Cols)
hc.WinCh <- WinSize{hc.Rows, hc.Cols} hc.WinCh <- WinSize{hc.Rows, hc.Cols}
} else if ctrlStatOp == CSOExitStatus { } else if ctrlStatOp == CSOExitStatus {
*hc.closeStat = uint8(payloadBytes[0]) if len(payloadBytes) > 0 {
*hc.closeStat = uint8(payloadBytes[0])
} else {
log.Println("[truncated payload, cannot determine CSOExitStatus]")
*hc.closeStat = 98
}
} else { } else {
hc.dBuf.Write(payloadBytes) hc.dBuf.Write(payloadBytes)
//log.Printf("hc.dBuf: %s\n", hex.Dump(hc.dBuf.Bytes())) //log.Printf("hc.dBuf: %s\n", hex.Dump(hc.dBuf.Bytes()))
@ -450,10 +454,14 @@ func (hc Conn) Read(b []byte) (n int, err error) {
hTmp := hc.rm.Sum(nil)[0:4] hTmp := hc.rm.Sum(nil)[0:4]
log.Printf("<%04x) HMAC:(i)%s (c)%02x\r\n", decryptN, hex.EncodeToString([]byte(hmacIn[0:])), hTmp) log.Printf("<%04x) HMAC:(i)%s (c)%02x\r\n", decryptN, hex.EncodeToString([]byte(hmacIn[0:])), hTmp)
// Log alert if hmac didn't match, corrupted channel if *hc.closeStat > 90 {
if !bytes.Equal(hTmp, []byte(hmacIn[0:])) /*|| hmacIn[0] > 0xf8*/ { log.Println("[cannot verify HMAC]")
fmt.Println("** ALERT - detected HMAC mismatch, possible channel tampering **") } else {
_, _ = hc.c.Write([]byte{CSOHmacInvalid}) // Log alert if hmac didn't match, corrupted channel
if !bytes.Equal(hTmp, []byte(hmacIn[0:])) /*|| hmacIn[0] > 0xf8*/ {
fmt.Println("** ALERT - detected HMAC mismatch, possible channel tampering **")
_, _ = hc.c.Write([]byte{CSOHmacInvalid})
}
} }
} }

1
hkexsh/hkexcp Symbolic link
View file

@ -0,0 +1 @@
hkexsh

501
hkexsh/hkexsh.go Normal file → Executable file
View file

@ -16,9 +16,11 @@ import (
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
"path"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"syscall"
hkexsh "blitter.com/go/hkexsh" hkexsh "blitter.com/go/hkexsh"
"blitter.com/go/hkexsh/hkexnet" "blitter.com/go/hkexsh/hkexnet"
@ -30,7 +32,7 @@ type cmdSpec struct {
who []byte who []byte
cmd []byte cmd []byte
authCookie []byte authCookie []byte
status int // though UNIX shell exit status is uint8, os.Exit() wants int status int // UNIX exit status is uint8, but os.Exit() wants int
} }
var ( var (
@ -52,8 +54,237 @@ func GetSize() (cols, rows int, err error) {
return return
} }
// Demo of a simple client that dials up to a simple test server to func parseNonSwitchArgs(a []string) (user, host, path string, isDest bool, otherArgs []string) {
// send data. // Whether fancyArg is src or dst file depends on flag.Args() index;
// fancyArg as last flag.Args() element denotes dstFile
// fancyArg as not-last flag.Args() element denotes srcFile
var fancyUser, fancyHost, fancyPath string
for i, arg := range a {
if strings.Contains(arg, ":") || strings.Contains(arg, "@") {
fancyArg := strings.Split(flag.Arg(i), "@")
var fancyHostPath []string
if len(fancyArg) < 2 {
//TODO: no user specified, use current
fancyUser = "[default:getUser]"
fancyHostPath = strings.Split(fancyArg[0], ":")
} else {
// user@....
fancyUser = fancyArg[0]
fancyHostPath = strings.Split(fancyArg[1], ":")
}
// [...@]host[:path]
if len(fancyHostPath) > 1 {
fancyPath = fancyHostPath[1]
}
fancyHost = fancyHostPath[0]
//if fancyPath == "" {
// fancyPath = "."
//}
if i == len(a)-1 {
isDest = true
fmt.Println("remote path isDest")
}
fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "path:", fancyPath)
} else {
otherArgs = append(otherArgs, a[i])
}
}
return fancyUser, fancyHost, fancyPath, isDest, otherArgs
}
// doCopyMode begins a secure hkexsh local<->remote file copy operation.
func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, rec *cmdSpec) (err error, exitStatus int) {
if remoteDest {
fmt.Println("local files:", files, "remote filepath:", string(rec.cmd))
var c *exec.Cmd
//os.Clearenv()
//os.Setenv("HOME", u.HomeDir)
//os.Setenv("TERM", "vt102") // TODO: server or client option?
cmdName := "/bin/tar"
cmdArgs := []string{"-cz", "-f", "/dev/stdout"}
files = strings.TrimSpace(files)
// Awesome fact: tar actually can take multiple -C args, and
// changes to the dest dir *as it sees each one*. This enables
// its use below, where clients can send scattered sets of source
// files and dirs to be extraced to a single dest dir server-side,
// whilst preserving the subtrees of dirs on the other side. :)
// Eg., tar -c -f /dev/stdout -C /dirA fileInA -C /some/where/dirB fileInB /foo/dirC
// packages fileInA, fileInB, and dirC at a single toplevel in the tar.
// The tar authors are/were real smarties :)
for _, v := range strings.Split(files, " ") {
dirTmp, fileTmp := path.Split(v)
cmdArgs = append(cmdArgs, "-C", dirTmp, fileTmp)
//cmdArgs = append(cmdArgs, v)
}
fmt.Printf("[%v %v]\n", cmdName, cmdArgs)
// 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{"-xvz", "-C", files, `--xform=s#.*/\(.*\)#\1#`}
c = exec.Command(cmdName, cmdArgs...)
c.Dir, _ = os.Getwd()
fmt.Println("[wd:", c.Dir, "]")
c.Stdout = conn
// Stderr sinkholing 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")
c.Stderr = nil
// Start the command (no pty)
err = c.Start() // returns immediately
if err != nil {
fmt.Println(err)
//log.Fatal(err)
} else {
if err = c.Wait(); err != nil {
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 = status.ExitStatus()
log.Printf("Exit Status: %d", exitStatus)
}
}
}
fmt.Println("*** client->server cp finished ***")
}
} else {
fmt.Println("remote filepath:", string(rec.cmd), "local files:", files)
var c *exec.Cmd
//os.Clearenv()
//os.Setenv("HOME", u.HomeDir)
//os.Setenv("TERM", "vt102") // TODO: server or client option?
cmdName := "/bin/tar"
destPath := files
cmdArgs := []string{"-xz", "-C", destPath}
fmt.Printf("[%v %v]\n", cmdName, cmdArgs)
// 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{"-xvz", "-C", destPath, `--xform=s#.*/\(.*\)#\1#`}
c = exec.Command(cmdName, cmdArgs...)
c.Stdin = conn
c.Stdout = os.Stdout
c.Stderr = os.Stderr
// Start the command (no pty)
err = c.Start() // returns immediately
if err != nil {
fmt.Println(err)
//log.Fatal(err)
} else {
if err = c.Wait(); err != nil {
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 = status.ExitStatus()
log.Printf("Exit Status: %d", exitStatus)
}
}
}
fmt.Println("*** server->client cp finished ***")
}
}
return
}
// doShellMode begins an hkexsh shell session (one-shot command or interactive).
func doShellMode(isInteractive bool, conn *hkexnet.Conn, oldState *hkexsh.State, rec *cmdSpec) {
//client reader (from server) goroutine
//Read remote end's stdout
wg.Add(1)
go func() {
defer wg.Done()
// By deferring a call to wg.Done(),
// each goroutine guarantees that it marks
// its direction's stream as finished.
// io.Copy() expects EOF so normally this will
// exit with inerr == nil
_, inerr := io.Copy(os.Stdout, conn)
if inerr != nil {
fmt.Println(inerr)
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
os.Exit(1)
}
rec.status = int(conn.GetStatus())
log.Println("rec.status:", rec.status)
if isInteractive {
log.Println("[* Got EOF *]")
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
}
}()
// Only look for data from stdin to send to remote end
// for interactive sessions.
if isInteractive {
handleTermResizes(conn)
// client writer (to server) goroutine
// Write local stdin to remote end
wg.Add(1)
go func() {
defer wg.Done()
//!defer wg.Done()
// Copy() expects EOF so this will
// exit with outerr == nil
//!_, outerr := io.Copy(conn, os.Stdin)
_, outerr := func(conn *hkexnet.Conn, r io.Reader) (w int64, e error) {
w, e = io.Copy(conn, r)
return w, e
}(conn, os.Stdin)
if outerr != nil {
log.Println(outerr)
fmt.Println(outerr)
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
os.Exit(255)
}
log.Println("[Sent EOF]")
}()
}
// Wait until both stdin and stdout goroutines finish before returning
// (ensure client gets all data from server before closing)
wg.Wait()
}
func UsageShell() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(os.Stderr, "%s [opts] [user]@server\n", os.Args[0])
flag.PrintDefaults()
}
func UsageCp() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(os.Stderr, "%s [opts] srcFileOrDir [...] [user]@server[:dstpath]\n", os.Args[0])
fmt.Fprintf(os.Stderr, "%s [opts] [user]@server[:srcFileOrDir] dstPath\n", os.Args[0])
flag.PrintDefaults()
}
// hkexsh - a client for secure shell and file copy operations.
// //
// While conforming to the basic net.Conn interface HKex.Conn has extra // While conforming to the basic net.Conn interface HKex.Conn has extra
// capabilities designed to allow apps to define connection options, // capabilities designed to allow apps to define connection options,
@ -68,44 +299,163 @@ func main() {
version := "0.1pre (NO WARRANTY)" version := "0.1pre (NO WARRANTY)"
var vopt bool var vopt bool
var dbg bool var dbg bool
var shellMode bool // if true act as shell, else file copier
var cAlg string var cAlg string
var hAlg string var hAlg string
var server string var server string
var port uint
var cmdStr string var cmdStr string
var altUser string
var copySrc []byte
var copyDst string
var authCookie string var authCookie string
var chaffEnabled bool var chaffEnabled bool
var chaffFreqMin uint var chaffFreqMin uint
var chaffFreqMax uint var chaffFreqMax uint
var chaffBytesMax uint var chaffBytesMax uint
var op []byte
isInteractive := false isInteractive := false
flag.BoolVar(&vopt, "v", false, "show version") flag.BoolVar(&vopt, "v", false, "show version")
flag.StringVar(&cAlg, "c", "C_AES_256", "cipher [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]")
flag.StringVar(&hAlg, "h", "H_SHA256", "hmac [\"H_SHA256\"]")
flag.StringVar(&server, "s", "localhost:2000", "server hostname/address[:port]")
flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)")
flag.StringVar(&altUser, "u", "", "specify alternate user")
flag.StringVar(&authCookie, "a", "", "auth cookie")
flag.BoolVar(&chaffEnabled, "cE", true, "enabled chaff pkts (default true)")
flag.UintVar(&chaffFreqMin, "cfm", 100, "chaff pkt freq min (msecs)")
flag.UintVar(&chaffFreqMax, "cfM", 5000, "chaff pkt freq max (msecs)")
flag.UintVar(&chaffBytesMax, "cbM", 64, "chaff pkt size max (bytes)")
flag.BoolVar(&dbg, "d", false, "debug logging") flag.BoolVar(&dbg, "d", false, "debug logging")
flag.StringVar(&cAlg, "c", "C_AES_256", "`cipher` [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]")
flag.StringVar(&hAlg, "m", "H_SHA256", "`hmac` [\"H_SHA256\"]")
flag.UintVar(&port, "p", 2000, "`port`")
flag.StringVar(&authCookie, "a", "", "auth cookie")
flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts (default true)")
flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt `freq` min (msecs)")
flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt `freq` max (msecs)")
flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt `size` max (bytes)")
// Find out what program we are (shell or copier)
myPath := strings.Split(os.Args[0], string(os.PathSeparator))
if myPath[len(myPath)-1] != "hkexcp" && myPath[len(myPath)-1] != "hkexcp.exe" {
// hkexsh accepts a command (-x) but not
// a srcpath (-r) or dstpath (-t)
flag.StringVar(&cmdStr, "x", "", "`command` to run (if not specified run interactive shell)")
shellMode = true
flag.Usage = UsageShell
} else {
flag.Usage = UsageCp
}
flag.Parse() flag.Parse()
remoteUser, tmpHost, tmpPath, pathIsDest, otherArgs :=
parseNonSwitchArgs(flag.Args())
fmt.Println("otherArgs:", otherArgs)
// Set defaults if user doesn't specify user, path or port
var uname string
if remoteUser == "" {
u, _ := user.Current()
uname = u.Username
} else {
uname = remoteUser
}
if tmpHost != "" {
server = tmpHost + ":" + fmt.Sprintf("%d", port)
}
if tmpPath == "" {
tmpPath = "."
}
var fileArgs string
if !shellMode /*&& tmpPath != ""*/ {
// -if pathIsSrc && len(otherArgs) > 1 ERROR
// -else flatten otherArgs into space-delim list => copySrc
if pathIsDest {
if len(otherArgs) == 0 {
log.Fatal("ERROR: Must specify at least one dest path for copy")
} else {
for _, v := range otherArgs {
copySrc = append(copySrc, ' ')
copySrc = append(copySrc, v...)
}
copyDst = tmpPath
fileArgs = string(copySrc)
}
} else {
if len(otherArgs) == 0 {
log.Fatal("ERROR: Must specify src path for copy")
} else if len(otherArgs) == 1 {
copyDst = otherArgs[0]
if strings.Contains(copyDst, "*") || strings.Contains(copyDst, "?") {
log.Fatal("ERROR: wildcards not allowed in dest path for copy")
}
} else {
log.Fatal("ERROR: cannot specify more than one dest path for copy")
}
copySrc = []byte(tmpPath)
fileArgs = copyDst
}
}
// Do some more option consistency checks
//fmt.Println("server finally is:", server)
if flag.NFlag() == 0 && server == "" {
flag.Usage()
os.Exit(0)
}
if vopt { if vopt {
fmt.Printf("version v%s\n", version) fmt.Printf("version v%s\n", version)
os.Exit(0) os.Exit(0)
} }
if len(cmdStr) != 0 && (len(copySrc) != 0 || len(copyDst) != 0) {
log.Fatal("incompatible options -- either cmd (-x) or copy ops but not both")
}
//-------------------------------------------------------------------
// Here we have parsed all options and can now carry out
// either the shell session or copy operation.
_ = shellMode
if dbg { if dbg {
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
} else { } else {
log.SetOutput(ioutil.Discard) log.SetOutput(ioutil.Discard)
} }
if shellMode {
// We must make the decision about interactivity before Dial()
// as it affects chaffing behaviour. 20180805
if len(cmdStr) == 0 {
op = []byte{'s'}
isInteractive = true
} else {
op = []byte{'c'}
// non-interactive cmds may complete quickly, so chaff earlier/faster
// to help ensure there's some cover to the brief traffic.
// (ignoring cmdline values)
chaffFreqMin = 2
chaffFreqMax = 10
}
} else {
// as copy mode is also non-interactive, set up chaffing
// just like the 'c' mode above
chaffFreqMin = 2
chaffFreqMax = 10
if pathIsDest {
// client->server file copy
// src file list is in copySrc
op = []byte{'D'}
fmt.Println("client->server copy:", string(copySrc), "->", copyDst)
cmdStr = copyDst
} else {
// server->client file copy
// remote src file(s) in copyDsr
op = []byte{'S'}
fmt.Println("server->client copy:", string(copySrc), "->", copyDst)
cmdStr = string(copySrc)
}
}
conn, err := hkexnet.Dial("tcp", server, cAlg, hAlg) conn, err := hkexnet.Dial("tcp", server, cAlg, hAlg)
if err != nil { if err != nil {
fmt.Println("Err!") fmt.Println("Err!")
@ -118,42 +468,16 @@ func main() {
// TODO: send flag to server side indicating this // TODO: send flag to server side indicating this
// affects shell command used // affects shell command used
var oldState *hkexsh.State var oldState *hkexsh.State
if isatty.IsTerminal(os.Stdin.Fd()) { if shellMode {
oldState, err = hkexsh.MakeRaw(int(os.Stdin.Fd())) if isatty.IsTerminal(os.Stdin.Fd()) {
if err != nil { oldState, err = hkexsh.MakeRaw(int(os.Stdin.Fd()))
panic(err) if err != nil {
panic(err)
}
defer func() { _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
} else {
log.Println("NOT A TTY")
} }
defer func() { _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
} else {
log.Println("NOT A TTY")
}
var uname string
if len(altUser) == 0 {
u, _ := user.Current()
uname = u.Username
} else {
uname = altUser
}
var op []byte
if len(cmdStr) == 0 {
op = []byte{'s'}
isInteractive = true
} else if cmdStr == "-" {
op = []byte{'c'}
cmdStdin, err := ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
cmdStr = strings.Trim(string(cmdStdin), "\r\n")
} else {
op = []byte{'c'}
// non-interactive cmds may complete quickly, so chaff earlier/faster
// to help ensure there's some cover to the brief traffic.
// (ignoring cmdline values)
chaffFreqMin = 2
chaffFreqMax = 10
} }
if len(authCookie) == 0 { if len(authCookie) == 0 {
@ -188,76 +512,19 @@ func main() {
conn.SetupChaff(chaffFreqMin, chaffFreqMax, chaffBytesMax) // enable client->server chaffing conn.SetupChaff(chaffFreqMin, chaffFreqMax, chaffBytesMax) // enable client->server chaffing
if chaffEnabled { if chaffEnabled {
conn.EnableChaff() conn.EnableChaff()
} defer conn.DisableChaff()
defer conn.DisableChaff() defer conn.ShutdownChaff()
defer conn.ShutdownChaff()
//client reader (from server) goroutine
wg.Add(1)
go func() {
// By deferring a call to wg.Done(),
// each goroutine guarantees that it marks
// its direction's stream as finished.
//
// Whichever direction's goroutine finishes first
// will call wg.Done() once more, explicitly, to
// hang up on the other side, so that this client
// exits immediately on an EOF from either side.
defer wg.Done()
// io.Copy() expects EOF so this will
// exit with inerr == nil
_, inerr := io.Copy(os.Stdout, conn)
if inerr != nil {
if inerr.Error() != "EOF" {
fmt.Println(inerr)
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
os.Exit(1)
}
}
rec.status = int(conn.GetStatus())
log.Println("rec.status:", rec.status)
if isInteractive {
log.Println("[* Got EOF *]")
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
wg.Done()
//os.Exit(rec.status)
}
}()
if isInteractive {
handleTermResizes(conn)
// client writer (to server) goroutine
wg.Add(1)
go func() {
defer wg.Done()
// Copy() expects EOF so this will
// exit with outerr == nil
//!_, outerr := io.Copy(conn, os.Stdin)
_, outerr := func(conn *hkexnet.Conn, r io.Reader) (w int64, e error) {
return io.Copy(conn, r)
}(conn, os.Stdin)
if outerr != nil {
log.Println(outerr)
if outerr.Error() != "EOF" {
fmt.Println(outerr)
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
os.Exit(255)
}
}
log.Println("[Sent EOF]")
wg.Done() // client hung up, close WaitGroup to exit client
}()
} }
// Wait until both stdin and stdout goroutines finish if shellMode {
wg.Wait() doShellMode(isInteractive, conn, oldState, rec)
} else {
doCopyMode(conn, pathIsDest, fileArgs, rec)
}
if oldState != nil {
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
}
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
os.Exit(rec.status) os.Exit(rec.status)
} }

235
hkexshd/hkexshd.go Normal file → Executable file
View file

@ -16,8 +16,10 @@ import (
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
"path"
"runtime" "runtime"
"strings" "strings"
"sync"
"syscall" "syscall"
"blitter.com/go/goutmp" "blitter.com/go/goutmp"
@ -36,53 +38,170 @@ type cmdSpec struct {
} }
/* -------------------------------------------------------------- */ /* -------------------------------------------------------------- */
// Perform a client->server copy
/* func runClientToServerCopyAs(who string, conn hkexnet.Conn, fpath string, chaffing bool) (err error, exitStatus int) {
// Run a command (via os.exec) as a specific user
//
// Uses ptys to support commands which expect a terminal.
func runCmdAs(who string, cmd string, conn hkex.Conn) (err error) {
u, _ := user.Lookup(who) u, _ := user.Lookup(who)
var uid, gid uint32 var uid, gid uint32
fmt.Sscanf(u.Uid, "%d", &uid) fmt.Sscanf(u.Uid, "%d", &uid)
fmt.Sscanf(u.Gid, "%d", &gid) fmt.Sscanf(u.Gid, "%d", &gid)
fmt.Println("uid:", uid, "gid:", gid) log.Println("uid:", uid, "gid:", gid)
args := strings.Split(cmd, " ") // Need to clear server's env and set key vars of the
arg0 := args[0] // target user. This isn't perfect (TERM doesn't seem to
args = args[1:] // work 100%; ANSI/xterm colour isn't working even
c := exec.Command(arg0, args...) // 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", "vt102") // TODO: server or client option?
var c *exec.Cmd
cmdName := "/bin/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#`}
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 = &syscall.SysProcAttr{}
c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid} c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid}
c.Stdin = conn c.Stdin = conn
c.Stdout = conn c.Stdout = os.Stdout
c.Stderr = conn c.Stderr = os.Stderr
// Start the command with a pty. if chaffing {
ptmx, err := pty.Start(c) // returns immediately with ptmx file conn.EnableChaff()
if err != nil {
return err
} }
// Make sure to close the pty at the end. defer conn.DisableChaff()
defer func() { _ = ptmx.Close() }() // Best effort. defer conn.ShutdownChaff()
// Copy stdin to the pty and the pty to stdout.
go func() { _, _ = io.Copy(ptmx, conn) }()
_, _ = io.Copy(conn, ptmx)
//err = c.Run() // returns when c finishes.
// Start the command (no pty)
log.Printf("[%v %v]\n", cmdName, cmdArgs)
err = c.Start() // returns immediately
if err != nil { if err != nil {
log.Printf("Command finished with error: %v", err) log.Printf("Command finished with error: %v", err)
log.Printf("[%s]\n", cmd) return err, 253 // !?
} 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 = status.ExitStatus()
log.Printf("Exit Status: %d", exitStatus)
}
}
}
fmt.Println("*** client->server cp finished ***")
return
}
}
// Perform a server->client copy
func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaffing bool) (err error, exitStatus int) {
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", "vt102") // TODO: server or client option?
var c *exec.Cmd
cmdName := "/bin/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 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")
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 err, 253 // !?
} 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 = status.ExitStatus()
log.Printf("Exit Status: %d", exitStatus)
}
}
}
fmt.Println("*** server->client cp finished ***")
return
} }
return
} }
*/
// Run a command (via default shell) as a specific user // Run a command (via default shell) as a specific user
// //
// Uses ptys to support commands which expect a terminal. // Uses ptys to support commands which expect a terminal.
func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, chaffing bool) (err error, exitStatus int) { func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, chaffing bool) (err error, exitStatus int) {
var wg sync.WaitGroup
u, _ := user.Lookup(who) u, _ := user.Lookup(who)
var uid, gid uint32 var uid, gid uint32
fmt.Sscanf(u.Uid, "%d", &uid) fmt.Sscanf(u.Uid, "%d", &uid)
@ -135,15 +254,16 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha
log.Printf("[Setting term size to: %v %v]\n", sz.Rows, sz.Cols) log.Printf("[Setting term size to: %v %v]\n", sz.Rows, sz.Cols)
pty.Setsize(ptmx, &pty.Winsize{Rows: sz.Rows, Cols: sz.Cols}) pty.Setsize(ptmx, &pty.Winsize{Rows: sz.Rows, Cols: sz.Cols})
} }
fmt.Println("*** WinCh goroutine done ***")
}() }()
// Copy stdin to the pty.. (bgnd goroutine) // Copy stdin to the pty.. (bgnd goroutine)
go func() { go func() {
_, e := io.Copy(ptmx, conn) _, e := io.Copy(ptmx, conn)
if e != nil { if e != nil {
log.Printf("** std->pty ended **\n") log.Println("** stdin->pty ended **:", e.Error())
return
} }
fmt.Println("*** stdin->pty goroutine done ***")
}() }()
if chaffing { if chaffing {
@ -153,17 +273,25 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha
defer conn.ShutdownChaff() defer conn.ShutdownChaff()
// ..and the pty to stdout. // ..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)
go func() { go func() {
defer wg.Done()
_, e := io.Copy(conn, ptmx) _, e := io.Copy(conn, ptmx)
if e != nil { if e != nil {
log.Printf("** pty->stdout ended **\n") log.Println("** pty->stdout ended **:", e.Error())
return
} }
// The above io.Copy() will exit when the command attached // The above io.Copy() will exit when the command attached
// to the pty exits // to the pty exits
fmt.Println("*** pty->stdout goroutine done ***")
}() }()
if err := c.Wait(); err != nil { if err := c.Wait(); err != nil {
fmt.Println("*** c.Wait() done ***")
if exiterr, ok := err.(*exec.ExitError); ok { if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0 // The program has exited with an exit code != 0
@ -177,6 +305,7 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha
} }
} }
} }
wg.Wait() // Wait on pty->stdout completion to client
} }
return return
} }
@ -202,10 +331,10 @@ func main() {
flag.BoolVar(&vopt, "v", false, "show version") flag.BoolVar(&vopt, "v", false, "show version")
flag.StringVar(&laddr, "l", ":2000", "interface[:port] to listen") flag.StringVar(&laddr, "l", ":2000", "interface[:port] to listen")
flag.BoolVar(&chaffEnabled, "cE", true, "enabled chaff pkts") flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts")
flag.UintVar(&chaffFreqMin, "cfm", 100, "chaff pkt freq min (msecs)") flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt freq min (msecs)")
flag.UintVar(&chaffFreqMax, "cfM", 5000, "chaff pkt freq max (msecs)") flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt freq max (msecs)")
flag.UintVar(&chaffBytesMax, "cbM", 64, "chaff pkt size max (bytes)") flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt size max (bytes)")
flag.BoolVar(&dbg, "d", false, "debug logging") flag.BoolVar(&dbg, "d", false, "debug logging")
flag.Parse() flag.Parse()
@ -354,6 +483,42 @@ func main() {
log.Printf("[Shell completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) log.Printf("[Shell completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus)
hc.SetStatus(uint8(cmdStatus)) hc.SetStatus(uint8(cmdStatus))
} }
} else if rec.op[0] == 'D' {
// File copy (destination) operation - client copy to server
log.Printf("[Client->Server copy]\n")
// TODO: call function with hc, rec.cmd, chaffEnabled etc.
// func hooks tar cmd right-half of pipe to hc Reader
addr := hc.RemoteAddr()
hname := strings.Split(addr.String(), ":")[0]
log.Printf("[Running copy for [%s@%s]]\n", rec.who, hname)
runErr, cmdStatus := runClientToServerCopyAs(string(rec.who), hc, string(rec.cmd), chaffEnabled)
// Returned hopefully via an EOF or exit/logout;
// Clear current op so user can enter next, or EOF
rec.op[0] = 0
if runErr != nil {
log.Printf("[Error spawning cp for %s@%s]\n", rec.who, hname)
} else {
log.Printf("[Command completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus)
hc.SetStatus(uint8(cmdStatus))
}
} else if rec.op[0] == 'S' {
// File copy (src) operation - server copy to client
log.Printf("[Server->Client copy]\n")
// TODO: call function to copy rec.cmd (file list) to
// tar cmd left-half of pipeline to hc.Writer ?
addr := hc.RemoteAddr()
hname := strings.Split(addr.String(), ":")[0]
log.Printf("[Running copy for [%s@%s]]\n", rec.who, hname)
runErr, cmdStatus := runServerToClientCopyAs(string(rec.who), hc, string(rec.cmd), chaffEnabled)
// Returned hopefully via an EOF or exit/logout;
// Clear current op so user can enter next, or EOF
rec.op[0] = 0
if runErr != nil {
log.Printf("[Error spawning cp for %s@%s]\n", rec.who, hname)
} else {
log.Printf("[Command completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus)
hc.SetStatus(uint8(cmdStatus))
}
} else { } else {
log.Println("[Bad cmdSpec]") log.Println("[Bad cmdSpec]")
} }