反向代理隧道¶
在上一篇反向代理教程中,利用端口转发实现了简单的反向代理功能,在本篇中将利用隧道服务实现类似于Cloudflare Tunnel的增强版反向代理。
隧道(Tunnel)¶
隧道是一条服务端和客户端之间的(逻辑上的)通道,服务端可以开启一个额外的公共入口点(EntryPoint),由入口点进入的流量会通过隧道发送给客户端。每个隧道有一个唯一的ID(合法的UUID),一个隧道可以有多个连接(连接池)来实现隧道的高可用性。
服务端
gost -L "tunnel://:8443?entrypoint=:80&tunnel=.example.com:4d21094e-b74c-4916-86c1-d9fa36ea677b,example.org:ac74d9dd-3125-442a-a7c1-f9e49e05faca"
命令行中使用tunnel
选项定义Ingress规则。tunnel
选项的值为,
分割的规则列表,每个规则为:
分割的主机名和隧道ID。
services:
- name: service-0
addr: :8443
handler:
type: tunnel
metadata:
entrypoint: ":80"
ingress: ingress-0
listener:
type: tcp
ingresses:
- name: ingress-0
rules:
- hostname: ".example.com"
endpoint: 4d21094e-b74c-4916-86c1-d9fa36ea677b
- hostname: "example.org"
endpoint: ac74d9dd-3125-442a-a7c1-f9e49e05faca
通过entrypoint
选项指定流量的公共入口点,同时通过ingress
选项指定Ingress对象来定义流量路由规则。
隧道ID分配
如果使用了Ingress,隧道将通过(虚拟)主机名进行路由,隧道的ID应当由服务端提前分配并记录在Ingress中。如果客户端使用了一个未在Ingress中注册的隧道ID,则流量无法路由到此客户端。
客户端
services:
- name: service-0
addr: :0
handler:
type: rtcp
listener:
type: rtcp
chain: chain-0
forwarder:
nodes:
- name: target-0
addr: 192.168.1.1:80
chains:
- name: chain-0
hops:
- name: hop-0
nodes:
- name: node-0
addr: :8443
connector:
type: tunnel
metadata:
tunnel.id: 4d21094e-b74c-4916-86c1-d9fa36ea677b
tunnel.weight: 1
dialer:
type: tcp
tunnel.id
(string)- 隧道ID,此时rtcp服务中指定的
addr
参数无效。 tunnel.weight
(uint8, default=1)- 客户端连接权重,取值范围[1, 255]。当权重值为
255
时,其他权重值小于255的客户端连接将被忽略。
本例中当流量进入公共入口点(服务端的80端口)后会嗅探流量信息获取所要访问的主机名,再通过主机名在Ingress中找到匹配的规则,获取对应的服务端点(endpoint即隧道ID),最后在隧道的连接池中获取一个有效连接将流量通过此连接发送到客户端。
当主机名为example.com
时,根据Ingress中的规则匹配到ID为4d21094e-b74c-4916-86c1-d9fa36ea677b的隧道。当流量到达客户端后再由rtcp服务转发给192.168.1.1:80服务。
高可用性
为了提高单个隧道的可用性,可以运行多个客户端,这些客户端使用相同的隧道ID。当需要从隧道获取连接时,将采用加权随机方式选择一个客户端连接,最多3次失败重试。
外部公共入口点¶
上面通过entrypoint
选项设置的入口点可以看作是隧道服务内部提供的一个公共入口点,也可以运行多个外部公共入口点将流量转发到隧道服务。
服务端
服务端通过entrypoint.id
指定入口点ID,客户端必须使用相同的ID才会被认为是一个公共入口点,否则会被当作私有入口点,仅能访问指定的隧道。
services:
- name: service-0
addr: :8443
handler:
type: tunnel
metadata:
entrypoint.id: 9fd6c586-86f9-49c1-a03a-d4876851695a
ingress: ingress-0
listener:
type: tcp
ingresses:
- name: ingress-0
rules:
- hostname: ".example.com"
endpoint: 4d21094e-b74c-4916-86c1-d9fa36ea677b
- hostname: "example.org"
endpoint: ac74d9dd-3125-442a-a7c1-f9e49e05faca
客户端
客户端通过tunnel.id
指定隧道ID,当隧道ID与服务端的entrypoint.id
相同时,此客户端会被当作一个公共入口点。
客户端路由¶
客户端也可以同时开启流量嗅探对流量进行再次路由。
services:
- name: service-0
addr: :0
handler:
type: rtcp
metadata:
sniffing: true
listener:
type: rtcp
chain: chain-0
forwarder:
nodes:
- name: example-com
addr: 192.168.1.1:80
filter:
host: example.com
- name: sub-example-com
addr: 192.168.1.2:80
filter:
host: sub.example.com
- name: fallback
addr: 192.168.2.1:80
chains:
- name: chain-0
hops:
- name: hop-0
nodes:
- name: node-0
addr: :8443
connector:
type: tunnel
metadata:
tunnel.id: 4d21094e-b74c-4916-86c1-d9fa36ea677b
dialer:
type: tcp
当主机名为example.com
时,根据Ingress中的规则匹配到Tunnel 4d21094e-b74c-4916-86c1-d9fa36ea677b。当流量到达客户端后再由rtcp服务转发给192.168.1.1:80服务。
当主机名为sub.example.com
时,根据Ingress中的规则匹配到Tunnel 4d21094e-b74c-4916-86c1-d9fa36ea677b。当流量到达客户端后再由rtcp服务转发给192.168.1.2:80服务。
当主机名为abc.example.com
时,根据Ingress中的规则匹配到Tunnel 4d21094e-b74c-4916-86c1-d9fa36ea677b。当流量到达客户端后再由rtcp服务转发给192.168.2.1:80服务。
私有隧道¶
在Ingress中可以通过将隧道标记为私有来限制对隧道的访问,由公共入口点进入的流量无法路由到私有隧道。
若要使用私有隧道,用户(访问端)需要开启一个私有入口点将流量转发到指定的隧道,通过设置隧道ID来指定想要访问的隧道(不仅限于私有隧道)。
服务端
services:
- name: service-0
addr: :8443
handler:
type: tunnel
metadata:
entryPoint: ":80"
ingress: ingress-0
listener:
type: tcp
ingresses:
- name: ingress-0
rules:
- hostname: "srv-0.local"
endpoint: 4d21094e-b74c-4916-86c1-d9fa36ea677b
- hostname: "srv-1.local"
endpoint: 4d21094e-b74c-4916-86c1-d9fa36ea677b
- hostname: "srv-2.local"
endpoint: $ac74d9dd-3125-442a-a7c1-f9e49e05faca # private tunnel
- hostname: "srv-3.local"
endpoint: ac74d9dd-3125-442a-a7c1-f9e49e05faca
- hostname: "ssh.srv-2.local"
endpoint: aede1f6a-762b-45da-b937-b6632356555a # tunnel for ssh TCP traffic
- hostname: "redis.srv-3.local"
endpoint: aede1f6a-762b-45da-b937-b6632356555a # tunnel for redis TCP traffic
- hostname: "dns.srv-2.local"
endpoint: aede1f6a-762b-45da-b937-b6632356555a # tunnel for DNS UDP traffic
- hostname: "dns.srv-3.local"
endpoint: aede1f6a-762b-45da-b937-b6632356555a # tunnel for DNS UDP traffic
在Ingress的规则中,通过在endpoint所代表的隧道ID值前添加$
便将此规则对应的隧道标记为私有,例如上面的srv-2.local主机对应的隧道ac74d9dd-3125-442a-a7c1-f9e49e05faca即为私有隧道,因此通过公共入口点80端口进入的流量无法使用此隧道。
私有性
私有性的作用范围为Ingress的规则,而不是隧道本身,同一个隧道在不同的规则中可以有不同的私有性。例如上面例子当中,srv-2.local和srv-3.local使用的是相同的隧道,但srv-3.local对应规则中隧道不是私有的,因此通过公共入口点80端口进入去往srv-3.local主机的流量可以路由到此隧道。
客户端
services:
- name: service-0
addr: :0
handler:
type: rtcp
listener:
type: rtcp
chain: chain-0
forwarder:
nodes:
- name: srv-2.local
addr: 192.168.2.1:80
chains:
- name: chain-0
hops:
- name: hop-0
nodes:
- name: node-0
addr: :8443
connector:
type: tunnel
metadata:
tunnel.id: ac74d9dd-3125-442a-a7c1-f9e49e05faca
dialer:
type: tcp
客户端的配置与之前一致。
访问端
自动嗅探主机名
或指定主机名
访问端开启私有入口服务监听在8000端口,通过设置tunnel.id
选项指定所要使用的隧道。
TCP服务¶
隧道并不限于Web流量,也可以应用于任何TCP服务(例如SSH)。例如上面服务端的Ingress中ssh.srv-2.local
和redis.srv-3.local
主机对应的隧道。
客户端
services:
- name: service-0
addr: :0
handler:
type: rtcp
listener:
type: rtcp
chain: chain-0
forwarder:
nodes:
- name: ssh
addr: 192.168.2.1:22
filter:
host: ssh.srv-2.local
- name: redis
addr: 192.168.2.2:6379
filter:
host: redis.srv-3.local
chains:
- name: chain-0
hops:
- name: hop-0
nodes:
- name: node-0
addr: :8443
connector:
type: tunnel
metadata:
tunnel.id: aede1f6a-762b-45da-b937-b6632356555a
dialer:
type: tcp
客户端的转发器设置了两个目标节点:192.168.2.1:22的ssh服务和192.168.2.2:6379的redis服务。 注意每个节点上的host
参数需要与服务端Ingress对应规则中的hostname
相匹配。
访问端
SSH服务
gost -L tcp://:2222/ssh.srv-2.local -F tunnel://:8443?tunnel.id=aede1f6a-762b-45da-b937-b6632356555a
或redis服务
services:
- name: service-0
addr: :2222
handler:
type: tcp
chain: chain-0
listener:
type: tcp
forwarder:
nodes:
- name: ssh
addr: ssh.srv-2.local
# - name: redis
# addr: redis.srv-3.local
chains:
- name: chain-0
hops:
- name: hop-0
nodes:
- name: node-0
addr: :8443
connector:
type: tunnel
metadata:
tunnel.id: aede1f6a-762b-45da-b937-b6632356555a
dialer:
type: tcp
访问端需要在转发器中指定目标节点主机名,需要与服务端Ingress对应规则中的hostname
相匹配。
UDP服务¶
隧道也可以应用于任何UDP服务(例如DNS)。例如上面服务端的Ingress中dns.srv-2.local
和dns.srv-3.local
主机对应的隧道。
客户端
services:
- name: service-0
addr: :0
handler:
type: rudp
listener:
type: rudp
chain: chain-0
forwarder:
nodes:
- name: dns-1
addr: 192.168.2.1:53
filter:
host: dns.srv-2.local
- name: dns-2
addr: 192.168.2.2:53
filter:
host: dns.srv-3.local
chains:
- name: chain-0
hops:
- name: hop-0
nodes:
- name: node-0
addr: :8443
connector:
type: tunnel
metadata:
tunnel.id: aede1f6a-762b-45da-b937-b6632356555a
dialer:
type: tcp
客户端的转发器设置了两个目标节点:192.168.2.1:53的DNS服务和192.168.2.2:53的DNS服务。 注意每个节点上的host
参数需要与服务端Ingress对应规则中的hostname
相匹配。
访问端
services:
- name: service-0
addr: :1053
handler:
type: udp
chain: chain-0
listener:
type: udp
forwarder:
nodes:
- name: dns-1
addr: dns.srv-2.local
- name: service-1
addr: :2053
handler:
type: udp
chain: chain-0
listener:
type: udp
forwarder:
nodes:
- name: dns-2
addr: dns.srv-3.local
chains:
- name: chain-0
hops:
- name: hop-0
nodes:
- name: node-0
addr: :8443
connector:
type: tunnel
metadata:
tunnel.id: aede1f6a-762b-45da-b937-b6632356555a
dialer:
type: tcp
访问端需要在转发器中指定目标节点主机名,需要与服务端Ingress对应规则中的hostname
相匹配。
直接路由¶
上面的隧道都是通过定义Ingress,根据Ingress规则中虚拟主机名来路由,这种方式可以看作是间接路由模式,Ingress在这里即是路由表,又可以看作是白名单。
也可以开启直接路由模式,访问端与客户端直接通过隧道ID进行匹配,当访问端未匹配上Ingress中的规则后会采用隧道ID直接匹配方式来查找客户端。Ingress是可选的。
提高安全性
当开启直接路由模式后,隧道的分配及使用完全由客户端控制,请确保服务端仅能够被受信任的用户访问,可以通过增加用户认证功能提高服务的安全性,以防止被滥用。
服务端通过tunnel.direct
选项开启直接路由模式。
多路复用¶
隧道本身支持多路复用,单个隧道不仅限于某一种类型的流量使用,也支持同时传输不同类型的流量(Web,TCP,UDP)。
TCP和UDP服务可以共用同一个隧道,隧道会对TCP和UDP的客户端连接作区分,对于TCP的访问端仅会匹配TCP客户端,对于UDP的访问端仅会区配UDP客户端。
下面将通过一个具体的示例来说明。
示例 - 通过隧道进行iperf测试¶
服务端
Ingress模式
或直接路由模式
客户端
由于转发的目标只有一个,因此可以使用命令行直接转发,如果要转发多个服务需要通过配置文件在转发器中为每个目标节点定义主机名(forwarder.nodes.host
),通过主机名来匹配不同的服务。
services:
- name: iperf-tcp
addr: :0
handler:
type: rtcp
listener:
type: rtcp
chain: chain-0
forwarder:
nodes:
- name: iperf
addr: :5201
filter:
host: iperf.local
- name: iperf-udp
addr: :0
handler:
type: rudp
listener:
type: rudp
chain: chain-0
forwarder:
nodes:
- name: iperf
addr: :5201
filter:
host: iperf.local
chains:
- name: chain-0
hops:
- name: hop-0
nodes:
- name: node-0
addr: :8443
connector:
type: tunnel
metadata:
tunnel.id: 22f43305-42f7-4232-bbbc-aa6c042e3bc3
dialer:
type: tcp
访问端
转发的目标地址需要与服务端的Ingress中规则对应的主机名匹配,如果要转发多个服务需要通过配置文件在转发器中为每个目标节点定义主机名(forwarder.nodes.host
),通过主机名来匹配不同的服务。
UDP连接保持
UDP端口转发服务默认在进行完一次数据交互后连接状态便失效,这对于像DNS这种服务会很有效。但是对于需要多次数据交互的UDP服务,需要通过keepalive
选项开启连接保持功能,另外可以通过ttl
选项来控制超时时长,默认超过5秒无数据交互连接状态将会失效。
Ingress模式
gost -L tcp://:15201/iperf.local -L udp://:15201/iperf.local?keepalive=true -F tunnel://:8443?tunnel.id=22f43305-42f7-4232-bbbc-aa6c042e3bc3
直接路由模式
services:
- name: iperf-tcp
addr: :15201
handler:
type: tcp
chain: chain-0
listener:
type: tcp
forwarder:
nodes:
- name: iperf
addr: iperf.local
services:
- name: iperf-udp
addr: :15201
handler:
type: udp
chain: chain-0
listener:
type: udp
metadata:
keepalive: true
# ttl: 5s
forwarder:
nodes:
- name: iperf
addr: iperf.local
chains:
- name: chain-0
hops:
- name: hop-0
nodes:
- name: node-0
addr: :8443
connector:
type: tunnel
metadata:
tunnel.id: 22f43305-42f7-4232-bbbc-aa6c042e3bc3
dialer:
type: tcp
iperf3服务¶
启动iperf3服务。
执行iperf3测试¶
TCP测试
UDP测试
公共反向代理服务¶
如果需要临时来反向代理内网服务提供公网访问,可以通过GOST.PLUS
提供的公共反向代理服务将内网服务匿名暴露到公网来访问。
或者手动指定隧道ID:
gost -L rtcp://:0/192.168.1.1:80 -F tunnel+wss://tunnel.gost.plus:443?tunnel.id=893787fd-fcd2-46a0-8dd4-f9103ae84df4
当正常连接到GOST.PLUS
服务后,会有类似如下日志信息:
{"connector":"tunnel","dialer":"wss","endpoint":"134c714b65d54a4f","hop":"hop-0","kind":"connector","level":"info",
"msg":"create tunnel on 134c714b65d54a4f:0/tcp OK, tunnel=893787fd-fcd2-46a0-8dd4-f9103ae84df4, connector=3464af8b-49c5-424c-89ea-b4e9af075a7d",
"node":"node-0","time":"2023-10-19T23:17:27.403+08:00",
"tunnel":"893787fd-fcd2-46a0-8dd4-f9103ae84df4"}
日志的endpoint
信息中134c714b65d54a4f
是为此服务生成的临时公共访问点,有效期为24小时。
如果192.168.1.1:80
是一个HTTP服务,通过https://134c714b65d54a4f.gost.plus便能立即访问。
TCP服务¶
对于TCP服务同样可以以私有隧道的方式来访问。这里假设192.168.1.1:22是一个SSH服务。
gost -L rtcp://:0/192.168.1.1:22 -F tunnel+wss://tunnel.gost.plus:443?tunnel.id=f8baa731-4057-4300-ab75-c4e603834f1b
内网服务不会在服务端暴露公开端口,需要在访问端开启一个私有入口点:
gost -L tcp://:2222/f1bbbb4aa9d9868a.gost.plus -F tunnel+wss://tunnel.gost.plus:443?tunnel.id=f8baa731-4057-4300-ab75-c4e603834f1b
注意两端的隧道ID必须匹配才能访问到隧道对应的服务。
此时在访问端执行以下命令便可以访问到192.168.1.1:22。
UDP服务¶
同样也可以以私有隧道的方式暴露UDP服务。这里假设192.168.1.1:53是一个DNS服务。
gost -L rudp://:0/192.168.1.1:53 -F tunnel+wss://tunnel.gost.plus:443?tunnel.id=f8baa731-4057-4300-ab75-c4e603834f1b
要访问此服务需要在访问端开启一个私有入口点:
gost -L udp://:1053/f1bbbb4aa9d9868a.gost.plus -F tunnel+wss://tunnel.gost.plus:443?tunnel.id=f8baa731-4057-4300-ab75-c4e603834f1b
注意两端的隧道ID必须匹配才能访问到隧道对应的服务。
此时在访问端执行以下命令便可以访问到192.168.1.1:53。