mirror of https://gogs.blitter.com/RLabs/xs
				
				
				
			Merge branch 'exitstatus' of Russtopia/hkexsh into master
This commit is contained in:
		
						commit
						ddc85a64f3
					
				
							
								
								
									
										206
									
								
								hkexnet.go
								
								
								
								
							
							
						
						
									
										206
									
								
								hkexnet.go
								
								
								
								
							| 
						 | 
				
			
			@ -11,7 +11,9 @@ package hkexsh
 | 
			
		|||
 | 
			
		||||
// Implementation of HKEx-wrapped versions of the golang standard
 | 
			
		||||
// net package interfaces, allowing clients and servers to simply replace
 | 
			
		||||
// 'net.Dial' and 'net.Listen' with 'hkex.Dial' and 'hkex.Listen'.
 | 
			
		||||
// 'net.Dial' and 'net.Listen' with 'hkex.Dial' and 'hkex.Listen'
 | 
			
		||||
// (though some extra methods are implemented and must be used
 | 
			
		||||
//  for things outside of the scope of plain sockets).
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"crypto/cipher"
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +55,7 @@ type ChaffConfig struct {
 | 
			
		|||
	szMax    uint // max size in bytes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Conn is a HKex connection - a drop-in replacement for net.Conn
 | 
			
		||||
// Conn is a HKex connection - a superset of net.Conn
 | 
			
		||||
type Conn struct {
 | 
			
		||||
	m          *sync.Mutex
 | 
			
		||||
	c          net.Conn // which also implements io.Reader, io.Writer, ...
 | 
			
		||||
| 
						 | 
				
			
			@ -66,11 +68,21 @@ type Conn struct {
 | 
			
		|||
 | 
			
		||||
	chaff ChaffConfig
 | 
			
		||||
 | 
			
		||||
	r    cipher.Stream //read cipherStream
 | 
			
		||||
	rm   hash.Hash
 | 
			
		||||
	w    cipher.Stream //write cipherStream
 | 
			
		||||
	wm   hash.Hash
 | 
			
		||||
	dBuf *bytes.Buffer //decrypt buffer for Read()
 | 
			
		||||
	closeStat *uint8        // close status (shell exit status: UNIX uint8)
 | 
			
		||||
	r         cipher.Stream //read cipherStream
 | 
			
		||||
	rm        hash.Hash
 | 
			
		||||
	w         cipher.Stream //write cipherStream
 | 
			
		||||
	wm        hash.Hash
 | 
			
		||||
	dBuf      *bytes.Buffer //decrypt buffer for Read()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (hc Conn) GetStatus() uint8 {
 | 
			
		||||
	return *hc.closeStat
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (hc *Conn) SetStatus(stat uint8) {
 | 
			
		||||
	*hc.closeStat = stat
 | 
			
		||||
	log.Println("closeStat:", *hc.closeStat)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ConnOpts returns the cipher/hmac options value, which is sent to the
 | 
			
		||||
| 
						 | 
				
			
			@ -78,16 +90,16 @@ type Conn struct {
 | 
			
		|||
//
 | 
			
		||||
// (Used for protocol-level negotiations after KEx such as
 | 
			
		||||
// cipher/HMAC algorithm options etc.)
 | 
			
		||||
func (c Conn) ConnOpts() uint32 {
 | 
			
		||||
	return c.cipheropts
 | 
			
		||||
func (hc Conn) ConnOpts() uint32 {
 | 
			
		||||
	return hc.cipheropts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetConnOpts sets the cipher/hmac options value, which is sent to the
 | 
			
		||||
// peer as part of KEx but not part of the KEx itself.
 | 
			
		||||
//
 | 
			
		||||
// opts - bitfields for cipher and hmac alg. to use after KEx
 | 
			
		||||
func (c *Conn) SetConnOpts(copts uint32) {
 | 
			
		||||
	c.cipheropts = copts
 | 
			
		||||
func (hc *Conn) SetConnOpts(copts uint32) {
 | 
			
		||||
	hc.cipheropts = copts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Opts returns the protocol options value, which is sent to the peer
 | 
			
		||||
| 
						 | 
				
			
			@ -95,8 +107,8 @@ func (c *Conn) SetConnOpts(copts uint32) {
 | 
			
		|||
//
 | 
			
		||||
// Consumers of this lib may use this for protocol-level options not part
 | 
			
		||||
// of the KEx or encryption info used by the connection.
 | 
			
		||||
func (c Conn) Opts() uint32 {
 | 
			
		||||
	return c.opts
 | 
			
		||||
func (hc Conn) Opts() uint32 {
 | 
			
		||||
	return hc.opts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetOpts sets the protocol options value, which is sent to the peer
 | 
			
		||||
| 
						 | 
				
			
			@ -106,32 +118,32 @@ func (c Conn) Opts() uint32 {
 | 
			
		|||
// of the KEx of encryption info used by the connection.
 | 
			
		||||
//
 | 
			
		||||
// opts - a uint32, caller-defined
 | 
			
		||||
func (c *Conn) SetOpts(opts uint32) {
 | 
			
		||||
	c.opts = opts
 | 
			
		||||
func (hc *Conn) SetOpts(opts uint32) {
 | 
			
		||||
	hc.opts = opts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Conn) applyConnExtensions(extensions ...string) {
 | 
			
		||||
func (hc *Conn) applyConnExtensions(extensions ...string) {
 | 
			
		||||
	for _, s := range extensions {
 | 
			
		||||
		switch s {
 | 
			
		||||
		case "C_AES_256":
 | 
			
		||||
			log.Println("[extension arg = C_AES_256]")
 | 
			
		||||
			c.cipheropts &= (0xFFFFFF00)
 | 
			
		||||
			c.cipheropts |= CAlgAES256
 | 
			
		||||
			hc.cipheropts &= (0xFFFFFF00)
 | 
			
		||||
			hc.cipheropts |= CAlgAES256
 | 
			
		||||
			break
 | 
			
		||||
		case "C_TWOFISH_128":
 | 
			
		||||
			log.Println("[extension arg = C_TWOFISH_128]")
 | 
			
		||||
			c.cipheropts &= (0xFFFFFF00)
 | 
			
		||||
			c.cipheropts |= CAlgTwofish128
 | 
			
		||||
			hc.cipheropts &= (0xFFFFFF00)
 | 
			
		||||
			hc.cipheropts |= CAlgTwofish128
 | 
			
		||||
			break
 | 
			
		||||
		case "C_BLOWFISH_64":
 | 
			
		||||
			log.Println("[extension arg = C_BLOWFISH_64]")
 | 
			
		||||
			c.cipheropts &= (0xFFFFFF00)
 | 
			
		||||
			c.cipheropts |= CAlgBlowfish64
 | 
			
		||||
			hc.cipheropts &= (0xFFFFFF00)
 | 
			
		||||
			hc.cipheropts |= CAlgBlowfish64
 | 
			
		||||
			break
 | 
			
		||||
		case "H_SHA256":
 | 
			
		||||
			log.Println("[extension arg = H_SHA256]")
 | 
			
		||||
			c.cipheropts &= (0xFFFF00FF)
 | 
			
		||||
			c.cipheropts |= (HmacSHA256 << 8)
 | 
			
		||||
			hc.cipheropts &= (0xFFFF00FF)
 | 
			
		||||
			hc.cipheropts |= (HmacSHA256 << 8)
 | 
			
		||||
			break
 | 
			
		||||
		default:
 | 
			
		||||
			log.Printf("[Dial ext \"%s\" ignored]\n", s)
 | 
			
		||||
| 
						 | 
				
			
			@ -154,7 +166,7 @@ func Dial(protocol string, ipport string, extensions ...string) (hc *Conn, err e
 | 
			
		|||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	// Init hkexnet.Conn hc over net.Conn c
 | 
			
		||||
	hc = &Conn{m: &sync.Mutex{}, c: c, h: New(0, 0), dBuf: new(bytes.Buffer)}
 | 
			
		||||
	hc = &Conn{m: &sync.Mutex{}, c: c, closeStat: new(uint8), h: New(0, 0), dBuf: new(bytes.Buffer)}
 | 
			
		||||
	hc.applyConnExtensions(extensions...)
 | 
			
		||||
 | 
			
		||||
	// Send hkexnet.Conn parameters to remote side
 | 
			
		||||
| 
						 | 
				
			
			@ -181,25 +193,29 @@ func Dial(protocol string, ipport string, extensions ...string) (hc *Conn, err e
 | 
			
		|||
 | 
			
		||||
	hc.r, hc.rm, err = hc.getStream(hc.h.fa)
 | 
			
		||||
	hc.w, hc.wm, err = hc.getStream(hc.h.fa)
 | 
			
		||||
 | 
			
		||||
	*hc.closeStat = 99 // open or prematurely-closed status
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Close a hkex.Conn
 | 
			
		||||
func (c Conn) Close() (err error) {
 | 
			
		||||
	c.DisableChaff()
 | 
			
		||||
	err = c.c.Close()
 | 
			
		||||
func (hc Conn) Close() (err error) {
 | 
			
		||||
	hc.DisableChaff()
 | 
			
		||||
	hc.WritePacket([]byte{byte(*hc.closeStat)}, CSOExitStatus)
 | 
			
		||||
	*hc.closeStat = 0
 | 
			
		||||
	err = hc.c.Close()
 | 
			
		||||
	log.Println("[Conn Closing]")
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LocalAddr returns the local network address.
 | 
			
		||||
func (c Conn) LocalAddr() net.Addr {
 | 
			
		||||
	return c.c.LocalAddr()
 | 
			
		||||
func (hc Conn) LocalAddr() net.Addr {
 | 
			
		||||
	return hc.c.LocalAddr()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RemoteAddr returns the remote network address.
 | 
			
		||||
func (c Conn) RemoteAddr() net.Addr {
 | 
			
		||||
	return c.c.RemoteAddr()
 | 
			
		||||
func (hc Conn) RemoteAddr() net.Addr {
 | 
			
		||||
	return hc.c.RemoteAddr()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetDeadline sets the read and write deadlines associated
 | 
			
		||||
| 
						 | 
				
			
			@ -217,8 +233,8 @@ func (c Conn) RemoteAddr() net.Addr {
 | 
			
		|||
// the deadline after successful Read or Write calls.
 | 
			
		||||
//
 | 
			
		||||
// A zero value for t means I/O operations will not time out.
 | 
			
		||||
func (c Conn) SetDeadline(t time.Time) error {
 | 
			
		||||
	return c.SetDeadline(t)
 | 
			
		||||
func (hc Conn) SetDeadline(t time.Time) error {
 | 
			
		||||
	return hc.c.SetDeadline(t)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetWriteDeadline sets the deadline for future Write calls
 | 
			
		||||
| 
						 | 
				
			
			@ -226,15 +242,15 @@ func (c Conn) SetDeadline(t time.Time) error {
 | 
			
		|||
// Even if write times out, it may return n > 0, indicating that
 | 
			
		||||
// some of the data was successfully written.
 | 
			
		||||
// A zero value for t means Write will not time out.
 | 
			
		||||
func (c Conn) SetWriteDeadline(t time.Time) error {
 | 
			
		||||
	return c.SetWriteDeadline(t)
 | 
			
		||||
func (hc Conn) SetWriteDeadline(t time.Time) error {
 | 
			
		||||
	return hc.c.SetWriteDeadline(t)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetReadDeadline sets the deadline for future Read calls
 | 
			
		||||
// and any currently-blocked Read call.
 | 
			
		||||
// A zero value for t means Read will not time out.
 | 
			
		||||
func (c Conn) SetReadDeadline(t time.Time) error {
 | 
			
		||||
	return c.SetReadDeadline(t)
 | 
			
		||||
func (hc Conn) SetReadDeadline(t time.Time) error {
 | 
			
		||||
	return hc.c.SetReadDeadline(t)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*---------------------------------------------------------------------*/
 | 
			
		||||
| 
						 | 
				
			
			@ -282,13 +298,13 @@ func (hl HKExListener) Accept() (hc Conn, err error) {
 | 
			
		|||
	// Open raw Conn c
 | 
			
		||||
	c, err := hl.l.Accept()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		hc := Conn{m: &sync.Mutex{}, c: nil, h: nil, cipheropts: 0, opts: 0,
 | 
			
		||||
		hc := Conn{m: &sync.Mutex{}, c: nil, h: nil, closeStat: new(uint8), cipheropts: 0, opts: 0,
 | 
			
		||||
			r: nil, w: nil}
 | 
			
		||||
		return hc, err
 | 
			
		||||
	}
 | 
			
		||||
	log.Println("[Accepted]")
 | 
			
		||||
 | 
			
		||||
	hc = Conn{m: &sync.Mutex{}, c: c, h: New(0, 0), WinCh: make(chan WinSize, 1),
 | 
			
		||||
	hc = Conn{m: &sync.Mutex{}, c: c, h: New(0, 0), closeStat: new(uint8), WinCh: make(chan WinSize, 1),
 | 
			
		||||
		dBuf: new(bytes.Buffer)}
 | 
			
		||||
 | 
			
		||||
	// Read in hkexnet.Conn parameters over raw Conn c
 | 
			
		||||
| 
						 | 
				
			
			@ -324,11 +340,11 @@ func (hl HKExListener) Accept() (hc Conn, err error) {
 | 
			
		|||
// Read into a byte slice
 | 
			
		||||
//
 | 
			
		||||
// See go doc io.Reader
 | 
			
		||||
func (c Conn) Read(b []byte) (n int, err error) {
 | 
			
		||||
func (hc Conn) Read(b []byte) (n int, err error) {
 | 
			
		||||
	//log.Printf("[Decrypting...]\r\n")
 | 
			
		||||
	for {
 | 
			
		||||
		//log.Printf("c.dBuf.Len(): %d\n", c.dBuf.Len())
 | 
			
		||||
		if c.dBuf.Len() > 0 /* len(b) */ {
 | 
			
		||||
		//log.Printf("hc.dBuf.Len(): %d\n", hc.dBuf.Len())
 | 
			
		||||
		if hc.dBuf.Len() > 0 /* len(b) */ {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -337,16 +353,16 @@ func (c Conn) Read(b []byte) (n int, err error) {
 | 
			
		|||
		var payloadLen uint32
 | 
			
		||||
 | 
			
		||||
		// Read ctrl/status opcode (CSOHmacInvalid on hmac mismatch)
 | 
			
		||||
		err = binary.Read(c.c, binary.BigEndian, &ctrlStatOp)
 | 
			
		||||
		err = binary.Read(hc.c, binary.BigEndian, &ctrlStatOp)
 | 
			
		||||
		log.Printf("[ctrlStatOp: %v]\n", ctrlStatOp)
 | 
			
		||||
		if ctrlStatOp == CSOHmacInvalid {
 | 
			
		||||
			// Other side indicated channel tampering, close channel
 | 
			
		||||
			c.Close()
 | 
			
		||||
			hc.Close()
 | 
			
		||||
			return 1, errors.New("** ALERT - remote end detected HMAC mismatch - possible channel tampering **")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Read the hmac and payload len first
 | 
			
		||||
		err = binary.Read(c.c, binary.BigEndian, &hmacIn)
 | 
			
		||||
		err = binary.Read(hc.c, binary.BigEndian, &hmacIn)
 | 
			
		||||
		// Normal client 'exit' from interactive session will cause
 | 
			
		||||
		// (on server side) err.Error() == "<iface/addr info ...>: use of closed network connection"
 | 
			
		||||
		if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -358,7 +374,7 @@ func (c Conn) Read(b []byte) (n int, err error) {
 | 
			
		|||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = binary.Read(c.c, binary.BigEndian, &payloadLen)
 | 
			
		||||
		err = binary.Read(hc.c, binary.BigEndian, &payloadLen)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if err.Error() != "EOF" {
 | 
			
		||||
				log.Println("unexpected Read() err:", err)
 | 
			
		||||
| 
						 | 
				
			
			@ -371,13 +387,13 @@ func (c Conn) Read(b []byte) (n int, err error) {
 | 
			
		|||
 | 
			
		||||
		if payloadLen > 16384 {
 | 
			
		||||
			log.Printf("[Insane payloadLen:%v]\n", payloadLen)
 | 
			
		||||
			c.Close()
 | 
			
		||||
			hc.Close()
 | 
			
		||||
			return 1, errors.New("Insane payloadLen")
 | 
			
		||||
		}
 | 
			
		||||
		//log.Println("payloadLen:", payloadLen)
 | 
			
		||||
 | 
			
		||||
		var payloadBytes = make([]byte, payloadLen)
 | 
			
		||||
		n, err = io.ReadFull(c.c, payloadBytes)
 | 
			
		||||
		n, err = io.ReadFull(hc.c, payloadBytes)
 | 
			
		||||
		//log.Print(" << Read ", n, " payloadBytes")
 | 
			
		||||
 | 
			
		||||
		// Normal client 'exit' from interactive session will cause
 | 
			
		||||
| 
						 | 
				
			
			@ -396,7 +412,7 @@ func (c Conn) Read(b []byte) (n int, err error) {
 | 
			
		|||
		// The StreamReader acts like a pipe, decrypting
 | 
			
		||||
		// whatever is available and forwarding the result
 | 
			
		||||
		// to the parameter of Read() as a normal io.Reader
 | 
			
		||||
		rs := &cipher.StreamReader{S: c.r, R: db}
 | 
			
		||||
		rs := &cipher.StreamReader{S: hc.r, R: db}
 | 
			
		||||
		// The caller isn't necessarily reading the full payload so we need
 | 
			
		||||
		// to decrypt ot an intermediate buffer, draining it on demand of caller
 | 
			
		||||
		decryptN, err := rs.Read(payloadBytes)
 | 
			
		||||
| 
						 | 
				
			
			@ -409,75 +425,77 @@ func (c Conn) Read(b []byte) (n int, err error) {
 | 
			
		|||
		if ctrlStatOp == CSOChaff {
 | 
			
		||||
			log.Printf("[Chaff pkt, discarded (len %d)]\n", decryptN)
 | 
			
		||||
		} else if ctrlStatOp == CSOTermSize {
 | 
			
		||||
			fmt.Sscanf(string(payloadBytes), "%d %d", &c.Rows, &c.Cols)
 | 
			
		||||
			log.Printf("[TermSize pkt: rows %v cols %v]\n", c.Rows, c.Cols)
 | 
			
		||||
			c.WinCh <- WinSize{c.Rows, c.Cols}
 | 
			
		||||
			fmt.Sscanf(string(payloadBytes), "%d %d", &hc.Rows, &hc.Cols)
 | 
			
		||||
			log.Printf("[TermSize pkt: rows %v cols %v]\n", hc.Rows, hc.Cols)
 | 
			
		||||
			hc.WinCh <- WinSize{hc.Rows, hc.Cols}
 | 
			
		||||
		} else if ctrlStatOp == CSOExitStatus {
 | 
			
		||||
			*hc.closeStat = uint8(payloadBytes[0])
 | 
			
		||||
		} else {
 | 
			
		||||
			c.dBuf.Write(payloadBytes)
 | 
			
		||||
			//log.Printf("c.dBuf: %s\n", hex.Dump(c.dBuf.Bytes()))
 | 
			
		||||
			hc.dBuf.Write(payloadBytes)
 | 
			
		||||
			//log.Printf("hc.dBuf: %s\n", hex.Dump(hc.dBuf.Bytes()))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Re-calculate hmac, compare with received value
 | 
			
		||||
		c.rm.Write(payloadBytes)
 | 
			
		||||
		hTmp := c.rm.Sum(nil)[0:4]
 | 
			
		||||
		hc.rm.Write(payloadBytes)
 | 
			
		||||
		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 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 **")
 | 
			
		||||
			_, _ = c.c.Write([]byte{CSOHmacInvalid})
 | 
			
		||||
			_, _ = hc.c.Write([]byte{CSOHmacInvalid})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	retN := c.dBuf.Len()
 | 
			
		||||
	retN := hc.dBuf.Len()
 | 
			
		||||
	if retN > len(b) {
 | 
			
		||||
		retN = len(b)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Printf("Read() got %d bytes\n", retN)
 | 
			
		||||
	copy(b, c.dBuf.Next(retN))
 | 
			
		||||
	//log.Printf("As Read() returns, c.dBuf is %d long: %s\n", c.dBuf.Len(), hex.Dump(c.dBuf.Bytes()))
 | 
			
		||||
	copy(b, hc.dBuf.Next(retN))
 | 
			
		||||
	//log.Printf("As Read() returns, hc.dBuf is %d long: %s\n", hc.dBuf.Len(), hex.Dump(hc.dBuf.Bytes()))
 | 
			
		||||
	return retN, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Write a byte slice
 | 
			
		||||
//
 | 
			
		||||
// See go doc io.Writer
 | 
			
		||||
func (c Conn) Write(b []byte) (n int, err error) {
 | 
			
		||||
	n, err = c.WritePacket(b, CSONone)
 | 
			
		||||
func (hc Conn) Write(b []byte) (n int, err error) {
 | 
			
		||||
	n, err = hc.WritePacket(b, CSONone)
 | 
			
		||||
	return n, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Write a byte slice with specified ctrlStatusOp byte
 | 
			
		||||
func (c Conn) WritePacket(b []byte, op byte) (n int, err error) {
 | 
			
		||||
func (hc Conn) WritePacket(b []byte, op byte) (n int, err error) {
 | 
			
		||||
	//log.Printf("[Encrypting...]\r\n")
 | 
			
		||||
	var hmacOut []uint8
 | 
			
		||||
	var payloadLen uint32
 | 
			
		||||
 | 
			
		||||
	// N.B. Originally this Lock() surrounded only the
 | 
			
		||||
	// calls to binary.Write(c.c ..) however there appears
 | 
			
		||||
	// calls to binary.Write(hc.c ..) however there appears
 | 
			
		||||
	// to be some other unshareable state in the Conn
 | 
			
		||||
	// struct that must be protected to serialize main and
 | 
			
		||||
	// chaff data written to it.
 | 
			
		||||
	//
 | 
			
		||||
	// Would be nice to determine if the mutex scope
 | 
			
		||||
	// could be tightened.
 | 
			
		||||
	c.m.Lock()
 | 
			
		||||
	hc.m.Lock()
 | 
			
		||||
	{
 | 
			
		||||
		log.Printf("  :>ptext:\r\n%s\r\n", hex.Dump(b))
 | 
			
		||||
 | 
			
		||||
		payloadLen = uint32(len(b))
 | 
			
		||||
 | 
			
		||||
		// Calculate hmac on payload
 | 
			
		||||
		c.wm.Write(b)
 | 
			
		||||
		hmacOut = c.wm.Sum(nil)[0:4]
 | 
			
		||||
		hc.wm.Write(b)
 | 
			
		||||
		hmacOut = hc.wm.Sum(nil)[0:4]
 | 
			
		||||
 | 
			
		||||
		log.Printf("  (%04x> HMAC(o):%s\r\n", payloadLen, hex.EncodeToString(hmacOut))
 | 
			
		||||
 | 
			
		||||
		var wb bytes.Buffer
 | 
			
		||||
		// The StreamWriter acts like a pipe, forwarding whatever is
 | 
			
		||||
		// written to it through the cipher, encrypting as it goes
 | 
			
		||||
		ws := &cipher.StreamWriter{S: c.w, W: &wb}
 | 
			
		||||
		ws := &cipher.StreamWriter{S: hc.w, W: &wb}
 | 
			
		||||
		_, err = ws.Write(b)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
| 
						 | 
				
			
			@ -486,19 +504,19 @@ func (c Conn) WritePacket(b []byte, op byte) (n int, err error) {
 | 
			
		|||
 | 
			
		||||
		ctrlStatOp := op
 | 
			
		||||
 | 
			
		||||
		err = binary.Write(c.c, binary.BigEndian, &ctrlStatOp)
 | 
			
		||||
		err = binary.Write(hc.c, binary.BigEndian, &ctrlStatOp)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			// Write hmac LSB, payloadLen followed by payload
 | 
			
		||||
			err = binary.Write(c.c, binary.BigEndian, hmacOut)
 | 
			
		||||
			err = binary.Write(hc.c, binary.BigEndian, hmacOut)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				err = binary.Write(c.c, binary.BigEndian, payloadLen)
 | 
			
		||||
				err = binary.Write(hc.c, binary.BigEndian, payloadLen)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					n, err = c.c.Write(wb.Bytes())
 | 
			
		||||
					n, err = hc.c.Write(wb.Bytes())
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	c.m.Unlock()
 | 
			
		||||
	hc.m.Unlock()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		//panic(err)
 | 
			
		||||
| 
						 | 
				
			
			@ -507,48 +525,48 @@ func (c Conn) WritePacket(b []byte, op byte) (n int, err error) {
 | 
			
		|||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Conn) EnableChaff() {
 | 
			
		||||
	c.chaff.shutdown = false
 | 
			
		||||
	c.chaff.enabled = true
 | 
			
		||||
func (hc *Conn) EnableChaff() {
 | 
			
		||||
	hc.chaff.shutdown = false
 | 
			
		||||
	hc.chaff.enabled = true
 | 
			
		||||
	log.Println("Chaffing ENABLED")
 | 
			
		||||
	c.chaffHelper()
 | 
			
		||||
	hc.chaffHelper()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Conn) DisableChaff() {
 | 
			
		||||
	c.chaff.enabled = false
 | 
			
		||||
func (hc *Conn) DisableChaff() {
 | 
			
		||||
	hc.chaff.enabled = false
 | 
			
		||||
	log.Println("Chaffing DISABLED")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Conn) ShutdownChaff() {
 | 
			
		||||
	c.chaff.shutdown = true
 | 
			
		||||
func (hc *Conn) ShutdownChaff() {
 | 
			
		||||
	hc.chaff.shutdown = true
 | 
			
		||||
	log.Println("Chaffing SHUTDOWN")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Conn) SetupChaff(msecsMin uint, msecsMax uint, szMax uint) {
 | 
			
		||||
	c.chaff.msecsMin = msecsMin //move these to params of chaffHelper() ?
 | 
			
		||||
	c.chaff.msecsMax = msecsMax
 | 
			
		||||
	c.chaff.szMax = szMax
 | 
			
		||||
func (hc *Conn) SetupChaff(msecsMin uint, msecsMax uint, szMax uint) {
 | 
			
		||||
	hc.chaff.msecsMin = msecsMin //move these to params of chaffHelper() ?
 | 
			
		||||
	hc.chaff.msecsMax = msecsMax
 | 
			
		||||
	hc.chaff.szMax = szMax
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper routine to spawn a chaffing goroutine for each Conn
 | 
			
		||||
func (c *Conn) chaffHelper() {
 | 
			
		||||
func (hc *Conn) chaffHelper() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			var nextDuration int
 | 
			
		||||
			if c.chaff.enabled {
 | 
			
		||||
				bufTmp := make([]byte, rand.Intn(int(c.chaff.szMax)))
 | 
			
		||||
				min := int(c.chaff.msecsMin)
 | 
			
		||||
				nextDuration = rand.Intn(int(c.chaff.msecsMax)-min) + min
 | 
			
		||||
			if hc.chaff.enabled {
 | 
			
		||||
				bufTmp := make([]byte, rand.Intn(int(hc.chaff.szMax)))
 | 
			
		||||
				min := int(hc.chaff.msecsMin)
 | 
			
		||||
				nextDuration = rand.Intn(int(hc.chaff.msecsMax)-min) + min
 | 
			
		||||
				_, _ = rand.Read(bufTmp)
 | 
			
		||||
				_, err := c.WritePacket(bufTmp, CSOChaff)
 | 
			
		||||
				_, err := hc.WritePacket(bufTmp, CSOChaff)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Println("[ *** error - chaffHelper quitting *** ]")
 | 
			
		||||
					c.chaff.enabled = false
 | 
			
		||||
					hc.chaff.enabled = false
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			time.Sleep(time.Duration(nextDuration) * time.Millisecond)
 | 
			
		||||
			if c.chaff.shutdown {
 | 
			
		||||
			if hc.chaff.shutdown {
 | 
			
		||||
				log.Println("*** chaffHelper shutting down")
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ type cmdSpec struct {
 | 
			
		|||
	who        []byte
 | 
			
		||||
	cmd        []byte
 | 
			
		||||
	authCookie []byte
 | 
			
		||||
	status     int
 | 
			
		||||
	status     int // though UNIX shell exit status is uint8, os.Exit() wants int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ func GetSize() (cols, rows int, err error) {
 | 
			
		|||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
		cols, rows = 80, 24  //failsafe
 | 
			
		||||
		cols, rows = 80, 24 //failsafe
 | 
			
		||||
	} else {
 | 
			
		||||
		fmt.Sscanf(string(out), "%d %d\n", &rows, &cols)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -190,7 +190,7 @@ func main() {
 | 
			
		|||
	}
 | 
			
		||||
	defer conn.DisableChaff()
 | 
			
		||||
	defer conn.ShutdownChaff()
 | 
			
		||||
	
 | 
			
		||||
 | 
			
		||||
	//client reader (from server) goroutine
 | 
			
		||||
	wg.Add(1)
 | 
			
		||||
	go func() {
 | 
			
		||||
| 
						 | 
				
			
			@ -215,11 +215,14 @@ func main() {
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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(0)
 | 
			
		||||
			//os.Exit(rec.status)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -243,7 +246,7 @@ func main() {
 | 
			
		|||
				if outerr.Error() != "EOF" {
 | 
			
		||||
					fmt.Println(outerr)
 | 
			
		||||
					_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
 | 
			
		||||
					os.Exit(2)
 | 
			
		||||
					os.Exit(255)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			log.Println("[Sent EOF]")
 | 
			
		||||
| 
						 | 
				
			
			@ -253,4 +256,7 @@ func main() {
 | 
			
		|||
 | 
			
		||||
	// Wait until both stdin and stdout goroutines finish
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
 | 
			
		||||
	_ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort.
 | 
			
		||||
	os.Exit(rec.status)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -80,7 +80,7 @@ func runCmdAs(who string, cmd string, conn hkex.Conn) (err error) {
 | 
			
		|||
// Run a command (via default shell) as a specific user
 | 
			
		||||
//
 | 
			
		||||
// Uses ptys to support commands which expect a terminal.
 | 
			
		||||
func runShellAs(who string, cmd string, interactive bool, conn hkexsh.Conn, chaffing bool) (err error) {
 | 
			
		||||
func runShellAs(who string, cmd string, interactive bool, conn hkexsh.Conn, chaffing bool) (err error, exitStatus int) {
 | 
			
		||||
	u, _ := user.Lookup(who)
 | 
			
		||||
	var uid, gid uint32
 | 
			
		||||
	fmt.Sscanf(u.Uid, "%d", &uid)
 | 
			
		||||
| 
						 | 
				
			
			@ -117,46 +117,64 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexsh.Conn, chaf
 | 
			
		|||
	// Start the command with a pty.
 | 
			
		||||
	ptmx, err := pty.Start(c) // returns immediately with ptmx file
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
		return err, 0
 | 
			
		||||
	}
 | 
			
		||||
	// Make sure to close the pty at the end.
 | 
			
		||||
	defer func() { _ = ptmx.Close() }() // Best effort.
 | 
			
		||||
 | 
			
		||||
	// Watch for term resizes
 | 
			
		||||
	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})
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// Copy stdin to the pty.. (bgnd goroutine)
 | 
			
		||||
	go func() {
 | 
			
		||||
		_, e := io.Copy(ptmx, conn)
 | 
			
		||||
		if e != nil {
 | 
			
		||||
			log.Printf("** std->pty ended **\n")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if chaffing {
 | 
			
		||||
		conn.EnableChaff()
 | 
			
		||||
	}
 | 
			
		||||
	defer conn.DisableChaff()
 | 
			
		||||
	defer conn.ShutdownChaff()
 | 
			
		||||
 | 
			
		||||
	// ..and the pty to stdout.
 | 
			
		||||
	_, e := io.Copy(conn, ptmx)
 | 
			
		||||
	if e != nil {
 | 
			
		||||
		log.Printf("** pty->stdout ended **\n")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//err = c.Run()  // returns when c finishes.
 | 
			
		||||
 | 
			
		||||
	log.Printf("[%s]\n", cmd)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("Command finished with error: %v", err)
 | 
			
		||||
	} else {
 | 
			
		||||
 | 
			
		||||
		// Watch for term resizes
 | 
			
		||||
		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})
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		// Copy stdin to the pty.. (bgnd goroutine)
 | 
			
		||||
		go func() {
 | 
			
		||||
			_, e := io.Copy(ptmx, conn)
 | 
			
		||||
			if e != nil {
 | 
			
		||||
				log.Printf("** std->pty ended **\n")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		if chaffing {
 | 
			
		||||
			conn.EnableChaff()
 | 
			
		||||
		}
 | 
			
		||||
		defer conn.DisableChaff()
 | 
			
		||||
		defer conn.ShutdownChaff()
 | 
			
		||||
 | 
			
		||||
		// ..and the pty to stdout.
 | 
			
		||||
		go func() {
 | 
			
		||||
			_, e := io.Copy(conn, ptmx)
 | 
			
		||||
			if e != nil {
 | 
			
		||||
				log.Printf("** pty->stdout ended **\n")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			// The above io.Copy() will exit when the command attached
 | 
			
		||||
			// to the pty exits
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -182,7 +200,7 @@ func main() {
 | 
			
		|||
 | 
			
		||||
	flag.BoolVar(&vopt, "v", false, "show version")
 | 
			
		||||
	flag.StringVar(&laddr, "l", ":2000", "interface[:port] to listen")
 | 
			
		||||
	flag.BoolVar(&chaffEnabled, "cE", true, "enabled chaff pkts (default true)")
 | 
			
		||||
	flag.BoolVar(&chaffEnabled, "cE", true, "enabled chaff pkts")
 | 
			
		||||
	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)")
 | 
			
		||||
| 
						 | 
				
			
			@ -234,8 +252,8 @@ func main() {
 | 
			
		|||
			// Handle the connection in a new goroutine.
 | 
			
		||||
			// The loop then returns to accepting, so that
 | 
			
		||||
			// multiple connections may be served concurrently.
 | 
			
		||||
			go func(c hkexsh.Conn) (e error) {
 | 
			
		||||
				defer c.Close()
 | 
			
		||||
			go func(hc hkexsh.Conn) (e error) {
 | 
			
		||||
				defer hc.Close()
 | 
			
		||||
 | 
			
		||||
				//We use io.ReadFull() here to guarantee we consume
 | 
			
		||||
				//just the data we want for the cmdSpec, and no more.
 | 
			
		||||
| 
						 | 
				
			
			@ -244,7 +262,7 @@ func main() {
 | 
			
		|||
				var rec cmdSpec
 | 
			
		||||
				var len1, len2, len3, len4 uint32
 | 
			
		||||
 | 
			
		||||
				n, err := fmt.Fscanf(c, "%d %d %d %d\n", &len1, &len2, &len3, &len4)
 | 
			
		||||
				n, err := fmt.Fscanf(hc, "%d %d %d %d\n", &len1, &len2, &len3, &len4)
 | 
			
		||||
				log.Printf("cmdSpec read:%d %d %d %d\n", len1, len2, len3, len4)
 | 
			
		||||
 | 
			
		||||
				if err != nil || n < 4 {
 | 
			
		||||
| 
						 | 
				
			
			@ -254,27 +272,27 @@ func main() {
 | 
			
		|||
				//fmt.Printf("  lens:%d %d %d %d\n", len1, len2, len3, len4)
 | 
			
		||||
 | 
			
		||||
				rec.op = make([]byte, len1, len1)
 | 
			
		||||
				_, err = io.ReadFull(c, rec.op)
 | 
			
		||||
				_, err = io.ReadFull(hc, rec.op)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Println("[Bad cmdSpec.op]")
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				rec.who = make([]byte, len2, len2)
 | 
			
		||||
				_, err = io.ReadFull(c, rec.who)
 | 
			
		||||
				_, err = io.ReadFull(hc, rec.who)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Println("[Bad cmdSpec.who]")
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				rec.cmd = make([]byte, len3, len3)
 | 
			
		||||
				_, err = io.ReadFull(c, rec.cmd)
 | 
			
		||||
				_, err = io.ReadFull(hc, rec.cmd)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Println("[Bad cmdSpec.cmd]")
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				rec.authCookie = make([]byte, len4, len4)
 | 
			
		||||
				_, err = io.ReadFull(c, rec.authCookie)
 | 
			
		||||
				_, err = io.ReadFull(hc, rec.authCookie)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Println("[Bad cmdSpec.authCookie]")
 | 
			
		||||
					return err
 | 
			
		||||
| 
						 | 
				
			
			@ -292,36 +310,46 @@ func main() {
 | 
			
		|||
 | 
			
		||||
				if !valid {
 | 
			
		||||
					log.Println("Invalid user", string(rec.who))
 | 
			
		||||
					c.Write([]byte(rejectUserMsg()))
 | 
			
		||||
					hc.Write([]byte(rejectUserMsg()))
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				log.Printf("[allowedCmds:%s]\n", allowedCmds)
 | 
			
		||||
 | 
			
		||||
				if rec.op[0] == 'c' {
 | 
			
		||||
					// Non-interactive command
 | 
			
		||||
					addr := c.RemoteAddr()
 | 
			
		||||
					addr := hc.RemoteAddr()
 | 
			
		||||
					hname := goutmp.GetHost(addr.String())
 | 
			
		||||
 | 
			
		||||
					log.Printf("[Running command for [%s@%s]]\n", rec.who, hname)
 | 
			
		||||
					runShellAs(string(rec.who), string(rec.cmd), false, conn, chaffEnabled)
 | 
			
		||||
					runErr, cmdStatus := runShellAs(string(rec.who), 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.op[0] = 0
 | 
			
		||||
					log.Printf("[Command completed for [%s@%s]\n", rec.who, hname)
 | 
			
		||||
					if runErr != nil {
 | 
			
		||||
						log.Printf("[Error spawning cmd 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' {
 | 
			
		||||
					// Interactive session
 | 
			
		||||
					addr := c.RemoteAddr()
 | 
			
		||||
					addr := hc.RemoteAddr()
 | 
			
		||||
					hname := goutmp.GetHost(addr.String())
 | 
			
		||||
					log.Printf("[Running shell for [%s@%s]]\n", rec.who, hname)
 | 
			
		||||
 | 
			
		||||
					utmpx := goutmp.Put_utmp(string(rec.who), hname)
 | 
			
		||||
					defer func() { goutmp.Unput_utmp(utmpx) }()
 | 
			
		||||
					goutmp.Put_lastlog_entry("hkexsh", string(rec.who), hname)
 | 
			
		||||
					runShellAs(string(rec.who), string(rec.cmd), true, conn, chaffEnabled)
 | 
			
		||||
					runErr, cmdStatus := runShellAs(string(rec.who), 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.op[0] = 0
 | 
			
		||||
					log.Printf("[Exiting shell for [%s@%s]]\n", rec.who, hname)
 | 
			
		||||
					if runErr != nil {
 | 
			
		||||
						log.Printf("[Error spawning shell for %s@%s]\n", rec.who, hname)
 | 
			
		||||
					} else {
 | 
			
		||||
						log.Printf("[Shell completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus)
 | 
			
		||||
						hc.SetStatus(uint8(cmdStatus))
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					log.Println("[Bad cmdSpec]")
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue