记录用

Gost源码分析心得

2019.07.31

转载

最近在研究gost的源码,想看看它是怎么实现代理的。虽然之前研究过goproxy-vps的源码,但是两者还是有一定区别的。goproxy-vps已经停更很久了,最后一次更新还是在2017年,原因大家都懂得。但是我发现作者还是有提供免费的goproxy-vps服务供大家使用,只是不再对外公布源码了而已,别问我是怎么知道的,哼,我是不会告诉你的。

至于gost,我之前就关注过,但没用过,之所以选择它的其中一点是,作为一款比较早的代理软件,到现在依然保持着更新。经过了这么多年的发展,gost成功保持着自己小众软件的用户群。这也是我看中它的原因之一。太热门的软件往往容易引发口水战,容易成为GFW的目标。其实在研究gost之前,我先后关注了goflyway以及和goproxy同名的代理另一个goproxy。在VPS被封,弄懂借用CDN复活VPS的原理之前,goflyway充当了我那阵子的主力,只是因为在弄懂了goflyway的工作原理之后发现,它毕竟只是一个加密的http代理,功能稍显单一。而另一个goproxy的作者已经走往商业化,代码也是延迟公布,部分代码已经被删除,所以最后我选择了gost。

gost不仅支持websocket协议(套CDN的关键),用它替代goflyway,还支持目前几乎所有主流的协议。造成gost小众的很大一部分原因就是因为作者很懒,不提供客户端。这对小白用户来说是致命的。v2ray以及SS之所以热门的原因就是因为它们都有比较友好的客户端。这里说一个我放弃v2ray的原因,v2ray的配置文件实在是太他妈复杂了。相比而已,gost的类url的配置写法就显得很方便,很简洁了。还有就是V2ray实在是太有名了,太有名的结果就是作者容易被请喝茶。

说回gost的源码分析,gost支持命令行以及配置文件两种配置方式。命令行最后也是转化为配置文件的写法,所以我们以配置文件为主来介绍。默认配置示例位于.config隐藏文件夹下的gost.json文件

{
    "Retries": 1,
    "Debug": false,
    "ServeNodes": [
    ],
    "ChainNodes": [
    ],

    "Routes": []
}

其中ServeNodes属性对应命令行-L参数,ChainNodes属性对应命令行-F参数,Debug属性对应-D参数。

gost.json配置文件对应的是程序中的baseConfig结构体。

// cfg.go
type baseConfig struct {
	route
	Routes []route
	Debug  bool
}

// route.go
type stringList []string

type route struct {
	ServeNodes stringList
	ChainNodes stringList
	Retries    int
}

其中,route结构体是gost的配置文件中的服务端(-L)和客户端(-F)的合集。

什么是ServeNodes?服务节点,可以看作是gost的服务端。

什么是ChainNodes?代理链节点,可以看作是gost的客户端。

gost本身就是客户端和服务端一体的程序(貌似现在的主流都是这样的)。

程序主流程

可以用一张图概括

img

客户端是怎么生成的

route.go文件中,route结构体下有一个parseChain方法,它是生成客户端的核心。我们先看一下该方法的返回值gost.Chain的组成

// Chain is a proxy chain that holds a list of proxy node groups.
type Chain struct {
	isRoute    bool
	Retries    int
	nodeGroups []*NodeGroup
	route      []Node // nodes in the selected route
}

这么看是看不出它和客户端的联系,要结合gost.NodeGroup结构体和gost.Node结构体一起看。两个结构体都位于node.go文件

// NodeGroup is a group of nodes.
type NodeGroup struct {
	ID              int
	nodes           []Node
	selectorOptions []SelectOption
	selector        NodeSelector
	mux             sync.RWMutex
}

// Node is a proxy node, mainly used to construct a proxy chain.
type Node struct {
	ID               int
	Addr             string
	Host             string
	Protocol         string
	Transport        string
	Remote           string   // remote address, used by tcp/udp port forwarding
	url              *url.URL // raw url
	User             *url.Userinfo
	Values           url.Values
	DialOptions      []DialOption
	HandshakeOptions []HandshakeOption
	ConnectOptions   []ConnectOption
	Client           *Client
	marker           *failMarker
	Bypass           *Bypass
}

注意,Node结构体为客户端和服务端共用,但是只有客户端才用到了Client属性。这是作为客户端的关键,因为Client属性对应的类型Client结构体,是一组实现了Connector以及Transporter接口的抽象类。

// Client is a proxy client.
// A client is divided into two layers: connector and transporter.
// Connector is responsible for connecting to the destination address through this proxy.
// Transporter performs a handshake with this proxy.
type Client struct {
	Connector   Connector
	Transporter Transporter
}

// Connector is responsible for connecting to the destination address.
type Connector interface {
	Connect(conn net.Conn, addr string, options ...ConnectOption) (net.Conn, error)
}

// Transporter is responsible for handshaking with the proxy server.
type Transporter interface {
	Dial(addr string, options ...DialOption) (net.Conn, error)
	Handshake(conn net.Conn, options ...HandshakeOption) (net.Conn, error)
	// Indicate that the Transporter supports multiplex
	Multiplex() bool
}

DialHandshakeMultiplexConnect是客户端连接服务端的四个重要抽象方法。

parseChainNode方法

parseChain方法中调用了parseChainNode方法,该方法根据代理链的配置写法决定了客户端使用哪种传输层以及协议层。配置的写法采用了类URL形式

[scheme://][user:pass@host]:port[?param1=value1&param2=value2]

不同的传输层,实现了不同的DialHandshakeMultiplex方法,即抽象接口Transporter。不同协议层,实现了不同的Connect方法,即抽象接口Connector

其实一个代理链节点返回一个Node对象就够了,那为什么该方法会返回一个Node对象数组呢?其实是为了后期加入的负载均衡功能而做的改变。

parseChain方法

parseChainNode方法返回的Node数组被包含在NodeGroup当中。解析的时候,每一个代理链节点对应一个NodeGroup。最后,NodeGroup数组又被包含在Chain对象当中。注意,该方法返回的Chain类型对象在整个程序中它只有一个。所以,我们把Chain对象看作是客户端也无妨。

随着配置文件中的ChainNodes数组被循环解析后,所有的客户端也就生成完毕。

服务端是怎么生成的

客户端生成之后,接下来就是要解析配置文件中的ServeNodes来生成服务端了。

GenRouters方法

该方法同样调用了ParseNode方法来解析配置节点,和解析客户端的配置写法一样,服务端的配置写法决定了服务端使用哪种Listener以及Handler(不知道该怎么翻译比较好)。

// server.go
// Listener is a proxy server listener, just like a net.Listener.
type Listener interface {
	net.Listener
}

// handler.go
// Handler is a proxy server handler
type Handler interface {
	Init(options ...HandlerOption)
	Handle(net.Conn)
}

和客户端很像,客户端的核心是要求实现Transporter以及Connector两个抽象接口。而服务端的核心则是要求实现Listener以及Handler两个抽象接口。

配置文件中的每一个服务端节点配置,都对应一个router结构体。

// route.go
type router struct {
	node     gost.Node
	server   *gost.Server
	handler  gost.Handler
	chain    *gost.Chain
	resolver gost.Resolver
	hosts    *gost.Hosts
}

// server.go
// Server is a proxy server.
type Server struct {
	Listener Listener
	Handler  Handler
	options  *ServerOptions
}

// Serve serves as a proxy server.
func (s *Server) Serve(h Handler, opts ...ServerOption) error {
  	s.Init(opts...)

	if s.Listener == nil {
		ln, err := TCPListener("")
		if err != nil {
			return err
		}
		s.Listener = ln
	}

	if h == nil {
		h = s.Handler
	}
	if h == nil {
		h = HTTPHandler()
	}

	l := s.Listener
	var tempDelay time.Duration
	for {
		conn, e := l.Accept()
		if e != nil {
			if ne, ok := e.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				log.Logf("server: Accept error: %v; retrying in %v", e, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return e
		}
		tempDelay = 0

		go h.Handle(conn)
	}
}

上面出现过的属性我就不重复贴了,重点看router结构体。

node属性对应当前服务端节点。

server属性的类型是Server结构体,该结构体有个Serve方法是整个服务端的核心所在,该方法负责监听客户端请求。Server结构体根据传入的Listener不同,决定了在哪个端口以何种传输层协议来等待监听。

handler属性根据传入的Handler不同,决定了以何种应用层协议来响应客户端的请求。

chain属性可以看作是客户端,因为同一个程序既可以看作服务端,又也可以看作客户端。

resolver以及hosts属性是根据dns以及hosts参数来的,不是必须的。

最后,该方法会返回一个router对象数组,分别对应配置文件中ServeNodes里的每一条服务端节点配置。

主程序

服务端和客户端都生成之后,回到主程序。程序会去读取配置文件中的Routes数组,一般来讲没必要再配置了,除非你有特殊的需求。官网对它的介绍是

Routes - 可选参数,额外的服务列表,每一项都拥有独立的转发链。

反正我还没研究过用到的场景。最后,在整个配置文件中,至少得有一条服务端节点的配置,否则程序就会报错。然后,通过调用每一个routerServe方法,服务端就正式启动监听了

// route.go
func (r *router) Serve() error {
	log.Logf("%s on %s", r.node.String(), r.server.Addr())
	return r.server.Serve(r.handler)
}

总结

总的来说,这只是一篇关于gost是如何运行的简单说明。