commit 7213e64b8949141ac8a58072edc61af5380b99d2 Author: wenyifan Date: Thu Sep 1 15:24:36 2022 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4a87d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +shadowTLS +shadowTLS.exe diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef1db95 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Shadow-TLS +### TLS伪装代理 -- 包装任意TCP连接为真正合法域名的TLS连接 +## 基本原理 +与服务端连接后,服务端会请求指定合法的HTTPS域名(例如www.apple.com)并转发TLS握手流量,与客户端TLS握手成功后后续将转发实际的TCP流量,对审计设备(防火墙/上网行为管理软件/零信任网关)而言你是访问一个合法且是真实证书的HTTPS网站. + +## 使用场景 +- 在有域名白名单的情况下需要将流量转发出去 +- 对抗网络审计设备的审查 + +## 使用方法 +- 服务端示例: +监听端口443,收到请求后先创造到www.apple.com的TLS握手,随后转发本地的8888端口流量到客户端 +```shell +./shadowtls server -l 0.0.0.0:443 -f www.apple.com:443 -t 127.0.0.1:8888 +``` + +- 客户端示例: +```shell +./shadowtls client -l 0.0.0.0:11222 -s 145.142.63.32:443 -d www.apple.com +``` + +## 功能特性 +- TLS连接多路复用,减少TLS握手次数 + +## 安全性特别说明 +- 包装的TCP流量没有加密,如有需求请加密后再转发 +- 多路复用特性使用了smux的框架,有协议特征,有更进一步需求需修改源码二次加密 +- TLS后续流量没有进行Application Data封装,深层次的协议分析可以发现此特征 + +## 特别说明 +- 感谢v2ex网友ihciah的思路灵感. +- 仅供技术研究,请勿用于非法用途. \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..4784663 --- /dev/null +++ b/build.bat @@ -0,0 +1,4 @@ +set GOOS=windows +go build -ldflags "-s -w" -trimpath -o shadowTLS.exe +set GOOS=linux +go build -ldflags "-s -w" -trimpath -o shadowTLS \ No newline at end of file diff --git a/cmd/client.go b/cmd/client.go new file mode 100644 index 0000000..0cf1b1a --- /dev/null +++ b/cmd/client.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "shadowTLS/shadow" +) + +type ClientParam struct { + ListenAddr string + ServerAddr string + SNI string +} + +var ( + clientParams *ClientParam + clientCmd = &cobra.Command{ + Use: "client", + Short: "Client mode", + Run: func(cmd *cobra.Command, args []string) { + client := shadow.NewClient(clientParams.ListenAddr, clientParams.ServerAddr, clientParams.SNI) + client.Start() + }, + } +) + +func init() { + clientParams = &ClientParam{} + clientCmd.Flags().StringVarP(&clientParams.ListenAddr, "listen", "l", "0.0.0.0:18080", "Listen address and port") + clientCmd.Flags().StringVarP(&clientParams.ServerAddr, "saddr", "s", "", "Server address and port") + clientCmd.Flags().StringVarP(&clientParams.SNI, "domain", "d", "", "Domain name used for TLS Handshake") + clientCmd.MarkFlagRequired("listen") + clientCmd.MarkFlagRequired("saddr") + clientCmd.MarkFlagRequired("sni") + rootCmd.AddCommand(clientCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..6a31c13 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" +) + +var ( + rootCmd = &cobra.Command{ + Use: "shadowTLS", + Short: "Pack TCP connection and perform real TLS handshake to confuse firewall", + } +) + +func init() { + rootCmd.CompletionOptions.DisableDefaultCmd = true +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..a3d0afb --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "shadowTLS/shadow" +) + +type ServerParam struct { + ListenAddr string + TargetAddr string + FakeAddr string +} + +var ( + serverParams *ServerParam + serverCmd = &cobra.Command{ + Use: "server", + Short: "Server mode", + Run: func(cmd *cobra.Command, args []string) { + server := shadow.NewServer(serverParams.ListenAddr, serverParams.TargetAddr, serverParams.FakeAddr) + server.Start() + }, + } +) + +func init() { + serverParams = &ServerParam{} + serverCmd.Flags().StringVarP(&serverParams.ListenAddr, "listen", "l", "0.0.0.0:443", "Listen address and port") + serverCmd.Flags().StringVarP(&serverParams.TargetAddr, "target", "t", "", "target address and port to be proxied") + serverCmd.Flags().StringVarP(&serverParams.FakeAddr, "faddr", "f", "", "Address and port of server to make TLS") + serverCmd.MarkFlagRequired("listen") + serverCmd.MarkFlagRequired("target") + serverCmd.MarkFlagRequired("faddr") + rootCmd.AddCommand(serverCmd) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6870d7a --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module shadowTLS + +go 1.18 + +require ( + github.com/spf13/cobra v1.5.0 + github.com/xtaci/smux v1.5.16 +) + +require ( + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e64da46 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/xtaci/smux v1.5.16 h1:FBPYOkW8ZTjLKUM4LI4xnnuuDC8CQ/dB04HD519WoEk= +github.com/xtaci/smux v1.5.16/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d0a3d4b --- /dev/null +++ b/main.go @@ -0,0 +1,8 @@ +package main + +import "shadowTLS/cmd" + +func main() { + + cmd.Execute() +} diff --git a/shadow/client.go b/shadow/client.go new file mode 100644 index 0000000..9299a8d --- /dev/null +++ b/shadow/client.go @@ -0,0 +1,51 @@ +package shadow + +import ( + "fmt" + "io" + "net" +) + +type Client struct { + ListenAddress string + ServerAddress string + FakeAddressSNI string +} + +func NewClient(listenAddress string, serverAddress string, fakeAddressSNI string) *Client { + client := &Client{ + ListenAddress: listenAddress, + ServerAddress: serverAddress, + FakeAddressSNI: fakeAddressSNI, + } + return client +} + +func (c *Client) Start() { + bridge := NewTLSBridge(c.ServerAddress, c.FakeAddressSNI) + + listen, err := net.Listen("tcp", c.ListenAddress) + if err != nil { + fmt.Printf("[Client] Start client error: %v\n", err) + return + } + defer listen.Close() + fmt.Printf("[Client] Listening at:%v\n", c.ListenAddress) + for { + conn, err := listen.Accept() + if err != nil { + fmt.Printf("[Client] accept error: %v\n", err) + continue + } + + stream := bridge.GetStream() + if stream == nil { + conn.Close() + fmt.Printf("[Client] connect to server error: %v\n", err) + continue + } + fmt.Printf("[Client] New TCP connection: %v <-> %v \n", conn.LocalAddr().String(), conn.RemoteAddr().String()) + go io.Copy(conn, stream) + go io.Copy(stream, conn) + } +} diff --git a/shadow/client_test.go b/shadow/client_test.go new file mode 100644 index 0000000..28adb62 --- /dev/null +++ b/shadow/client_test.go @@ -0,0 +1,128 @@ +package shadow + +import ( + "fmt" + "github.com/xtaci/smux" + "io" + "net" + "testing" +) + +func Test(t *testing.T) { + listen, err := net.Listen("tcp", "0.0.0.0:11222") + if err != nil { + fmt.Printf("Start server failed : %v\n", err) + return + } + defer listen.Close() + for { + conn, err := listen.Accept() + if err != nil { + fmt.Printf("Accept error: %v\n", err) + continue + } + fmt.Println("A") + go handler(conn) + } +} + +func handler(conn net.Conn) { + + fakeConn, err := net.Dial("tcp", "127.0.0.1:5900") + if err != nil { + fmt.Printf("Dial fake failed : %v\n", err) + return + } + + if err != nil { + return + } + go io.Copy(fakeConn, conn) + go io.Copy(conn, fakeConn) +} + +func TestSmuxServer(t *testing.T) { + listen, err := net.Listen("tcp", "0.0.0.0:7556") + if err != nil { + fmt.Printf("Start server failed : %v\n", err) + return + } + defer listen.Close() + for { + conn, err := listen.Accept() + if err != nil { + fmt.Printf("Accept error: %v\n", err) + continue + } + session, err := smux.Server(conn, nil) + if err != nil { + fmt.Printf("smux error: %v\n", err) + continue + } + + go func() { + for { + stream, err := session.AcceptStream() + if err != nil { + fmt.Printf("AcceptStream error: %v\n", err) + continue + } + fmt.Println("A") + go handlerMuxTest(stream) + } + }() + } + +} + +func TestSmuxClient(t *testing.T) { + listen, err := net.Listen("tcp", "0.0.0.0:11222") + if err != nil { + fmt.Printf("Start server failed : %v\n", err) + return + } + defer listen.Close() + + smuxConn, err := net.Dial("tcp", "127.0.0.1:7556") + if err != nil { + fmt.Printf("Start smuxConn failed : %v\n", err) + return + } + session, err := smux.Client(smuxConn, nil) + if err != nil { + fmt.Printf("Start smux.Client failed : %v\n", err) + return + } + for { + conn, err := listen.Accept() + if err != nil { + fmt.Printf("Accept error: %v\n", err) + continue + } + fmt.Println("A") + + stream, err := session.OpenStream() + if err != nil { + fmt.Printf("OpenStream error: %v\n", err) + continue + + } + go io.Copy(conn, stream) + go io.Copy(stream, conn) + } +} + +func handlerMuxTest(conn *smux.Stream) { + + fakeConn, err := net.Dial("tcp", "127.0.0.1:5900") + if err != nil { + fmt.Printf("Dial fake failed : %v\n", err) + return + } + fmt.Println("UUUUUUUUUUUUUU") + if err != nil { + return + } + go io.Copy(fakeConn, conn) + go io.Copy(conn, fakeConn) +} diff --git a/shadow/server.go b/shadow/server.go new file mode 100644 index 0000000..1bd4f40 --- /dev/null +++ b/shadow/server.go @@ -0,0 +1,127 @@ +package shadow + +import ( + "fmt" + "github.com/xtaci/smux" + "io" + "net" +) + +type Server struct { + ListenAddress string + TargetAddress string + FakeAddress string +} + +func NewServer(listenAddress string, targetAddress string, fakeAddress string) *Server { + server := &Server{ + ListenAddress: listenAddress, + TargetAddress: targetAddress, + FakeAddress: fakeAddress, + } + return server +} + +func (s *Server) Start() { + listen, err := net.Listen("tcp", s.ListenAddress) + if err != nil { + fmt.Printf("[Server] Start server error: %v\n", err) + return + } + defer listen.Close() + fmt.Printf("[Server] Listening at:%v\n", s.ListenAddress) + for { + conn, err := listen.Accept() + if err != nil { + fmt.Printf("[Server] Accept error: %v\n", err) + continue + } + go handler(conn, s.TargetAddress, s.FakeAddress) + } +} + +func handler(conn net.Conn, targetAddress string, fakeAddress string) { + + //Process fake TLS handshake + fmt.Println("[Server] Perform handshake") + fakeConn, err := net.Dial("tcp", fakeAddress) + if err != nil { + fmt.Printf("[Server] Dial fake error : %v\n", err) + return + } + waitCh := make(chan int, 2) + + go processHandshake(conn, fakeConn, waitCh) + go processHandshake(fakeConn, conn, waitCh) + + <-waitCh + <-waitCh + + //Process real tcp connection + session, err := smux.Server(conn, nil) + if err != nil { + fmt.Printf("[Server] smux error: %v\n", err) + return + } + for { + stream, err := session.AcceptStream() + if err != nil { + fmt.Printf("[Server] AcceptStream error: %v\n", err) + break + } + go handlerMux(stream, targetAddress) + } + +} + +func processHandshake(src net.Conn, dst net.Conn, waitCh chan int) { + buf := make([]byte, 32*1024) + for { + nr, er := src.Read(buf) + if nr > 0 { + + header := ParseAndVerifyTLSHeader(buf[0:nr]) + nw, ew := dst.Write(buf[0:nr]) + if header != nil && header.Type == ChangeCipherSpec { + fmt.Println("[Server] handshake complete") + dst.Close() + break + } + if nw < 0 || nr < nw { + nw = 0 + if ew == nil { + //fmt.Printf("ERR1 %v \n", ew) + } + } + if ew != nil { + //fmt.Printf("ERR2 %v \n", ew) + break + } + if nr != nw { + //fmt.Printf("ERR3 %v \n", "shortwrite") + break + } + } + if er != nil { + if er != io.EOF { + //fmt.Printf("ERR4 %v \n", er) + } + break + } + } + waitCh <- 1 +} + +func handlerMux(conn *smux.Stream, targetAddress string) { + + realConnection, err := net.Dial("tcp", targetAddress) + if err != nil { + fmt.Printf("[Server] Dial target error : %v\n", err) + return + } + if err != nil { + return + } + go io.Copy(realConnection, conn) + go io.Copy(conn, realConnection) +} diff --git a/shadow/tls_bridge.go b/shadow/tls_bridge.go new file mode 100644 index 0000000..19e49f6 --- /dev/null +++ b/shadow/tls_bridge.go @@ -0,0 +1,70 @@ +package shadow + +import ( + "crypto/tls" + "github.com/xtaci/smux" + "net" + "sync" + "time" +) + +type TLSBridge struct { + session *smux.Session + locker sync.Mutex + serverAddress string + fakeAddressSNI string +} + +func NewTLSBridge(serverAddress string, fakeAddressSNI string) *TLSBridge { + t := &TLSBridge{ + session: nil, + locker: sync.Mutex{}, + serverAddress: serverAddress, + fakeAddressSNI: fakeAddressSNI, + } + return t +} + +func (t *TLSBridge) dial() error { + if t.session != nil { + t.session.Close() + } + dial, err := tls.DialWithDialer(&net.Dialer{ + Timeout: time.Second * 5, + }, "tcp", t.serverAddress, &tls.Config{ + ServerName: t.fakeAddressSNI, + }) + if err != nil { + return err + } + err = dial.Handshake() + if err != nil { + return err + } + session, err := smux.Client(dial.NetConn(), nil) + if err != nil { + return err + } + t.session = session + return nil +} + +func (t *TLSBridge) GetStream() *smux.Stream { + t.locker.Lock() + defer t.locker.Unlock() + + if t.session == nil { + err := t.dial() + if err != nil { + return nil + } + } + + openStream, err := t.session.OpenStream() + if err != nil { + t.session.Close() + t.session = nil + } + return openStream + +} diff --git a/shadow/tls_util.go b/shadow/tls_util.go new file mode 100644 index 0000000..6659619 --- /dev/null +++ b/shadow/tls_util.go @@ -0,0 +1,87 @@ +package shadow + +import ( + "encoding/binary" +) + +const ( + RecordHeaderLen = 5 + + ChangeCipherSpec = 0x14 + EncryptedAlert = 0x15 + Handshake = 0x16 + AppData = 0x17 + + VersionTLS10 = 0x0301 + VersionTLS11 = 0x0302 + VersionTLS12 = 0x0303 + VersionTLS13 = 0x0304 + + ServerHello = 2 + ClientHello = 1 +) + +type TLSHeader struct { + Type uint8 + Version uint16 + Length int + HandshakeType uint8 +} + +func (t *TLSHeader) toString() string { + if t == nil { + return "nul" + } + desc := "" + switch t.Type { + case Handshake: + desc += "Type=handshake;" + switch t.HandshakeType { + case ClientHello: + desc += "HandshakeType=ClientHello;" + break + case ServerHello: + desc += "HandshakeType=ServerHello;" + break + } + break + case ChangeCipherSpec: + desc += "Type=ChangeCipherSpec;" + break + case EncryptedAlert: + desc += "Type=EncryptedAlert;" + break + case AppData: + desc += "Type=AppData;" + break + } + return desc +} + +func ParseAndVerifyTLSHeader(data []byte) *TLSHeader { + if len(data) < RecordHeaderLen { + return nil + } + header := &TLSHeader{ + Type: data[0], + Version: binary.BigEndian.Uint16(data[1:3]), + Length: int(binary.BigEndian.Uint16(data[3:5])), + } + //Check type + if header.Type != Handshake && header.Type != AppData && header.Type != EncryptedAlert && header.Type != ChangeCipherSpec { + return nil + } + //Check version + if header.Version != VersionTLS10 && header.Version != VersionTLS11 && header.Version != VersionTLS12 && header.Version != VersionTLS13 { + return nil + } + + if header.Type == Handshake { + header.HandshakeType = data[5] + //Check Handshake type + if header.HandshakeType != ServerHello && header.HandshakeType != ClientHello { + return nil + } + } + return header +} diff --git a/startCmd.bat b/startCmd.bat new file mode 100644 index 0000000..74853fb --- /dev/null +++ b/startCmd.bat @@ -0,0 +1 @@ +start \ No newline at end of file