前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于 faas、http 的 tcp 隧道

基于 faas、http 的 tcp 隧道

原创
作者头像
王磊-字节跳动
发布2021-09-25 21:37:03
1.4K0
发布2021-09-25 21:37:03
举报
文章被收录于专栏:01ZOO01ZOO

背景

tcp 隧道我们见得比较多了,在 这篇文章 就给了一些来例子,其中有一些 tcp 隧道是用来穿越防火墙,或者 "科学上网"; 但是如果去看这些隧道的实现,本质上都是基于 http 的 connect 方法,具体区别可以看这个 wiki, 即实现其实是使用 http 的连接方法,然后 reuse http 底层的 conncetion,比如 websocket 等也是基于类似的实现

代码语言:txt
复制
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)并没有考虑性能优化,实际上,很多点性能可以大幅优化

  1. 首先我们要实现一个 client, 一个 server,client 监听本地的 sock5 端口,转发 tcp 请求到 server 端,这时候 tcp 请求被转化为 http 请求;
  2. server 端收到请求之后代替 client 像远端建立 tcp 连接,将 tcp 连接中的数据返回到 client
  3. client 使用 http 请求模拟一个 tcp 连接,因此,我们要有三种请求 "connect", "write", "read"
  4. server 端需要保持对远端的 连接,即一个 conncetion,这点很重要,如果用 faas 实现,那么 faas 的实例数量要限制为 1(即使用单实例并发,这点 腾讯云的 scf 还没有支持,阿里云已经支持)
代码语言:txt
复制
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 的实现也是类似的,注意代码里面省略了很多,仅仅演示了核心的部分)

代码语言:txt
复制
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 的

  1. 部署 server 端代码到一台云主机并启动
  2. 启动 client,并且 client 设置对 google.com 走 http tunnel
  3. 设置本地代理走 sock5, 以我的 macos 为例,设置 网络>wifi>高级>代理>socks代理【填写 client 监听 ip】

演示

代码语言:txt
复制
# 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
代码语言:txt
复制
# 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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 实现
  • 部署
  • 演示
相关产品与服务
云服务器
云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com