背景
tcp 隧道我们见得比较多了, 在 这篇文章 就给了一些来例子, 其中有一些 tcp 隧道是用来穿越防火墙, 或者 "科学上网"; 但是如果去看这些隧道的实现, 本质上都是基于 http 的 connect 方法, 具体区别可以看这个 https://en.wikipedia.org/wiki/HTTP_tunnel , 即实现其实是使用 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) 并没有考虑性能优化, 实际上, 很多点性能可以大幅优化
首先我们要实现一个 client, 一个 server,client 监听本地的 sock5 端口, 转发 tcp 请求到 server 端, 这时候 tcp 请求被转化为 http 请求;
server 端收到请求之后代替 client 像远端建立 tcp 连接, 将 tcp 连接中的数据返回到 client
client 使用 http 请求模拟一个 tcp 连接, 因此, 我们要有三种请求 "connect", "write", "read"
server 端需要保持对远端的 连接, 即一个 conncetion, 这点很重要, 如果用 faas 实现, 那么 faas 的实例数量要限制为 1(即使用单实例并发, 这点 腾讯云的 scf 还没有支持, 阿里云已经支持)
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 的
部署 server 端代码到一台云主机并启动
启动 client, 并且 client 设置对 google.com 走 http tunnel
设置本地代理走 sock5, 以我的 macos 为例, 设置 网络 > Wi-Fi > 高级 > 代理 > socks 代理 [填写 client 监听 ip]
演示
- # 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 隧道实现之后可以在上面做更多复杂的功能, 接下来就可以发挥想象力了.
完整的代码在这里 https://github.com/u2takey/v2simple
来源: https://www.qcloud.com/developer/article/1882313