Compare commits

..

41 Commits
master ... tls

Author SHA1 Message Date
dingjun d8a17f217f update go.mod 5 years ago
dingjun ab06e58d48 update dependence 6 years ago
dingjun b0eb5ada1c Merge branch 'tls' of https://github.com/fangdingjun/obfssh into tls 6 years ago
dingjun eb34216c7a seperate proto listener 6 years ago
dingjun db1a4f8605 change log level for server 6 years ago
dingjun 1a4351c626 add signal handler 6 years ago
dingjun 37e161a63f update .gitignore 6 years ago
dingjun ebb0dc0cf2 add go module files 6 years ago
dingjun f6b305c18e add proxy proto support 6 years ago
Dingjun aaf03cdceb
Merge pull request #1 from nlimpid/tls
add build option darwin for pty
6 years ago
nlimpid e372dc11f4
add build option darwin for pty 6 years ago
dingjun bfe65cf0e6 log library updated 6 years ago
fangdingjun 99e82f9943 fix close before write issue 6 years ago
fangdingjun 29107f0035 update log library 6 years ago
fangdingjun 10125aea13 send exit-status to client after sftp done 6 years ago
fangdingjun 2e364939bd update obfssh usage message 6 years ago
fangdingjun 8c32fb3bb1 use context.Context to signal exit 6 years ago
fangdingjun 311c076e55 use defer to make sure terminal.Restore to run 6 years ago
fangdingjun b39998c6d3 Merge branch 'tls' of github.com:fangdingjun/obfssh into tls 6 years ago
fangdingjun 16c39a262b add shell/exec support on server 6 years ago
fangdingjun e002d3d1c8 change max packet size 6 years ago
fangdingjun 106c3b07e0 fix name in README 6 years ago
fangdingjun c0543037df update README 6 years ago
fangdingjun 3a5b7b1bd6 change level for log message 6 years ago
fangdingjun e821ce6ecb use CertChecker for public key auth 6 years ago
fangdingjun 911c955dd9 change log library 7 years ago
fangdingjun 2b973b326d fix hang issue on close remote listener 7 years ago
fangdingjun 397f60da61 fix hang on tls handshake issue 7 years ago
fangdingjun d91bc0bcf6 add dynamic http forward throungh secure channel
like dynamic forward, but this accept HTTP request incoming,
not socks5.
The destination is determined by http request, the quest is forwarded
through ssh secure channel.
7 years ago
fangdingjun 2413a56408 avoid double close the channel 7 years ago
fangdingjun a64906e384 fix keepalive issue 7 years ago
fangdingjun 3b9fb72ce3 set dialer timeout to 15 seconds 7 years ago
Dingjun 4a182a8ef7 update doc 7 years ago
Dingjun fb6c630cec use dialer on package obfssh 7 years ago
Dingjun 441017d722 update README 7 years ago
Dingjun f39fed1a4b use dialer 7 years ago
Dingjun 5bde493445 update sample config 7 years ago
Dingjun 016df73cfd add timeout for dial 7 years ago
Dingjun 283245e12f update commandline help 7 years ago
Dingjun ea706dc06b update sample code 7 years ago
Dingjun d7abfb0d26 remove obfsucation, use tls instead 7 years ago

@ -1,16 +1,7 @@
obfssh obfssh
===== =====
obfssh is wrapper for ssh protocol, use AES or RC4 to encrypt the transport data, obfssh is wrapper for golang.org/x/crypto/ssh protocol, add support for listen or connect ssh via TLS
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 server usage example
@ -19,11 +10,6 @@ server usage example
import "github.com/fangdingjun/obfssh" import "github.com/fangdingjun/obfssh"
import "golang.org/x/crypto/ssh" import "golang.org/x/crypto/ssh"
// key for encryption
obfs_key := "some keyword"
// encrypt method
obfs_method := "rc4"
config := &ssh.ServerConfig{ config := &ssh.ServerConfig{
// add ssh server configure here // add ssh server configure here
@ -31,12 +17,27 @@ server usage example
... ...
} }
l, err := net.Listen(":2022") var l net.Listener
c, err := l.Accept() var err error
if useTLS {
sc, err := obfssh.NewServer(c, config, obfs_method, obfs_key) cert, err := tls.LoadX509KeyPair(certFile, keyFile)
l, err = tls.Listen("tcp", ":2022", &tls.Config{
sc.Run() Certificates: []tls.Certificate{cert},
}
}else{
l, err = net.Listen(":2022")
}
defer l.Close()
for {
c, err := l.Accept()
go func(c net.Conn){
defer c.Close()
sc, err := obfssh.NewServer(c, config, &obfssh.Conf{})
sc.Run()
}(c)
}
client usage example client usage example
@ -48,22 +49,25 @@ client usage example
addr := "localhost:2022" addr := "localhost:2022"
// key for encryption
obfs_key := "some keyword"
// encrypt method
obfs_method := "rc4"
config := ssh.ClientConfig{ config := ssh.ClientConfig{
// add ssh client config here // add ssh client config here
// for example auth method // for example auth method
... ...
} }
c, err := net.Dial("tcp", addr) var c net.Conn
var err error
if useTLS {
c, err = tls.Dial("tcp", addr, &tls.Config{
ServerName: "localhost",
InsecureSkipVerify: true,
}
}else{
c, err = net.Dial("tcp", addr)
}
// create connection // create connection
client, err := obfssh.NewClient(c, config, addr, obfs_method, obfs_key) client, err := obfssh.NewClient(c, config, addr, &obfssh.Conf{})
// local to remote port forward // local to remote port forward
client.AddLocalForward(":2234:10.0.0.1:3221") client.AddLocalForward(":2234:10.0.0.1:3221")
@ -83,7 +87,6 @@ limitions
now, the server side only implements the port forward function, start shell or execute a command is not suppurted 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 License
======= =======

@ -1,15 +1,23 @@
package obfssh package obfssh
import ( import (
"bufio"
"context"
"errors"
"fmt" "fmt"
socks "github.com/fangdingjun/socks-go"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
"net" "net"
"net/http"
"os" "os"
"os/signal" "os/signal"
"strings"
"sync"
"syscall" "syscall"
"time" "time"
"github.com/fangdingjun/go-log"
socks "github.com/fangdingjun/socks-go"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
) )
// Client is ssh client connection // Client is ssh client connection
@ -18,43 +26,30 @@ type Client struct {
sshConn ssh.Conn sshConn ssh.Conn
client *ssh.Client client *ssh.Client
listeners []net.Listener listeners []net.Listener
ch chan struct{}
err error err error
ctx context.Context
cancel context.CancelFunc
} }
// NewClient create a new ssh Client // NewClient create a new ssh Client
// //
// addr is server address // addr is server address
// //
// method is obfs encrypt method, value is rc4, aes or none or ""
//
// key is obfs encrypt key
//
// conf is the client configure // conf is the client configure
// //
// 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 string, conf *Conf) (*Client, error) { func NewClient(c net.Conn, config *ssh.ClientConfig, addr string, conf *Conf) (*Client, error) {
Log(DEBUG, "create obfs conn with method %s", conf.ObfsMethod) //obfsConn := &TimedOutConn{c, conf.Timeout}
obfsConn, err := NewObfsConn(&TimedOutConn{c, conf.Timeout}, conf.ObfsMethod, conf.ObfsKey, false) sshConn, newch, reqs, err := ssh.NewClientConn(c, addr, config)
if err != nil {
return nil, err
}
sshConn, newch, reqs, err := ssh.NewClientConn(obfsConn, addr, config)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if conf.DisableObfsAfterHandshake {
obfsConn.DisableObfs()
}
sshClient := ssh.NewClient(sshConn, newch, reqs) sshClient := ssh.NewClient(sshConn, newch, reqs)
client := &Client{ client := &Client{
conn: c, sshConn: sshConn, client: sshClient, conn: c, sshConn: sshConn, client: sshClient,
ch: make(chan struct{}),
} }
client.ctx, client.cancel = context.WithCancel(context.Background())
go client.keepAlive(conf.KeepAliveInterval, conf.KeepAliveMax) go client.keepAlive(conf.KeepAliveInterval, conf.KeepAliveMax)
return client, nil return client, nil
} }
@ -66,50 +61,75 @@ func (cc *Client) Client() *ssh.Client {
// Run wait ssh connection to finish // Run wait ssh connection to finish
func (cc *Client) Run() error { func (cc *Client) Run() error {
defer cc.Close()
defer cc.cancel()
select { select {
case <-time.After(1 * time.Second): case <-time.After(1 * time.Second):
} }
// wait port forward to finish // wait port forward to finish
if cc.listeners != nil { if cc.listeners != nil {
Log(DEBUG, "wait all channel to be done") log.Debugf("wait all channel to be done")
go cc.registerSignal() go cc.registerSignal()
go func() { go func() {
cc.err = cc.sshConn.Wait() cc.err = cc.sshConn.Wait()
Log(DEBUG, "connection hang up") log.Debugf("connection hang up")
close(cc.ch) cc.cancel()
//close(cc.ch)
}() }()
<-cc.ctx.Done()
// wait exit signal
select {
case <-cc.ch:
Log(INFO, "got signal, exit")
}
} }
Log(DEBUG, "Done")
cc.Close()
return cc.err return cc.err
} }
func (cc *Client) closeListener() {
if len(cc.listeners) == 0 {
return
}
// close remote listener may block, because of connection issue
// so only 1 second to wait
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
wg := &sync.WaitGroup{}
for _, l := range cc.listeners {
go func(l net.Listener) {
log.Debugf("begin to close listener %s", l.Addr().String())
l.Close()
log.Debugf("close listener %s done", l.Addr().String())
wg.Done()
}(l)
wg.Add(1)
}
go func() {
wg.Wait()
cancel()
}()
<-ctx.Done()
}
// Close close the ssh connection // Close close the ssh connection
// and free all the port forward resources // and free all the port forward resources
func (cc *Client) Close() { func (cc *Client) Close() {
for _, l := range cc.listeners { cc.closeListener()
Log(INFO, "close the listener %s", l.Addr())
l.Close() log.Debugf("close ssh connection")
}
//Log(DEBUG, "close ssh connection")
cc.sshConn.Close() cc.sshConn.Close()
cc.conn.Close() cc.conn.Close()
log.Debugf("close ssh connection done")
} }
// RunCmd run a single command on server // RunCmd run a single command on server
func (cc *Client) RunCmd(cmd string) ([]byte, error) { func (cc *Client) RunCmd(cmd string) ([]byte, error) {
Log(INFO, "run command %s", cmd) log.Debugf("run command %s", cmd)
session, err := cc.client.NewSession() session, err := cc.client.NewSession()
if err != nil { if err != nil {
Log(DEBUG, "command exited with error: %s", err.Error()) log.Debugf("command exited with error: %s", err.Error())
} else { } else {
Log(DEBUG, "command exited with no error") log.Debugf("command exited with no error")
} }
if err != nil { if err != nil {
@ -122,7 +142,7 @@ func (cc *Client) RunCmd(cmd string) ([]byte, error) {
// Shell start a login shell on server // Shell start a login shell on server
func (cc *Client) Shell() error { func (cc *Client) Shell() error {
Log(DEBUG, "request new session") log.Debugf("request new session")
session, err := cc.client.NewSession() session, err := cc.client.NewSession()
if err != nil { if err != nil {
return err return err
@ -138,34 +158,34 @@ func (cc *Client) Shell() error {
} }
// this make CTRL+C works // this make CTRL+C works
Log(DEBUG, "turn terminal mode to raw") log.Debugf("turn terminal mode to raw")
oldState, _ := terminal.MakeRaw(0) oldState, _ := terminal.MakeRaw(0)
defer func() {
log.Debugf("restore terminal mode")
terminal.Restore(0, oldState)
}()
w, h, _ := terminal.GetSize(0) w, h, _ := terminal.GetSize(0)
Log(DEBUG, "request pty") log.Debugf("request pty")
if err := session.RequestPty("xterm", h, w, modes); err != nil { if err := session.RequestPty("xterm", h, w, modes); err != nil {
Log(ERROR, "request pty error: %s", err.Error()) log.Errorf("request pty error: %s", err.Error())
Log(DEBUG, "restore terminal mode")
terminal.Restore(0, oldState)
return err return err
} }
Log(DEBUG, "request shell") log.Debugf("request shell")
if err := session.Shell(); err != nil { if err := session.Shell(); err != nil {
Log(ERROR, "start shell error: %s", err.Error()) log.Errorf("start shell error: %s", err.Error())
Log(DEBUG, "restore terminal mode")
terminal.Restore(0, oldState)
return err return err
} }
session.Wait() session.Wait()
Log(DEBUG, "session closed") log.Debugf("session closed")
terminal.Restore(0, oldState)
Log(DEBUG, "restore terminal mode")
return nil return nil
} }
// AddLocalForward add a local to remote port forward // AddLocalForward add a local to remote port forward
func (cc *Client) AddLocalForward(local, remote string) error { func (cc *Client) AddLocalForward(local, remote string) error {
Log(DEBUG, "add local forward %s -> %s", local, remote) log.Debugf("add local forward %s -> %s", local, remote)
l, err := net.Listen("tcp", local) l, err := net.Listen("tcp", local)
if err != nil { if err != nil {
return err return err
@ -176,10 +196,10 @@ func (cc *Client) AddLocalForward(local, remote string) error {
for { for {
c, err := l.Accept() c, err := l.Accept()
if err != nil { if err != nil {
Log(DEBUG, "local listen %s closed", l.Addr()) log.Debugf("local listen %s closed", l.Addr())
return return
} }
Log(DEBUG, "connection accepted from %s", c.RemoteAddr()) log.Debugf("connection accepted from %s", c.RemoteAddr())
go cc.handleLocalForward(c, remote) go cc.handleLocalForward(c, remote)
} }
}(l) }(l)
@ -189,7 +209,7 @@ func (cc *Client) AddLocalForward(local, remote string) error {
// AddRemoteForward add a remote to local port forward // AddRemoteForward add a remote to local port forward
func (cc *Client) AddRemoteForward(local, remote string) error { func (cc *Client) AddRemoteForward(local, remote string) error {
Log(DEBUG, "add remote forward %s -> %s", remote, local) log.Debugf("add remote forward %s -> %s", remote, local)
l, err := cc.client.Listen("tcp", remote) l, err := cc.client.Listen("tcp", remote)
if err != nil { if err != nil {
return err return err
@ -201,10 +221,10 @@ func (cc *Client) AddRemoteForward(local, remote string) error {
for { for {
c, err := l.Accept() c, err := l.Accept()
if err != nil { if err != nil {
Log(DEBUG, "remote listener %s closed", l.Addr()) log.Debugf("remote listener %s closed", l.Addr())
return return
} }
Log(DEBUG, "accept remote forward connection from %s", c.RemoteAddr()) log.Debugf("accept remote forward connection from %s", c.RemoteAddr())
go cc.handleRemoteForward(c, local) go cc.handleRemoteForward(c, local)
} }
}(l) }(l)
@ -213,7 +233,7 @@ func (cc *Client) AddRemoteForward(local, remote string) error {
// AddDynamicForward add a dynamic port forward // AddDynamicForward add a dynamic port forward
func (cc *Client) AddDynamicForward(local string) error { func (cc *Client) AddDynamicForward(local string) error {
Log(DEBUG, "add dynamic forward %s", local) log.Debugf("add dynamic forward %s", local)
l, err := net.Listen("tcp", local) l, err := net.Listen("tcp", local)
if err != nil { if err != nil {
return err return err
@ -224,10 +244,10 @@ func (cc *Client) AddDynamicForward(local string) error {
for { for {
c, err := l.Accept() c, err := l.Accept()
if err != nil { if err != nil {
Log(DEBUG, "local listener %s closed", l.Addr()) log.Debugf("local listener %s closed", l.Addr())
return return
} }
Log(DEBUG, "accept connection from %s", c.RemoteAddr()) log.Debugf("accept connection from %s", c.RemoteAddr())
go cc.handleDynamicForward(c) go cc.handleDynamicForward(c)
} }
}(l) }(l)
@ -237,22 +257,22 @@ func (cc *Client) AddDynamicForward(local string) error {
func (cc *Client) handleLocalForward(conn net.Conn, remote string) { func (cc *Client) handleLocalForward(conn net.Conn, remote string) {
rconn, err := cc.client.Dial("tcp", remote) rconn, err := cc.client.Dial("tcp", remote)
if err != nil { if err != nil {
Log(ERROR, "connect to %s failed: %s", remote, err.Error()) log.Errorf("connect to %s failed: %s", remote, err.Error())
conn.Close() conn.Close()
return return
} }
Log(DEBUG, "remote connect to %s success", remote) log.Debugf("remote connect to %s success", remote)
PipeAndClose(rconn, conn) PipeAndClose(rconn, conn)
} }
func (cc *Client) handleRemoteForward(conn net.Conn, local string) { func (cc *Client) handleRemoteForward(conn net.Conn, local string) {
lconn, err := net.Dial("tcp", local) lconn, err := dialer.Dial("tcp", local)
if err != nil { if err != nil {
Log(ERROR, "connect to %s failed: %s", local, err.Error()) log.Errorf("connect to %s failed: %s", local, err.Error())
conn.Close() conn.Close()
return return
} }
Log(DEBUG, "connect to %s success", local) log.Debugf("connect to %s success", local)
PipeAndClose(conn, lconn) PipeAndClose(conn, lconn)
} }
@ -262,19 +282,19 @@ func (cc *Client) handleDynamicForward(conn net.Conn) {
if addr.String() != conn.LocalAddr().String() { if addr.String() != conn.LocalAddr().String() {
// transparent proxy // transparent proxy
// iptables redirect the packet to this port // iptables redirect the packet to this port
Log(DEBUG, "transparent %s -> %s", conn.RemoteAddr(), addr) log.Debugf("transparent %s -> %s", conn.RemoteAddr(), addr)
cc.handleTransparentProxy(conn, addr) cc.handleTransparentProxy(conn, addr)
return return
} }
} else { } else {
// SO_ORIGNAL_DST failed // SO_ORIGNAL_DST failed
// just ignore it // just ignore it
Log(DEBUG, "get original destination on %s failed: %s, ignore", log.Debugf("get original destination on %s failed: %s, ignore",
conn.LocalAddr(), err) conn.LocalAddr(), err)
} }
// socks5 to this port // socks5 to this port
Log(DEBUG, "socks %s", conn.RemoteAddr()) log.Debugf("socks %s", conn.RemoteAddr())
s := socks.Conn{Conn: conn, Dial: cc.client.Dial} s := socks.Conn{Conn: conn, Dial: cc.client.Dial}
s.Serve() s.Serve()
} }
@ -282,33 +302,54 @@ func (cc *Client) handleDynamicForward(conn net.Conn) {
func (cc *Client) handleTransparentProxy(c net.Conn, addr net.Addr) { func (cc *Client) handleTransparentProxy(c net.Conn, addr net.Addr) {
c2, err := cc.client.Dial("tcp", addr.String()) c2, err := cc.client.Dial("tcp", addr.String())
if err != nil { if err != nil {
Log(ERROR, "%s", err) log.Errorf("%s", err)
c.Close() c.Close()
return return
} }
PipeAndClose(c2, c) PipeAndClose(c2, c)
} }
func doKeepAlive(conn ssh.Conn, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ch := make(chan error, 1)
go func() {
_, _, err := conn.SendRequest("keepalive@openssh.org", true, nil)
ch <- err
}()
select {
case <-ctx.Done():
return errors.New("keepalive timeout")
case err := <-ch:
if err != nil {
return err
}
return nil
}
}
func (cc *Client) keepAlive(interval time.Duration, maxCount int) { func (cc *Client) keepAlive(interval time.Duration, maxCount int) {
count := 0 count := 0
c := time.NewTicker(interval) c := time.NewTicker(interval)
defer c.Stop()
for { for {
select { select {
case <-cc.ctx.Done():
return
case <-c.C: case <-c.C:
_, _, err := cc.sshConn.SendRequest("keepalive@openssh.org", true, nil) if err := doKeepAlive(cc.sshConn, 3*time.Second); err != nil {
if err != nil {
Log(DEBUG, "keep alive error: %s", err.Error())
count++ count++
} else { } else {
count = 0 count = 0
} }
if count >= maxCount { if count >= maxCount {
cc.err = fmt.Errorf("keep alive detects connection hang up") cc.err = fmt.Errorf("keep alive detects connection hang up")
Log(ERROR, "keep alive hit max count, exit") log.Errorf("keep alive hit max count, exit")
//cc.sshConn.Close() cc.cancel()
//cc.conn.Close()
// send exit signal
close(cc.ch)
return return
} }
} }
@ -321,7 +362,96 @@ func (cc *Client) registerSignal() {
select { select {
case s1 := <-c: case s1 := <-c:
cc.err = fmt.Errorf("signal %v", s1) cc.err = fmt.Errorf("signal %v", s1)
Log(ERROR, "signal %d received, exit", s1) log.Errorf("signal %d received, exit", s1)
close(cc.ch) //close(cc.ch)
cc.cancel()
}
}
// AddDynamicHTTPForward add a http dynamic forward through
// secure channel
func (cc *Client) AddDynamicHTTPForward(addr string) error {
log.Debugf("add dynamic http listen: %s", addr)
l, err := net.Listen("tcp", addr)
if err != nil {
log.Errorf("listen on %s failed, %s", addr, err)
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.Errorf("accept error %s", err)
break
}
go cc.handleHTTPIncoming(c)
}
}(l)
return nil
}
func (cc *Client) handleHTTPIncoming(c net.Conn) {
//defer c.Close()
r := bufio.NewReader(c)
req, err := http.ReadRequest(r)
if err != nil {
log.Errorf("read http request error %s", err)
c.Close()
return
}
if req.Method == "CONNECT" {
cc.handleConnect(req, c)
return
}
cc.handleHTTPReq(req, c)
}
func (cc *Client) handleConnect(req *http.Request, c net.Conn) {
log.Debugf("connect to %s", req.RequestURI)
c1, err := cc.client.Dial("tcp", req.RequestURI)
if err != nil {
fmt.Fprintf(c, "HTTP/1.0 503 connection failed\r\n\r\n")
log.Errorf("dial error %s", err)
c.Close()
return
}
//defer c1.Close()
fmt.Fprintf(c, "HTTP/1.0 200 connection established\r\n\r\n")
PipeAndClose(c, c1)
}
func (cc *Client) handleHTTPReq(req *http.Request, c net.Conn) {
host := req.Host
if !strings.Contains(host, ":") {
host = fmt.Sprintf("%s:80", host)
}
log.Debugf("request to %s", host)
c1, err := cc.client.Dial("tcp", host)
if err != nil {
fmt.Fprintf(c, "HTTP/1.1 503 connection failed\r\nConnection: close\r\n\r\n")
log.Errorf("connection failed %s", err)
c.Close()
return
}
//defer c1.Close()
if err = req.Write(c1); err != nil {
fmt.Fprintf(c, "HTTP/1.1 503 write to server error\r\nConnection: close\r\n\r\n")
log.Errorf("write request to server error %s", err)
c.Close()
c1.Close()
return
} }
PipeAndClose(c, c1)
} }

@ -6,18 +6,10 @@ import (
// Conf keeps the configure of server or client // Conf keeps the configure of server or client
type Conf struct { type Conf struct {
// ObfsMethod is the encrpt method
ObfsMethod string
// ObfsKey is key for encrypt
ObfsKey string
// Timeout is the socket timeout on read/write // Timeout is the socket timeout on read/write
Timeout time.Duration Timeout time.Duration
// DisableObfsAfterHandShake disable the obfs encryption after ssh handshake done
DisableObfsAfterHandshake bool
// KeepAliveInterval the keep alive interval // KeepAliveInterval the keep alive interval
KeepAliveInterval time.Duration KeepAliveInterval time.Duration

@ -1,270 +1,11 @@
package obfssh package obfssh
import ( import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"crypto/rc4"
"crypto/sha1"
"crypto/sha512"
"encoding/binary"
"errors"
"io"
//"log"
"math/big"
"net" "net"
"strings"
"time" "time"
) )
const ( var dialer = &net.Dialer{Timeout: 10 * time.Second}
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
}
}
}
} 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 // TimedOutConn is a net.Conn with read/write timeout set
type TimedOutConn struct { type TimedOutConn struct {

@ -7,81 +7,7 @@ import (
"time" "time"
) )
func TestObfsConn(t *testing.T) { func testTimedOutConn(t *testing.T, _timeout bool) {
obfsMethod := "rc4"
obfsKey := "hello"
// test rc4
testObfsConn(t, obfsMethod, obfsKey)
obfsMethod = "aes"
// test aes
testObfsConn(t, obfsMethod, obfsKey)
}
func testObfsConn(t *testing.T, obfsMethod, obfsKey string) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen socket failed: %s", err)
}
defer l.Close()
addr := l.Addr()
go func() {
// server
s, err := l.Accept()
if err != nil {
t.Fatalf("acceept failed: %s", err)
}
defer s.Close()
sConn, err := NewObfsConn(s, obfsMethod, obfsKey, true)
if err != nil {
t.Fatalf("create obfsconn failed: %s", err)
}
buf := make([]byte, 100)
n, err := sConn.Read(buf)
if err != nil {
t.Fatalf("server read failed: %s", err)
}
sConn.Write(buf[:n])
}()
c, err := net.Dial("tcp", addr.String())
if err != nil {
t.Fatalf("dail failed: %s", err)
}
defer c.Close()
cConn, err := NewObfsConn(c, obfsMethod, obfsKey, false)
if err != nil {
t.Fatalf("create client obfsconn failed: %s", err)
}
str := "hello, world"
cConn.Write([]byte(str))
buf := make([]byte, 100)
n, err := cConn.Read(buf)
if str != string(buf[:n]) {
t.Errorf("data transport failed")
}
}
func TestTimedOutConn(t *testing.T) {
testTimedOutConn(t, false)
testTimedOutConn(t, true)
}
func testTimedOutConn(t *testing.T, timeout bool) {
l, err := net.Listen("tcp", "127.0.0.1:0") l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
t.Fatalf("listen failed: %s", err) t.Fatalf("listen failed: %s", err)
@ -111,7 +37,7 @@ func testTimedOutConn(t *testing.T, timeout bool) {
t.Fatalf("server read failed: %s", err) t.Fatalf("server read failed: %s", err)
} }
if timeout { if _timeout {
time.Sleep(timeout + 1*time.Second) time.Sleep(timeout + 1*time.Second)
} }
@ -136,7 +62,7 @@ func testTimedOutConn(t *testing.T, timeout bool) {
buf := make([]byte, 100) buf := make([]byte, 100)
n, err := cConn.Read(buf) n, err := cConn.Read(buf)
if timeout { if _timeout {
if err == nil { if err == nil {
t.Errorf("expeced timeout error, got nil") t.Errorf("expeced timeout error, got nil")
} else { } else {

@ -1,38 +1,39 @@
package obfssh package obfssh
/* /*
obfssh is wrapper for ssh protocol, use AES or RC4 to encrypt the transport data, Package obfssh is wrapper for ssh protocol, support connect to server via TLS
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 server usage example
import "github.com/fangdingjun/obfssh" import "github.com/fangdingjun/obfssh"
import "golang.org/x/crypto/ssh" import "golang.org/x/crypto/ssh"
// key for encryption
obfs_key := "some keyword"
// encrypt method
obfs_method := "rc4"
config := &ssh.ServerConfig{ config := &ssh.ServerConfig{
// add ssh server configure here // add ssh server configure here
// for example auth method, cipher, MAC // for example auth method, cipher, MAC
... ...
} }
l, err := net.Listen(":2022") var l net.Listener
c, err := l.Accept() var err error
if useTLS {
sc, err := obfssh.NewServer(c, config, obfs_method, obfs_key) cert, err := tls.LoadX509KeyPair(certFile, keyFile)
l, err = tls.Listen("tcp", ":2022", &tls.Config{
sc.Run() Certificates: []tls.Certificate{cert},
})
}else{
l, err = net.Listen(":2022")
}
defer l.Close()
for{
c, err := l.Accept()
go func(c net.Conn){
defer c.Close()
sc, err := obfssh.NewServer(c, config, &obfssh.Conf{})
sc.Run()
}(c)
}
client usage example client usage example
@ -42,22 +43,26 @@ client usage example
addr := "localhost:2022" addr := "localhost:2022"
// key for encryption
obfs_key := "some keyword"
// encrypt method
obfs_method := "rc4"
config := ssh.ClientConfig{ config := ssh.ClientConfig{
// add ssh client config here // add ssh client config here
// for example auth method // for example auth method
... ...
} }
c, err := net.Dial("tcp", addr) var c net.Conn
var err error
if useTLS{
c, err = tls.Dial("tcp", addr, &tls.Config{
ServerName: "localhost",
InsecureSkipVerify: true,
})
}else{
c, err = net.Dial("tcp", addr)
}
// create connection // create connection
client, err := obfssh.NewClient(c, config, addr, obfs_method, obfs_key) client, err := obfssh.NewClient(c, config, addr, &obfssh.Conf{})
// local to remote port forward // local to remote port forward
client.AddLocalForward(":2234:10.0.0.1:3221") client.AddLocalForward(":2234:10.0.0.1:3221")

@ -0,0 +1,19 @@
module github.com/fangdingjun/obfssh
go 1.13
require (
github.com/bgentry/speakeasy v0.1.0
github.com/fangdingjun/go-log v0.0.0-20190821073628-ae332053d6dc
github.com/fangdingjun/protolistener v0.0.0-20190821093313-6d5d2138f296
github.com/fangdingjun/socks-go v0.0.0-20180926100003-fc6f0a9ee1f4
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/kr/fs v0.1.0
github.com/kr/pretty v0.1.0 // indirect
github.com/kr/pty v1.1.8
github.com/pkg/errors v0.8.1 // indirect
github.com/pkg/sftp v1.10.0
github.com/stretchr/testify v1.4.0 // indirect
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

@ -0,0 +1,48 @@
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fangdingjun/go-log v0.0.0-20190821073628-ae332053d6dc h1:Tdk7VsmsFo3d0NqHTy3SRoRnkduOxwXgR65gQsq8kXY=
github.com/fangdingjun/go-log v0.0.0-20190821073628-ae332053d6dc/go.mod h1:oi7jbIScCbha6TbVzmrJP6igIHt+jcvvEgSJ7Ww1GkI=
github.com/fangdingjun/protolistener v0.0.0-20190821093313-6d5d2138f296 h1:2c6agkdoPVSyvdJ0B+5DhOb1BQpso7a7zlBxXUnttmY=
github.com/fangdingjun/protolistener v0.0.0-20190821093313-6d5d2138f296/go.mod h1:RoT81rjdN8gQ1w/z7NiFkxV6VzkT4NZ43XIt0lu8tcc=
github.com/fangdingjun/socks-go v0.0.0-20180926100003-fc6f0a9ee1f4 h1:c3Iw/znf2xe2uut9zUTueO6XHyTTLugrbN9fAE4NAkg=
github.com/fangdingjun/socks-go v0.0.0-20180926100003-fc6f0a9ee1f4/go.mod h1:0P4kTlyyh76uY1Li3cyw4pOIKGL9RmXXWTQYFLS1ZaM=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc h1:lNOt1SMsgHXTdpuGw+RpnJtzUcCb/oRKZP65pBy9pr8=
github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc/go.mod h1:6/gX3+E/IYGa0wMORlSMla999awQFdbaeQCHjSMKIzY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.0 h1:DGA1KlA9esU6WcicH+P8PxFZOl15O6GYtab1cIJdOlE=
github.com/pkg/sftp v1.10.0/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a h1:Igim7XhdOpBnWPuYJ70XcNpq8q3BCACtVgNfoJxOV7g=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

2
obfscp/.gitignore vendored

@ -1 +1 @@
obfssh_scp* obfscp*

@ -1,16 +1,16 @@
obfssh\_scp obfscp
========= =========
obfssh\_scp is a scp style sftp client support use obfssh encryption obfscp is a scp style sftp client by golang, support connect to server via TLS
usage usage
==== ====
obfssh\_scp user@host:/path/to/file local obfscp user@host:/path/to/file local
or or
obfssh\_scp local user@host:/path/to/file obfscp local user@host:/path/to/file

@ -1,63 +1,86 @@
package main package main
import ( import (
"crypto/tls"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"github.com/bgentry/speakeasy"
"github.com/fangdingjun/obfssh"
"github.com/kr/fs"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall" "syscall"
"time" "time"
"github.com/bgentry/speakeasy"
"github.com/fangdingjun/go-log"
"github.com/fangdingjun/obfssh"
"github.com/kr/fs"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
) )
type options struct { type options struct {
Debug bool Debug bool
Port int Port int
User string User string
Passwd string Passwd string
Recursive bool TLS bool
ObfsMethod string TLSInsecure bool
ObfsKey string Recursive bool
DisableObfsAfterHandshake bool PrivateKey string
PrivateKey string
} }
var dialer = &net.Dialer{Timeout: 10 * time.Second}
func main() { func main() {
var cfg options var cfg options
var logfile string
var logFileCount int
var logFileSize int64
var loglevel string
flag.Usage = usage flag.Usage = usage
flag.BoolVar(&cfg.Debug, "d", false, "verbose mode") flag.BoolVar(&cfg.Debug, "d", false, "verbose mode")
flag.IntVar(&cfg.Port, "p", 22, "port") flag.IntVar(&cfg.Port, "p", 22, "port")
flag.StringVar(&cfg.User, "l", os.Getenv("USER"), "user") flag.StringVar(&cfg.User, "l", os.Getenv("USER"), "user")
flag.BoolVar(&cfg.TLS, "tls", false, "use tls or not")
flag.BoolVar(&cfg.TLSInsecure, "tls-insecure", false, "insecure tls connection")
flag.StringVar(&cfg.Passwd, "pw", "", "password") flag.StringVar(&cfg.Passwd, "pw", "", "password")
flag.StringVar(&cfg.PrivateKey, "i", "", "private key") flag.StringVar(&cfg.PrivateKey, "i", "", "private key")
flag.BoolVar(&cfg.Recursive, "r", false, "recursively copy entries") flag.BoolVar(&cfg.Recursive, "r", false, "recursively copy entries")
flag.StringVar(&cfg.ObfsMethod, "obfs_method", "none", "obfs encrypt method, rc4, aes or none") flag.StringVar(&logfile, "log_file", "", "log file, default stdout")
flag.StringVar(&cfg.ObfsKey, "obfs_key", "", "obfs encrypt key") flag.IntVar(&logFileCount, "log_count", 10, "max count of log to keep")
flag.BoolVar(&cfg.DisableObfsAfterHandshake, "disable_obfs_after_handshake", false, "disable obfs after handshake") flag.Int64Var(&logFileSize, "log_size", 10, "max log file size MB")
flag.StringVar(&loglevel, "log_level", "INFO", "log level, values:\nOFF, FATAL, PANIC, ERROR, WARN, INFO, DEBUG")
flag.Parse() flag.Parse()
if cfg.Debug {
obfssh.SSHLogLevel = obfssh.DEBUG
}
args := flag.Args() args := flag.Args()
if len(args) < 2 { if len(args) < 2 {
flag.Usage() flag.Usage()
os.Exit(1) os.Exit(1)
} }
if logfile != "" {
log.Default.Out = &log.FixedSizeFileWriter{
MaxCount: logFileCount,
Name: logfile,
MaxSize: logFileSize * 1024 * 1024,
}
}
if loglevel != "" {
lv, err := log.ParseLevel(loglevel)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
log.Default.Level = lv
}
var err error var err error
@ -81,13 +104,13 @@ func createSFTPConn(host, user string, cfg *options) (*sftp.Client, error) {
if aconn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { if aconn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
//auths = append(auths, ssh.PublicKeysCallback(agent.NewClient(aconn).Signers)) //auths = append(auths, ssh.PublicKeysCallback(agent.NewClient(aconn).Signers))
if signers, err := agent.NewClient(aconn).Signers(); err == nil { if signers, err := agent.NewClient(aconn).Signers(); err == nil {
debuglog("add private key from agent") log.Debugf("add private key from agent")
pkeys = append(pkeys, signers...) pkeys = append(pkeys, signers...)
} else { } else {
debuglog("get key from agent failed: %s", err) log.Debugf("get key from agent failed: %s", err)
} }
} else { } else {
debuglog("dial to agent failed: %s", err) log.Debugf("dial to agent failed: %s", err)
} }
home := os.Getenv("HOME") home := os.Getenv("HOME")
@ -103,25 +126,25 @@ func createSFTPConn(host, user string, cfg *options) (*sftp.Client, error) {
if priKey, err := ssh.ParsePrivateKey(pemBytes); err == nil { if priKey, err := ssh.ParsePrivateKey(pemBytes); err == nil {
//auths = append(auths, ssh.PublicKeys(priKey)) //auths = append(auths, ssh.PublicKeys(priKey))
pkeys = append(pkeys, priKey) pkeys = append(pkeys, priKey)
debuglog("add private key %s", k1) log.Debugf("add private key %s", k1)
} else { } else {
debuglog("parse private key failed: %s", err) log.Debugf("parse private key failed: %s", err)
} }
} }
} }
if len(pkeys) != 0 { if len(pkeys) != 0 {
debuglog("totol %d private keys", len(pkeys)) log.Debugf("totol %d private keys", len(pkeys))
auths = append(auths, ssh.PublicKeys(pkeys...)) auths = append(auths, ssh.PublicKeys(pkeys...))
} }
} }
if cfg.Passwd != "" { if cfg.Passwd != "" {
debuglog("add password auth") log.Debugf("add password auth")
auths = append(auths, ssh.Password(cfg.Passwd)) auths = append(auths, ssh.Password(cfg.Passwd))
} else { } else {
debuglog("add keyboard interactive") log.Debugf("add keyboard interactive")
auths = append(auths, auths = append(auths,
ssh.RetryableAuthMethod(ssh.PasswordCallback(passwordAuth), 3)) ssh.RetryableAuthMethod(ssh.PasswordCallback(passwordAuth), 3))
} }
@ -129,13 +152,13 @@ func createSFTPConn(host, user string, cfg *options) (*sftp.Client, error) {
if cfg.PrivateKey != "" { if cfg.PrivateKey != "" {
if buf, err := ioutil.ReadFile(cfg.PrivateKey); err == nil { if buf, err := ioutil.ReadFile(cfg.PrivateKey); err == nil {
if p, err := ssh.ParsePrivateKey(buf); err == nil { if p, err := ssh.ParsePrivateKey(buf); err == nil {
debuglog("add private key: %s", cfg.PrivateKey) log.Debugf("add private key: %s", cfg.PrivateKey)
auths = append(auths, ssh.PublicKeys(p)) auths = append(auths, ssh.PublicKeys(p))
} else { } else {
debuglog("parse private key failed: %s", err) log.Debugf("parse private key failed: %s", err)
} }
} else { } else {
debuglog("read private key failed: %s", err) log.Debugf("read private key failed: %s", err)
} }
} }
if user == "" { if user == "" {
@ -146,23 +169,33 @@ func createSFTPConn(host, user string, cfg *options) (*sftp.Client, error) {
User: user, User: user,
Auth: auths, Auth: auths,
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
} }
rhost := net.JoinHostPort(host, fmt.Sprintf("%d", cfg.Port)) rhost := net.JoinHostPort(host, fmt.Sprintf("%d", cfg.Port))
c, err := net.Dial("tcp", rhost) var c net.Conn
var err error
if cfg.TLS {
c, err = tls.DialWithDialer(dialer, "tcp", rhost, &tls.Config{
ServerName: host,
InsecureSkipVerify: cfg.TLSInsecure,
})
} else {
c, err = dialer.Dial("tcp", rhost)
}
if err != nil { if err != nil {
//log.Fatal(err) //log.Fatal(err)
return nil, err return nil, err
} }
conf := &obfssh.Conf{ conf := &obfssh.Conf{
ObfsMethod: cfg.ObfsMethod, Timeout: 10 * time.Second,
ObfsKey: cfg.ObfsKey, KeepAliveInterval: 10 * time.Second,
Timeout: 10 * time.Second, KeepAliveMax: 5,
KeepAliveInterval: 10 * time.Second,
KeepAliveMax: 5,
DisableObfsAfterHandshake: cfg.DisableObfsAfterHandshake,
} }
conn, err := obfssh.NewClient(c, config, rhost, conf) conn, err := obfssh.NewClient(c, config, rhost, conf)
@ -173,7 +206,7 @@ func createSFTPConn(host, user string, cfg *options) (*sftp.Client, error) {
//defer conn.Close() //defer conn.Close()
sftpConn, err := sftp.NewClient(conn.Client(), sftp.MaxPacket(64*1024)) sftpConn, err := sftp.NewClient(conn.Client(), sftp.MaxPacket(32*1024))
if err != nil { if err != nil {
//log.Fatal(err) //log.Fatal(err)
return nil, err return nil, err
@ -236,18 +269,18 @@ func download(args []string, cfg *options) error {
st1, err := sftpConn.Stat(path) st1, err := sftpConn.Stat(path)
if err != nil { if err != nil {
err1 = err err1 = err
debuglog("%s", err) log.Debugf("%s", err)
sftpConn.Close() sftpConn.Close()
continue continue
} }
if st1.Mode().IsDir() { if st1.Mode().IsDir() {
if !cfg.Recursive { if !cfg.Recursive {
debuglog("omit remote directory %s", path) log.Debugf("omit remote directory %s", path)
sftpConn.Close() sftpConn.Close()
continue continue
} }
if err := rget(sftpConn, path, localFile); err != nil { if err := rget(sftpConn, path, localFile); err != nil {
debuglog("download error: %s", err) log.Debugf("download error: %s", err)
err1 = err err1 = err
} }
sftpConn.Close() sftpConn.Close()
@ -262,14 +295,14 @@ func download(args []string, cfg *options) error {
lfile = clean(lfile) lfile = clean(lfile)
if err := get(sftpConn, path, lfile); err != nil { if err := get(sftpConn, path, lfile); err != nil {
debuglog("download error: %s", err) log.Debugf("download error: %s", err)
err1 = err err1 = err
} }
sftpConn.Close() sftpConn.Close()
} }
debuglog("done") log.Debugf("done")
return err1 return err1
} }
@ -289,6 +322,7 @@ func upload(args []string, cfg *options) error {
sftpConn, err := createSFTPConn(host, user, cfg) sftpConn, err := createSFTPConn(host, user, cfg)
if err != nil { if err != nil {
log.Debugf("create sftp failed: %s", err)
return err return err
} }
defer sftpConn.Close() defer sftpConn.Close()
@ -318,7 +352,7 @@ func upload(args []string, cfg *options) error {
// local file not exists // local file not exists
if err != nil { if err != nil {
debuglog("%s", err) log.Debugf("%s", err)
err1 = err err1 = err
continue continue
} }
@ -326,12 +360,12 @@ func upload(args []string, cfg *options) error {
// directory // directory
if st1.Mode().IsDir() { if st1.Mode().IsDir() {
if !cfg.Recursive { if !cfg.Recursive {
debuglog("omit directory %s", localFile) log.Debugf("omit directory %s", localFile)
continue continue
} }
// transfer directory // transfer directory
if err := rput(sftpConn, localFile, path); err != nil { if err := rput(sftpConn, localFile, path); err != nil {
debuglog("%s", err) log.Debugf("%s", err)
err1 = err err1 = err
} }
@ -350,7 +384,7 @@ func upload(args []string, cfg *options) error {
remoteFile = clean(remoteFile) remoteFile = clean(remoteFile)
if err := put(sftpConn, localFile, remoteFile); err != nil { if err := put(sftpConn, localFile, remoteFile); err != nil {
debuglog("upload %s failed: %s", localFile, err.Error()) log.Debugf("upload %s failed: %s", localFile, err.Error())
err1 = err err1 = err
} }
} }
@ -359,7 +393,7 @@ func upload(args []string, cfg *options) error {
func get(sftpConn *sftp.Client, remoteFile, localFile string) error { func get(sftpConn *sftp.Client, remoteFile, localFile string) error {
debuglog("download %s -> %s", remoteFile, localFile) log.Debugf("download %s -> %s", remoteFile, localFile)
fp, err := sftpConn.Open(remoteFile) fp, err := sftpConn.Open(remoteFile)
if err != nil { if err != nil {
@ -396,13 +430,13 @@ func get(sftpConn *sftp.Client, remoteFile, localFile string) error {
return err return err
} }
debuglog("done") log.Debugf("done")
return nil return nil
} }
func put(sftpConn *sftp.Client, localFile, remoteFile string) error { func put(sftpConn *sftp.Client, localFile, remoteFile string) error {
debuglog("upload %s -> %s", localFile, remoteFile) log.Debugf("upload %s -> %s", localFile, remoteFile)
fpw, err := sftpConn.OpenFile(remoteFile, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_TRUNC) fpw, err := sftpConn.OpenFile(remoteFile, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_TRUNC)
if err != nil { if err != nil {
@ -438,7 +472,7 @@ func put(sftpConn *sftp.Client, localFile, remoteFile string) error {
return err return err
} }
debuglog("done") log.Debugf("done")
return nil return nil
} }
@ -452,7 +486,7 @@ func rput(sftpConn *sftp.Client, localDir, remoteDir string) error {
} }
if st := walker.Stat(); !st.Mode().IsRegular() { if st := walker.Stat(); !st.Mode().IsRegular() {
debuglog("skip %s", walker.Path()) log.Debugf("skip %s", walker.Path())
continue continue
} }
@ -476,7 +510,7 @@ func rput(sftpConn *sftp.Client, localDir, remoteDir string) error {
} }
func rget(sftpConn *sftp.Client, remoteDir, localDir string) error { func rget(sftpConn *sftp.Client, remoteDir, localDir string) error {
debuglog("transfer recusive from remote to local, %s -> %s", remoteDir, localDir) log.Debugf("transfer recusive from remote to local, %s -> %s", remoteDir, localDir)
walker := sftpConn.Walk(remoteDir) walker := sftpConn.Walk(remoteDir)
for walker.Step() { for walker.Step() {
@ -485,7 +519,7 @@ func rget(sftpConn *sftp.Client, remoteDir, localDir string) error {
} }
if st := walker.Stat(); !st.Mode().IsRegular() { if st := walker.Stat(); !st.Mode().IsRegular() {
debuglog("skip %s", walker.Path()) log.Debugf("skip %s", walker.Path())
continue continue
} }
@ -525,13 +559,13 @@ type dirInterface interface {
func makeDirs(p string, c dirInterface) error { func makeDirs(p string, c dirInterface) error {
p = clean(p) p = clean(p)
debuglog("make directory for %s", p) log.Debugf("make directory for %s", p)
for i := 1; i < len(p); i++ { for i := 1; i < len(p); i++ {
if p[i] == '/' { if p[i] == '/' {
p1 := p[:i] p1 := p[:i]
if _, err := c.Stat(p1); err != nil { if _, err := c.Stat(p1); err != nil {
debuglog("make directory %s", p1) log.Debugf("make directory %s", p1)
if err := c.Mkdir(p1); err != nil { if err := c.Mkdir(p1); err != nil {
return err return err
} }
@ -547,10 +581,6 @@ func passwordAuth() (string, error) {
return strings.Trim(s, " \r\n"), err return strings.Trim(s, " \r\n"), err
} }
func debuglog(format string, args ...interface{}) {
obfssh.Log(obfssh.DEBUG, format, args...)
}
// //
// when use pkg/sftp client transfer a big file from pkg/sftp server, // when use pkg/sftp client transfer a big file from pkg/sftp server,
// io.Copy while cause connection hang, // io.Copy while cause connection hang,
@ -558,7 +588,7 @@ func debuglog(format string, args ...interface{}) {
// use this function has no problem // use this function has no problem
// //
func copyFile(w io.Writer, r io.Reader) error { func copyFile(w io.Writer, r io.Reader) error {
buf := make([]byte, 34*1024) buf := make([]byte, 32*1024)
for { for {
n, err := r.Read(buf) n, err := r.Read(buf)
if n > 0 { if n > 0 {
@ -600,22 +630,24 @@ Options:
-r recursively copy the directories -r recursively copy the directories
Options for obfuscation: -tls
-obfs_method method connect to server via TLS
Specifies the encryption method.
when this option is specified, the entire connection -tls-insecure
will be encrypted. do not verify server's certificate
when set to none, the encryption is disabled.
Avaliable methods: rc4, aes, none(default) -log_file
log file, default stdout
-obfs_key key
Specifies the key to encrypt the connection, -log_count
if the server enable the obfs, only known the max count of log file to keep, default 10
right key can connect to the server.
-log_size
-disable_obfs_after_handshake max log size MB, default 10
when this option is specified, only encrypt the
ssh handshake message. -log_level
log level, values:
OFF, FATAL, PANIC, ERROR, WARN, INFO, DEBUG
` `
fmt.Printf("%s", usageStr) fmt.Printf("%s", usageStr)
os.Exit(1) os.Exit(1)

@ -1,7 +1,7 @@
obfssh\_client obfssh
============= =============
this is obfssh\_client example this is obfssh example
usage usage
@ -9,27 +9,27 @@ usage
run server run server
go get github.com/fangdingjun/obfssh/obfssh_server go get github.com/fangdingjun/obfssh/obfsshd
cp $GOPATH/src/github.com/fangdingjun/obfssh/obfssh_server/config_example.yaml server_config.yaml cp $GOPATH/src/github.com/fangdingjun/obfssh/obfsshd/config_example.yaml server_config.yaml
vim server_config.yaml vim server_config.yaml
ssh-keygen -f ssh_host_rsa_key -t rsa ssh-keygen -f ssh_host_rsa_key -t rsa
$GOPATH/bin/obfssh_server -c server_config.yaml $GOPATH/bin/obfsshd -c server_config.yaml
run client run client
go get github.com/fangdingjun/obfssh/obfssh_client go get github.com/fangdingjun/obfssh/obfssh
$GOPATH/bin/obfssh_client -N -D :1234 -obfs_key some_keyworld -obfs_method rc4 -p 2022 -l user2 -pw user2 localhost $GOPATH/bin/obfssh -N -D :1234 -p 2022 -l user2 -pw user2 localhost
or or
cp $GOPATH/src/github.com/fangdingjun/obfssh/obfssh_client/config_example.yaml client_config.yaml cp $GOPATH/src/github.com/fangdingjun/obfssh/obfssh/config_example.yaml client_config.yaml
vim client_config.yaml vim client_config.yaml
$GOPATH/bin/obfssh_client -f client_config.yaml $GOPATH/bin/obfssh -f client_config.yaml

@ -2,9 +2,10 @@ package main
import ( import (
"flag" "flag"
"github.com/go-yaml/yaml"
"io/ioutil" "io/ioutil"
"strings" "strings"
"github.com/go-yaml/yaml"
) )
// stringSlice implemnts the flag.Value interface // stringSlice implemnts the flag.Value interface
@ -44,22 +45,22 @@ func (lf *stringSlice) String() string {
} }
type config struct { type config struct {
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
PrivateKey string `yaml:"private_key"` TLS bool `yaml:"tls"`
ObfsMethod string `yaml:"obfs_method"` TLSInsecure bool `yaml:"tls-insecure"`
ObfsKey string `yaml:"obfs_key"` PrivateKey string `yaml:"private_key"`
Username string `yaml:"username"` Username string `yaml:"username"`
Password string `yaml:"password"` Password string `yaml:"password"`
KeepaliveInterval int `yaml:"keepalive_interval"` KeepaliveInterval int `yaml:"keepalive_interval"`
KeepaliveMax int `yaml:"keepalive_max"` KeepaliveMax int `yaml:"keepalive_max"`
Debug bool `yaml:"debug"` Debug bool `yaml:"debug"`
DisableObfsAfterHandshake bool `yaml:"disable_obfs_after_handshake"` NotRunCmd bool `yaml:"not_run_cmd"`
NotRunCmd bool `yaml:"not_run_cmd"` LocalForwards stringSlice `yaml:"local_forward"`
LocalForwards stringSlice `yaml:"local_forward"` RemoteForwards stringSlice `yaml:"remote_forward"`
RemoteForwards stringSlice `yaml:"remote_forward"` DynamicForwards stringSlice `yaml:"dynamic_forward"`
DynamicForwards stringSlice `yaml:"dynamic_forward"` DynamicHTTP stringSlice `yaml:"dynamic_http"`
Proxy proxy Proxy proxy
} }
type proxy struct { type proxy struct {

@ -14,6 +14,18 @@
# port: 2223 # port: 2223
# tls
#
# connect to server via TLS
#
# tls: true
# tls-insecure
#
# do not verify server's TLS certificate
#
# tls-insecure: true
# proxy # proxy
# the proxy server to connect to # the proxy server to connect to
# supported scheme: http, https, socks5 # supported scheme: http, https, socks5
@ -40,27 +52,6 @@
# sni: example.com # sni: example.com
# insecure: false # insecure: false
# obfs_method
#
# Specifies the encryption method.
# when this option is specified, the entire connection
# will be encrypted.
# when set to none, the encryption is disabled.
# Avaliable methods: rc4, aes, none(default)
# obfs_method: rc4
# obfs_key
#
# Specifies the key to encrypt the connection,
# if the server enable the obfs, only known the
# right key can connect to the server.
#obfs_key: some_keyword
# username # username
# #
# specifies the user to log in as on the remote machine. # specifies the user to log in as on the remote machine.
@ -163,3 +154,16 @@
# dynamic_forward: # dynamic_forward:
# - :3224 # - :3224
# - 127.0.0.1:9883 # - 127.0.0.1:9883
# dynamic_http
#
# Listen a port to accept http request incoming
# and dynamic forward the request
# to remote server through secure channel.
# This option can be specified multiple times.
# format [bind_adress:]port
# dynamic_http:
# - :8080
# - :127.0.0.1:8180

@ -4,8 +4,6 @@ import (
"bufio" "bufio"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/fangdingjun/obfssh"
socks "github.com/fangdingjun/socks-go"
"io" "io"
"net" "net"
"net/textproto" "net/textproto"
@ -14,6 +12,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/fangdingjun/go-log"
socks "github.com/fangdingjun/socks-go"
) )
type httpProxyConn struct { type httpProxyConn struct {
@ -57,7 +58,7 @@ var _ net.Conn = &httpProxyConn{}
func updateProxyFromEnv(cfg *config) { func updateProxyFromEnv(cfg *config) {
if cfg.Proxy.Scheme != "" && cfg.Proxy.Host != "" && cfg.Proxy.Port != 0 { if cfg.Proxy.Scheme != "" && cfg.Proxy.Host != "" && cfg.Proxy.Port != 0 {
obfssh.Log(obfssh.DEBUG, "proxy already specified by config, not parse environment proxy") log.Debugf("proxy already specified by config, not parse environment proxy")
return return
} }
@ -80,7 +81,7 @@ func updateProxyFromEnv(cfg *config) {
u, err := url.Parse(proxyStr) u, err := url.Parse(proxyStr)
if err != nil { if err != nil {
obfssh.Log(obfssh.DEBUG, "parse proxy from environment failed: %s", err) log.Debugf("parse proxy from environment failed: %s", err)
return return
} }

@ -1,54 +1,85 @@
package main package main
import ( import (
"crypto/tls"
"flag" "flag"
"fmt" "fmt"
"github.com/bgentry/speakeasy"
"github.com/fangdingjun/obfssh"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"io/ioutil" "io/ioutil"
"log"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/bgentry/speakeasy"
"github.com/fangdingjun/go-log"
"github.com/fangdingjun/obfssh"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
) )
var dialer = &net.Dialer{Timeout: 10 * time.Second} var dialer = &net.Dialer{Timeout: 15 * time.Second}
func main() { func main() {
var configfile string var configfile string
var cfg config var cfg config
var logfile string
var logFileCount int
var logFileSize int64
var loglevel string
flag.StringVar(&configfile, "f", "", "configure file") flag.StringVar(&configfile, "f", "", "configure file")
flag.StringVar(&cfg.Username, "l", os.Getenv("USER"), "ssh username") flag.StringVar(&cfg.Username, "l", os.Getenv("USER"), "ssh username")
flag.StringVar(&cfg.Password, "pw", "", "ssh password") flag.StringVar(&cfg.Password, "pw", "", "ssh password")
flag.IntVar(&cfg.Port, "p", 22, "remote port") flag.IntVar(&cfg.Port, "p", 22, "remote port")
flag.StringVar(&cfg.PrivateKey, "i", "", "private key file") flag.StringVar(&cfg.PrivateKey, "i", "", "private key file")
flag.BoolVar(&cfg.TLS, "tls", false, "use tls or not")
flag.BoolVar(&cfg.TLSInsecure, "tls-insecure", false, "insecure tls connnection")
flag.Var(&cfg.LocalForwards, "L", "forward local port to remote, format [local_host:]local_port:remote_host:remote_port") flag.Var(&cfg.LocalForwards, "L", "forward local port to remote, format [local_host:]local_port:remote_host:remote_port")
flag.Var(&cfg.RemoteForwards, "R", "forward remote port to local, format [remote_host:]remote_port:local_host:local_port") flag.Var(&cfg.RemoteForwards, "R", "forward remote port to local, format [remote_host:]remote_port:local_host:local_port")
flag.BoolVar(&cfg.NotRunCmd, "N", false, "not run remote command, useful when do port forward") flag.BoolVar(&cfg.NotRunCmd, "N", false, "not run remote command, useful when do port forward")
flag.Var(&cfg.DynamicForwards, "D", "enable dynamic forward, format [local_host:]local_port") flag.Var(&cfg.DynamicForwards, "D", "enable dynamic forward, format [local_host:]local_port")
flag.StringVar(&cfg.ObfsMethod, "obfs_method", "", "transport encrypt method, avaliable: rc4, aes, empty means disable encrypt")
flag.StringVar(&cfg.ObfsKey, "obfs_key", "", "transport encrypt key")
flag.BoolVar(&cfg.Debug, "d", false, "verbose mode") flag.BoolVar(&cfg.Debug, "d", false, "verbose mode")
flag.IntVar(&cfg.KeepaliveInterval, "keepalive_interval", 10, "keep alive interval") flag.IntVar(&cfg.KeepaliveInterval, "keepalive_interval", 10, "keep alive interval")
flag.IntVar(&cfg.KeepaliveMax, "keepalive_max", 5, "keep alive max") flag.IntVar(&cfg.KeepaliveMax, "keepalive_max", 5, "keep alive max")
flag.BoolVar(&cfg.DisableObfsAfterHandshake, "disable_obfs_after_handshake", false, "disable obfs after handshake") flag.Var(&cfg.DynamicHTTP, "http", "listen for http port")
flag.StringVar(&logfile, "log_file", "", "log file, default stdout")
flag.IntVar(&logFileCount, "log_count", 10, "max count of log to keep")
flag.Int64Var(&logFileSize, "log_size", 10, "max log file size MB")
flag.StringVar(&loglevel, "log_level", "INFO", "log level, values:\nOFF, FATAL, PANIC, ERROR, WARN, INFO, DEBUG")
flag.Usage = usage flag.Usage = usage
flag.Parse() flag.Parse()
if logfile != "" {
log.Default.Out = &log.FixedSizeFileWriter{
MaxCount: logFileCount,
Name: logfile,
MaxSize: logFileSize * 1024 * 1024,
}
}
if cfg.Debug {
loglevel = "DEBUG"
}
if loglevel != "" {
lv, err := log.ParseLevel(loglevel)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
log.Default.Level = lv
}
if configfile != "" { if configfile != "" {
if err := loadConfig(&cfg, configfile); err != nil { if err := loadConfig(&cfg, configfile); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if cfg.Debug { log.Debugf("obfssh client start")
obfssh.SSHLogLevel = obfssh.DEBUG
}
auth := []ssh.AuthMethod{} auth := []ssh.AuthMethod{}
@ -71,7 +102,7 @@ func main() {
k1 := filepath.Join(home, f) k1 := filepath.Join(home, f)
if pemBytes, err := ioutil.ReadFile(k1); err == nil { if pemBytes, err := ioutil.ReadFile(k1); err == nil {
if priKey, err := ssh.ParsePrivateKey(pemBytes); err == nil { if priKey, err := ssh.ParsePrivateKey(pemBytes); err == nil {
obfssh.Log(obfssh.DEBUG, "add private key: %s", k1) log.Debugf("add private key: %s", k1)
//auth = append(auth, ssh.PublicKeys(priKey)) //auth = append(auth, ssh.PublicKeys(priKey))
pkeys = append(pkeys, priKey) pkeys = append(pkeys, priKey)
} }
@ -82,21 +113,21 @@ func main() {
agentConn, err = net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) agentConn, err = net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
if err == nil { if err == nil {
defer agentConn.Close() defer agentConn.Close()
obfssh.Log(obfssh.DEBUG, "add auth method with agent %s", os.Getenv("SSH_AUTH_SOCK")) log.Debugf("add auth method with agent %s", os.Getenv("SSH_AUTH_SOCK"))
agentClient := agent.NewClient(agentConn) agentClient := agent.NewClient(agentConn)
//auth = append(auth, ssh.PublicKeysCallback(agentClient.Signers)) //auth = append(auth, ssh.PublicKeysCallback(agentClient.Signers))
signers, err := agentClient.Signers() signers, err := agentClient.Signers()
if err == nil { if err == nil {
pkeys = append(pkeys, signers...) pkeys = append(pkeys, signers...)
} else { } else {
obfssh.Log(obfssh.DEBUG, "get key from agent failed: %s", err) log.Debugf("get key from agent failed: %s", err)
} }
} else { } else {
obfssh.Log(obfssh.DEBUG, "connect to agent failed") log.Debugf("connect to agent failed")
} }
if len(pkeys) != 0 { if len(pkeys) != 0 {
obfssh.Log(obfssh.DEBUG, "private key length %d", len(pkeys)) log.Debugf("private key length %d", len(pkeys))
auth = append(auth, ssh.PublicKeys(pkeys...)) auth = append(auth, ssh.PublicKeys(pkeys...))
} }
@ -137,15 +168,15 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
obfssh.Log(obfssh.DEBUG, "add private key %s", cfg.PrivateKey) log.Debugf("add private key %s", cfg.PrivateKey)
auth = append(auth, ssh.PublicKeys(priKey)) auth = append(auth, ssh.PublicKeys(priKey))
} }
if cfg.Password != "" { if cfg.Password != "" {
obfssh.Log(obfssh.DEBUG, "add password auth method") log.Debugf("add password auth method")
auth = append(auth, ssh.Password(cfg.Password)) auth = append(auth, ssh.Password(cfg.Password))
} else { } else {
obfssh.Log(obfssh.DEBUG, "add keyboard interactive auth") log.Debugf("add keyboard interactive auth")
//auth = append(auth, //auth = append(auth,
// ssh.RetryableAuthMethod(ssh.KeyboardInteractive(keyboardAuth), 3)) // ssh.RetryableAuthMethod(ssh.KeyboardInteractive(keyboardAuth), 3))
auth = append(auth, auth = append(auth,
@ -158,7 +189,7 @@ func main() {
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
HostKeyCallback: func(hostname string, remote net.Addr, HostKeyCallback: func(hostname string, remote net.Addr,
key ssh.PublicKey) error { key ssh.PublicKey) error {
obfssh.Log(obfssh.INFO, "%s %s %+v", hostname, remote, key) log.Debugf("%s %s %+v", hostname, remote, key)
return nil return nil
}, },
} }
@ -172,21 +203,22 @@ func main() {
if cfg.Proxy.Scheme != "" && cfg.Proxy.Host != "" && cfg.Proxy.Port != 0 { if cfg.Proxy.Scheme != "" && cfg.Proxy.Host != "" && cfg.Proxy.Port != 0 {
switch cfg.Proxy.Scheme { switch cfg.Proxy.Scheme {
case "http": case "http":
obfssh.Log(obfssh.DEBUG, "use http proxy %s:%d to connect to server", log.Debugf("use http proxy %s:%d to connect to server",
cfg.Proxy.Host, cfg.Proxy.Port) cfg.Proxy.Host, cfg.Proxy.Port)
c, err = dialHTTPProxy(host, cfg.Port, cfg.Proxy) c, err = dialHTTPProxy(host, cfg.Port, cfg.Proxy)
case "https": case "https":
obfssh.Log(obfssh.DEBUG, "use https proxy %s:%d to connect to server", log.Debugf("use https proxy %s:%d to connect to server",
cfg.Proxy.Host, cfg.Proxy.Port) cfg.Proxy.Host, cfg.Proxy.Port)
c, err = dialHTTPSProxy(host, cfg.Port, cfg.Proxy) c, err = dialHTTPSProxy(host, cfg.Port, cfg.Proxy)
case "socks5": case "socks5":
obfssh.Log(obfssh.DEBUG, "use socks proxy %s:%d to connect to server", log.Debugf("use socks proxy %s:%d to connect to server",
cfg.Proxy.Host, cfg.Proxy.Port) cfg.Proxy.Host, cfg.Proxy.Port)
c, err = dialSocks5Proxy(host, cfg.Port, cfg.Proxy) c, err = dialSocks5Proxy(host, cfg.Port, cfg.Proxy)
default: default:
err = fmt.Errorf("unsupported scheme: %s", cfg.Proxy.Scheme) err = fmt.Errorf("unsupported scheme: %s", cfg.Proxy.Scheme)
} }
} else { } else {
log.Debugf("dail to %s", rhost)
c, err = dialer.Dial("tcp", rhost) c, err = dialer.Dial("tcp", rhost)
} }
@ -194,20 +226,40 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("dail success")
timeout := time.Duration(cfg.KeepaliveInterval*2) * time.Second
var _conn = c
conn := &obfssh.TimedOutConn{Conn: c, Timeout: timeout}
if cfg.TLS {
log.Debugf("begin tls handshake")
_conn = tls.Client(conn, &tls.Config{
ServerName: host,
InsecureSkipVerify: cfg.TLSInsecure,
})
if err := _conn.(*tls.Conn).Handshake(); err != nil {
log.Fatal(err)
}
log.Debugf("tls handshake done")
}
conf := &obfssh.Conf{ conf := &obfssh.Conf{
ObfsMethod: cfg.ObfsMethod, Timeout: timeout,
ObfsKey: cfg.ObfsKey, KeepAliveInterval: time.Duration(cfg.KeepaliveInterval) * time.Second,
Timeout: time.Duration(cfg.KeepaliveInterval*2) * time.Second, KeepAliveMax: cfg.KeepaliveMax,
KeepAliveInterval: time.Duration(cfg.KeepaliveInterval) * time.Second,
KeepAliveMax: cfg.KeepaliveMax,
DisableObfsAfterHandshake: cfg.DisableObfsAfterHandshake,
} }
client, err := obfssh.NewClient(c, config, rhost, conf) log.Debugf("ssh negotation")
client, err := obfssh.NewClient(_conn, config, rhost, conf)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("ssh negotation success")
var local, remote string var local, remote string
// process port forward // process port forward
@ -215,7 +267,7 @@ func main() {
for _, p := range cfg.LocalForwards { for _, p := range cfg.LocalForwards {
addr := parseForwardAddr(p) addr := parseForwardAddr(p)
if len(addr) != 4 && len(addr) != 3 { if len(addr) != 4 && len(addr) != 3 {
log.Printf("wrong forward addr %s, format: [local_host:]local_port:remote_host:remote_port", p) log.Errorf("wrong forward addr %s, format: [local_host:]local_port:remote_host:remote_port", p)
continue continue
} }
if len(addr) == 4 { if len(addr) == 4 {
@ -227,14 +279,14 @@ func main() {
} }
//log.Printf("add local to remote %s->%s", local, remote) //log.Printf("add local to remote %s->%s", local, remote)
if err := client.AddLocalForward(local, remote); err != nil { if err := client.AddLocalForward(local, remote); err != nil {
log.Println(err) log.Errorln(err)
} }
} }
for _, p := range cfg.RemoteForwards { for _, p := range cfg.RemoteForwards {
addr := parseForwardAddr(p) addr := parseForwardAddr(p)
if len(addr) != 4 && len(addr) != 3 { if len(addr) != 4 && len(addr) != 3 {
log.Printf("wrong forward addr %s, format: [local_host:]local_port:remote_host:remote_port", p) log.Errorf("wrong forward addr %s, format: [local_host:]local_port:remote_host:remote_port", p)
continue continue
} }
if len(addr) == 4 { if len(addr) == 4 {
@ -246,7 +298,7 @@ func main() {
} }
//log.Printf("add remote to local %s->%s", remote, local) //log.Printf("add remote to local %s->%s", remote, local)
if err := client.AddRemoteForward(local, remote); err != nil { if err := client.AddRemoteForward(local, remote); err != nil {
log.Println(err) log.Errorln(err)
} }
} }
for _, p := range cfg.DynamicForwards { for _, p := range cfg.DynamicForwards {
@ -258,8 +310,21 @@ func main() {
} }
//log.Printf("listen on %s", local) //log.Printf("listen on %s", local)
if err := client.AddDynamicForward(local); err != nil { if err := client.AddDynamicForward(local); err != nil {
log.Println(err) log.Errorln(err)
}
}
for _, p := range cfg.DynamicHTTP {
if strings.Index(p, ":") == -1 {
local = fmt.Sprintf(":%s", p)
} else {
local = p
} }
//log.Printf("listen on %s", local)
if err := client.AddDynamicHTTPForward(local); err != nil {
log.Errorln(err)
}
} }
hasErr := false hasErr := false
@ -267,7 +332,7 @@ func main() {
if !cfg.NotRunCmd { if !cfg.NotRunCmd {
if cmd != "" { if cmd != "" {
if d, err := client.RunCmd(cmd); err != nil { if d, err := client.RunCmd(cmd); err != nil {
log.Println(err) log.Errorln(err)
hasErr = true hasErr = true
} else { } else {
//log.Printf("%s", string(d)) //log.Printf("%s", string(d))
@ -276,16 +341,17 @@ func main() {
} else { } else {
if err := client.Shell(); err != nil { if err := client.Shell(); err != nil {
hasErr = true hasErr = true
log.Println(err) log.Errorln(err)
} }
} }
} }
if err := client.Run(); err != nil { if err := client.Run(); err != nil {
log.Println(err) log.Errorln(err)
hasErr = true hasErr = true
} }
log.Debugf("obfssh client exit")
if hasErr { if hasErr {
os.Exit(1) os.Exit(1)
} }
@ -326,10 +392,13 @@ func passwordAuth() (string, error) {
func usage() { func usage() {
usageStr := `Usage: usageStr := `Usage:
obfss_client -N -d -D [bind_address:]port -f configfile obfssh -N -d -D [bind_address:]port -f configfile
-tls -tls-insecure -log_file /path/to/file
-log_count 10 -log_size 10
-log_level INFO
-i identity_file -L [bind_address:]port:host:hostport -i identity_file -L [bind_address:]port:host:hostport
-l login_name -pw password -p port -obfs_method method -l login_name -pw password -p port
-obfs_key key -disable_obfs_after_handshake -http [bind_addr:]port
-R [bind_address:]port:host:hostport [user@]hostname [command] -R [bind_address:]port:host:hostport [user@]hostname [command]
Options: Options:
@ -385,22 +454,30 @@ Options:
when the count reach the max, the connection will when the count reach the max, the connection will
be abort. be abort.
Options for obfuscation: -http [bind_addr:]port
-obfs_method method listen a port and act as http proxy server,
Specifies the encryption method. but connect to target server through
when this option is specified, the entire connection encrypt ssh connection
will be encrypted. similar like -D, but input is http, not socks5
when set to none, the encryption is disabled.
Avaliable methods: rc4, aes, none(default) -tls
connect to server via TLS
-obfs_key key
Specifies the key to encrypt the connection, -tls-insecure
if the server enable the obfs, only known the do not verify server's tls ceritificate
right key can connect to the server.
-log_file
-disable_obfs_after_handshake log file, default stdout
when this option is specified, only encrypt the
ssh handshake message. -log_count
max count of log file to keep, default 10
-log_size
max log size MB, default 10
-log_level
log level, values:
OFF, FATAL, PANIC, ERROR, WARN, INFO, DEBUG
` `
fmt.Printf("%s", usageStr) fmt.Printf("%s", usageStr)
os.Exit(1) os.Exit(1)

@ -1,2 +1,2 @@
obfssh_server* obfsshd*
authorized_keys authorized_keys

@ -1,7 +1,7 @@
obfssh\_server obfsshd
============= =============
this is obfssh\_server example this is obfsshd example
usage usage
@ -9,21 +9,21 @@ usage
run server run server
go get github.com/fangdingjun/obfssh/obfssh_server go get github.com/fangdingjun/obfssh/obfsshd
cp $GOPATH/src/github.com/fangdingjun/obfssh/obfssh_server/config_example.yaml config.yaml cp $GOPATH/src/github.com/fangdingjun/obfssh/obfsshd/config_example.yaml config.yaml
vim config.yaml vim config.yaml
ssh-keygen -f ssh_host_rsa_key -t rsa ssh-keygen -f ssh_host_rsa_key -t rsa
$GOPATH/bin/obfssh_server -c config.yaml $GOPATH/bin/obfsshd -c config.yaml
run client run client
go get github.com/fangdingjun/obfssh/obfssh_client go get github.com/fangdingjun/obfssh/obfssh
$GOPATH/bin/obfssh_client -N -D :1234 -obfs_key some_keyworld -obfs_method rc4 -p 2022 -l user2 -pw user2 localhost $GOPATH/bin/obfssh -N -D :1234 -p 2022 -l user2 -pw user2 localhost
this will create a socks proxy on :1234 this will create a socks proxy on :1234

@ -2,20 +2,24 @@ package main
import ( import (
"bytes" "bytes"
"io/ioutil"
"github.com/fangdingjun/go-log"
"github.com/go-yaml/yaml" "github.com/go-yaml/yaml"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"io/ioutil"
"log"
) )
type listen struct {
Port int
Key string
Cert string
}
type serverConfig struct { type serverConfig struct {
Port int `yaml:"port"` Listen []listen `yaml:"listen"`
Key string `yaml:"obfs_key"` Debug bool `yaml:"debug"`
Debug bool `yaml:"debug"` HostKey string `yaml:"host_key_file"`
HostKey string `yaml:"host_key_file"` Users []serverUser `yaml:"users"`
Method string `yaml:"obfs_method"`
DisableObfsAfterHandshake bool `yaml:"disable_obfs_after_handshake"`
Users []serverUser `yaml:"users"`
} }
type serverUser struct { type serverUser struct {
@ -48,7 +52,7 @@ func loadConfig(f string) (*serverConfig, error) {
for i := range c.Users { for i := range c.Users {
buf1, err := ioutil.ReadFile(c.Users[i].AuthorizedKeyFile) buf1, err := ioutil.ReadFile(c.Users[i].AuthorizedKeyFile)
if err != nil { if err != nil {
log.Printf("read publickey for %s failed, ignore", c.Users[i].Username) log.Warnf("read publickey for %s failed, ignore", c.Users[i].Username)
continue continue
} }

@ -4,31 +4,20 @@
# port # port
# the ssh port listen on # the ssh port listen on
port: 2022 listen:
-
# obfs_key port: 2022
# key:
# Specifies the key to encrypt the connection, cert:
# if obfs enabled, only client known this key -
# can connect # this listen for TLS
obfs_key: some_keyword port: 2023
key: server.key
cert: server.crt
# ssh host key file # ssh host key file
host_key_file: ./ssh_host_rsa_key host_key_file: ./ssh_host_rsa_key
# obfs_method
#
# Specifies the encryption method.
# when this option is specified, the entire connection
# will be encrypted.
# when set to none, the encryption is disabled.
# Avaliable methods: rc4, aes, none(default)
#
obfs_method: "rc4"
# when set to true, only the ssh handshake packet is encrypted
disable_obfs_after_handshake: true
# show more log message # show more log message
# value true or false # value true or false
debug: true debug: true

@ -2,38 +2,62 @@ package main
import ( import (
"bytes" "bytes"
"crypto/tls"
"errors"
"flag" "flag"
"fmt" "fmt"
"github.com/fangdingjun/obfssh"
"golang.org/x/crypto/ssh"
"io/ioutil" "io/ioutil"
"log"
"net" "net"
"os"
"os/signal"
"syscall"
"github.com/fangdingjun/go-log"
"github.com/fangdingjun/obfssh"
"github.com/fangdingjun/protolistener"
"golang.org/x/crypto/ssh"
) )
func main() { func main() {
var configfile string var configfile string
var logfile string
var logFileCount int
var logFileSize int64
var loglevel string
flag.StringVar(&configfile, "c", "config.yaml", "configure file") flag.StringVar(&configfile, "c", "config.yaml", "configure file")
flag.StringVar(&logfile, "log_file", "", "log file, default stdout")
flag.IntVar(&logFileCount, "log_count", 10, "max count of log to keep")
flag.Int64Var(&logFileSize, "log_size", 10, "max log file size MB")
flag.StringVar(&loglevel, "log_level", "INFO", "log level, values:\nOFF, FATAL, PANIC, ERROR, WARN, INFO, DEBUG")
flag.Parse() flag.Parse()
conf, err := loadConfig(configfile) if logfile != "" {
if err != nil { log.Default.Out = &log.FixedSizeFileWriter{
log.Fatal(err) MaxCount: logFileCount,
Name: logfile,
MaxSize: logFileSize * 1024 * 1024,
}
} }
// set log level if loglevel != "" {
if conf.Debug { lv, err := log.ParseLevel(loglevel)
obfssh.SSHLogLevel = obfssh.DEBUG if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
log.Default.Level = lv
} }
sconf := &obfssh.Conf{ conf, err := loadConfig(configfile)
ObfsMethod: conf.Method, if err != nil {
ObfsKey: conf.Key, log.Fatal(err)
DisableObfsAfterHandshake: conf.DisableObfsAfterHandshake,
} }
sconf := &obfssh.Conf{}
config := &ssh.ServerConfig{ config := &ssh.ServerConfig{
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
if u, err := conf.getUser(c.User()); err == nil { if u, err := conf.getUser(c.User()); err == nil {
@ -45,23 +69,38 @@ func main() {
}, },
PublicKeyCallback: func(c ssh.ConnMetadata, k ssh.PublicKey) (*ssh.Permissions, error) { PublicKeyCallback: func(c ssh.ConnMetadata, k ssh.PublicKey) (*ssh.Permissions, error) {
if u, err := conf.getUser(c.User()); err == nil { checker := &ssh.CertChecker{
for _, pk := range u.publicKeys { IsUserAuthority: func(k ssh.PublicKey) bool {
if k.Type() == pk.Type() && bytes.Compare(k.Marshal(), pk.Marshal()) == 0 { if u, err := conf.getUser(c.User()); err == nil {
return nil, nil for _, pk := range u.publicKeys {
if k.Type() == pk.Type() &&
bytes.Compare(k.Marshal(), pk.Marshal()) == 0 {
return true
}
}
} }
return false
},
}
checker.UserKeyFallback = func(c1 ssh.ConnMetadata, k1 ssh.PublicKey) (*ssh.Permissions, error) {
log.Debug("user key fallback")
if checker.IsUserAuthority(k1) {
return nil, nil
} }
return nil, errors.New("public not acceptable")
} }
return nil, fmt.Errorf("publickey reject for user %s", c.User()) return checker.Authenticate(c, k)
}, },
// auth log // auth log
AuthLogCallback: func(c ssh.ConnMetadata, method string, err error) { AuthLogCallback: func(c ssh.ConnMetadata, method string, err error) {
if err != nil { if err != nil {
obfssh.Log(obfssh.ERROR, "%s", err.Error()) log.Debugf("%s", err.Error())
obfssh.Log(obfssh.ERROR, "%s auth failed for %s from %s", method, c.User(), c.RemoteAddr()) if method != "none" {
log.Errorf("%s auth failed for %s from %s", method, c.User(), c.RemoteAddr())
}
} else { } else {
obfssh.Log(obfssh.INFO, "Accepted %s for user %s from %s", method, c.User(), c.RemoteAddr()) log.Printf("Accepted %s for user %s from %s", method, c.User(), c.RemoteAddr())
} }
}, },
} }
@ -77,29 +116,57 @@ func main() {
} }
config.AddHostKey(private) config.AddHostKey(private)
l, err := net.Listen("tcp", fmt.Sprintf(":%d", conf.Port)) for _, lst := range conf.Listen {
if err != nil { go func(lst listen) {
log.Fatal(err) var l net.Listener
} var err error
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) { l, err = net.Listen("tcp", fmt.Sprintf(":%d", lst.Port))
sc, err := obfssh.NewServer(c, config, sconf)
if err != nil { if err != nil {
c.Close() log.Fatal(err)
obfssh.Log(obfssh.ERROR, "%s", err.Error())
return
} }
sc.Run() defer l.Close()
}(c)
l = protolistener.New(l)
if lst.Key != "" && lst.Cert != "" {
cert, err := tls.LoadX509KeyPair(lst.Cert, lst.Key)
if err != nil {
log.Fatal(err)
}
l = tls.NewListener(l, &tls.Config{
Certificates: []tls.Certificate{cert},
})
}
for {
c, err := l.Accept()
if err != nil {
fmt.Println(err)
return
}
log.Infof("accept tcp connection from %s", c.RemoteAddr())
go func(c net.Conn) {
defer c.Close()
sc, err := obfssh.NewServer(c, config, sconf)
if err != nil {
c.Close()
log.Errorf("%s", err.Error())
return
}
sc.Run()
}(c)
}
}(lst)
}
ch := make(chan os.Signal, 2)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
select {
case s := <-ch:
log.Printf("received signal %s, exit.", s)
} }
} }

@ -0,0 +1,14 @@
// +build linux darwin
package obfssh
import (
"io"
"os/exec"
"github.com/kr/pty"
)
func startPty(cmd *exec.Cmd) (io.ReadWriteCloser, error) {
return pty.Start(cmd)
}

@ -0,0 +1,11 @@
package obfssh
import (
"errors"
"io"
"os/exec"
)
func startPty(cmd *exec.Cmd) (io.ReadWriteCloser, error) {
return nil, errors.New("not implement")
}

@ -2,9 +2,16 @@ package obfssh
import ( import (
"fmt" "fmt"
"io"
"net"
"os/exec"
"runtime"
"syscall"
"time"
"github.com/fangdingjun/go-log"
"github.com/pkg/sftp" "github.com/pkg/sftp"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"net"
) )
// Server is server connection // Server is server connection
@ -21,30 +28,17 @@ type Server struct {
// //
// config is &ssh.ServerConfig // config is &ssh.ServerConfig
// //
// method is obfs encrypt method, value is rc4, aes or none or ""
//
// key is obfs encrypt key
//
// conf is the server configure // conf is the server configure
// //
// 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, conf *Conf) (*Server, error) { func NewServer(c net.Conn, config *ssh.ServerConfig, conf *Conf) (*Server, error) {
wc, err := NewObfsConn(c, conf.ObfsMethod, conf.ObfsKey, true) sshConn, ch, req, err := ssh.NewServerConn(&TimedOutConn{c, 15 * 60 * time.Second}, config)
if err != nil {
return nil, err
}
sshConn, ch, req, err := ssh.NewServerConn(wc, config)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if conf.DisableObfsAfterHandshake { sc := &Server{
wc.DisableObfs() conn: c,
}
sc := &Server{conn: c,
sshConn: sshConn, sshConn: sshConn,
forwardedPorts: map[string]net.Listener{}, forwardedPorts: map[string]net.Listener{},
exitCh: make(chan struct{})} exitCh: make(chan struct{})}
@ -56,16 +50,16 @@ func NewServer(c net.Conn, config *ssh.ServerConfig, conf *Conf) (*Server, error
// Run waits for server connection finish // Run waits for server connection finish
func (sc *Server) Run() { func (sc *Server) Run() {
sc.sshConn.Wait() sc.sshConn.Wait()
Log(DEBUG, "ssh connection closed") log.Debugf("ssh connection closed")
sc.close() sc.close()
} }
func (sc *Server) close() { func (sc *Server) close() {
Log(DEBUG, "close connection") log.Debugf("close connection")
sc.sshConn.Close() sc.sshConn.Close()
//Log(DEBUG, "close listener") //log.Debugf( "close listener")
for _, l := range sc.forwardedPorts { for _, l := range sc.forwardedPorts {
Log(DEBUG, "close listener %s", l.Addr()) log.Debugf("close listener %s", l.Addr())
l.Close() l.Close()
} }
} }
@ -73,7 +67,7 @@ func (sc *Server) close() {
func (sc *Server) handleNewChannelRequest(ch <-chan ssh.NewChannel) { func (sc *Server) handleNewChannelRequest(ch <-chan ssh.NewChannel) {
for newch := range ch { for newch := range ch {
Log(DEBUG, "request channel %s", newch.ChannelType()) log.Debugf("request channel %s", newch.ChannelType())
switch newch.ChannelType() { switch newch.ChannelType() {
case "session": case "session":
@ -85,7 +79,7 @@ func (sc *Server) handleNewChannelRequest(ch <-chan ssh.NewChannel) {
continue continue
} }
Log(DEBUG, "reject channel request %s", newch.ChannelType()) log.Errorf("reject channel request %s", newch.ChannelType())
newch.Reject(ssh.UnknownChannelType, "unknown channel type") newch.Reject(ssh.UnknownChannelType, "unknown channel type")
} }
@ -94,15 +88,15 @@ func (sc *Server) handleNewChannelRequest(ch <-chan ssh.NewChannel) {
func (sc *Server) handleGlobalRequest(req <-chan *ssh.Request) { func (sc *Server) handleGlobalRequest(req <-chan *ssh.Request) {
for r := range req { for r := range req {
Log(DEBUG, "global request %s", r.Type) log.Debugf("global request %s", r.Type)
switch r.Type { switch r.Type {
case "tcpip-forward": case "tcpip-forward":
Log(DEBUG, "request port forward") log.Debugf("request port forward")
go sc.handleTcpipForward(r) go sc.handleTcpipForward(r)
continue continue
case "cancel-tcpip-forward": case "cancel-tcpip-forward":
Log(DEBUG, "request cancel port forward") log.Debugf("request cancel port forward")
go sc.handleCancelTcpipForward(r) go sc.handleCancelTcpipForward(r)
continue continue
} }
@ -119,14 +113,17 @@ func serveSFTP(ch ssh.Channel) {
server, err := sftp.NewServer(ch) server, err := sftp.NewServer(ch)
if err != nil { if err != nil {
Log(DEBUG, "start sftp server failed: %s", err) log.Debugf("start sftp server failed: %s", err)
ch.SendRequest("exit-status", false, ssh.Marshal(exitStatus{Status: 127}))
return return
} }
if err := server.Serve(); err != nil { if err := server.Serve(); err != nil {
Log(DEBUG, "sftp server finished with error: %s", err) log.Debugf("sftp server finished with error: %s", err)
ch.SendRequest("exit-status", false, ssh.Marshal(exitStatus{Status: 127}))
return return
} }
ch.SendRequest("exit-status", false, ssh.Marshal(exitStatus{Status: 0}))
} }
type directTcpipMsg struct { type directTcpipMsg struct {
@ -140,39 +137,90 @@ type args struct {
Arg string Arg string
} }
type envArgs struct {
Name string
Value string
}
type exitStatus struct {
Status uint32
}
func (sc *Server) handleSession(newch ssh.NewChannel) { func (sc *Server) handleSession(newch ssh.NewChannel) {
ch, req, err := newch.Accept() ch, req, err := newch.Accept()
if err != nil { if err != nil {
Log(ERROR, "%s", err.Error()) log.Errorf("%s", err.Error())
return return
} }
var cmd args var _cmd args
ret := false ret := false
var cmd *exec.Cmd
var env []string
for r := range req { for r := range req {
switch r.Type { switch r.Type {
case "subsystem": case "subsystem":
if err := ssh.Unmarshal(r.Payload, &cmd); err != nil { if err := ssh.Unmarshal(r.Payload, &_cmd); err == nil {
if _cmd.Arg == "sftp" { // only support sftp
ret = true
log.Debugf("handle sftp request")
go serveSFTP(ch)
} else {
log.Debugln("subsystem", _cmd.Arg, "not support")
}
} else {
ret = false ret = false
log.Debugln("get subsystem arg error", err)
}
case "shell":
ret = true
if runtime.GOOS == "windows" {
cmd = exec.Command("powershell")
} else { } else {
if cmd.Arg != "sftp" { // only support sftp cmd = exec.Command("bash", "-l")
ret = false }
cmd.Env = env
go handleShell(cmd, ch)
case "signal":
log.Debugln("got signal")
ret = true
case "exec":
ret = true
if err = ssh.Unmarshal(r.Payload, &_cmd); err == nil {
log.Infoln("execute command", _cmd.Arg)
if runtime.GOOS == "windows" {
cmd = exec.Command("powershell", "-Command", _cmd.Arg)
} else { } else {
cmd = exec.Command("bash", "-c", _cmd.Arg)
ret = true
Log(DEBUG, "handle sftp request")
go serveSFTP(ch)
} }
cmd.Env = env
//cmd.Stdin = ch
go handleCommand(cmd, ch)
} else {
log.Debugln(err)
ret = false
}
case "pty-req":
ret = true
case "env":
var arg envArgs
ret = true
if err = ssh.Unmarshal(r.Payload, &arg); err == nil {
log.Debugf("got env %s=%s", arg.Name, arg.Value)
env = append(env, fmt.Sprintf("%s=%s", arg.Name, arg.Value))
} else {
log.Debugln("parse env failed", err)
ret = false
} }
case "window-change":
ret = true
default: default:
ret = false ret = false
} }
Log(DEBUG, "session request %s, reply %v", r.Type, ret) log.Debugf("session request %s, reply %v", r.Type, ret)
if r.WantReply { if r.WantReply {
r.Reply(ret, nil) r.Reply(ret, nil)
@ -180,6 +228,92 @@ func (sc *Server) handleSession(newch ssh.NewChannel) {
} }
} }
func handleShell(cmd *exec.Cmd, ch ssh.Channel) {
defer ch.Close()
var _pty io.ReadWriteCloser
var err error
log.Infoln("start shell")
//_pty, err = pty.Start(cmd)
if runtime.GOOS == "unix" || runtime.GOOS == "linux" {
_pty, err = startPty(cmd)
if err != nil {
log.Debugln("start pty", err)
ch.SendRequest("exit-status", false,
ssh.Marshal(exitStatus{Status: 127}))
return
}
}
if runtime.GOOS == "unix" || runtime.GOOS == "linux" {
defer _pty.Close()
go io.Copy(ch, _pty)
go io.Copy(_pty, ch)
} else { // windows
cmd.Stderr = ch
cmd.Stdout = ch
in, err := cmd.StdinPipe()
if err != nil {
ch.SendRequest("exit-status", false,
ssh.Marshal(exitStatus{Status: 127}))
return
}
go func() {
defer in.Close()
io.Copy(in, ch)
}()
if err := cmd.Start(); err != nil {
log.Debugln("start command ", err)
ch.SendRequest("exit-status", false,
ssh.Marshal(exitStatus{Status: 126}))
return
}
}
code := 0
if err = cmd.Wait(); err != nil {
log.Debugln(err)
if exiterr, ok := err.(*exec.ExitError); ok {
if s, ok := exiterr.Sys().(syscall.WaitStatus); ok {
code = s.ExitStatus()
}
}
}
ch.SendRequest("exit-status", false,
ssh.Marshal(exitStatus{Status: uint32(code)}))
}
func handleCommand(cmd *exec.Cmd, ch ssh.Channel) {
defer ch.Close()
cmd.Stdout = ch
cmd.Stderr = ch
//log.Debugln("execute command", cmd)
in, err := cmd.StdinPipe()
if err != nil {
log.Debugln(err)
ch.SendRequest("exit-status", false,
ssh.Marshal(exitStatus{Status: 127}))
return
}
go func() {
defer in.Close()
io.Copy(in, ch)
}()
code := 0
if err := cmd.Run(); err != nil {
log.Debugln(err)
if exiterr, ok := err.(*exec.ExitError); ok {
if s, ok := exiterr.Sys().(syscall.WaitStatus); ok {
code = s.ExitStatus()
}
}
}
ch.SendRequest("exit-status", false,
ssh.Marshal(exitStatus{Status: uint32(code)}))
}
func handleDirectTcpip(newch ssh.NewChannel) { func handleDirectTcpip(newch ssh.NewChannel) {
var r directTcpipMsg var r directTcpipMsg
@ -187,16 +321,16 @@ func handleDirectTcpip(newch ssh.NewChannel) {
err := ssh.Unmarshal(data, &r) err := ssh.Unmarshal(data, &r)
if err != nil { if err != nil {
Log(DEBUG, "invalid ssh parameter") log.Errorln("invalid ssh parameter")
newch.Reject(ssh.ConnectionFailed, "invalid argument") newch.Reject(ssh.ConnectionFailed, "invalid argument")
return return
} }
Log(DEBUG, "create connection to %s:%d", r.Raddr, r.Rport) log.Debugf("create connection to %s:%d", r.Raddr, r.Rport)
rconn, err := net.Dial("tcp", net.JoinHostPort(r.Raddr, fmt.Sprintf("%d", r.Rport))) rconn, err := dialer.Dial("tcp", net.JoinHostPort(r.Raddr, fmt.Sprintf("%d", r.Rport)))
if err != nil { if err != nil {
Log(ERROR, "%s", err.Error()) log.Errorf("%s", err.Error())
newch.Reject(ssh.ConnectionFailed, "invalid argument") newch.Reject(ssh.ConnectionFailed, "invalid argument")
return return
} }
@ -204,7 +338,7 @@ func handleDirectTcpip(newch ssh.NewChannel) {
channel, requests, err := newch.Accept() channel, requests, err := newch.Accept()
if err != nil { if err != nil {
rconn.Close() rconn.Close()
Log(ERROR, "%s", err.Error()) log.Errorf("%s", err.Error())
return return
} }
@ -223,7 +357,7 @@ func (sc *Server) handleCancelTcpipForward(req *ssh.Request) {
var a tcpipForwardAddr var a tcpipForwardAddr
if err := ssh.Unmarshal(req.Payload, &a); err != nil { if err := ssh.Unmarshal(req.Payload, &a); err != nil {
Log(ERROR, "invalid ssh parameter for cancel port forward") log.Errorf("invalid ssh parameter for cancel port forward")
if req.WantReply { if req.WantReply {
req.Reply(false, nil) req.Reply(false, nil)
} }
@ -244,7 +378,7 @@ func (sc *Server) handleCancelTcpipForward(req *ssh.Request) {
func (sc *Server) handleTcpipForward(req *ssh.Request) { func (sc *Server) handleTcpipForward(req *ssh.Request) {
var addr tcpipForwardAddr var addr tcpipForwardAddr
if err := ssh.Unmarshal(req.Payload, &addr); err != nil { if err := ssh.Unmarshal(req.Payload, &addr); err != nil {
Log(ERROR, "parse ssh data error: %s", err) log.Errorf("parse ssh data error: %s", err)
if req.WantReply { if req.WantReply {
req.Reply(false, nil) req.Reply(false, nil)
} }
@ -252,7 +386,7 @@ func (sc *Server) handleTcpipForward(req *ssh.Request) {
} }
if addr.Port > 65535 || addr.Port < 0 { if addr.Port > 65535 || addr.Port < 0 {
Log(ERROR, "invalid port %d", addr.Port) log.Errorf("invalid port %d", addr.Port)
if req.WantReply { if req.WantReply {
req.Reply(false, nil) req.Reply(false, nil)
} }
@ -261,7 +395,7 @@ func (sc *Server) handleTcpipForward(req *ssh.Request) {
ip := net.ParseIP(addr.Addr) ip := net.ParseIP(addr.Addr)
if ip == nil { if ip == nil {
Log(ERROR, "invalid ip %d", addr.Port) log.Errorf("invalid ip %d", addr.Port)
if req.WantReply { if req.WantReply {
req.Reply(false, nil) req.Reply(false, nil)
} }
@ -272,19 +406,19 @@ func (sc *Server) handleTcpipForward(req *ssh.Request) {
if _, ok := sc.forwardedPorts[k]; ok { if _, ok := sc.forwardedPorts[k]; ok {
// port in use // port in use
Log(ERROR, "port in use: %s", k) log.Errorf("port in use: %s", k)
if req.WantReply { if req.WantReply {
req.Reply(false, nil) req.Reply(false, nil)
} }
return return
} }
//Log(DEBUG, "get request for addr: %s, port: %d", addr.Addr, addr.Port) //log.Debugf( "get request for addr: %s, port: %d", addr.Addr, addr.Port)
l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: ip, Port: int(addr.Port)}) l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: ip, Port: int(addr.Port)})
if err != nil { if err != nil {
// listen failed // listen failed
Log(ERROR, "%s", err.Error()) log.Errorf("%s", err.Error())
if req.WantReply { if req.WantReply {
req.Reply(false, nil) req.Reply(false, nil)
} }
@ -292,7 +426,7 @@ func (sc *Server) handleTcpipForward(req *ssh.Request) {
} }
a1 := l.Addr() a1 := l.Addr()
Log(DEBUG, "Listening port %s", a1) log.Infof("Listening port %s", a1)
p := struct { p := struct {
Port uint32 Port uint32
}{ }{
@ -308,10 +442,10 @@ func (sc *Server) handleTcpipForward(req *ssh.Request) {
for { for {
c, err := l.Accept() c, err := l.Accept()
if err != nil { if err != nil {
Log(ERROR, "%s", err.Error()) log.Debugf("%s", err.Error())
return return
} }
Log(DEBUG, "accept connection from %s", c.RemoteAddr()) log.Infof("accept connection from %s", c.RemoteAddr())
go func(c net.Conn) { go func(c net.Conn) {
laddr := c.LocalAddr() laddr := c.LocalAddr()
raddr := c.RemoteAddr() raddr := c.RemoteAddr()
@ -328,7 +462,7 @@ func (sc *Server) handleTcpipForward(req *ssh.Request) {
} }
ch, r, err := sc.sshConn.OpenChannel("forwarded-tcpip", ssh.Marshal(a2)) ch, r, err := sc.sshConn.OpenChannel("forwarded-tcpip", ssh.Marshal(a2))
if err != nil { if err != nil {
Log(ERROR, "forward port failed: %s", err.Error()) log.Errorf("forward port failed: %s", err.Error())
c.Close() c.Close()
return return
} }

@ -2,27 +2,15 @@ package obfssh
import ( import (
"io" "io"
"log"
)
const ( "github.com/fangdingjun/go-log"
_ = 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 // PipeAndClose pipe the data between c and s, close both when done
func PipeAndClose(c io.ReadWriteCloser, s io.ReadWriteCloser) { func PipeAndClose(c io.ReadWriteCloser, s io.ReadWriteCloser) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Printf("recovered: %+v", err) log.Errorf("recovered: %+v", err)
} }
}() }()
defer c.Close() defer c.Close()
@ -41,10 +29,3 @@ func PipeAndClose(c io.ReadWriteCloser, s io.ReadWriteCloser) {
<-cc <-cc
} }
// Log log the message by level
func Log(level int, s string, args ...interface{}) {
if level >= SSHLogLevel {
log.Printf(s, args...)
}
}

Loading…
Cancel
Save