From 7f6b4ceb12bb875ef51ce8821819d277ead2bce1 Mon Sep 17 00:00:00 2001 From: mzz2017 Date: Mon, 20 Jun 2022 14:55:56 +0800 Subject: [PATCH] feat: support gRPC client --- feature.go | 1 + go.mod | 9 +- go.sum | 25 +++ proxy/grpc/client.go | 408 +++++++++++++++++++++++++++++++++++++++++++ proxy/grpc/grpc.go | 69 ++++++++ 5 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 proxy/grpc/client.go create mode 100644 proxy/grpc/grpc.go diff --git a/feature.go b/feature.go index 21b49fc..3222199 100644 --- a/feature.go +++ b/feature.go @@ -5,6 +5,7 @@ import ( // _ "github.com/nadoo/glider/service/xxx" // comment out the protocols you don't need to make the compiled binary smaller. + _ "github.com/nadoo/glider/proxy/grpc" _ "github.com/nadoo/glider/proxy/http" _ "github.com/nadoo/glider/proxy/kcp" _ "github.com/nadoo/glider/proxy/mixed" diff --git a/go.mod b/go.mod index f448d7d..639890c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/nadoo/glider go 1.18 require ( + github.com/Qv2ray/gun v0.0.0-20210314140700-95a65981f2f8 github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb @@ -12,11 +13,15 @@ require ( github.com/nadoo/ipset v0.5.0 github.com/xtaci/kcp-go/v5 v5.6.1 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e + golang.org/x/net v0.0.0-20220531201128-c960675eff93 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a + google.golang.org/grpc v1.36.0 ) require ( github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 // indirect + github.com/golang/protobuf v1.4.3 // indirect + github.com/google/go-cmp v0.5.7 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect github.com/klauspost/reedsolomon v1.9.16 // indirect github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 // indirect @@ -26,7 +31,9 @@ require ( github.com/templexxx/xorsimd v0.4.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect - golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect + google.golang.org/protobuf v1.25.0 // indirect ) // Replace dependency modules with local developing copy diff --git a/go.sum b/go.sum index dbbe04a..2cf7520 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Qv2ray/gun v0.0.0-20210314140700-95a65981f2f8 h1:vXZO6gMcQwOcFR8cF/brd/F6vP5pk5fAI7Az6avsbFw= +github.com/Qv2ray/gun v0.0.0-20210314140700-95a65981f2f8/go.mod h1:BxLCVtH80Ox0wPYPeX1BjEXQGUI2cTONpACpac6Ri98= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 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/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d h1:CPqTNIigGweVPT4CYb+OO2E6XyRKFOmvTHwWRLgCAlE= @@ -16,7 +19,9 @@ github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152/go.mod h1:I9fhc/EvS github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 h1:fBHFH+Y/GPGFGo7LIrErQc3p2MeAhoIQNgaxPWYsSxk= github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:ucvhdsUCE3TH0LoLRb6ShHiJl8e39dGlx6A4g/ujlow= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -29,14 +34,20 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f h1:l1QCwn715k8nYkj4Ql50rzEog3WnMdrd4YYMMwemxEo= @@ -84,6 +95,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= @@ -169,6 +181,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -181,22 +195,33 @@ golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/proxy/grpc/client.go b/proxy/grpc/client.go new file mode 100644 index 0000000..ebd62f3 --- /dev/null +++ b/proxy/grpc/client.go @@ -0,0 +1,408 @@ +package grpc + +import ( + "context" + "crypto/x509" + "fmt" + "github.com/Qv2ray/gun/pkg/cert" + "github.com/Qv2ray/gun/pkg/proto" + "github.com/nadoo/glider/pkg/pool" + "github.com/nadoo/glider/proxy" + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/keepalive" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" + "io" + "net" + "os" + "sync" + "time" +) + +// https://github.com/v2fly/v2ray-core/blob/v5.0.6/transport/internet/grpc/dial.go +var ( + globalCCMap map[string]*grpc.ClientConn + globalCCAccess sync.Mutex +) + +type ccCanceller func() + +type ClientConn struct { + tun proto.GunService_TunClient + closer context.CancelFunc + muReading sync.Mutex // muReading protects reading + muWriting sync.Mutex // muWriting protects writing + muRecv sync.Mutex // muReading protects recv + muSend sync.Mutex // muWriting protects send + buf []byte + offset int + + deadlineMu sync.Mutex + readDeadline *time.Timer + writeDeadline *time.Timer + readClosed chan struct{} + writeClosed chan struct{} + closed chan struct{} +} + +func NewClientConn(tun proto.GunService_TunClient, closer context.CancelFunc) *ClientConn { + return &ClientConn{ + tun: tun, + closer: closer, + readClosed: make(chan struct{}), + writeClosed: make(chan struct{}), + closed: make(chan struct{}), + } +} + +type RecvResp struct { + hunk *proto.Hunk + err error +} + +func (c *ClientConn) Read(p []byte) (n int, err error) { + select { + case <-c.readClosed: + return 0, os.ErrDeadlineExceeded + case <-c.closed: + return 0, io.EOF + default: + } + + c.muReading.Lock() + defer c.muReading.Unlock() + if c.buf != nil { + n = copy(p, c.buf[c.offset:]) + c.offset += n + if c.offset == len(c.buf) { + pool.PutBuffer(c.buf) + c.buf = nil + } + return n, nil + } + // set 1 to avoid channel leak + readDone := make(chan RecvResp, 1) + // pass channel to the function to avoid closure leak + go func(readDone chan RecvResp) { + // FIXME: not really abort the send so there is some problems when recover + c.muRecv.Lock() + defer c.muRecv.Unlock() + recv, e := c.tun.Recv() + readDone <- RecvResp{ + hunk: recv, + err: e, + } + }(readDone) + select { + case <-c.readClosed: + return 0, os.ErrDeadlineExceeded + case <-c.closed: + return 0, io.EOF + case recvResp := <-readDone: + err = recvResp.err + if err != nil { + if code := status.Code(err); code == codes.Unavailable || status.Code(err) == codes.OutOfRange { + err = io.EOF + } + return 0, err + } + n = copy(p, recvResp.hunk.Data) + c.buf = pool.GetBuffer(len(recvResp.hunk.Data) - n) + copy(c.buf, recvResp.hunk.Data[n:]) + c.offset = 0 + return n, nil + } +} + +func (c *ClientConn) Write(p []byte) (n int, err error) { + select { + case <-c.writeClosed: + return 0, os.ErrDeadlineExceeded + case <-c.closed: + return 0, io.EOF + default: + } + + c.muWriting.Lock() + defer c.muWriting.Unlock() + // set 1 to avoid channel leak + sendDone := make(chan error, 1) + // pass channel to the function to avoid closure leak + go func(sendDone chan error) { + // FIXME: not really abort the send so there is some problems when recover + c.muSend.Lock() + defer c.muSend.Unlock() + e := c.tun.Send(&proto.Hunk{Data: p}) + sendDone <- e + }(sendDone) + select { + case <-c.writeClosed: + return 0, os.ErrDeadlineExceeded + case <-c.closed: + return 0, io.EOF + case err = <-sendDone: + if code := status.Code(err); code == codes.Unavailable || status.Code(err) == codes.OutOfRange { + err = io.EOF + } + return len(p), err + } +} + +func (c *ClientConn) Close() error { + select { + case <-c.closed: + default: + close(c.closed) + } + c.closer() + return nil +} +func (c *ClientConn) CloseWrite() error { + return c.tun.CloseSend() +} +func (c *ClientConn) LocalAddr() net.Addr { + // FIXME + return nil +} +func (c *ClientConn) RemoteAddr() net.Addr { + p, _ := peer.FromContext(c.tun.Context()) + return p.Addr +} + +func (c *ClientConn) SetDeadline(t time.Time) error { + c.deadlineMu.Lock() + defer c.deadlineMu.Unlock() + if now := time.Now(); t.After(now) { + // refresh the deadline if the deadline has been exceeded + select { + case <-c.readClosed: + c.readClosed = make(chan struct{}) + default: + } + select { + case <-c.writeClosed: + c.writeClosed = make(chan struct{}) + default: + } + // reset the deadline timer to make the c.readClosed and c.writeClosed with the new pointer (if it is) + if c.readDeadline != nil { + c.readDeadline.Stop() + } + c.readDeadline = time.AfterFunc(t.Sub(now), func() { + c.deadlineMu.Lock() + defer c.deadlineMu.Unlock() + select { + case <-c.readClosed: + default: + close(c.readClosed) + } + }) + if c.writeDeadline != nil { + c.writeDeadline.Stop() + } + c.writeDeadline = time.AfterFunc(t.Sub(now), func() { + c.deadlineMu.Lock() + defer c.deadlineMu.Unlock() + select { + case <-c.writeClosed: + default: + close(c.writeClosed) + } + }) + } else { + select { + case <-c.readClosed: + default: + close(c.readClosed) + } + select { + case <-c.writeClosed: + default: + close(c.writeClosed) + } + } + return nil +} + +func (c *ClientConn) SetReadDeadline(t time.Time) error { + c.deadlineMu.Lock() + defer c.deadlineMu.Unlock() + if now := time.Now(); t.After(now) { + // refresh the deadline if the deadline has been exceeded + select { + case <-c.readClosed: + c.readClosed = make(chan struct{}) + default: + } + // reset the deadline timer to make the c.readClosed and c.writeClosed with the new pointer (if it is) + if c.readDeadline != nil { + c.readDeadline.Stop() + } + c.readDeadline = time.AfterFunc(t.Sub(now), func() { + c.deadlineMu.Lock() + defer c.deadlineMu.Unlock() + select { + case <-c.readClosed: + default: + close(c.readClosed) + } + }) + } else { + select { + case <-c.readClosed: + default: + close(c.readClosed) + } + } + return nil +} + +func (c *ClientConn) SetWriteDeadline(t time.Time) error { + c.deadlineMu.Lock() + defer c.deadlineMu.Unlock() + if now := time.Now(); t.After(now) { + // refresh the deadline if the deadline has been exceeded + select { + case <-c.writeClosed: + c.writeClosed = make(chan struct{}) + default: + } + if c.writeDeadline != nil { + c.writeDeadline.Stop() + } + c.writeDeadline = time.AfterFunc(t.Sub(now), func() { + c.deadlineMu.Lock() + defer c.deadlineMu.Unlock() + select { + case <-c.writeClosed: + default: + close(c.writeClosed) + } + }) + } else { + select { + case <-c.writeClosed: + default: + close(c.writeClosed) + } + } + return nil +} + +// NewGRPCDialer returns a gRPC proxy dialer. +func NewGRPCDialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + w, err := NewGRPC(s, d, nil) + if err != nil { + return nil, fmt.Errorf("[gRPC] create instance error: %s", err) + } + + if w.certFile != "" { + certData, err := os.ReadFile(w.certFile) + if err != nil { + return nil, fmt.Errorf("[gRPC] read cert file error: %s", err) + } + + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(certData) { + return nil, fmt.Errorf("[gRPC] can not append cert file: %s", w.certFile) + } + w.certPool = certPool + } else { + w.certPool, err = cert.GetSystemCertPool() + if err != nil { + return nil, fmt.Errorf("failed to get system certificate pool") + } + } + + return w, err +} + +// Addr returns forwarder's address. +func (g *GRPC) Addr() string { + if g.addr == "" { + return g.dialer.Addr() + } + return g.addr +} + +// Dial connects to the address addr on the network net via the proxy. +func (g *GRPC) Dial(network string, address string) (net.Conn, error) { + cc, cancel, err := getGrpcClientConn(g.dialer, g.addr, g.serverName, g.certPool, g.skipVerify) + if err != nil { + cancel() + return nil, err + } + client := proto.NewGunServiceClient(cc) + + clientX := client.(proto.GunServiceClientX) + serviceName := g.serviceName + if serviceName == "" { + serviceName = "GunService" + } + // ctx is the lifetime of the tun + ctxStream, streamCloser := context.WithCancel(context.Background()) + tun, err := clientX.TunCustomName(ctxStream, serviceName) + if err != nil { + streamCloser() + return nil, err + } + return NewClientConn(tun, streamCloser), nil +} + +// DialUDP connects to the given address via the proxy. +func (g *GRPC) DialUDP(network, addr string) (net.PacketConn, error) { + return nil, proxy.ErrNotSupported +} + +func getGrpcClientConn(dialer proxy.Dialer, address string, serverName string, certPool *x509.CertPool, skipVerify bool) (*grpc.ClientConn, ccCanceller, error) { + globalCCAccess.Lock() + if globalCCMap == nil { + globalCCMap = make(map[string]*grpc.ClientConn) + } + globalCCAccess.Unlock() + + canceller := func() { + globalCCAccess.Lock() + defer globalCCAccess.Unlock() + globalCCMap[address].Close() + delete(globalCCMap, address) + } + + // TODO Should support chain proxy to the same destination + globalCCAccess.Lock() + if client, found := globalCCMap[address]; found && client.GetState() != connectivity.Shutdown { + globalCCAccess.Unlock() + return client, canceller, nil + } + globalCCAccess.Unlock() + dialOptions := []grpc.DialOption{ + grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(certPool, serverName)), + grpc.WithContextDialer(func(ctxGrpc context.Context, s string) (net.Conn, error) { + return dialer.Dial("tcp", s) + }), grpc.WithConnectParams(grpc.ConnectParams{ + Backoff: backoff.Config{ + BaseDelay: 500 * time.Millisecond, + Multiplier: 1.5, + Jitter: 0.2, + MaxDelay: 19 * time.Second, + }, + MinConnectTimeout: 5 * time.Second, + }), grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: 30 * time.Second, + Timeout: 10 * time.Second, + PermitWithoutStream: true, + }), + } + if skipVerify { + dialOptions = append(dialOptions, grpc.WithInsecure()) + } + cc, err := grpc.Dial(address, dialOptions...) + globalCCAccess.Lock() + globalCCMap[address] = cc + globalCCAccess.Unlock() + return cc, canceller, err +} diff --git a/proxy/grpc/grpc.go b/proxy/grpc/grpc.go new file mode 100644 index 0000000..36ea7ba --- /dev/null +++ b/proxy/grpc/grpc.go @@ -0,0 +1,69 @@ +package grpc + +import ( + "crypto/x509" + "fmt" + "github.com/nadoo/glider/proxy" + "net" + "net/url" + "strings" +) + +func init() { + proxy.RegisterDialer("grpc", NewGRPCDialer) +} + +// GRPC is the base gRPC proxy struct. +type GRPC struct { + dialer proxy.Dialer + addr string + certPool *x509.CertPool + serviceName string + serverName string + skipVerify bool + certFile string +} + +// NewGRPC returns a websocket proxy. +func NewGRPC(s string, d proxy.Dialer, p proxy.Proxy) (*GRPC, error) { + u, err := url.Parse(s) + if err != nil { + return nil, fmt.Errorf("parse url err: %s", err) + } + + addr := u.Host + if addr == "" && d != nil { + addr = d.Addr() + } + + if _, p, _ := net.SplitHostPort(addr); p == "" { + addr = net.JoinHostPort(addr, "443") + } + + query := u.Query() + g := &GRPC{ + dialer: d, + addr: addr, + skipVerify: query.Get("skipVerify") == "true", + serviceName: query.Get("serviceName"), + serverName: query.Get("serverName"), + certFile: query.Get("cert"), + } + + if g.serviceName == "" { + g.serviceName = "GunService" + } + + if g.serverName == "" { + g.serverName = g.addr[:strings.LastIndex(g.addr, ":")] + } + + return g, nil +} + +func init() { + proxy.AddUsage("grpc", ` +gRPC client scheme: + grpc://host:port[?serviceName=SERVICENAME][&serverName=SERVERNAME][&skipVerify=true][&cert=PATH] +`) +}