Xray 搭建内网穿透

12 min

Xray 搭建内网穿透

家里 NAS 没有公网 IP ,因为部署了一些在外面也需要访问的服务,最近一直在研究怎么实现内网穿透。前后尝试过 tailscale、frp、lucky、cloudflare tunnel,但是使用起来感觉都不太符合我的要求,主要问题就是:

  • tailscale:

    • 虚拟局域网方式使用,与本地 VPN 没法共存,这是我最不能接受的。
    • 默认的中继服务器在国内使用并不稳定,对于同步场景影响较大。
    • 考虑过自建中继,但是我的 VPS 在香港,tailscale 默认的隧道协议很容易被 GFW 识别。
  • frp:

    • 没有公网 IP ,只能 VPS 中转,这样就需要在 NAS 和 VPS 两端配置,如果 NAS 端需要新增一个穿透服务就需要两边都进行配置,比较繁琐。
    • 整体体验不错,但是默认加密传输过 GFW 还是不够保险,很容易被识别在跑 FRP 。
  • lucky:

    • 内网穿透需要 NAT1 网络,现在基本都是 NAT2 或者 NAT3 ,基本没法满足。
    • 附带的 WEB 服务、证书管理这些对小白来说还是不错的,可以很方便的提供 https 外网服务,当然前提是有公网 IP 。
    • WEB 服务自定义程度较低,反代某些服务没法进行优化。
  • cloudflare tunnel:

    • 各方面使用体验都很棒,配置简单,依托 cloudflare 平台有保障,唯一的缺点就是在国内环境下速度真的堪忧,应急可以。

Xray 作为一款高性能网络代理工具,很多人都是用来进行科学的,我想到用它来搭建内网穿透也是最近看文档突然发现这款工具支持了 反向代理 。配合现在主流的 Vless + Reality 就能构建一个隐蔽性强、安全性高的隧道,刚好可以利用上香港 VPS 进行中转。

Xray 的反向代理是一个通用反向代理,不限制协议类型,也就是说你可以用 Xray 支持的任何协议,如果在国内服务器上搭建就不需要使用 vless 这种较重的协议了。反向代理流程如下图所示:

xray 反向代理.webp
xray 反向代理.webp

配置分为 portal​ 端和 bridge 端,如果用于内网穿透则 bridge 端部署在本地服务器上,portal 端部署到 VPS 服务器上。portal 端至少提供两个入站和一个出站,其中 vless 入站用于建立隧道与 bridge 端通信,一个 tunnel 入站用于端口转发,一个默认 direct 出站避免某个反向代理成为默认出站流量都通过隧道发往 bridge 端,通过路由模块配置流量流转方向。bridge 端提供两个出站,不需要入站,一个 vless 出站用于连接 portal 端 vless 隧道,一个默认 diretc 出站与内网服务进行通信,避免回环。

在反向代理过程中,bridge 端反向代理首先通过特定的 domian 或者特定标志发出连接建立请求,路由模块判断是反向代理发出的请求,如果 domain 或者特殊标志匹配则转发到 vless 隧道向 portal 端发送,否则说明是公网端向内网服务发出的请求,则通过 direct 出站发往内网服务。

portal 端隧道收到请求,路由模块将其转发到 reverse 模块判断是否是反向代理建立的请求,如果是则建立长期连接,不主动关闭这条连接,并且会定期进行保活,如果不是说明是内网服务的响应,路由模块会根据情况转发到 direct 出站并最终返回外网用户设备。portal 端 tunnel 隧道收到请求,路由模块判断这是公网用户的请求,将其转发到 reverse 进行处理,最终会发送到 bridge 端。

一个极简配置的内网穿透配置:

Portal 端:

{
	"inbounds": [
		{
			"tag": "vless_tunnel"
			"listen": "0.0.0.0",
			"port": 8443,
			"protocol": "vless",
			"settings": {
				"description": "none",
				"clients": [{"id": "<UUID>", "flow": "xtls-rprx-vision"}]
			},
			"streamSettings": {
				// 此处省略 reality 配置
			}
		},
		{
			"tag": "external",
			"listen": "127.0.0.1",
			"port": 10086,
			"protocol": "dokodemo-door",
			"settings": {
				"address": "127.0.0.1",
				"network": "tcp"
			}
		}
	],
	"outbounds": [
		{ "tag": "direct", "protocol": "freedom" },
		{ "tag": "block", "protocol": "blackhole" }
	],
	// 配置反向代理模块
	"reverse": {
		"portals": [
			{
				"tag": "portal",
				// 这里 domain 随便填写,不需要是真实域名,仅用作 xray 内部识别
				"domain": "nas-tunnel" 
			}
		]
	},
	// 重要!路由配置,决定流量如何处理和流动
	"routing": {
		"rules": [
			// 外部流量以及隧道流量都交由反向代理模块处理
			{ "type": "field", "inboundTag": "external", "outboundTag": "portal" },
			{ "type": "field", "inboundTag": "vless_tunnel", "outboundTag": "portal" },
		]
	}
}

Bridge 端:

{
	"inbounds": [],
	"outbounds": [
		{
			"tag": "to_local",
			"protocol": "freedom",
			"settings": {
				// 转发到本地服务
				"redirect": "http://127.0.0.1:xxx"
			}
		},
		{
			"tag": "vless_tunnel"
			"protocol": "vless",
			"settings": {
				"vnext": [
					{
						"address": "<VPS IP>",
						"port": 8443,
						"users": [ {"id": "<UUID>", "flow": "xtls-rprx-vision", "encryption": "none"} ]
					}
				]
			},
			"streamSettings": {
				// 此处省略 reality 配置
			}
		}
	],
	// 配置反向代理模块
	"reverse": {
		"bridges": [
			{
				"tag": "bridge",
				// 这里 domain 随便填写,不需要是真实域名,仅用作 xray 内部识别
				"domain": "nas-tunnel" 
			}
		]
	},
	// 重要!路由配置,决定流量如何处理和流动
	"routing": {
		"rules": [
			{ "type": "field", "inboundTag": "bridge", "outboundTag": "vless_tunnel", "domain": ["full:nas-tunnel"] },
			{ "type": "field", "inboundTag": "bridge", "outboundTag": "to_local" },
		]
	}
}

这套配置虽然能工作,但是并不方便,每次新增或者修改服务都需要在 portal 和 bridge 端进行修改然后重启,跟 frp 相比也就多了个能自定义隧道协议的优势,关键还很绕难以理解。我希望的效果是服务端跟客户端配置运行后就不用动它,即使需要修改内网穿透服务端口也不用动它。

xray 的任意门(现在应该叫 tunnel )入站给了我启发,既然都是端口转发为何不用更方便的 iptables​ 或者 nftables​ 呢?通过 nftables 对端口流量进行转发,对于需要内网穿透的服务,都将其转发到 xray tunnel 监听的端口,同时配置 followRedirect 识别出 nftables 转发的原始端口,这样 bridge 端也能识别到对应端口,只要保持 NAS 本地服务端口跟 VPS 端 nftables 配置的端口一致即可。nftables 还可以批量转发某一段端口(比如 10000-12000 范围所有端口),也能控制端口是否对公网开放,这样能让 VPS 端 Nginx 安全反代,不用担心暴露多余的端口。

xray 在 vless 部分新增了 reverse 字段,可以实现通用反向代理,配置反向代理更加方便,优化后的配置:

NAS 端 xray 配置:

{
  "log": { "loglevel": "info" },
  "inbounds": [],
  "outbounds": [
    {
      "protocol": "direct"
    },
    {
      "protocol": "vless",
      "settings": {
        "vnext": [
          {
            "address": "<公网VPS IP>",
            "port": 8443,
            "users": [
              {
                 "id": "<xray uuid生成,与服务端保持一致>",
                 "encryption": "none",
                 "flow": "xtls-rprx-vision",
                 "email": "与服务端一致",
                 "reverse": { "tag": "tunnel-in" }
               }
            ]
          }
        ]
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
           "serverName": "firefox.org",
           "publicKey": "<xray x25519 -i 服务端私钥生成>",
           "shortId": "<16进制随便写几个,与服务端保持一致>",
           "spiderX": "/"
        }
      }
    }
  ],
  "routing": {
    "rules": []
  }
}

VPS 端 xray 配置:

{
  "log": {
    "access": "/var/log/xray/access.log",
    "dnsLog": false,
    "error": "/var/log/xray/error.log",
    "loglevel": "info"
  },
  "inbounds": [
	  {
		  "listen": "0.0.0.0",
		  "port": 8443,
		  "protocol": "vless",
		  "settings": {
			  "clients": [
				  {
					  "id": "<xray uuid生成>",
					  "flow": "xtls-rprx-vision",
					  "email": "邮箱,其标记作用,与客户端保持一致",
					  "reverse": {
						  "tag": "tunnel-out"
					  }
				  }
			  ],
			  "decryption": "none"
		  },
		  "streamSettings": {
			  "network": "tcp",
			  "security": "reality",
			  "realitySettings": {
				  "show": false,
				  "dest": "firefox.org:443",
				  "serverNames": ["firefox.org"],
				  "privateKey": "<xray x25519生成>",
				  "shortIds": ["与客户端保持一致"]
			  }
		  },
		  "sniffing": {
			  "enabled": true,
			  "destOverride": ["http", "tls"]
		  }
	  },
	  {
		  "listen": "127.0.0.1",
		  "port": 10086,
		  "protocol": "tunnel",
		  "settings": {
			  "address": "127.0.0.1",
			  "network": "tcp,udp",
			  "followRedirect": true
		  },
		  "tag": "tunnel-in"
	  }
  ],
  "outbounds": [
	  {
		  "protocol": "direct",
		  "tag": "direct"
	  },
	  {
		  "protocol": "blackhole",
		  "tag": "block"
	  }
  ],
  "routing": {
	  "rules": [
		{
			"type": "field",
			"user": [ "与上面邮箱保持一致" ],
			"outboundTag": "tunnel-out"
		},
		{
			"type": "field",
			"inboundTag": ["tunnel-in"],
			"outboundTag": "tunnel-out"
	  	}
	  ]
  }
}

VPS 端 nftables 配置:

#!/usr/sbin/nft -f

flush ruleset

table inet inet_firewall {
	## 这个控制穿透并且开放给公网访问的端口
	set tunnel_ports_public {
		type inet_service
		flags interval
        ## 开放并穿透 NAS 10010 端口
		elements = { 10010 }
	}
    ## 这个控制穿透端口,给 VPS 本地用,比如 Nginx 反向代理
	set tunnel_ports_local {
		type inet_service
		flags interval
        ## 我直接开放这么多端口哟
		elements = { 10000-20000 }
	}

	chain prerouting {
		type nat hook prerouting priority -100; policy accept;

        ## 10086 端口是 xray 服务端隧道监听端口,会转发给隧道
		iifname != "lo" tcp dport @tunnel_ports_public redirect to :10086
		iifname != "lo" udp dport @tunnel_ports_public redirect to :10086
	}

	chain input {
		type filter hook input priority 0;

		policy drop;

		ct state established,related accept
		iif lo accept
		ct state invalid drop

        ## tunnel port
		tcp dport { 8443,10086 } accept
		udp dport {8443, 10086 } accept

        ## 这里就当日常防火墙用就行
		## 开放常用端口
		tcp dport { ssh,http,https,9000,9001,81 } accept

        ## 有点多余了,前面已经转发了
		# tcp dport @tunnel_ports_public accept
		# udp dport @tunnel_ports_public accept

	}
	chain output {
		type nat hook output priority -100; policy accept;

		ip daddr 127.0.0.1 tcp dport @tunnel_ports_local redirect to :10086
		ip daddr 127.0.0.1 udp dport @tunnel_ports_local redirect to :10086
	}
}

优化后的配置 xray 运行后不需要再动,只需要在 nftables 配置文件中指定内网穿透需要的端口(你要是懒得一个个添加可以直接转发一段端口,但是我不推荐这样搞),然后使用 systemctl restart nftables​ 重启即可生效。推荐使用 Nginx 进行反代,不要直接开放端口,nginx 反代部分填写 127.0.0.1:<NAS端某个服务端口> 即可。