From 4bb5b31e91966da1ba38fe887c4c2265611bc4ba Mon Sep 17 00:00:00 2001 From: p4gefau1t Date: Sat, 25 Apr 2020 05:02:14 -0400 Subject: [PATCH] add shadow, update api --- api/api.pb.go | 49 +++- api/api.proto | 6 +- api/client.go | 34 ++- api/client_test.go | 8 +- common/io.go | 27 +- conf/conf.go | 3 +- conf/parse.go | 2 +- protocol/direct/outbound.go | 2 +- protocol/socks/inbound.go | 6 +- protocol/{nat => tproxy}/inbound.go | 20 +- protocol/{nat/nat.go => tproxy/tproxy.go} | 2 +- protocol/trojan/inbound.go | 61 ++--- protocol/trojan/websocket.go | 124 +++++++-- proxy/client/client.go | 13 +- proxy/client/forward.go | 2 +- proxy/client/mux.go | 6 +- proxy/client/nat.go | 10 +- proxy/client/tls.go | 2 +- proxy/server/server.go | 73 +++-- shadow/shadow.go | 66 +++++ test/proxy_test.go | 37 ++- test/socks.go | 318 ---------------------- 22 files changed, 385 insertions(+), 486 deletions(-) rename protocol/{nat => tproxy}/inbound.go (87%) rename protocol/{nat/nat.go => tproxy/tproxy.go} (99%) create mode 100644 shadow/shadow.go delete mode 100644 test/socks.go diff --git a/api/api.pb.go b/api/api.pb.go index 444ebc5..336f2d2 100644 --- a/api/api.pb.go +++ b/api/api.pb.go @@ -64,8 +64,10 @@ func (m *StatsRequest) GetPassword() string { } type StatsReply struct { - Upload uint64 `protobuf:"varint,1,opt,name=upload,proto3" json:"upload,omitempty"` - Download uint64 `protobuf:"varint,2,opt,name=download,proto3" json:"download,omitempty"` + UploadTraffic uint64 `protobuf:"varint,1,opt,name=upload_traffic,json=uploadTraffic,proto3" json:"upload_traffic,omitempty"` + DownloadTraffic uint64 `protobuf:"varint,2,opt,name=download_traffic,json=downloadTraffic,proto3" json:"download_traffic,omitempty"` + UploadSpeed uint64 `protobuf:"varint,3,opt,name=upload_speed,json=uploadSpeed,proto3" json:"upload_speed,omitempty"` + DownloadSpeed uint64 `protobuf:"varint,4,opt,name=download_speed,json=downloadSpeed,proto3" json:"download_speed,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -96,16 +98,30 @@ func (m *StatsReply) XXX_DiscardUnknown() { var xxx_messageInfo_StatsReply proto.InternalMessageInfo -func (m *StatsReply) GetUpload() uint64 { +func (m *StatsReply) GetUploadTraffic() uint64 { if m != nil { - return m.Upload + return m.UploadTraffic } return 0 } -func (m *StatsReply) GetDownload() uint64 { +func (m *StatsReply) GetDownloadTraffic() uint64 { if m != nil { - return m.Download + return m.DownloadTraffic + } + return 0 +} + +func (m *StatsReply) GetUploadSpeed() uint64 { + if m != nil { + return m.UploadSpeed + } + return 0 +} + +func (m *StatsReply) GetDownloadSpeed() uint64 { + if m != nil { + return m.DownloadSpeed } return 0 } @@ -120,18 +136,21 @@ func init() { } var fileDescriptor_00212fb1f9d3bf1c = []byte{ - // 161 bytes of a gzipped FileDescriptorProto + // 214 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x4c, 0x2c, 0xc8, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x06, 0x32, 0x95, 0xb4, 0xb8, 0x78, 0x82, 0x4b, 0x12, 0x4b, 0x8a, 0x83, 0x52, 0x0b, 0x4b, 0x53, 0x8b, 0x4b, 0x84, 0xa4, 0xb8, 0x38, 0x0a, 0x12, 0x8b, - 0x8b, 0xcb, 0xf3, 0x8b, 0x52, 0x24, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, 0xe0, 0x7c, 0x25, 0x07, - 0x2e, 0x2e, 0xa8, 0xda, 0x82, 0x9c, 0x4a, 0x21, 0x31, 0x2e, 0xb6, 0xd2, 0x82, 0x9c, 0xfc, 0x44, - 0x88, 0x3a, 0x96, 0x20, 0x28, 0x0f, 0x64, 0x42, 0x4a, 0x7e, 0x79, 0x1e, 0x58, 0x86, 0x09, 0x2c, - 0x03, 0xe7, 0x1b, 0x39, 0x73, 0xf1, 0x86, 0x14, 0xe5, 0x67, 0x25, 0xe6, 0x05, 0xa7, 0x16, 0x95, - 0x65, 0x26, 0xa7, 0x0a, 0x19, 0x71, 0x71, 0x05, 0x96, 0xa6, 0x16, 0x55, 0x82, 0xcd, 0x15, 0x12, - 0xd4, 0x03, 0xb9, 0x0e, 0xd9, 0x3d, 0x52, 0xfc, 0xc8, 0x42, 0x40, 0x6b, 0x95, 0x18, 0x92, 0xd8, - 0xc0, 0xce, 0x37, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xea, 0x6a, 0xb8, 0xe4, 0xcb, 0x00, 0x00, - 0x00, + 0x8b, 0xcb, 0xf3, 0x8b, 0x52, 0x24, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, 0xe0, 0x7c, 0xa5, 0x15, + 0x8c, 0x5c, 0x5c, 0x50, 0xc5, 0x05, 0x39, 0x95, 0x42, 0xaa, 0x5c, 0x7c, 0xa5, 0x05, 0x39, 0xf9, + 0x89, 0x29, 0xf1, 0x25, 0x45, 0x89, 0x69, 0x69, 0x99, 0xc9, 0x60, 0x0d, 0x2c, 0x41, 0xbc, 0x10, + 0xd1, 0x10, 0x88, 0xa0, 0x90, 0x26, 0x97, 0x40, 0x4a, 0x7e, 0x79, 0x1e, 0x8a, 0x42, 0x26, 0xb0, + 0x42, 0x7e, 0x98, 0x38, 0x4c, 0xa9, 0x22, 0x17, 0x0f, 0xd4, 0xc4, 0xe2, 0x82, 0xd4, 0xd4, 0x14, + 0x09, 0x66, 0xb0, 0x32, 0x6e, 0x88, 0x58, 0x30, 0x48, 0x08, 0x64, 0x29, 0xdc, 0x34, 0x88, 0x22, + 0x16, 0x88, 0xa5, 0x30, 0x51, 0xb0, 0x32, 0x23, 0x67, 0x2e, 0xde, 0x90, 0xa2, 0xfc, 0xac, 0xc4, + 0xbc, 0xe0, 0xd4, 0xa2, 0xb2, 0xcc, 0xe4, 0x54, 0x21, 0x23, 0x2e, 0xae, 0xc0, 0xd2, 0xd4, 0xa2, + 0x4a, 0xb0, 0xfb, 0x85, 0x04, 0xf5, 0x40, 0xc1, 0x80, 0xec, 0x71, 0x29, 0x7e, 0x64, 0x21, 0xa0, + 0xf7, 0x94, 0x18, 0x92, 0xd8, 0xc0, 0xe1, 0x64, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x89, 0xd5, + 0x8f, 0xd9, 0x34, 0x01, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/api/api.proto b/api/api.proto index 9fa9b84..b109631 100644 --- a/api/api.proto +++ b/api/api.proto @@ -10,6 +10,8 @@ message StatsRequest{ } message StatsReply { - uint64 upload = 1; - uint64 download = 2; + uint64 upload_traffic = 1; + uint64 download_traffic = 2; + uint64 upload_speed = 3; + uint64 download_speed = 4; } \ No newline at end of file diff --git a/api/client.go b/api/client.go index 1b4b953..95c8e64 100644 --- a/api/client.go +++ b/api/client.go @@ -2,8 +2,8 @@ package api import ( "context" + "time" - "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/conf" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/stat" @@ -13,26 +13,46 @@ import ( type ClientAPIService struct { TrojanServiceServer - meter stat.TrafficMeter + meter stat.TrafficMeter + uploadSpeed uint64 + downloadSpeed uint64 + lastSent uint64 + lastRecv uint64 + ctx context.Context } func (s *ClientAPIService) QueryStats(ctx context.Context, req *StatsRequest) (*StatsReply, error) { log.Debug("query stats, password", req.Password) - password := req.Password - passwordHash := common.SHA224String(password) - sent, recv := s.meter.Query(passwordHash) + //password := req.Password + //passwordHash := common.SHA224String(password) + sent, recv := s.meter.Query("") reply := &StatsReply{ - Upload: sent, - Download: recv, + UploadTraffic: sent, + DownloadTraffic: recv, + UploadSpeed: s.uploadSpeed, + DownloadSpeed: s.downloadSpeed, } return reply, nil } +func (s *ClientAPIService) calcSpeed() { + select { + case <-time.After(time.Second): + sent, recv := s.meter.Query("") + s.uploadSpeed = sent - s.lastSent + s.downloadSpeed = recv - s.lastRecv + case <-s.ctx.Done(): + return + } +} + func RunClientAPIService(ctx context.Context, config *conf.GlobalConfig, meter stat.TrafficMeter) error { server := grpc.NewServer() service := &ClientAPIService{ meter: meter, + ctx: ctx, } + go service.calcSpeed() RegisterTrojanServiceServer(server, service) listener, err := net.Listen("tcp", config.API.APIAddress.String()) if err != nil { diff --git a/api/client_test.go b/api/client_test.go index aa5f980..d863e4c 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "testing" + "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/conf" @@ -19,13 +20,14 @@ func TestClientAPI(t *testing.T) { }, }, meter) meter.Count("test", 123, 456) + time.Sleep(time.Second) conn, err := grpc.Dial("127.0.0.1:10000", grpc.WithInsecure()) common.Must(err) client := NewTrojanServiceClient(conn) reply, err := client.QueryStats(context.Background(), &StatsRequest{}) common.Must(err) - fmt.Println(reply.Download, reply.Upload) - if reply.Download != 456 || reply.Upload != 123 { + fmt.Println(reply.DownloadTraffic, reply.UploadTraffic) + if reply.DownloadTraffic != 456 || reply.UploadTraffic != 123 { t.Fatal("wrong result") } } @@ -36,5 +38,5 @@ func TestRealClientAPI(t *testing.T) { client := NewTrojanServiceClient(conn) reply, err := client.QueryStats(context.Background(), &StatsRequest{}) common.Must(err) - fmt.Println(reply.Download, reply.Upload) + fmt.Println(reply.DownloadTraffic, reply.UploadTraffic) } diff --git a/common/io.go b/common/io.go index cf067f3..e648073 100644 --- a/common/io.go +++ b/common/io.go @@ -1,6 +1,11 @@ package common -import "io" +import ( + "io" + "net" + + "github.com/p4gefau1t/trojan-go/log" +) type RewindReader struct { io.Reader @@ -26,8 +31,8 @@ func (r *RewindReader) Read(p []byte) (int, error) { n, err := r.rawReader.Read(p) if r.buffered { r.buf = append(r.buf, p[:n]...) - if len(r.buf) > r.bufferSize { - //panic("too long") + if len(r.buf) > r.bufferSize*2 { + log.Debug("read buffer too long") } } return n, err @@ -119,3 +124,19 @@ func ReadByte(r io.Reader) (byte, error) { _, err := r.Read(buf[:]) return buf[0], err } + +type RewindConn struct { + R *RewindReader + net.Conn +} + +func (c *RewindConn) Read(p []byte) (int, error) { + return c.R.Read(p) +} + +func NewRewindConn(conn net.Conn) *RewindConn { + return &RewindConn{ + Conn: conn, + R: NewRewindReader(conn), + } +} diff --git a/conf/conf.go b/conf/conf.go index 3f2145d..748f5e8 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -28,6 +28,7 @@ type TLSConfig struct { PreferServerCipher bool `json:"prefer_server_cipher"` SNI string `json:"sni"` HTTPFile string `json:"plain_http_response"` + FallbackHost string `json:"fallback_addr"` FallbackPort int `json:"fallback_port"` ReuseSession bool `json:"reuse_session"` @@ -102,7 +103,7 @@ type WebsocketConfig struct { Enabled bool `json:"enabled"` HostName string `json:"hostname"` Path string `json:"path"` - Obfsucation bool `json:"obfuscation"` + Obfuscation bool `json:"obfuscation"` DoubleTLS bool `json:"double_tls"` DoubleTLSVerify bool `json:"double_tls_verify"` } diff --git a/conf/parse.go b/conf/parse.go index a478fe3..ba2947e 100644 --- a/conf/parse.go +++ b/conf/parse.go @@ -292,7 +292,7 @@ func ParseJSON(data []byte) (*GlobalConfig, error) { config.Router.GeoSiteFilename = common.GetProgramDir() + "/geosite.dat" config.Websocket.DoubleTLS = true config.Websocket.DoubleTLSVerify = true - config.Websocket.Obfsucation = true + config.Websocket.Obfuscation = true err := json.Unmarshal(data, config) if err != nil { diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index 87a9083..9c21997 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -107,7 +107,7 @@ func (o *DirectOutboundPacketSession) WritePacket(req *protocol.Request, packet if err != nil { return 0, common.NewError("cannot dial udp").Base(err) } - log.Debug("UDP directly dialing to", remote) + log.Debug("udp directly dialing to", remote) go o.listenConn(req, conn) n, err := conn.Write(packet) return n, err diff --git a/protocol/socks/inbound.go b/protocol/socks/inbound.go index d1235ff..3a3563e 100644 --- a/protocol/socks/inbound.go +++ b/protocol/socks/inbound.go @@ -26,7 +26,7 @@ func (i *SocksConnInboundSession) checkVersion() error { return err } if version != 0x5 { - return common.NewError("unsupported version") + return common.NewError("unsupported socks version") } return nil } @@ -194,7 +194,7 @@ func (i *SocksInboundPacketSession) ReadPacket() (*protocol.Request, []byte, err i.tableMutex.Lock() i.sessionTable[req.String()] = session i.tableMutex.Unlock() - log.Debug("UDP read from", src, "req", req) + log.Debug("udp read from", src, "req", req) return req, payload, err } @@ -211,7 +211,7 @@ func (i *SocksInboundPacketSession) WritePacket(req *protocol.Request, packet [] return 0, common.NewError("session not found") } client.expire = time.Now().Add(protocol.UDPTimeout) - log.Debug("UDP write to", client.src, "req", req) + log.Debug("udp write to", client.src, "req", req) return i.conn.WriteToUDP(w.Bytes(), client.src) } diff --git a/protocol/nat/inbound.go b/protocol/tproxy/inbound.go similarity index 87% rename from protocol/nat/inbound.go rename to protocol/tproxy/inbound.go index da3b993..9d1ec6d 100644 --- a/protocol/nat/inbound.go +++ b/protocol/tproxy/inbound.go @@ -1,6 +1,6 @@ // +build linux -package nat +package tproxy import ( "context" @@ -15,29 +15,29 @@ import ( "github.com/p4gefau1t/trojan-go/protocol" ) -type NATInboundConnSession struct { +type TProxyInboundConnSession struct { protocol.ConnSession reqeust *protocol.Request conn net.Conn } -func (i *NATInboundConnSession) Read(p []byte) (int, error) { +func (i *TProxyInboundConnSession) Read(p []byte) (int, error) { return i.conn.Read(p) } -func (i *NATInboundConnSession) Write(p []byte) (int, error) { +func (i *TProxyInboundConnSession) Write(p []byte) (int, error) { return i.conn.Write(p) } -func (i *NATInboundConnSession) Close() error { +func (i *TProxyInboundConnSession) Close() error { return i.conn.Close() } -func (i *NATInboundConnSession) GetRequest() *protocol.Request { +func (i *TProxyInboundConnSession) GetRequest() *protocol.Request { return i.reqeust } -func (i *NATInboundConnSession) parseRequest() error { +func (i *TProxyInboundConnSession) parseRequest() error { addr, err := getOriginalTCPDest(i.conn.(*net.TCPConn)) if err != nil { return common.NewError("failed to get original dst").Base(err) @@ -59,7 +59,7 @@ func (i *NATInboundConnSession) parseRequest() error { } func NewInboundConnSession(conn net.Conn) (protocol.ConnSession, *protocol.Request, error) { - i := &NATInboundConnSession{ + i := &TProxyInboundConnSession{ conn: conn, } if err := i.parseRequest(); err != nil { @@ -135,7 +135,7 @@ func (i *NATInboundPacketSession) ReadPacket() (*protocol.Request, []byte, error expire: time.Now().Add(protocol.UDPTimeout), } i.tableMutex.Unlock() - log.Debug("tproxy UDP packet from", src, "to", dst) + log.Debug("tproxy udp packet from", src, "to", dst) req := &protocol.Request{ Address: &common.Address{ IP: dst.IP, @@ -167,7 +167,7 @@ func NewInboundPacketSession(ctx context.Context, config *conf.GlobalConfig) (pr } conn, err := tproxy.ListenUDP("udp", addr) if err != nil { - return nil, common.NewError("failed to listen UDP addr").Base(err) + return nil, common.NewError("failed to listen udp addr").Base(err) } ctx, cancel := context.WithCancel(ctx) i := &NATInboundPacketSession{ diff --git a/protocol/nat/nat.go b/protocol/tproxy/tproxy.go similarity index 99% rename from protocol/nat/nat.go rename to protocol/tproxy/tproxy.go index a624b7c..48477b4 100644 --- a/protocol/nat/nat.go +++ b/protocol/tproxy/tproxy.go @@ -1,6 +1,6 @@ // +build linux -package nat +package tproxy import ( "net" diff --git a/protocol/trojan/inbound.go b/protocol/trojan/inbound.go index c9f2da3..6aa3914 100644 --- a/protocol/trojan/inbound.go +++ b/protocol/trojan/inbound.go @@ -9,6 +9,7 @@ import ( "github.com/p4gefau1t/trojan-go/conf" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/protocol" + "github.com/p4gefau1t/trojan-go/shadow" "github.com/p4gefau1t/trojan-go/stat" ) @@ -89,67 +90,61 @@ func (i *TrojanInboundConnSession) SetMeter(meter stat.TrafficMeter) { i.meter = meter } -func NewInboundConnSession(ctx context.Context, conn net.Conn, config *conf.GlobalConfig, auth stat.Authenticator) (protocol.ConnSession, *protocol.Request, error) { +func NewInboundConnSession(ctx context.Context, conn net.Conn, config *conf.GlobalConfig, auth stat.Authenticator, shadowMan *shadow.ShadowManager) (protocol.ConnSession, *protocol.Request, error) { ctx, cancel := context.WithCancel(context.Background()) - rwc := common.NewRewindReadWriteCloser(conn) + //rwc := common.NewRewindReadWriteCloser(conn) + rewindConn := common.NewRewindConn(conn) i := &TrojanInboundConnSession{ config: config, auth: auth, passwordHash: "INVALID_HASH", ctx: ctx, cancel: cancel, - rwc: rwc, + rwc: rewindConn, } //start buffering - rwc.SetBufferSize(512) - defer rwc.StopBuffering() + rewindConn.R.SetBufferSize(512) + defer rewindConn.R.StopBuffering() + if i.config.Websocket.Enabled { //try to treat it as a websocket connection first - ws, err := NewInboundWebsocket(i.ctx, conn, rwc.RewindReader, config) + ws, err := NewInboundWebsocket(i.ctx, rewindConn, config, shadowMan) if err != nil { - //websocket with wrong url path/origin, no need to continue parsing - rwc.Rewind() - rwc.StopBuffering() - i.request = &protocol.Request{ - Address: config.RemoteAddress, - Command: protocol.Connect, - } - log.Warn("remote", conn.RemoteAddr(), "is a invalid websocket conn | ", err) - return i, i.request, nil + return nil, nil, common.NewError("invalid websocket request").Base(err) } if ws != nil { //a websocket conn, try to verify it log.Debug("websocket conn") - //disable the read buffer, use ws as new transport layer - rwc.SetBufferSize(0) - rwc = common.NewRewindReadWriteCloser(ws) - i.rwc = rwc + //disable the current read buffer, use ws as the new transport layer + rewindConn.R.SetBufferSize(0) + newTrapsport := common.NewRewindReadWriteCloser(ws) + i.rwc = newTrapsport //parse it with trojan protocol format - if err := i.parseRequest(rwc.RewindReader); err != nil { - //not valid, just simply close it + if err := i.parseRequest(newTrapsport.RewindReader); err != nil { + //invalid ws, just simply close it ws.Close() return nil, nil, common.NewError("invalid trojan over ws conn").Base(err) } return i, i.request, nil } //not a websocket conn, it might be a normal trojan conn - rwc.Rewind() + rewindConn.R.Rewind() } //normal trojan conn - if err := i.parseRequest(rwc.RewindReader); err != nil { + if err := i.parseRequest(rewindConn.R); err != nil { //not a valid trojan request, proxy it to the remote_addr - rwc.Rewind() - rwc.StopBuffering() - i.request = &protocol.Request{ - Address: i.config.RemoteAddress, - Command: protocol.Connect, - } - log.Warn(common.NewError("invalid trojan protocol over websocket from " + conn.RemoteAddr().String()).Base(err)) - return i, i.request, nil + rewindConn.R.Rewind() + err := common.NewError("invalid trojan protocol over websocket from " + conn.RemoteAddr().String()).Base(err) + shadowMan.CommitScapegoat(&shadow.Scapegoat{ + Conn: rewindConn, + ShadowAddress: i.config.RemoteAddress, + Info: err.Error(), + }) + return nil, nil, err } - rwc.SetBufferSize(0) - rwc.StopBuffering() + //release the buffer + rewindConn.R.SetBufferSize(0) return i, i.request, nil } diff --git a/protocol/trojan/websocket.go b/protocol/trojan/websocket.go index 4f8994c..eae71c5 100644 --- a/protocol/trojan/websocket.go +++ b/protocol/trojan/websocket.go @@ -17,13 +17,14 @@ import ( "github.com/p4gefau1t/trojan-go/conf" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/protocol" + "github.com/p4gefau1t/trojan-go/shadow" "golang.org/x/crypto/pbkdf2" "golang.org/x/net/websocket" ) //this AES layer is used for obfuscation purpose only type obfReadWriteCloser struct { - *websocket.Conn + net.Conn r cipher.StreamReader w cipher.StreamWriter bufrw *bufio.ReadWriter @@ -70,7 +71,7 @@ func NewOutboundObfReadWriteCloser(password string, conn *websocket.Conn) *obfRe } } -func NewInboundObfReadWriteCloser(password string, conn *websocket.Conn) (*obfReadWriteCloser, error) { +func NewInboundObfReadWriteCloser(password string, conn net.Conn) (*obfReadWriteCloser, error) { bufrw := common.NewBufioReadWriter(conn) randomBytes := [aes.BlockSize + 8]byte{} _, err := bufrw.Read(randomBytes[:]) @@ -127,14 +128,14 @@ func NewOutboundWebosocket(conn net.Conn, config *conf.GlobalConfig) (io.ReadWri return nil, err } var transport net.Conn = wsConn - if config.Websocket.Obfsucation { + if config.Websocket.Obfuscation { log.Debug("ws obfs enabled") transport = NewOutboundObfReadWriteCloser(config.Passwords[0], wsConn) } if !config.Websocket.DoubleTLS { return transport, nil } - log.Debug("ws double TLS enabled") + log.Debug("ws double tls enabled") tlsConfig := &tls.Config{ CipherSuites: config.TLS.CipherSuites, RootCAs: config.TLS.CertPool, @@ -150,7 +151,7 @@ func NewOutboundWebosocket(conn net.Conn, config *conf.GlobalConfig) (io.ReadWri if config.LogLevel == 0 { state := tlsConn.ConnectionState() chain := state.VerifiedChains - log.Debug("websocket TLS handshaked", "cipher:", tls.CipherSuiteName(state.CipherSuite), "resume:", state.DidResume) + log.Debug("websocket tls handshaked", "cipher:", tls.CipherSuiteName(state.CipherSuite), "resume:", state.DidResume) for i := range chain { for j := range chain[i] { log.Debug("subject:", chain[i][j].Subject, ", issuer:", chain[i][j].Issuer) @@ -160,22 +161,66 @@ func NewOutboundWebosocket(conn net.Conn, config *conf.GlobalConfig) (io.ReadWri return tlsConn, nil } -func NewInboundWebsocket(ctx context.Context, conn net.Conn, r *common.RewindReader, config *conf.GlobalConfig) (io.ReadWriteCloser, error) { - bufrw := bufio.NewReadWriter(bufio.NewReader(r), bufio.NewWriter(conn)) - httpRequest, err := http.ReadRequest(bufrw.Reader) +func dialToWebosocketServer(config *conf.GlobalConfig, url, origin string) (*websocket.Conn, error) { + wsConfig, err := websocket.NewConfig(url, origin) if err != nil { + return nil, err + } + conn, err := net.Dial("tcp", config.RemoteAddress.String()) + if err != nil { + return nil, err + } + newWsConn, err := websocket.NewClient(wsConfig, conn) + if err != nil { + return nil, err + } + return newWsConn, nil +} + +func getWebsocketScapegoat(config *conf.GlobalConfig, url, origin, info string, conn net.Conn) (*shadow.Scapegoat, error) { + shadowConn, err := dialToWebosocketServer(config, url, origin) + if err != nil { + return nil, err + } + return &shadow.Scapegoat{ + Conn: conn, + ShadowConn: shadowConn, + Info: info, + }, nil +} + +func NewInboundWebsocket(ctx context.Context, conn net.Conn, config *conf.GlobalConfig, shadowMan *shadow.ShadowManager) (io.ReadWriteCloser, error) { + rewindConn := common.NewRewindConn(conn) + rewindConn.R.SetBufferSize(512) + defer rewindConn.R.StopBuffering() + + bufrw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) + httpRequest, obfErr := http.ReadRequest(bufrw.Reader) + if obfErr != nil { + log.Debug(common.NewError("not a http request:").Base(obfErr)) return nil, nil } + //this is a http request if (config.Websocket.HostName != "" && httpRequest.Host != config.Websocket.HostName) || //check hostname httpRequest.URL.Path != config.Websocket.Path || //check url path httpRequest.Header.Get("Upgrade") != "websocket" { //check upgrade field + //not a valid websocket conn + rewindConn.R.Rewind() + shadowMan.CommitScapegoat(&shadow.Scapegoat{ + Conn: rewindConn, + ShadowAddress: config.RemoteAddress, + Info: "not a valid http upgrade request from " + conn.RemoteAddr().String(), + }) return nil, common.NewError("invalid ws url or hostname") } + //this is a websocket upgrade request + //no need to record the recv content for now + rewindConn.R.SetBufferSize(0) url := "wss://" + config.Websocket.HostName + config.Websocket.Path origin := "https://" + config.Websocket.HostName - wsConfig, err := websocket.NewConfig(url, origin) + wsConfig, obfErr := websocket.NewConfig(url, origin) handshaked := make(chan struct{}) @@ -209,17 +254,47 @@ func NewInboundWebsocket(ctx context.Context, conn net.Conn, r *common.RewindRea } if wsConn == nil { + //conn has been closed at this point return nil, common.NewError("failed to perform websocket handshake") } - var transport net.Conn = wsConn - if config.Websocket.Obfsucation { - transport, err = NewInboundObfReadWriteCloser(config.Passwords[0], wsConn) - if err != nil { - return nil, common.NewError("failed to init obfus layer").Base(err) + var transport net.Conn + transport = common.NewRewindConn(wsConn) + + //start buffering the websocket payload + rewindConn.R.SetBufferSize(512) + defer rewindConn.R.StopBuffering() + + if config.Websocket.Obfuscation { + log.Debug("ws obfs") + + //deadline for sending the iv and hash + rewindConn.SetDeadline(time.Now().Add(protocol.TCPTimeout)) + transport, obfErr = NewInboundObfReadWriteCloser(config.Passwords[0], rewindConn) + rewindConn.SetDeadline(time.Time{}) + + if obfErr != nil { + rewindConn.R.Rewind() + //proxy this to our own ws server + obfErr = common.NewError("remote websocket conn:" + conn.RemoteAddr().String() + "didn't send any valid iv/hash").Base(obfErr) + goat, err := getWebsocketScapegoat( + config, + url, + origin, + obfErr.Error(), + rewindConn, + ) + if err != nil { + log.Error(common.NewError("failed to obtain websocket scapegoat").Base(err)) + wsConn.WriteClose(500) + } else { + shadowMan.CommitScapegoat(goat) + } + return nil, obfErr } } if !config.Websocket.DoubleTLS { + rewindConn.R.SetBufferSize(0) return transport, nil } tlsConfig := &tls.Config{ @@ -229,8 +304,25 @@ func NewInboundWebsocket(ctx context.Context, conn net.Conn, r *common.RewindRea SessionTicketsDisabled: !config.TLS.SessionTicket, } tlsConn := tls.Server(transport, tlsConfig) - if err := tlsConn.Handshake(); err != nil { - return nil, err + if tlsErr := tlsConn.Handshake(); tlsErr != nil { + rewindConn.R.Rewind() + //proxy this to our own ws server + tlsErr = common.NewError("invalid double tls handshake from" + conn.RemoteAddr().String()).Base(tlsErr) + goat, err := getWebsocketScapegoat( + config, + url, + origin, + tlsErr.Error(), + rewindConn, + ) + if err != nil { + log.Error(common.NewError("failed to obtain websocket scapegoat").Base(err)) + wsConn.WriteClose(500) + } else { + shadowMan.CommitScapegoat(goat) + } + return nil, tlsErr } + rewindConn.R.SetBufferSize(0) return tlsConn, nil } diff --git a/proxy/client/client.go b/proxy/client/client.go index 690685e..36c6a58 100644 --- a/proxy/client/client.go +++ b/proxy/client/client.go @@ -67,7 +67,7 @@ func (c *Client) handleSocksConn(conn io.ReadWriteCloser) { rwc := common.NewRewindReadWriteCloser(conn) inboundConn, req, err := socks.NewInboundConnSession(rwc) if err != nil { - log.Error(common.NewError("failed to start new inbound session").Base(err)) + log.Error(common.NewError("failed to handle socks requests").Base(err)) rwc.Close() return } @@ -92,7 +92,7 @@ func (c *Client) handleSocksConn(conn io.ReadWriteCloser) { //notify listenUDP to get ready for relaying udp packets c.associated.Signal() - log.Debug("UDP associated, req", req) + log.Debug("udp associated to", req) if err := inboundConn.(protocol.NeedRespond).Respond(); err != nil { log.Error("failed to repsond") return @@ -101,7 +101,7 @@ func (c *Client) handleSocksConn(conn io.ReadWriteCloser) { //stop relaying UDP once TCP connection is closed var buf [1]byte _, err = rwc.Read(buf[:]) - log.Debug(common.NewError("UDP conn ends").Base(err)) + log.Debug(common.NewError("udp conn ends").Base(err)) return } @@ -142,11 +142,12 @@ func (c *Client) handleHTTPConn(conn io.ReadWriteCloser) { rwc := common.NewRewindReadWriteCloser(conn) inboundConn, req, inboundPacket, err := http.NewHTTPInbound(rwc) if err != nil { - log.Error(common.NewError("failed to start new inbound session:").Base(err)) + log.Error(common.NewError("failed to handle HTTP requests").Base(err)) + rwc.Close() return } - if inboundConn != nil { //CONNECT request + if inboundConn != nil { //CONNECT requests defer inboundConn.Close() if err := inboundConn.(protocol.NeedRespond).Respond(); err != nil { @@ -182,7 +183,7 @@ func (c *Client) handleHTTPConn(conn io.ReadWriteCloser) { log.Info("conn tunneling to", req) outboundConn.(protocol.NeedMeter).SetMeter(c.meter) proxy.ProxyConn(c.ctx, inboundConn, outboundConn, c.config.BufferSize) - } else { //GET/POST + } else { //GET/POST requests defer inboundPacket.Close() packetChan := make(chan *packetInfo, 512) errChan := make(chan error, 1) diff --git a/proxy/client/forward.go b/proxy/client/forward.go index 5a94f47..5fc97ac 100644 --- a/proxy/client/forward.go +++ b/proxy/client/forward.go @@ -149,7 +149,7 @@ func (f *Forward) listenUDP(errChan chan error) { } f.clientPackets <- &dispatchInfo{ addr: addr, - payload: buf[0:n], + payload: buf[:n], } } } diff --git a/proxy/client/mux.go b/proxy/client/mux.go index 2f63a40..8d186f6 100644 --- a/proxy/client/mux.go +++ b/proxy/client/mux.go @@ -105,10 +105,10 @@ func (m *MuxManager) DialToServer() (io.ReadWriteCloser, error) { defer m.Unlock() delete(m.muxPool, info.id) info.client.Close() - log.Info("somthing wrong with mux", info.id, ", closing") + log.Info("somthing wrong with mux client", info.id, ", closing") return nil, err } - log.Debug("new mux conn", info.id) + log.Debug("new mux conn established, client", info.id) info.lastActiveTime = time.Now() return stream, nil } @@ -146,7 +146,7 @@ func (m *MuxManager) checkAndCloseIdleMuxClient() { m.Lock() for id, info := range m.muxPool { info.client.Close() - log.Info("mux", id, "closed") + log.Info("mux client", id, "closed") } m.Unlock() return diff --git a/proxy/client/nat.go b/proxy/client/nat.go index face439..9aa414b 100644 --- a/proxy/client/nat.go +++ b/proxy/client/nat.go @@ -11,8 +11,8 @@ import ( "github.com/p4gefau1t/trojan-go/conf" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/protocol" - "github.com/p4gefau1t/trojan-go/protocol/nat" "github.com/p4gefau1t/trojan-go/protocol/simplesocks" + "github.com/p4gefau1t/trojan-go/protocol/tproxy" "github.com/p4gefau1t/trojan-go/protocol/trojan" "github.com/p4gefau1t/trojan-go/proxy" ) @@ -49,7 +49,7 @@ func (n *NAT) openOutboundConn(req *protocol.Request) (protocol.ConnSession, err } func (n *NAT) handleConn(conn net.Conn) { - inboundConn, req, err := nat.NewInboundConnSession(conn) + inboundConn, req, err := tproxy.NewInboundConnSession(conn) if err != nil { log.Error(common.NewError("failed to start inbound session").Base(err)) return @@ -66,7 +66,7 @@ func (n *NAT) handleConn(conn net.Conn) { } func (n *NAT) listenUDP(errChan chan error) { - inboundPacket, err := nat.NewInboundPacketSession(n.ctx, n.config) + inboundPacket, err := tproxy.NewInboundPacketSession(n.ctx, n.config) if err != nil { errChan <- err return @@ -125,7 +125,7 @@ func (n *NAT) listenTCP(errChan chan error) { } func (n *NAT) Run() error { - log.Info("nat running at", n.config.LocalAddress) + log.Info("tproxy running at", n.config.LocalAddress) errChan := make(chan error, 2) go n.listenUDP(errChan) go n.listenTCP(errChan) @@ -138,7 +138,7 @@ func (n *NAT) Run() error { } func (n *NAT) Close() error { - log.Info("shutting down nat...") + log.Info("shutting down tproxy...") n.cancel() if n.listener != nil { n.listener.Close() diff --git a/proxy/client/tls.go b/proxy/client/tls.go index 5ce1be9..3807443 100644 --- a/proxy/client/tls.go +++ b/proxy/client/tls.go @@ -29,7 +29,7 @@ func (m *TLSManager) DialToServer() (io.ReadWriteCloser, error) { if m.config.LogLevel == 0 { state := tlsConn.ConnectionState() chain := state.VerifiedChains - log.Debug("TLS handshaked", "cipher:", tls.CipherSuiteName(state.CipherSuite), "resume:", state.DidResume) + log.Debug("tls handshaked", "cipher:", tls.CipherSuiteName(state.CipherSuite), "resume:", state.DidResume) for i := range chain { for j := range chain[i] { log.Debug("subject:", chain[i][j].Subject, ", issuer:", chain[i][j].Issuer) diff --git a/proxy/server/server.go b/proxy/server/server.go index 2c3cb92..7c7022e 100644 --- a/proxy/server/server.go +++ b/proxy/server/server.go @@ -6,7 +6,6 @@ import ( "database/sql" "fmt" "net" - "reflect" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/conf" @@ -16,6 +15,7 @@ import ( "github.com/p4gefau1t/trojan-go/protocol/simplesocks" "github.com/p4gefau1t/trojan-go/protocol/trojan" "github.com/p4gefau1t/trojan-go/proxy" + "github.com/p4gefau1t/trojan-go/shadow" "github.com/p4gefau1t/trojan-go/stat" "github.com/xtaci/smux" ) @@ -28,6 +28,7 @@ type Server struct { auth stat.Authenticator meter stat.TrafficMeter config *conf.GlobalConfig + shadow *shadow.ShadowManager ctx context.Context cancel context.CancelFunc } @@ -62,8 +63,9 @@ func (s *Server) handleMuxConn(stream *smux.Stream) { } func (s *Server) handleConn(conn net.Conn) { - inboundConn, req, err := trojan.NewInboundConnSession(s.ctx, conn, s.config, s.auth) + inboundConn, req, err := trojan.NewInboundConnSession(s.ctx, conn, s.config, s.auth, s.shadow) if err != nil { + //once the auth is failed, the conn will be took over by shadow manager. don't close it log.Error(common.NewError("failed to start inbound session, remote:" + conn.RemoteAddr().String()).Base(err)) return } @@ -94,9 +96,9 @@ func (s *Server) handleConn(conn net.Conn) { return } defer outboundPacket.Close() - log.Info("UDP tunnel established") + log.Info("udp tunnel established") proxy.ProxyPacket(s.ctx, inboundPacket, outboundPacket) - log.Debug("UDP tunnel closed") + log.Debug("udp tunnel closed") return } @@ -112,39 +114,6 @@ func (s *Server) handleConn(conn net.Conn) { proxy.ProxyConn(s.ctx, inboundConn, outboundConn, s.config.BufferSize) } -func (s *Server) handleInvalidConn(conn net.Conn, tlsConn *tls.Conn) { - defer conn.Close() - if len(s.config.TLS.HTTPResponse) > 0 { - log.Warn("trying to response with a plain http response") - conn.Write(s.config.TLS.HTTPResponse) - return - } - - if s.config.TLS.FallbackAddress != nil { - defer func() { - if r := recover(); r != nil { - log.Error("recovered", r) - } - }() - //HACK - //obtain the bytes buffered by the tls conn - v := reflect.ValueOf(*tlsConn) - buf := v.FieldByName("rawInput").FieldByName("buf").Bytes() - log.Debug("payload:" + string(buf)) - - remote, err := net.Dial("tcp", s.config.TLS.FallbackAddress.String()) - if err != nil { - log.Warn(common.NewError("failed to dial to tls fallback server").Base(err)) - return - } - log.Warn("proxying this invalid tls conn to the tls fallback server") - remote.Write(buf) - proxy.ProxyConn(s.ctx, conn, remote, s.config.BufferSize) - } else { - log.Warn("tls fallback port is unspecified, closing") - } -} - func (s *Server) Run() error { var db *sql.DB var err error @@ -219,15 +188,36 @@ func (s *Server) Run() error { } return err } + log.Info("conn accepted from", conn.RemoteAddr()) go func(conn net.Conn) { - tlsConn := tls.Server(conn, tlsConfig) + rewindConn := common.NewRewindConn(conn) + rewindConn.R.SetBufferSize(512) + + tlsConn := tls.Server(rewindConn, tlsConfig) err = tlsConn.Handshake() + + rewindConn.R.StopBuffering() + if err != nil { - log.Warn(common.NewError("failed to perform tls handshake, remote:" + conn.RemoteAddr().String()).Base(err)) - go s.handleInvalidConn(conn, tlsConn) + rewindConn.R.Rewind() + err = common.NewError("failed to perform tls handshake with " + conn.RemoteAddr().String()).Base(err) + log.Warn(err) + if s.config.TLS.FallbackAddress != nil { + s.shadow.CommitScapegoat(&shadow.Scapegoat{ + Conn: rewindConn, + ShadowAddress: s.config.TLS.FallbackAddress, + Info: err.Error(), + }) + } else if s.config.TLS.HTTPResponse != nil { + rewindConn.Write(s.config.TLS.HTTPResponse) + rewindConn.Close() + } else { + rewindConn.Close() + } return } - go s.handleConn(tlsConn) + defer tlsConn.Close() + s.handleConn(tlsConn) }(conn) } } @@ -242,6 +232,7 @@ func (s *Server) Close() error { func (s *Server) Build(config *conf.GlobalConfig) (common.Runnable, error) { s.config = config s.ctx, s.cancel = context.WithCancel(context.Background()) + s.shadow = shadow.NewShadowManager(s.ctx, s.config) return s, nil } diff --git a/shadow/shadow.go b/shadow/shadow.go new file mode 100644 index 0000000..0ace986 --- /dev/null +++ b/shadow/shadow.go @@ -0,0 +1,66 @@ +package shadow + +import ( + "context" + "io" + "net" + + "github.com/p4gefau1t/trojan-go/common" + "github.com/p4gefau1t/trojan-go/conf" + "github.com/p4gefau1t/trojan-go/log" + "github.com/p4gefau1t/trojan-go/proxy" +) + +type Scapegoat struct { + Conn io.ReadWriteCloser + ShadowConn io.ReadWriteCloser + ShadowAddress *common.Address + Info string +} + +type ShadowManager struct { + config *conf.GlobalConfig + ctx context.Context + scapegoatChan chan *Scapegoat +} + +func (m *ShadowManager) CommitScapegoat(goat *Scapegoat) { + m.scapegoatChan <- goat + log.Debug("scapegoat commited") +} + +func (m *ShadowManager) handleScapegoat() { + for { + select { + case goat := <-m.scapegoatChan: + if goat.Info != "" { + log.Info("scapegoat: ", goat.Info) + } + if goat.ShadowConn == nil { + if goat.ShadowAddress == nil { + panic("incorrect shadow server") + } + var err error + goat.ShadowConn, err = net.Dial("tcp", goat.ShadowAddress.String()) + if err != nil { + log.Error(common.NewError("failed to dial to shadow server").Base(err)) + continue + } + } + go proxy.ProxyConn(m.ctx, goat.Conn, goat.ShadowConn, m.config.BufferSize) + case <-m.ctx.Done(): + log.Debug("shadow manager exiting..") + return + } + } +} + +func NewShadowManager(ctx context.Context, config *conf.GlobalConfig) *ShadowManager { + m := &ShadowManager{ + config: config, + ctx: ctx, + scapegoatChan: make(chan *Scapegoat, 1024), + } + go m.handleScapegoat() + return m +} diff --git a/test/proxy_test.go b/test/proxy_test.go index cc2cbfb..d7b3112 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -90,13 +90,14 @@ func getTLSConfig() conf.TLSConfig { panic("invalid cert") } c := conf.TLSConfig{ - SNI: "localhost", - CertPool: pool, - KeyPair: KeyPair, - Verify: true, - VerifyHostname: true, - ReuseSession: true, - SessionTicket: true, + SNI: "localhost", + CertPool: pool, + KeyPair: KeyPair, + Verify: true, + VerifyHostname: true, + ReuseSession: true, + SessionTicket: true, + FallbackAddress: common.NewAddress("127.0.0.1", 80, "tcp"), } return c } @@ -141,7 +142,7 @@ func addWsConfig(config *conf.GlobalConfig) *conf.GlobalConfig { Enabled: true, HostName: "127.0.0.1", Path: "/websocket", - Obfsucation: true, + Obfuscation: false, DoubleTLS: true, } return config @@ -269,16 +270,16 @@ func SingleThreadSpeedTestClientServer(b *testing.B, clientConfig *conf.GlobalCo conn.Write(payload) t2 := time.Now() speed := float64(mbytes) / t2.Sub(t1).Seconds() - b.Log("Single thread link speed:", speed, "MB/s") + b.Log("Single thread link speed:", speed*8/1024, "Gbps") conn.Close() cancel() } func TestIt(t *testing.T) { - //clientConfig := getBasicClientConfig() - //serverConfig := getBasicServerConfig() - //go RunClient(context.Background(), clientConfig) - //RunServer(context.Background(), serverConfig) + clientConfig := getBasicClientConfig() + serverConfig := getBasicServerConfig() + go RunClient(context.Background(), clientConfig) + RunServer(context.Background(), serverConfig) } func TestNormal(t *testing.T) { @@ -357,9 +358,15 @@ func TestHTTPProxy(t *testing.T) { wsClient, err := websocket.NewClient(wsConfig, conn) common.Must(err) buf := [100]byte{} - common.Must2(wsClient.Write([]byte("I'm GFW"))) - wsClient.Read(buf[:]) + common.Must2(wsClient.Write([]byte("I'm GFW1231231231231212391273871283719823791237912398721933123"))) + common.Must2(wsClient.Read(buf[:])) fmt.Println(buf) common.Must(err) + conn.Close() + + resp, err = http.Get("http://127.0.0.1:4445") + common.Must(err) + resp.Body.Read(buf[:]) + fmt.Println(buf) cancel() } diff --git a/test/socks.go b/test/socks.go deleted file mode 100644 index a96b99e..0000000 --- a/test/socks.go +++ /dev/null @@ -1,318 +0,0 @@ -package test - -// Copyright 2012, Hailiang Wang. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Package socks implements a SOCKS (SOCKS4, SOCKS4A and SOCKS5) proxy client. - -A complete example using this package: - package main - - import ( - "h12.io/socks" - "fmt" - "net/http" - "io/ioutil" - ) - - func main() { - dialSocksProxy := socks.Dial("socks5://127.0.0.1:1080?timeout=5s") - tr := &http.Transport{Dial: dialSocksProxy} - httpClient := &http.Client{Transport: tr} - - bodyText, err := TestHttpsGet(httpClient, "https://h12.io/about") - if err != nil { - fmt.Println(err.Error()) - } - fmt.Print(bodyText) - } - - func TestHttpsGet(c *http.Client, url string) (bodyText string, err error) { - resp, err := c.Get(url) - if err != nil { return } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { return } - bodyText = string(body) - return - } -*/ - -import ( - "errors" - "fmt" - "net" - "net/url" - "strconv" - "time" -) - -// Constants to choose which version of SOCKS protocol to use. -const ( - SOCKS4 = iota - SOCKS4A - SOCKS5 -) - -type ( - Config struct { - Proto int - Host string - Auth Auth - Timeout time.Duration - } - Auth struct { - Username string - Password string - } -) - -func parse(proxyURI string) (*Config, error) { - uri, err := url.Parse(proxyURI) - if err != nil { - return nil, err - } - cfg := &Config{} - switch uri.Scheme { - case "socks4": - cfg.Proto = SOCKS4 - case "socks4a": - cfg.Proto = SOCKS4A - case "socks5": - cfg.Proto = SOCKS5 - default: - return nil, fmt.Errorf("unknown SOCKS protocol %s", uri.Scheme) - } - cfg.Host = uri.Host - if uri.User != nil { - cfg.Auth.Username = uri.User.Username() - cfg.Auth.Password, _ = uri.User.Password() - } - query := uri.Query() - timeout := query.Get("timeout") - if timeout != "" { - var err error - cfg.Timeout, err = time.ParseDuration(timeout) - if err != nil { - return nil, err - } - } - return cfg, nil -} - -// Dial returns the dial function to be used in http.Transport object. -// Argument proxyURI should be in the format: "socks5://user:password@127.0.0.1:1080?timeout=5s". -// The protocol could be socks5, socks4 and socks4a. -func Dial(proxyURI string) func(string, string) (net.Conn, error) { - cfg, err := parse(proxyURI) - if err != nil { - return dialError(err) - } - return cfg.dialFunc() -} - -// DialSocksProxy returns the dial function to be used in http.Transport object. -// Argument socksType should be one of SOCKS4, SOCKS4A and SOCKS5. -// Argument proxy should be in this format "127.0.0.1:1080". -func DialSocksProxy(socksType int, proxy string) func(string, string) (net.Conn, error) { - return (&Config{Proto: socksType, Host: proxy}).dialFunc() -} - -func (c *Config) dialFunc() func(string, string) (net.Conn, error) { - switch c.Proto { - case SOCKS5: - return func(_, targetAddr string) (conn net.Conn, err error) { - return c.dialSocks5(targetAddr) - } - case SOCKS4, SOCKS4A: - return func(_, targetAddr string) (conn net.Conn, err error) { - return c.dialSocks4(targetAddr) - } - } - return dialError(fmt.Errorf("unknown SOCKS protocol %v", c.Proto)) -} - -func (cfg *Config) dialSocks5(targetAddr string) (conn net.Conn, err error) { - proxy := cfg.Host - - // dial TCP - conn, err = net.Dial("tcp", proxy) - if err != nil { - return - } - - // version identifier/method selection request - req := []byte{ - 5, // version number - 1, // number of methods - 0, // method 0: no authentication (only anonymous access supported for now) - } - resp, err := cfg.sendReceive(conn, req) - if err != nil { - return - } else if len(resp) != 2 { - err = errors.New("Server does not respond properly.") - return - } else if resp[0] != 5 { - err = errors.New("Server does not support Socks 5.") - return - } else if resp[1] != 0 { // no auth - err = errors.New("socks method negotiation failed.") - return - } - - // detail request - host, port, err := splitHostPort(targetAddr) - if err != nil { - return nil, err - } - req = []byte{ - 5, // version number - //1, // connect command - 3, // associate command - 0, // reserved, must be zero - 3, // address type, 3 means domain name - byte(len(host)), // address length - } - req = append(req, []byte(host)...) - req = append(req, []byte{ - byte(port >> 8), // higher byte of destination port - byte(port), // lower byte of destination port (big endian) - }...) - resp, err = cfg.sendReceive(conn, req) - if err != nil { - return - } else if len(resp) != 10 { - err = errors.New("Server does not respond properly.") - } else if resp[1] != 0 { - err = errors.New("Can't complete SOCKS5 connection.") - } - - return -} - -func (cfg *Config) dialSocks4(targetAddr string) (conn net.Conn, err error) { - socksType := cfg.Proto - proxy := cfg.Host - - // dial TCP - conn, err = net.Dial("tcp", proxy) - if err != nil { - return - } - - // connection request - host, port, err := splitHostPort(targetAddr) - if err != nil { - return - } - ip := net.IPv4(0, 0, 0, 1).To4() - if socksType == SOCKS4 { - ip, err = lookupIP(host) - if err != nil { - return - } - } - req := []byte{ - 4, // version number - 1, // command CONNECT - byte(port >> 8), // higher byte of destination port - byte(port), // lower byte of destination port (big endian) - ip[0], ip[1], ip[2], ip[3], // special invalid IP address to indicate the host name is provided - 0, // user id is empty, anonymous proxy only - } - if socksType == SOCKS4A { - req = append(req, []byte(host+"\x00")...) - } - - resp, err := cfg.sendReceive(conn, req) - if err != nil { - return - } else if len(resp) != 8 { - err = errors.New("Server does not respond properly.") - return - } - switch resp[1] { - case 90: - // request granted - case 91: - err = errors.New("Socks connection request rejected or failed.") - case 92: - err = errors.New("Socks connection request rejected becasue SOCKS server cannot connect to identd on the client.") - case 93: - err = errors.New("Socks connection request rejected because the client program and identd report different user-ids.") - default: - err = errors.New("Socks connection request failed, unknown error.") - } - // clear the deadline before returning - if err := conn.SetDeadline(time.Time{}); err != nil { - return nil, err - } - return -} - -func (cfg *Config) sendReceive(conn net.Conn, req []byte) (resp []byte, err error) { - if cfg.Timeout > 0 { - if err := conn.SetWriteDeadline(time.Now().Add(cfg.Timeout)); err != nil { - return nil, err - } - } - _, err = conn.Write(req) - if err != nil { - return - } - resp, err = cfg.readAll(conn) - return -} - -func (cfg *Config) readAll(conn net.Conn) (resp []byte, err error) { - resp = make([]byte, 1024) - if cfg.Timeout > 0 { - if err := conn.SetReadDeadline(time.Now().Add(cfg.Timeout)); err != nil { - return nil, err - } - } - n, err := conn.Read(resp) - resp = resp[:n] - return -} - -func lookupIP(host string) (ip net.IP, err error) { - ips, err := net.LookupIP(host) - if err != nil { - return - } - if len(ips) == 0 { - err = fmt.Errorf("Cannot resolve host: %s.", host) - return - } - ip = ips[0].To4() - if len(ip) != net.IPv4len { - fmt.Println(len(ip), ip) - err = errors.New("IPv6 is not supported by SOCKS4.") - return - } - return -} - -func splitHostPort(addr string) (host string, port uint16, err error) { - host, portStr, err := net.SplitHostPort(addr) - if err != nil { - return "", 0, err - } - portInt, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { - return "", 0, err - } - port = uint16(portInt) - return -} - -func dialError(err error) func(string, string) (net.Conn, error) { - return func(_, _ string) (net.Conn, error) { - return nil, err - } -}