commit 233ba2732a28bf214db7e1b4fb8b9a1358439951 Author: fangdingjun Date: Thu Dec 1 18:05:54 2016 +0800 first version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..341c30b --- /dev/null +++ b/LICENSE @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..968d0b7 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +obfssh +===== + +obfssh is wrapper for ssh protocol, use AES or RC4 to encrypt the transport data, +ssh is a good designed protocol and with the good encryption, but the protocol has a especially figerprint, +the firewall can easily identify the protocol and block it or QOS it, especial when we use its port forward function to escape from the state censorship. + +obfssh encrypt the ssh protocol and hide the figerprint, the firewall can not identify the protocol. + +We borrow the idea from https://github.com/brl/obfuscated-openssh, but not compatible with it, +beause the limitions of golang ssh library. + + + + +server usage example +==================== + + import "github.com/fangdingjun/obfssh" + import "golang.org/x/crypto/ssh" + + // key for encryption + obfs_key := "some keyword" + + // encrypt method + obfs_method := "rc4" + + config := &ssh.ServerConfig{ + // add ssh server configure here + // for example auth method, cipher, MAC + ... + } + + l, err := net.Listen(":2022") + c, err := l.Accept() + + sc, err := obfssh.NewServer(c, config, obfs_method, obfs_key) + + sc.Run() + + +client usage example +==================== + + + import "github.com/fangdingjun/obfssh" + import "golang.org/x/crypto/ssh" + + addr := "localhost:2022" + + // key for encryption + obfs_key := "some keyword" + + // encrypt method + obfs_method := "rc4" + + config := ssh.ClientConfig{ + // add ssh client config here + // for example auth method + ... + } + + c, err := net.Dial("tcp", addr) + + // create connection + client, err := obfssh.NewClient(c, config, addr, obfs_method, obfs_key) + + // local to remote port forward + client.AddLocalForward(":2234:10.0.0.1:3221") + + // remote to local port forward + client.AddRemoteForward(":2234:10.2.0.1:3221") + + // dynamic port forward + client.AddDynamicForward(":4321") + + // wait to be done + client.Run() + + +limitions +======== + + now, the server side only implements the port forward function, start shell or execute a command is not suppurted + + if set the `obfs_method` to `none`, obfssh is compatible with standard ssh server/client(OpenSSH) + +License +======= + + GPLv3, see LICENSE file details diff --git a/client.go b/client.go new file mode 100644 index 0000000..725fef7 --- /dev/null +++ b/client.go @@ -0,0 +1,288 @@ +package obfssh + +import ( + socks "github.com/fangdingjun/socks-go" + "github.com/golang/crypto/ssh" + "github.com/golang/crypto/ssh/terminal" + "net" + "os" + "os/signal" + "syscall" + "time" +) + +// Client is ssh client connection +type Client struct { + conn net.Conn + sshConn ssh.Conn + client *ssh.Client + listeners []net.Listener + ch chan int +} + +// NewClient create a new ssh Client +// +// addr is server address +// +// method is obfs encrypt method, value is rc4, aes or none or "" +// +// key is obfs encrypt key +// +// if set method to none or "", means disable the obfs, +// when the obfs is disabled, the client can connect to standard ssh server, like OpenSSH server +// +func NewClient(c net.Conn, config *ssh.ClientConfig, addr, method, key string) (*Client, error) { + Log(DEBUG, "create obfs conn with method %s", method) + obfsConn, err := NewObfsConn(&TimedOutConn{c, 15 * time.Second}, method, key, false) + if err != nil { + return nil, err + } + sshConn, newch, reqs, err := ssh.NewClientConn(obfsConn, addr, config) + if err != nil { + return nil, err + } + sshClient := ssh.NewClient(sshConn, newch, reqs) + client := &Client{ + conn: c, sshConn: sshConn, client: sshClient, + ch: make(chan int), + } + go client.keepAlive(10*time.Second, 4) + return client, nil +} + +// Run wait ssh connection to finish +func (cc *Client) Run() { + select { + case <-time.After(1 * time.Second): + } + // wait port forward to finish + if cc.listeners != nil { + Log(DEBUG, "wait all channel to be done") + go cc.registerSignal() + go func() { + cc.sshConn.Wait() + select { + case cc.ch <- 1: + default: + } + }() + + // wait exit signal + select { + case <-cc.ch: + Log(INFO, "got signal, exit") + } + } + Log(DEBUG, "Done") + cc.Close() +} + +// Close close the ssh connection +// and free all the port forward resources +func (cc *Client) Close() { + for _, l := range cc.listeners { + Log(INFO, "close the listener %s", l.Addr()) + l.Close() + } + //Log(DEBUG, "close ssh connection") + //cc.sshConn.Close() +} + +// RunCmd run a single command on server +func (cc *Client) RunCmd(cmd string) ([]byte, error) { + Log(INFO, "run command %s", cmd) + session, err := cc.client.NewSession() + if err != nil { + Log(DEBUG, "command exited with error: %s", err.Error()) + } else { + Log(DEBUG, "command exited with no error") + } + + if err != nil { + return nil, err + } + d, err := session.CombinedOutput(cmd) + session.Close() + return d, err +} + +// Shell start a login shell on server +func (cc *Client) Shell() error { + Log(DEBUG, "request new session") + session, err := cc.client.NewSession() + if err != nil { + return err + } + + session.Stdin = os.Stdin + session.Stdout = os.Stdout + session.Stderr = os.Stderr + modes := ssh.TerminalModes{ + ssh.ECHO: 1, + ssh.TTY_OP_ISPEED: 14400, + ssh.TTY_OP_OSPEED: 14400, + } + + // this make CTRL+C works + Log(DEBUG, "turn terminal mode to raw") + oldState, _ := terminal.MakeRaw(0) + w, h, _ := terminal.GetSize(0) + Log(DEBUG, "request pty") + if err := session.RequestPty("xterm", h, w, modes); err != nil { + Log(ERROR, "request pty error: %s", err.Error()) + Log(DEBUG, "restore terminal mode") + terminal.Restore(0, oldState) + return err + } + Log(DEBUG, "request shell") + if err := session.Shell(); err != nil { + Log(ERROR, "start shell error: %s", err.Error()) + Log(DEBUG, "restore terminal mode") + terminal.Restore(0, oldState) + return err + } + + session.Wait() + Log(DEBUG, "session closed") + terminal.Restore(0, oldState) + Log(DEBUG, "restore terminal mode") + return nil +} + +// AddLocalForward add a local to remote port forward +func (cc *Client) AddLocalForward(local, remote string) error { + Log(DEBUG, "add local forward %s -> %s", local, remote) + l, err := net.Listen("tcp", local) + if err != nil { + return err + } + cc.listeners = append(cc.listeners, l) + go func(l net.Listener) { + //defer l.Close() + for { + c, err := l.Accept() + if err != nil { + Log(DEBUG, "local listen %s closed", l.Addr()) + return + } + Log(DEBUG, "connection accepted from %s", c.RemoteAddr()) + go cc.handleLocalForward(c, remote) + } + }(l) + + return nil +} + +// AddRemoteForward add a remote to local port forward +func (cc *Client) AddRemoteForward(local, remote string) error { + Log(DEBUG, "add remote forward %s -> %s", remote, local) + l, err := cc.client.Listen("tcp", remote) + if err != nil { + return err + } + + cc.listeners = append(cc.listeners, l) + go func(l net.Listener) { + //defer l.Close() + for { + c, err := l.Accept() + if err != nil { + Log(DEBUG, "remote listener %s closed", l.Addr()) + return + } + Log(DEBUG, "accept remote forward connection from %s", c.RemoteAddr()) + go cc.handleRemoteForward(c, local) + } + }(l) + return nil +} + +// AddDynamicForward add a dynamic port forward +func (cc *Client) AddDynamicForward(local string) error { + Log(DEBUG, "add dynamic forward %s", local) + l, err := net.Listen("tcp", local) + if err != nil { + return err + } + cc.listeners = append(cc.listeners, l) + go func(l net.Listener) { + //defer l.Close() + for { + c, err := l.Accept() + if err != nil { + Log(DEBUG, "local listener %s closed", l.Addr()) + return + } + Log(DEBUG, "accept connection from %s", c.RemoteAddr()) + go cc.handleDynamicForward(c) + } + }(l) + return nil +} + +func (cc *Client) handleLocalForward(conn net.Conn, remote string) { + rconn, err := cc.client.Dial("tcp", remote) + if err != nil { + Log(ERROR, "connect to %s failed: %s", remote, err.Error()) + conn.Close() + return + } + Log(DEBUG, "remote connect to %s success", remote) + PipeAndClose(rconn, conn) +} + +func (cc *Client) handleRemoteForward(conn net.Conn, local string) { + lconn, err := net.Dial("tcp", local) + if err != nil { + Log(ERROR, "connect to %s failed: %s", local, err.Error()) + conn.Close() + return + } + Log(DEBUG, "connect to %s success", local) + PipeAndClose(conn, lconn) +} + +func (cc *Client) handleDynamicForward(conn net.Conn) { + s := socks.SocksConn{conn, cc.client.Dial} + s.Serve() +} + +func (cc *Client) keepAlive(interval time.Duration, maxCount int) { + count := 0 + c := time.NewTicker(interval) + for { + select { + case <-c.C: + _, _, err := cc.sshConn.SendRequest("keepalive@openssh.org", true, nil) + if err != nil { + Log(DEBUG, "keep alive error: %s", err.Error()) + count++ + } else { + count = 0 + } + if count >= maxCount { + Log(ERROR, "keep alive hit max count, exit") + cc.sshConn.Close() + // send exit signal + select { + case cc.ch <- 1: + default: + } + return + } + } + } +} + +func (cc *Client) registerSignal() { + c := make(chan os.Signal, 5) + signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) + select { + case s1 := <-c: + Log(ERROR, "signal %d received, exit", s1) + select { + case cc.ch <- 1: + default: + } + } +} diff --git a/conn.go b/conn.go new file mode 100644 index 0000000..a280613 --- /dev/null +++ b/conn.go @@ -0,0 +1,284 @@ +package obfssh + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rand" + "crypto/rc4" + "crypto/sha1" + "crypto/sha512" + "encoding/binary" + "errors" + "io" + //"log" + "math/big" + "net" + "strings" + "time" +) + +const ( + keyLength = 16 + seedLength = 16 + maxPadding = 1024 + magicValue uint32 = 0x0BF5CA7E + loopCount = 10 +) + +// ObfsConn implement the net.Conn interface which enrytp/decrpt +// the data automatic +type ObfsConn struct { + net.Conn + key []byte + cipherRead cipher.Stream + cipherWrite cipher.Stream + cipherDisabled bool + method string + writeBuf []byte + writeBufLen int + //isServer bool +} + +// NewObfsConn initial a ObfsConn +// after new return, seed handshake is done +func NewObfsConn(c net.Conn, method, key string, isServer bool) (*ObfsConn, error) { + + wc := &ObfsConn{ + Conn: c, + key: []byte(key), + cipherDisabled: false, + method: method, + writeBuf: make([]byte, 8192), + writeBufLen: 8192, + // isServer: isServer, + } + + // do not initial chiper when encrypt method is empty or none + if method == "" || method == "none" { + wc.DisableObfs() + return wc, nil + } + + if isServer { + if err := wc.readSeed(); err != nil { + buf := make([]byte, 1024) + Log(DEBUG, "read forever") + // read forever + for { + if _, err1 := wc.Conn.Read(buf); err1 != nil { + return nil, err + } + } + return nil, err + } + } else { + if err := wc.writeSeed(); err != nil { + return nil, err + } + } + return wc, nil +} + +func generateKey(seed, keyword, iv []byte) []byte { + buf := make([]byte, seedLength+len(keyword)+len(iv)) + + copy(buf[0:], seed) + + // user key + if keyword != nil { + copy(buf[seedLength:], keyword) + } + + copy(buf[seedLength+len(keyword):], iv) + + o := sha512.Sum512(buf[0:]) + + for i := 0; i < loopCount; i++ { + o = sha512.Sum512(o[0:]) + } + + return o[0:keyLength] +} + +// EnableObfs enable the encryption +func (wc *ObfsConn) EnableObfs() { + Log(DEBUG, "enable the encryption") + wc.cipherDisabled = false +} + +// DisableObfs disable the encryption +func (wc *ObfsConn) DisableObfs() { + Log(DEBUG, "disable the encryption") + wc.cipherDisabled = true +} + +func (wc *ObfsConn) writeSeed() error { + Log(DEBUG, "begin to write the seed") + + ii, err := rand.Int(rand.Reader, big.NewInt(int64(maxPadding))) + if err != nil { + //Log(ERROR, "initial the random seed failed: %s", err.Error()) + return err + } + i := ii.Int64() + Log(DEBUG, "use padding data length %d\n", int(i)) + buf := make([]byte, seedLength+8+int(i)) + + // generate seed + rand.Read(buf[0:seedLength]) + + // put magic value + binary.BigEndian.PutUint32(buf[seedLength:seedLength+4], magicValue) + + // put padding length + binary.BigEndian.PutUint32(buf[seedLength+4:seedLength+8], uint32(i)) + + // generate padding data + rand.Read(buf[24:]) + + // generate the key + keyToServer := generateKey(buf[0:seedLength], wc.key, []byte("client_to_server")) + keyToClient := generateKey(buf[0:seedLength], wc.key, []byte("server_to_client")) + + var r, w cipher.Stream + + // initial the cipher + switch strings.ToLower(wc.method) { + case "aes": + w, r = newAESCipher(keyToServer, keyToClient) + case "rc4": + w, r = newRC4Cipher(keyToServer, keyToClient) + default: + return errors.New("unknown cipher type") + } + + wc.cipherWrite = w + wc.cipherRead = r + + // encrypt the data, except the seed + wc.cipherWrite.XORKeyStream(buf[seedLength:], buf[seedLength:]) + + _, err = wc.Conn.Write(buf[0:]) + if err != nil { + return err + } + + Log(DEBUG, "write seed done") + return nil +} + +func (wc *ObfsConn) readSeed() error { + Log(DEBUG, "begin to read the seed") + buf := make([]byte, seedLength+8) + + // read the data except padding + _, err := io.ReadFull(wc.Conn, buf) + if err != nil { + return err + } + + // generate the key + keyToServer := generateKey(buf[0:seedLength], wc.key, []byte("client_to_server")) + keyToClient := generateKey(buf[0:seedLength], wc.key, []byte("server_to_client")) + + var w, r cipher.Stream + switch strings.ToLower(wc.method) { + case "aes": + w, r = newAESCipher(keyToClient, keyToServer) + case "rc4": + w, r = newRC4Cipher(keyToClient, keyToServer) + } + + wc.cipherWrite = w + wc.cipherRead = r + + // decrypt the magic and padding length + wc.cipherRead.XORKeyStream(buf[seedLength:seedLength+8], buf[seedLength:seedLength+8]) + + // check magic value + magic := binary.BigEndian.Uint32(buf[seedLength : seedLength+4]) + if magic != magicValue { + Log(ERROR, "magic %x check failed from %s", magic, wc.Conn.RemoteAddr()) + return errors.New("wrong magic value") + } + + // read the padding data + padLen := binary.BigEndian.Uint32(buf[seedLength+4 : seedLength+8]) + + Log(DEBUG, "padding %d", padLen) + + buf = make([]byte, padLen) + if _, err := io.ReadFull(wc, buf[0:]); err != nil { + return err + } + + Log(DEBUG, "read seed done") + return nil +} + +// Read read the data from underlying connection +// if encryption enabled, decrypt the data and return to plain data to upstream +func (wc *ObfsConn) Read(buf []byte) (int, error) { + n, err := wc.Conn.Read(buf) + if err != nil { + return 0, err + } + if !wc.cipherDisabled { + wc.cipherRead.XORKeyStream(buf[0:n], buf[0:n]) + } + //log.Printf("%+q", buf[0:n]) + return n, err +} + +// Write write the data to underlying connection +// if encryption enabled, encrypt it before write +func (wc *ObfsConn) Write(buf []byte) (int, error) { + if !wc.cipherDisabled { + bufLen := len(buf) + if bufLen > wc.writeBufLen { + wc.writeBufLen = bufLen + 8192 + wc.writeBuf = make([]byte, wc.writeBufLen) + } + wc.cipherWrite.XORKeyStream(wc.writeBuf[0:bufLen], buf[0:bufLen]) + return wc.Conn.Write(wc.writeBuf[0:bufLen]) + } + return wc.Conn.Write(buf[0:]) +} + +func newAESCipher(key1, key2 []byte) (cipher.Stream, cipher.Stream) { + b1, _ := aes.NewCipher(key1) + b2, _ := aes.NewCipher(key2) + + m1 := sha1.Sum(key1) + iv1 := md5.Sum(m1[0:]) + + m2 := sha1.Sum(key2) + iv2 := md5.Sum(m2[0:]) + + w := cipher.NewCFBEncrypter(b1, iv1[0:]) + r := cipher.NewCFBDecrypter(b2, iv2[0:]) + return w, r +} + +func newRC4Cipher(key1, key2 []byte) (cipher.Stream, cipher.Stream) { + w, _ := rc4.NewCipher(key1) + r, _ := rc4.NewCipher(key2) + return w, r +} + +// TimedOutConn is a net.Conn with read/write timeout set +type TimedOutConn struct { + net.Conn + Timeout time.Duration +} + +func (tc *TimedOutConn) Read(b []byte) (int, error) { + tc.Conn.SetDeadline(time.Now().Add(tc.Timeout)) + return tc.Conn.Read(b) +} + +func (tc *TimedOutConn) Write(b []byte) (int, error) { + tc.Conn.SetDeadline(time.Now().Add(tc.Timeout)) + return tc.Conn.Write(b) +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..9ae4f1b --- /dev/null +++ b/doc.go @@ -0,0 +1,75 @@ +package obfssh + +/* +obfssh is wrapper for ssh protocol, use AES or RC4 to encrypt the transport data, +ssh is a good designed protocol and with the good encryption, but the protocol has a especially figerprint, +the firewall can easily identify the protocol and block it or QOS it, especial when we use its port forward function to escape from the state censorship. + +obfssh encrypt the ssh protocol and hide the figerprint, the firewall can not identify the protocol. + +We borrow the idea from https://github.com/brl/obfuscated-openssh, but not compatible with it, +beause the limitions of golang ssh library. + +server usage example + + import "github.com/fangdingjun/obfssh" + import "golang.org/x/crypto/ssh" + + // key for encryption + obfs_key := "some keyword" + + // encrypt method + obfs_method := "rc4" + + config := &ssh.ServerConfig{ + // add ssh server configure here + // for example auth method, cipher, MAC + ... + } + + l, err := net.Listen(":2022") + c, err := l.Accept() + + sc, err := obfssh.NewServer(c, config, obfs_method, obfs_key) + + sc.Run() + + +client usage example + + import "github.com/fangdingjun/obfssh" + import "golang.org/x/crypto/ssh" + + addr := "localhost:2022" + + // key for encryption + obfs_key := "some keyword" + + // encrypt method + obfs_method := "rc4" + + config := ssh.ClientConfig{ + // add ssh client config here + // for example auth method + ... + } + + c, err := net.Dial("tcp", addr) + + // create connection + client, err := obfssh.NewClient(c, config, addr, obfs_method, obfs_key) + + // local to remote port forward + client.AddLocalForward(":2234:10.0.0.1:3221") + + // remote to local port forward + client.AddRemoteForward(":2234:10.2.0.1:3221") + + // dynamic port forward + client.AddDynamicForward(":4321") + + // wait to be done + client.Run() + + +*/ diff --git a/obfssh_client/.gitignore b/obfssh_client/.gitignore new file mode 100644 index 0000000..ecc9f25 --- /dev/null +++ b/obfssh_client/.gitignore @@ -0,0 +1,2 @@ +obfssh_client* +run.bat diff --git a/obfssh_client/README.md b/obfssh_client/README.md new file mode 100644 index 0000000..d584642 --- /dev/null +++ b/obfssh_client/README.md @@ -0,0 +1,29 @@ +obfssh\_client +============= + +this is obfssh\_client example + + +usage +===== + +run server + + go get github.com/fangdingjun/obfssh/obfssh_server + + cp $GOPATH/src/github.com/fangdingjun/obfssh/obfssh_server/config_example.yaml config.yaml + + vim config.yaml + + ssh-keygen -f ssh_host_rsa_key -t rsa 1024 + + $GOPATH/bin/obfssh_server -c config.yaml + + +run client + + go get github.com/fangdingjun/obfssh/obfssh_client + + $GOPATH/bin/obfssh_client -N -D :1234 -obfs_key some_keyworld -obfs_method rc4 -p 2022 -l user2 -pw user2 localhost + + this will create a socks proxy on :1234 diff --git a/obfssh_client/ssh.go b/obfssh_client/ssh.go new file mode 100644 index 0000000..cf04065 --- /dev/null +++ b/obfssh_client/ssh.go @@ -0,0 +1,228 @@ +package main + +import ( + //"bytes" + "flag" + "fmt" + "github.com/fangdingjun/obfssh" + "github.com/golang/crypto/ssh" + "github.com/golang/crypto/ssh/agent" + //"github.com/golang/crypto/ssh/terminal" + "time" + //"io" + "io/ioutil" + "log" + "net" + "os" + //"os/signal" + "path/filepath" + "strings" + //"sync" + //"syscall" +) + +var method, encryptKey string + +type stringSlice []string + +func (lf *stringSlice) Set(val string) error { + *lf = append(*lf, val) + return nil +} + +func (lf *stringSlice) String() string { + s := "" + if lf == nil { + return s + } + for _, v := range *lf { + s += " " + s += v + } + return s +} + +var localForwards stringSlice +var remoteForwards stringSlice +var dynamicForwards stringSlice + +func main() { + var host, port, user, pass, key string + //var localForward, remoteForward, dynamicForward string + var notRunCmd bool + var debug bool + + flag.StringVar(&user, "l", os.Getenv("USER"), "ssh username") + flag.StringVar(&pass, "pw", "", "ssh password") + flag.StringVar(&port, "p", "22", "remote port") + flag.StringVar(&key, "i", "", "private key file") + flag.Var(&localForwards, "L", "forward local port to remote, format [local_host:]local_port:remote_host:remote_port") + flag.Var(&remoteForwards, "R", "forward remote port to local, format [remote_host:]remote_port:local_host:local_port") + flag.BoolVar(¬RunCmd, "N", false, "not run remote command, useful when do port forward") + flag.Var(&dynamicForwards, "D", "enable dynamic forward, format [local_host:]local_port") + flag.StringVar(&method, "obfs_method", "", "transport encrypt method, avaliable: rc4, aes, empty means disable encrypt") + flag.StringVar(&encryptKey, "obfs_key", "", "transport encrypt key") + flag.BoolVar(&debug, "d", false, "verbose mode") + flag.Parse() + + if debug { + obfssh.SSHLogLevel = obfssh.DEBUG + } + auth := []ssh.AuthMethod{} + + // read ssh agent and default auth key + if pass == "" && key == "" { + if aconn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { + obfssh.Log(obfssh.DEBUG, "add auth method with agent %s", os.Getenv("SSH_AUTH_SOCK")) + auth = append(auth, ssh.PublicKeysCallback(agent.NewClient(aconn).Signers)) + } + + home := os.Getenv("HOME") + for _, f := range []string{ + ".ssh/id_rsa", + ".ssh/id_dsa", + ".ssh/identity", + ".ssh/id_ecdsa", + ".ssh/id_ed25519", + } { + k1 := filepath.Join(home, f) + if pemBytes, err := ioutil.ReadFile(k1); err == nil { + if priKey, err := ssh.ParsePrivateKey(pemBytes); err == nil { + obfssh.Log(obfssh.DEBUG, "add private key: %s", k1) + auth = append(auth, ssh.PublicKeys(priKey)) + } + } + } + } + + args := flag.Args() + var cmd string + switch len(args) { + case 0: + flag.PrintDefaults() + log.Fatal("you must specify the remote host") + case 1: + host = args[0] + cmd = "" + default: + host = args[0] + cmd = strings.Join(args[1:], " ") + } + + if strings.Contains(host, "@") { + ss := strings.SplitN(host, "@", 2) + user = ss[0] + host = ss[1] + } + + if pass != "" { + obfssh.Log(obfssh.DEBUG, "add password auth method") + auth = append(auth, ssh.Password(pass)) + } + + if key != "" { + pemBytes, err := ioutil.ReadFile(key) + if err != nil { + log.Fatal(err) + } + priKey, err := ssh.ParsePrivateKey(pemBytes) + if err != nil { + log.Fatal(err) + } + obfssh.Log(obfssh.DEBUG, "add private key %s", key) + auth = append(auth, ssh.PublicKeys(priKey)) + } + + config := &ssh.ClientConfig{ + User: user, + Auth: auth, + Timeout: 10 * time.Second, + } + + h := net.JoinHostPort(host, port) + c, err := net.Dial("tcp", h) + if err != nil { + log.Fatal(err) + } + + client, err := obfssh.NewClient(c, config, h, method, encryptKey) + if err != nil { + log.Fatal(err) + } + var local, remote string + for _, p := range localForwards { + addr := parseForwardAddr(p) + if len(addr) != 4 && len(addr) != 3 { + log.Printf("wrong forward addr %s, format: [local_host:]local_port:remote_host:remote_port", p) + continue + } + if len(addr) == 4 { + local = strings.Join(addr[:2], ":") + remote = strings.Join(addr[2:], ":") + } else { + local = fmt.Sprintf(":%s", addr[0]) + remote = strings.Join(addr[1:], ":") + } + //log.Printf("add local to remote %s->%s", local, remote) + if err := client.AddLocalForward(local, remote); err != nil { + log.Println(err) + } + } + + for _, p := range remoteForwards { + addr := parseForwardAddr(p) + if len(addr) != 4 && len(addr) != 3 { + log.Printf("wrong forward addr %s, format: [local_host:]local_port:remote_host:remote_port", p) + continue + } + if len(addr) == 4 { + remote = strings.Join(addr[:2], ":") + local = strings.Join(addr[2:], ":") + } else { + remote = fmt.Sprintf("0.0.0.0:%s", addr[0]) + local = strings.Join(addr[1:], ":") + } + //log.Printf("add remote to local %s->%s", remote, local) + if err := client.AddRemoteForward(local, remote); err != nil { + log.Println(err) + } + } + for _, p := range dynamicForwards { + + if strings.Index(p, ":") == -1 { + local = fmt.Sprintf(":%s", p) + } else { + local = p + } + //log.Printf("listen on %s", local) + if err := client.AddDynamicForward(local); err != nil { + log.Println(err) + } + } + + if !notRunCmd { + if cmd != "" { + if d, err := client.RunCmd(cmd); err != nil { + log.Println(err) + } else { + //log.Printf("%s", string(d)) + fmt.Printf("%s", string(d)) + } + } else { + if err := client.Shell(); err != nil { + log.Println(err) + } + } + } + client.Run() +} + +func parseForwardAddr(s string) []string { + ss := strings.FieldsFunc(s, func(c rune) bool { + if c == ':' { + return true + } + return false + }) + return ss +} diff --git a/obfssh_server/.gitignore b/obfssh_server/.gitignore new file mode 100644 index 0000000..7b07c98 --- /dev/null +++ b/obfssh_server/.gitignore @@ -0,0 +1,2 @@ +obfssh_server* +authorized_keys diff --git a/obfssh_server/README.md b/obfssh_server/README.md new file mode 100644 index 0000000..acbb9b8 --- /dev/null +++ b/obfssh_server/README.md @@ -0,0 +1,29 @@ +obfssh\_server +============= + +this is obfssh\_server example + + +usage +===== + +run server + + go get github.com/fangdingjun/obfssh/obfssh_server + + cp $GOPATH/src/github.com/fangdingjun/obfssh/obfssh_server/config_example.yaml config.yaml + + vim config.yaml + + ssh-keygen -f ssh_host_rsa_key -t rsa 1024 + + $GOPATH/bin/obfssh_server -c config.yaml + + +run client + + go get github.com/fangdingjun/obfssh/obfssh_client + + $GOPATH/bin/obfssh_client -N -D :1234 -obfs_key some_keyworld -obfs_method rc4 -p 2022 -l user2 -pw user2 localhost + + this will create a socks proxy on :1234 diff --git a/obfssh_server/config.go b/obfssh_server/config.go new file mode 100644 index 0000000..75a9e90 --- /dev/null +++ b/obfssh_server/config.go @@ -0,0 +1,72 @@ +package main + +import ( + "bytes" + "github.com/go-yaml/yaml" + "github.com/golang/crypto/ssh" + "io/ioutil" + "log" +) + +type serverConfig struct { + Port int `yaml:"port"` + Key string `yaml:"obfs_key"` + Debug bool `yaml:"debug"` + HostKey string `yaml:"host_key_file"` + Method string `yaml:"obfs_method"` + Users []serverUser `yaml:"users"` +} + +type serverUser struct { + Username string `yaml:"username"` + Password string `yaml:"password"` + AuthorizedKeyFile string `yaml:"authorized_key_file"` + publicKeys []ssh.PublicKey +} + +func (c *serverConfig) getUser(user string) (serverUser, error) { + for _, u := range c.Users { + if u.Username == user { + return u, nil + } + } + return serverUser{}, nil +} + +func loadConfig(f string) (*serverConfig, error) { + buf, err := ioutil.ReadFile(f) + if err != nil { + return nil, err + } + + var c serverConfig + if err := yaml.Unmarshal(buf, &c); err != nil { + return nil, err + } + + for i := range c.Users { + buf1, err := ioutil.ReadFile(c.Users[i].AuthorizedKeyFile) + if err != nil { + log.Printf("read publickey for %s failed, ignore", c.Users[i].Username) + continue + } + + // parse authorized_key + //var err error + var p1 ssh.PublicKey + r := bytes.Trim(buf1, " \r\n") + for { + p1, _, _, r, err = ssh.ParseAuthorizedKey(r) + if err != nil { + //log.Println(err) + //log.Printf("%+v %+v", r, p1) + return nil, err + } + c.Users[i].publicKeys = append(c.Users[i].publicKeys, p1) + if len(r) == 0 { + break + } + } + } + return &c, nil +} diff --git a/obfssh_server/config_example.yaml b/obfssh_server/config_example.yaml new file mode 100644 index 0000000..d389755 --- /dev/null +++ b/obfssh_server/config_example.yaml @@ -0,0 +1,46 @@ +# vim: set ft=yaml: +# +# + +# listen port +port: 2022 + +# the key to encrypt the transport data +obfs_key: some_keyword + +# ssh host key file +host_key_file: ssh_host_rsa_key + +# the method to encrypt the transport data +# avaiable methods: rc4, aes, none or "" +# none or "" means disable the obfs encrypt +#obfs_method: rc4 +obfs_method: "rc4" + +# show more log message +# value true or false +debug: true + +# the users used by ssh server +# user can authorite by passwrod or by public key +# public key as same as OpenSSH +# public key or password must be specify one +# +users: + - + # username + username: user1 + # password, empty password means disable password authorize + password: "" + # public key file + authorized_key_file: /path/to/user/authorized_keys + - + username: user2 + password: user2 + authorized_key_file: /path/to/user/authorized_keys + - + username: user3 + password: "" + authorized_key_file: /path/to/authorized_keys + + diff --git a/obfssh_server/config_test.go b/obfssh_server/config_test.go new file mode 100644 index 0000000..f5c35c6 --- /dev/null +++ b/obfssh_server/config_test.go @@ -0,0 +1,16 @@ +package main + +import ( + //"github.com/go-yaml/yaml" + //"io/ioutil" + "log" + "testing" +) + +func TestConfig(t *testing.T) { + c, err := loadConfig("config_example.yaml") + if err != nil { + log.Fatal(err) + } + log.Printf("%+v", c) +} diff --git a/obfssh_server/server.go b/obfssh_server/server.go new file mode 100644 index 0000000..d63147d --- /dev/null +++ b/obfssh_server/server.go @@ -0,0 +1,99 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "github.com/fangdingjun/obfssh" + "github.com/golang/crypto/ssh" + "io/ioutil" + "log" + "net" +) + +func main() { + + var configfile string + + flag.StringVar(&configfile, "c", "config.yaml", "configure file") + flag.Parse() + + conf, err := loadConfig(configfile) + if err != nil { + log.Fatal(err) + } + + // set log level + if conf.Debug { + obfssh.SSHLogLevel = obfssh.DEBUG + } + + config := &ssh.ServerConfig{ + PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { + if u, err := conf.getUser(c.User()); err == nil { + if u.Password != "" && c.User() == u.Username && string(pass) == u.Password { + return nil, nil + } + } + return nil, fmt.Errorf("password reject for user %s", c.User()) + }, + + PublicKeyCallback: func(c ssh.ConnMetadata, k ssh.PublicKey) (*ssh.Permissions, error) { + if u, err := conf.getUser(c.User()); err == nil { + for _, pk := range u.publicKeys { + if k.Type() == pk.Type() && bytes.Compare(k.Marshal(), pk.Marshal()) == 0 { + return nil, nil + } + } + } + return nil, fmt.Errorf("publickey reject for user %s", c.User()) + }, + + // auth log + AuthLogCallback: func(c ssh.ConnMetadata, method string, err error) { + if err != nil { + obfssh.Log(obfssh.ERROR, "%s", err.Error()) + obfssh.Log(obfssh.ERROR, "%s auth failed for %s from %s", method, c.User(), c.RemoteAddr()) + } else { + obfssh.Log(obfssh.INFO, "Accepted %s for user %s from %s", method, c.User(), c.RemoteAddr()) + } + }, + } + + privateBytes, err := ioutil.ReadFile(conf.HostKey) + if err != nil { + log.Fatal(err) + } + + private, err := ssh.ParsePrivateKey(privateBytes) + if err != nil { + log.Fatal(err) + } + + config.AddHostKey(private) + l, err := net.Listen("tcp", fmt.Sprintf(":%d", conf.Port)) + if err != nil { + log.Fatal(err) + } + defer l.Close() + for { + c, err := l.Accept() + if err != nil { + fmt.Println(err) + return + } + + obfssh.Log(obfssh.DEBUG, "accept tcp connection from %s", c.RemoteAddr()) + + go func(c net.Conn) { + sc, err := obfssh.NewServer(c, config, conf.Method, conf.Key) + if err != nil { + c.Close() + obfssh.Log(obfssh.ERROR, "%s", err.Error()) + return + } + sc.Run() + }(c) + } + +} diff --git a/obfssh_server/ssh_host_rsa_key b/obfssh_server/ssh_host_rsa_key new file mode 100644 index 0000000..a2f0031 --- /dev/null +++ b/obfssh_server/ssh_host_rsa_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA2n/6710+F060gDjqmHXWwBgKKKhykVVH91PcBW9txzCG2HHr +dM0RV7GmC703TVTaJZjHb8xWQCLrUorDEdabKte49QrsM5F6tjTqPG+LEazFUmyI +EH8/B4ISIKRzW1+G75dFx28CTC8Zr1Ue6PJRDuBJYOJA104EuIxS3fCNiQU0oD/A +lkbfl90FmFjth47Pu/hdHN+kbfOA9TO2cpztgjz+6vEd9JjNuc9xR83okfXPjI4i +RvjPk4K70LGCGyPkIjbK1vu5zDffwmROGi9BBOR2X17LnCsez2GDP6paz7sIwOOU +kikqxm2fjSfOjS1kBB0Yc2o3w7kZ4CTHO/U6BwIDAQABAoIBAQCflpg2Wjkultq5 +SFj4cCEg/q300kuToOFGYSazhZZ9xRDIaDRchCclkOhBbLtGrTIEAdmw62MXxylv +iVA+6Cs/GH7L42VvqNMi3/UxnRrLFxCsSf77ZaUom7UXyGVFGLmapzddGdRoxoIR +EK/Z3pqbre+KZtaLKz3BeSRRXBBUQXJopvvfh7UNr/wFQQmLzqFWUO8ommxr6J3w +a1JhVPS1MGmxn18teQyB6I9xtw347MAt7NQ1ZQXPbCjr/v1ZNt4bLQrae/S/jWNc +4/WmxnK4fAhHNdBjofNKa+SLbcoDkHtUBZquh418oPNE6BUn14qocKlUPASYS+5E +zC5UCxzRAoGBAPRylOjt5OTAhl5vMC7EFESK8Wzma2WBZnrjkP+u8NFOLo3cRcvj ++jsTO1ilKR7PQJfeU/NBWrIU+wgm22nMGlayHAXrtbueN9/devURbOb7md75AGvS +DB0mB4Acuxqn4a2ThTjDi+H6+Oqz0Ab7ps3Wm6cFH0ZRlREE0woAljmJAoGBAOTT +eVkJmSzNLqEDtKuBdysTJp+j0hhUediWh8rallvo5e9zMrXJksfAstHJcR8NkqLD +VL9K4Vydudw70efPg8siSyNDUjDXCy18Vl3x+xqIXsFVXGybTpLIGj22irKX8ZQZ +qpZhHBbFw1terQCFdkAt+B6skJUU4zeiE1uQEUMPAoGAJwt2RY5aFT+7NrJD2/Rt +2FTpIx/a36e/mrlmm7BxvrziKr6YV2zetzjnLc2Tt9wa0Sct+Zjix7caMb8jJM75 +Fgf0+e0gZgtrmVJjJWnXHz3o4fib3Jz8WluMryXnrOZL4dHCYcK6QSo5QCPggn0H +s7Enw5HJ4Q1+5e0DWIGnfSECgYEAio1JkpHvP2NVcoUN5jLT9y73Wf4VfknYJT6w +JjHIjQot/5ifAdd1mqGhJMl2RzkuqoLfU5yBbFTMbv+Bj3zk7iBrooRmxc/PotEA +co3MXzpnNWT8O36mStYCnY9j19OMoQIRelB+c4N3UGG5GvG0shOjgt82BC7LjaoD +UpOfAB0CgYBHuda3elvCalfDevia3ZaKKw/JT0CS4J0DKkrWk97Yet/MaDE0k/wk +ammJCNckfGI5nH7m6Ljd6A5cKjL3i5TgvMUjNz8qb9f6GiK5fBxLzO3KhgnsoIft +r7VjCr/nYvvM4asYwSdoHA6P2XKtOsuB7XEFL4k2EJ3WvCyYcjMvOw== +-----END RSA PRIVATE KEY----- diff --git a/server.go b/server.go new file mode 100644 index 0000000..6c232d1 --- /dev/null +++ b/server.go @@ -0,0 +1,300 @@ +package obfssh + +import ( + "fmt" + "github.com/golang/crypto/ssh" + "github.com/golang/crypto/ssh/terminal" + //"log" + "net" +) + +// Server is server connection +type Server struct { + conn net.Conn + sshConn *ssh.ServerConn + forwardedPorts map[string]net.Listener + exitCh chan int +} + +// NewServer create a new struct for Server +// +// c is net.Conn +// +// config is &ssh.ServerConfig +// +// method is obfs encrypt method, value is rc4, aes or none or "" +// +// key is obfs encrypt key +// +// if set method to none or "", means disable obfs encryption, when the obfs is disabled, +// the server can accept connection from standard ssh client, like OpenSSH client +// +func NewServer(c net.Conn, config *ssh.ServerConfig, method, key string) (*Server, error) { + wc, err := NewObfsConn(c, method, key, true) + if err != nil { + return nil, err + } + sshConn, ch, req, err := ssh.NewServerConn(wc, config) + if err != nil { + return nil, err + } + //wc.DisableObfs() + sc := &Server{conn: c, + sshConn: sshConn, + forwardedPorts: map[string]net.Listener{}, + exitCh: make(chan int)} + go sc.handleGlobalRequest(req) + go sc.handleNewChannelRequest(ch) + return sc, nil +} + +// Run waits for server connection finish +func (sc *Server) Run() { + sc.sshConn.Wait() + Log(DEBUG, "ssh connection closed") + sc.close() +} + +func (sc *Server) close() { + Log(DEBUG, "close connection") + sc.sshConn.Close() + //Log(DEBUG, "close listener") + for _, l := range sc.forwardedPorts { + Log(DEBUG, "close listener %s", l.Addr()) + l.Close() + } +} + +func (sc *Server) handleNewChannelRequest(ch <-chan ssh.NewChannel) { + for newch := range ch { + switch newch.ChannelType() { + case "session": + //go sc.handleSession(newch) + //continue + case "direct-tcpip": + go handleDirectTcpip(newch) + continue + } + Log(DEBUG, "reject channel request %s", newch.ChannelType()) + newch.Reject(ssh.UnknownChannelType, "unknown channel type") + //channel, request, err := newch.Accept() + } +} + +func (sc *Server) handleGlobalRequest(req <-chan *ssh.Request) { + for r := range req { + switch r.Type { + case "tcpip-forward": + Log(DEBUG, "request port forward") + go sc.handleTcpipForward(r) + continue + case "cancel-tcpip-forward": + Log(DEBUG, "request cancel port forward") + go sc.handleCancelTcpipForward(r) + continue + } + Log(DEBUG, "global request %s", r.Type) + if r.WantReply { + r.Reply(false, nil) + } + } +} + +func (sc *Server) handleChannelRequest(req <-chan *ssh.Request) { + ret := false + for r := range req { + switch r.Type { + case "shell": + ret = true + case "pty-req": + ret = true + case "env": + ret = true + case "exec": + ret = false + case "subsystem": + default: + ret = false + } + if r.WantReply { + r.Reply(ret, nil) + } + } +} + +type directTcpipMsg struct { + Raddr string + Rport uint32 + Laddr string + Lport uint32 +} + +func (sc *Server) handleSession(newch ssh.NewChannel) { + ch, req, err := newch.Accept() + if err != nil { + Log(ERROR, "%s", err.Error()) + return + } + go sc.handleChannelRequest(req) + term := terminal.NewTerminal(ch, "shell>") + defer ch.Close() + for { + line, err := term.ReadLine() + if err != nil { + break + } + term.Write([]byte(line)) + term.Write([]byte("\n")) + } +} + +func handleDirectTcpip(newch ssh.NewChannel) { + data := newch.ExtraData() + var r directTcpipMsg + err := ssh.Unmarshal(data, &r) + if err != nil { + Log(DEBUG, "invalid ssh parameter") + newch.Reject(ssh.ConnectionFailed, "invalid argument") + return + } + Log(DEBUG, "create connection to %s:%d", r.Raddr, r.Rport) + rconn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", r.Raddr, r.Rport)) + if err != nil { + Log(ERROR, "%s", err.Error()) + newch.Reject(ssh.ConnectionFailed, "invalid argument") + return + } + channel, requests, err := newch.Accept() + if err != nil { + rconn.Close() + Log(ERROR, "%s", err.Error()) + return + } + //log.Println("forward") + go ssh.DiscardRequests(requests) + PipeAndClose(channel, rconn) +} + +type tcpipForwardAddr struct { + Addr string + Port uint32 +} + +func (sc *Server) handleCancelTcpipForward(req *ssh.Request) { + var a tcpipForwardAddr + + if err := ssh.Unmarshal(req.Payload, &a); err != nil { + Log(ERROR, "invalid ssh parameter for cancel port forward") + if req.WantReply { + req.Reply(false, nil) + } + return + } + + k := fmt.Sprintf("%s:%d", a.Addr, a.Port) + if l, ok := sc.forwardedPorts[k]; ok { + l.Close() + delete(sc.forwardedPorts, k) + } + + if req.WantReply { + req.Reply(true, nil) + } +} + +func (sc *Server) handleTcpipForward(req *ssh.Request) { + var addr tcpipForwardAddr + if err := ssh.Unmarshal(req.Payload, &addr); err != nil { + Log(ERROR, "parse ssh data error: %s", err.Error) + if req.WantReply { + req.Reply(false, nil) + } + return + } + + if addr.Port > 65535 || addr.Port < 0 { + Log(ERROR, "invalid port %d", addr.Port) + if req.WantReply { + req.Reply(false, nil) + } + return + } + + ip := net.ParseIP(addr.Addr) + if ip == nil { + Log(ERROR, "invalid ip %d", addr.Port) + if req.WantReply { + req.Reply(false, nil) + } + return + } + + k := fmt.Sprintf("%s:%d", addr.Addr, addr.Port) + + if _, ok := sc.forwardedPorts[k]; ok { + // port in use + Log(ERROR, "port in use: %s", k) + if req.WantReply { + req.Reply(false, nil) + } + return + } + + //Log(DEBUG, "get request for addr: %s, port: %d", addr.Addr, addr.Port) + + l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: ip, Port: int(addr.Port)}) + if err != nil { + // listen failed + Log(ERROR, "%s", err.Error()) + if req.WantReply { + req.Reply(false, nil) + } + return + } + + a1 := l.Addr() + Log(DEBUG, "Listening port %s", a1) + p := struct { + Port uint32 + }{ + uint32(a1.(*net.TCPAddr).Port), + } + + sc.forwardedPorts[k] = l + + if req.WantReply { + req.Reply(true, ssh.Marshal(p)) + } + + for { + c, err := l.Accept() + if err != nil { + Log(ERROR, "%s", err.Error()) + return + } + Log(DEBUG, "accept connection from %s", c.RemoteAddr()) + go func(c net.Conn) { + laddr := c.LocalAddr() + raddr := c.RemoteAddr() + a2 := struct { + laddr string + lport uint32 + raddr string + rport uint32 + }{ + addr.Addr, + uint32(laddr.(*net.TCPAddr).Port), + raddr.(*net.TCPAddr).IP.String(), + uint32(raddr.(*net.TCPAddr).Port), + } + ch, r, err := sc.sshConn.OpenChannel("forwarded-tcpip", ssh.Marshal(a2)) + if err != nil { + Log(ERROR, "forward port failed: %s", err.Error()) + c.Close() + return + } + go ssh.DiscardRequests(r) + PipeAndClose(c, ch) + }(c) + } +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..b8fce2b --- /dev/null +++ b/util.go @@ -0,0 +1,45 @@ +package obfssh + +import ( + "io" + "log" +) + +const ( + _ = iota + // DEBUG log level debug + DEBUG + // INFO log level info + INFO + // ERROR log level error + ERROR +) + +// SSHLogLevel global value for log level +var SSHLogLevel = ERROR + +// PipeAndClose pipe the data between c and s, close both when done +func PipeAndClose(c io.ReadWriteCloser, s io.ReadWriteCloser) { + defer c.Close() + defer s.Close() + cc := make(chan int, 2) + + go func() { + io.Copy(c, s) + cc <- 1 + }() + + go func() { + io.Copy(s, c) + cc <- 1 + }() + + <-cc +} + +// Log log the message by level +func Log(level int, s string, args ...interface{}) { + if level >= SSHLogLevel { + log.Printf(s, args...) + } +}