2018-04-07 20:04:10 +00:00
|
|
|
// hkexsh client
|
|
|
|
//
|
|
|
|
// Copyright (c) 2017-2018 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)
|
2018-01-06 15:30:56 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2018-01-12 07:01:39 +00:00
|
|
|
"flag"
|
2018-01-06 15:30:56 +00:00
|
|
|
"fmt"
|
2018-01-13 06:47:57 +00:00
|
|
|
"io"
|
2018-01-18 05:27:00 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
2018-05-02 19:28:56 +00:00
|
|
|
"math/rand"
|
2018-01-13 06:47:57 +00:00
|
|
|
"os"
|
2018-04-29 02:28:37 +00:00
|
|
|
"os/exec"
|
|
|
|
"os/signal"
|
2018-01-21 23:46:40 +00:00
|
|
|
"os/user"
|
|
|
|
"strings"
|
2018-01-18 04:36:53 +00:00
|
|
|
"sync"
|
2018-04-29 02:28:37 +00:00
|
|
|
"syscall"
|
2018-05-01 09:39:45 +00:00
|
|
|
"time"
|
2018-01-06 20:26:08 +00:00
|
|
|
|
2018-04-28 23:05:33 +00:00
|
|
|
hkexsh "blitter.com/go/hkexsh"
|
2018-01-19 05:17:57 +00:00
|
|
|
isatty "github.com/mattn/go-isatty"
|
2018-01-06 15:30:56 +00:00
|
|
|
)
|
|
|
|
|
2018-01-21 04:37:27 +00:00
|
|
|
type cmdSpec struct {
|
|
|
|
op []byte
|
|
|
|
who []byte
|
|
|
|
cmd []byte
|
|
|
|
authCookie []byte
|
|
|
|
status int
|
|
|
|
}
|
|
|
|
|
2018-04-29 02:28:37 +00:00
|
|
|
// get terminal size using 'stty' command
|
|
|
|
// (Most portable btwn Linux and MSYS/win32, but
|
|
|
|
// TODO: remove external dep on 'stty' utility)
|
|
|
|
func getTermSize() (rows int, cols int, err error) {
|
|
|
|
cmd := exec.Command("stty", "size")
|
|
|
|
cmd.Stdin = os.Stdin
|
|
|
|
out, err := cmd.Output()
|
|
|
|
//fmt.Printf("out: %#v\n", string(out))
|
|
|
|
//fmt.Printf("err: %#v\n", err)
|
|
|
|
|
|
|
|
fmt.Sscanf(string(out), "%d %d\n", &rows, &cols)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-09 03:16:55 +00:00
|
|
|
// Demo of a simple client that dials up to a simple test server to
|
|
|
|
// send data.
|
2018-01-13 18:01:27 +00:00
|
|
|
//
|
|
|
|
// While conforming to the basic net.Conn interface HKex.Conn has extra
|
|
|
|
// capabilities designed to allow apps to define connection options,
|
|
|
|
// encryption/hmac settings and operations across the encrypted channel.
|
|
|
|
//
|
|
|
|
// Initial setup is the same as using plain net.Dial(), but one may
|
|
|
|
// specify extra extension tags (strings) to set the cipher and hmac
|
|
|
|
// setting desired; as well as the intended operation mode for the
|
|
|
|
// connection (app-specific, passed through to the server to use or
|
|
|
|
// ignore at its discretion).
|
2018-01-06 15:30:56 +00:00
|
|
|
func main() {
|
2018-01-18 04:36:53 +00:00
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
2018-01-21 23:46:40 +00:00
|
|
|
var dbg bool
|
2018-01-12 07:01:39 +00:00
|
|
|
var cAlg string
|
2018-01-13 06:13:01 +00:00
|
|
|
var hAlg string
|
2018-01-13 06:24:40 +00:00
|
|
|
var server string
|
2018-01-21 23:46:40 +00:00
|
|
|
var cmdStr string
|
|
|
|
var altUser string
|
2018-01-22 06:02:08 +00:00
|
|
|
var authCookie string
|
2018-01-19 05:17:57 +00:00
|
|
|
isInteractive := false
|
2018-01-13 06:47:57 +00:00
|
|
|
|
2018-01-12 07:01:39 +00:00
|
|
|
flag.StringVar(&cAlg, "c", "C_AES_256", "cipher [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]")
|
2018-01-13 06:13:01 +00:00
|
|
|
flag.StringVar(&hAlg, "h", "H_SHA256", "hmac [\"H_SHA256\"]")
|
2018-01-13 06:24:40 +00:00
|
|
|
flag.StringVar(&server, "s", "localhost:2000", "server hostname/address[:port]")
|
2018-01-21 23:46:40 +00:00
|
|
|
flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)")
|
|
|
|
flag.StringVar(&altUser, "u", "", "specify alternate user")
|
2018-01-22 06:02:08 +00:00
|
|
|
flag.StringVar(&authCookie, "a", "", "auth cookie (MultiCheese3999(tm) 2FA cookie")
|
2018-01-21 23:46:40 +00:00
|
|
|
flag.BoolVar(&dbg, "d", false, "debug logging")
|
2018-01-12 07:01:39 +00:00
|
|
|
flag.Parse()
|
|
|
|
|
2018-01-21 23:46:40 +00:00
|
|
|
if dbg {
|
|
|
|
log.SetOutput(os.Stdout)
|
|
|
|
} else {
|
|
|
|
log.SetOutput(ioutil.Discard)
|
|
|
|
}
|
2018-01-18 05:27:00 +00:00
|
|
|
|
2018-04-04 22:43:27 +00:00
|
|
|
conn, err := hkexsh.Dial("tcp", server, cAlg, hAlg)
|
2018-01-06 15:30:56 +00:00
|
|
|
if err != nil {
|
2018-01-08 06:05:14 +00:00
|
|
|
fmt.Println("Err!")
|
2018-01-13 06:47:57 +00:00
|
|
|
panic(err)
|
|
|
|
}
|
2018-01-18 04:36:53 +00:00
|
|
|
defer conn.Close()
|
2018-01-23 21:53:05 +00:00
|
|
|
// From this point on, conn is a secure encrypted channel
|
2018-01-18 04:36:53 +00:00
|
|
|
|
2018-04-29 02:28:37 +00:00
|
|
|
rows := 0
|
|
|
|
cols := 0
|
|
|
|
|
2018-01-19 05:17:57 +00:00
|
|
|
// Set stdin in raw mode if it's an interactive session
|
2018-01-21 23:46:40 +00:00
|
|
|
// TODO: send flag to server side indicating this
|
|
|
|
// affects shell command used
|
2018-04-15 19:58:24 +00:00
|
|
|
var oldState *hkexsh.State
|
2018-01-19 05:17:57 +00:00
|
|
|
if isatty.IsTerminal(os.Stdin.Fd()) {
|
2018-04-15 19:58:24 +00:00
|
|
|
oldState, err = hkexsh.MakeRaw(int(os.Stdin.Fd()))
|
2018-01-19 05:17:57 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2018-04-04 22:43:27 +00:00
|
|
|
defer func() { _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
|
2018-01-19 05:17:57 +00:00
|
|
|
} else {
|
2018-01-22 06:02:08 +00:00
|
|
|
log.Println("NOT A TTY")
|
2018-01-19 02:57:37 +00:00
|
|
|
}
|
|
|
|
|
2018-01-21 23:46:40 +00:00
|
|
|
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'}
|
|
|
|
}
|
|
|
|
|
2018-01-22 06:02:08 +00:00
|
|
|
if len(authCookie) == 0 {
|
|
|
|
fmt.Printf("Gimme cookie:")
|
2018-04-04 22:43:27 +00:00
|
|
|
ab, err := hkexsh.ReadPassword(int(os.Stdin.Fd()))
|
2018-01-23 21:53:05 +00:00
|
|
|
fmt.Printf("\r\n")
|
2018-01-22 06:02:08 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
authCookie = string(ab)
|
|
|
|
}
|
|
|
|
|
2018-01-21 23:46:40 +00:00
|
|
|
rec := &cmdSpec{
|
|
|
|
op: op,
|
|
|
|
who: []byte(uname),
|
|
|
|
cmd: []byte(cmdStr),
|
2018-01-22 06:02:08 +00:00
|
|
|
authCookie: []byte(authCookie),
|
2018-01-21 04:37:27 +00:00
|
|
|
status: 0}
|
|
|
|
|
2018-04-29 02:28:37 +00:00
|
|
|
_, err = fmt.Fprintf(conn, "%d %d %d %d\n",
|
|
|
|
len(rec.op), len(rec.who), len(rec.cmd), len(rec.authCookie))
|
|
|
|
|
2018-01-21 04:37:27 +00:00
|
|
|
_, err = conn.Write(rec.op)
|
|
|
|
_, err = conn.Write(rec.who)
|
|
|
|
_, err = conn.Write(rec.cmd)
|
|
|
|
_, err = conn.Write(rec.authCookie)
|
2018-03-26 02:58:04 +00:00
|
|
|
|
2018-01-21 04:37:27 +00:00
|
|
|
//client reader (from server) goroutine
|
2018-01-18 04:36:53 +00:00
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
2018-01-19 05:17:57 +00:00
|
|
|
// By deferring a call to wg.Done(),
|
|
|
|
// each goroutine guarantees that it marks
|
|
|
|
// its direction's stream as finished.
|
|
|
|
//
|
2018-01-18 04:36:53 +00:00
|
|
|
// Whichever direction's goroutine finishes first
|
2018-01-19 05:17:57 +00:00
|
|
|
// will call wg.Done() once more, explicitly, to
|
|
|
|
// hang up on the other side, so that this client
|
2018-01-18 04:36:53 +00:00
|
|
|
// 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)
|
2018-04-15 19:58:24 +00:00
|
|
|
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
|
2018-01-18 04:36:53 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
2018-03-26 02:58:04 +00:00
|
|
|
|
2018-01-19 05:17:57 +00:00
|
|
|
if isInteractive {
|
2018-01-21 04:37:27 +00:00
|
|
|
log.Println("[Got EOF]")
|
|
|
|
wg.Done() // server hung up, close WaitGroup to exit client
|
2018-01-19 05:17:57 +00:00
|
|
|
}
|
2018-01-18 04:36:53 +00:00
|
|
|
}()
|
|
|
|
|
2018-05-02 20:22:37 +00:00
|
|
|
//m := &sync.Mutex{}
|
2018-05-01 09:39:45 +00:00
|
|
|
|
2018-01-21 23:46:40 +00:00
|
|
|
if isInteractive {
|
2018-04-29 02:28:37 +00:00
|
|
|
// Handle pty resizes (notify server side)
|
|
|
|
ch := make(chan os.Signal, 1)
|
|
|
|
signal.Notify(ch, syscall.SIGWINCH)
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
for range ch {
|
|
|
|
// Query client's term size so we can communicate it to server
|
|
|
|
// pty after interactive session starts
|
|
|
|
rows, cols, err = getTermSize()
|
|
|
|
log.Printf("[rows %v cols %v]\n", rows, cols)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
termSzPacket := fmt.Sprintf("%d %d", rows, cols)
|
2018-05-02 20:22:37 +00:00
|
|
|
conn.Rwmut.Lock()
|
2018-04-29 02:28:37 +00:00
|
|
|
conn.WritePacket([]byte(termSzPacket), hkexsh.CSOTermSize)
|
2018-05-02 20:22:37 +00:00
|
|
|
conn.Rwmut.Unlock()
|
2018-04-29 02:28:37 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
ch <- syscall.SIGWINCH // Initial resize.
|
|
|
|
|
2018-05-01 09:39:45 +00:00
|
|
|
// client chaffing goroutine
|
2018-05-02 19:28:56 +00:00
|
|
|
// TODO: Consider making this a feature of hkexsh.Conn itself
|
2018-05-01 09:39:45 +00:00
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
for {
|
2018-05-02 19:28:56 +00:00
|
|
|
chaff := make([]byte, rand.Intn(512))
|
|
|
|
nextDurationMin := 1000 //ms
|
|
|
|
nextDuration := rand.Intn(5000-nextDurationMin) + nextDurationMin
|
|
|
|
_, _ = rand.Read(chaff)
|
2018-05-02 20:22:37 +00:00
|
|
|
conn.Rwmut.Lock()
|
2018-05-02 19:28:56 +00:00
|
|
|
conn.WritePacket(chaff, hkexsh.CSOChaff)
|
2018-05-02 20:22:37 +00:00
|
|
|
conn.Rwmut.Unlock()
|
2018-05-02 19:28:56 +00:00
|
|
|
time.Sleep(time.Duration(nextDuration) * time.Millisecond)
|
2018-05-01 09:39:45 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2018-01-21 23:46:40 +00:00
|
|
|
// client writer (to server) goroutine
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
// io.Copy() expects EOF so this will
|
|
|
|
// exit with outerr == nil
|
2018-05-01 09:39:45 +00:00
|
|
|
//!_, outerr := io.Copy(conn, os.Stdin)
|
2018-05-02 20:22:37 +00:00
|
|
|
_, outerr := func(conn *hkexsh.Conn, r io.Reader) (w int64, e error) {
|
|
|
|
return hkexsh.Copy(&conn.Rwmut, conn, r)
|
|
|
|
}(conn, os.Stdin)
|
2018-05-01 09:39:45 +00:00
|
|
|
|
2018-01-21 23:46:40 +00:00
|
|
|
if outerr != nil {
|
2018-03-27 04:58:42 +00:00
|
|
|
log.Println(outerr)
|
2018-01-21 23:46:40 +00:00
|
|
|
if outerr.Error() != "EOF" {
|
|
|
|
fmt.Println(outerr)
|
2018-04-15 19:58:24 +00:00
|
|
|
_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
|
2018-01-21 23:46:40 +00:00
|
|
|
os.Exit(2)
|
|
|
|
}
|
2018-01-18 04:36:53 +00:00
|
|
|
}
|
2018-01-21 23:46:40 +00:00
|
|
|
log.Println("[Sent EOF]")
|
|
|
|
wg.Done() // client hung up, close WaitGroup to exit client
|
|
|
|
}()
|
|
|
|
}
|
2018-01-18 04:36:53 +00:00
|
|
|
|
|
|
|
// Wait until both stdin and stdout goroutines finish
|
|
|
|
wg.Wait()
|
2018-01-06 15:30:56 +00:00
|
|
|
}
|