tcp 隧道我们见得比较多了,在 这篇文章 就给了一些来例子,其中有一些 tcp 隧道是用来穿越防火墙,或者 "科学上网"; 但是如果去看这些隧道的实现,本质上都是基于 http 的 connect 方法,具体区别可以看这个 wiki, 即实现其实是使用 http 的连接方法,然后 reuse http 底层的 conncetion,比如 websocket 等也是基于类似的实现
Example negotiation
The client connects to the proxy server and requests tunneling by specifying the port and the host computer to which it would like to connect. The port is used to indicate the protocol being requested.[3]
CONNECT streamline.t-mobile.com:22 HTTP/1.1
Proxy-Authorization: Basic encoded-credentials
If the connection was allowed and the proxy has connected to the specified host then the proxy will return a 2XX success response.[3]
HTTP/1.1 200 OK
但是很多时候 http 底层的 connection 我们都不能使用,即无法基于 connect 实现,只能只用 put, get, delete, post 方法,甚至,如果我们使用 faas 实现,比如腾讯云上的 scf,我们甚至连这几种方法都没有,我们只能假设所有的方法都是 post.
如果是这种情况,我们该如何实现呢。
注意:本实现仅仅是 poc(prove of concept)并没有考虑性能优化,实际上,很多点性能可以大幅优化
sequenceDiagram
local->>client: tcp 代理本地的请求
client->>server: http 请求,类型: connect
server->>remote: tcp 连接到远端, 读写数据
client->>server: http 请求,类型: write
client->>server: http 请求,类型: read
client->>local: tcp 请求返回
为了快速开始,我们 fork 了一个基础的项目: https://github.com/jarvisgally/v2simple, 这个项目实现了一套基础设施(即协议),我们在这上面实现基于 http/faas 的两套实现【再一次声明,这套 http 实现没有使用 connect 方法】
其中 http 的实现主体部分如下(faas 的实现也是类似的,注意代码里面省略了很多,仅仅演示了核心的部分)
const Name = "http"
type HttpClient struct {
client *resty.Client
addr string
}
func NewHttpClient(url *url.URL) (proxy.Client, error) {
return &HttpClient{
client: resty.New(),
addr: url.String(),
}, nil
}
func (c *HttpClient) Handshake(_ net.Conn, target string) (io.ReadWriter, error) {
conn := &httpConnection{
client: c,
target: target,
connectionId: RandStringRunes(8),
}
return conn, conn.Connect()
}
func (c *HttpClient) post(r *TunnelRequest) (*TunnelResponse, error) {
ret := &TunnelResponse{}
_, err := c.client.NewRequest().SetResult(ret).SetBody(r).SetHeader("Content-Type", "application/json").Post(c.addr)
return ret, err
}
type TunnelRequest struct {
Target string
Action string // create, read, write
Data []byte
ConnectionId string
}
type TunnelResponse struct {
Target string
Action string
Data []byte
ConnectionId string
Eof bool
Code int
Message string
}
type httpConnection struct {
client *HttpClient
target string
readBuffer []byte
writeBuffer []byte
connectionId string
eof bool
lastWrite time.Time
}
func (c *httpConnection) Connect() (err error) {
_, err = c.client.post(&TunnelRequest{
Target: c.target,
Action: "connect",
ConnectionId: c.connectionId,
})
return err
}
func (c *httpConnection) Read(p []byte) (n int, err error) {
if c.eof {
return 0, io.EOF
}
if len(c.readBuffer) == 0 {
resp, err := c.client.post(&TunnelRequest{
Target: c.target,
Action: "read",
ConnectionId: c.connectionId,
})
if err != nil {
return 0, err
}
c.readBuffer = append(c.readBuffer, resp.Data...)
if resp.Eof {
c.eof = true
}
}
n = copy(p, c.readBuffer)
c.readBuffer = c.readBuffer[:len(c.readBuffer)-n]
return n, nil
}
func (c *httpConnection) Write(p []byte) (n int, err error) {
c.writeBuffer = append(c.writeBuffer, p...)
if len(c.writeBuffer) > 1024 || time.Now().Sub(c.lastWrite) > time.Millisecond*100 {
resp, err := c.client.post(&TunnelRequest{
Target: c.target,
Action: "write",
Data: c.writeBuffer,
ConnectionId: c.connectionId,
})
if err != nil {
return 0, err
}
_ = resp
c.writeBuffer = c.writeBuffer[:0]
}
return len(p), nil
}
由于 scf 暂时不支持单实例并发,我们暂时只部署 http 版本,但是 faas 版本我们有理由相信他是 work 的
# client 端启动
➜ go build -o main .
v2simple/cmd/client on master by 🐹 v1.16 ☪️ 21:29:49
➜ ./main -f client.example.http.json
V2Simple 0.1.0 (V2Simple, a simple implementation of V2Ray 4.25.0), go1.16 darwin amd64
2021/09/25 21:30:02 using /Users/leiwang/Work/go-librarys/src/github.com/u2takey/v2simple/cmd/client/client.example.http.json
2021/09/25 21:30:02 using /Users/leiwang/Work/go-librarys/src/github.com/u2takey/v2simple/cmd/client/blacklist
2021/09/25 21:30:02 socks5 listening TCP on 127.0.0.1:1081
# server 端启动
ubuntu@VM-0-7-ubuntu:~$ ./main
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST / --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8081
打开 google.com, 连接成功, tcp 隧道实现之后可以在上面做更多复杂的功能,接下来就可以发挥想象力了.
完整的代码在这里
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。