Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support tun mode #44

Merged
merged 28 commits into from
Nov 1, 2023
Merged

feat: support tun mode #44

merged 28 commits into from
Nov 1, 2023

Conversation

Mythologyli
Copy link
Owner

@Mythologyli Mythologyli commented Oct 22, 2023

此 PR 希望为 zju-connect 带来 TUN 模式,具体的好处有:

  1. 更方便地进行 SSH/RDP 等连接
  2. 运行在主路由时带来便利
  3. 便于对接 Android VpnService

目前的初步想法为:

  1. TUN 接口默认路由 10.0.0.0/8。如果路由 0.0.0.0 可能分流比较复杂
  2. 在 Windows 平台上主要作为 SOCKS5/HTTP 代理的补充,使得 zju-connect 在 SSH/RDP/正版软件认证等方面的体验与 EasyConnect 类似

存在的问题:

  1. 当前版本开启 TUN 模式后禁用 SOCKS5/HTTP 代理、端口转发等功能,因为这些功能是基于 gVisor 网络栈实现的。目前想法是开启 TUN 后禁用 gVisor 网络栈,然后 net.Dial 走 TUN 网卡
  2. Linux 下设置 TUN 网卡是用 ifconfig 实现的,某些发行版可能不自带 ifconfig,如果能用别的方法实现更好
  3. github.com/songgao/water 对 Android 的支持似乎不好,所以目前暂未实现 Android

@cxz66666
Copy link
Collaborator

好功能,我今天之前看看 😸

core/protocol.go Outdated
continue
}

if header.Protocol != 6 && header.Protocol != 17 {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

服务端似乎不会响应 TCP/UDP 之外的东西,比如 ICMP,所以这里做了一下过滤。不过考虑到过滤对性能的影响,不知道是不是去掉更好

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这点过滤没啥性能影响,全是cache的操作,不过最好能把6和17用常量定义

@@ -68,4 +71,10 @@ func SetupTunStack(ip []byte, endpoint *EasyConnectTunEndpoint) {
if err != nil {
log.Printf("SetIPAddresses failed: %v", err)
}

cmd := exec.Command("route", "add", "0.0.0.0", "mask", "0.0.0.0", ipStr, "metric", "9999")
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果不加这条路由,Windows 不允许在这个接口上通过目的地为公网的流量

@Mythologyli
Copy link
Owner Author

IPv4 Route Table
===========================================================================
Active Routes:
Network Destination        Netmask          Gateway       Interface  Metric
          0.0.0.0          0.0.0.0      192.168.8.1    192.168.8.200     25
          0.0.0.0          0.0.0.0         On-link      10.190.65.75  10004
         10.0.0.0        255.0.0.0         On-link      10.190.65.75    261
     10.190.65.75  255.255.255.255         On-link      10.190.65.75    261
   10.255.255.255  255.255.255.255         On-link      10.190.65.75    261

目前运行时路由表是这个样子

@cxz66666
Copy link
Collaborator

可以考虑使用ip命令取代ifconfig,所有Linux发行版基本上都提供了ip命令(吧)

@Mythologyli
Copy link
Owner Author

可以考虑使用ip命令取代ifconfig,所有Linux发行版基本上都提供了ip命令(吧)

确实,不过 macOS 上貌似还没有,所以 darwin 那个还是用 ifconfig

https://superuser.com/questions/687310/ip-command-in-mac-os-x-terminal/

@cxz66666
Copy link
Collaborator

想到一个可能需要的功能
开了tun mode后,尽管只在10.0.0.0/8网段下提供服务,但是可能还是需要有一个全局生效的dns服务器,开在tun设备的53端口上,来负责把*.zju.edu.cn解析到10.0.0.0/8, windows可以通过设置tun网卡的dns1,并把tun网卡metrics调低,用命令netsh interface ipv4 show interface就可以看到dns优先级,linux就稍微麻烦点,得把/etc/resolve.conf覆写掉

@Mythologyli
Copy link
Owner Author

Windows 下 DNS 设置现在我正在做,目前在写 DNS Server
Linux 下也许可以用这种方法:https://github.com/xjasonlyu/tun2socks/wiki/Hijack-DNS

@Mythologyli
Copy link
Owner Author

Mythologyli commented Oct 22, 2023

如果在 TUN 模式下,把 zju-connect 内部的 DNS 逻辑暴露成一个 DNS Server,并设置 TUN 接口使用这个 DNS,下面这个本地解析的逻辑会不会导致循环:

func (resolve *DnsResolve) ResolveWithLocal(ctx context.Context, host string) (context.Context, net.IP, error) {
	if target, err := net.ResolveIPAddr("ip4", host); err != nil {
		log.Printf("Resolve IPv4 addr failed using local DNS: " + host + ". Try IPv6 addr.")

		if target, err = net.ResolveIPAddr("ip6", host); err != nil {
			log.Printf("Resolve IPv6 addr failed using local DNS: " + host + ". Reject connection.")
			return ctx, nil, err
		} else {
			log.Printf("%s -> %s", host, target.IP.String())
			return ctx, target.IP, nil
		}
	} else {
		log.Printf("%s -> %s", host, target.IP.String())
		return ctx, target.IP, nil
	}
}

感觉如果暴露出一个 DNS Server 的话,干脆取消解析失败后用 local DNS 的逻辑,这样就可以得到一个使用 ZJU DNS + DNS 规则 + 自定义 DNS 规则的 DNS Server。如果以上方式都失败就直接放弃

@cxz66666
Copy link
Collaborator

我也想过这个问题,感觉是会的,但是手头账号用于ical了没法试 😢

@Mythologyli
Copy link
Owner Author

Mythologyli commented Oct 22, 2023

要么就取消解析失败后用 local DNS 的逻辑,这样就可以得到一个使用 ZJU DNS + 服务端下发的 DNS 规则 + 自定义 DNS 规则的 DNS Server。如果以上方式都失败就直接放弃

或者更复杂一点,给一个设置项手动指定后备 DNS

@cxz66666
Copy link
Collaborator

cxz66666 commented Oct 22, 2023

如果不设置后备dns的话,tun mode接管所有dns会不会有问题,可以考虑像clash tun mode那样默认几个国内的dns
不过windows下想了一下应该也没啥问题,毕竟可以支持使用多个dns, dns server没响应就用其他的。但是linux使用劫持了之后,还能使用其他网卡下的dns吗

@Mythologyli
Copy link
Owner Author

Mythologyli commented Oct 22, 2023

flag.StringVar(&core.ZjuDnsServer, "zju-dns-server", "10.10.0.21", "ZJU DNS server address")
flag.StringVar(&core.SecondaryDnsServer, "secondary-dns-server", "114.114.114.114", "Secondary DNS server address. Leave empty to use system default DNS server")
flag.StringVar(&core.DnsServerBind, "dns-server-bind", "", "The address DNS server listens on (e.g. 127.0.0.1:53)")
flag.StringVar(&core.TunDnsServer, "tun-dns-server", "", "DNS Server address for TUN interface (e.g. 127.0.0.1). You should not specify the port")

打算用 secondary-dns-server 决定 ZJU DNS 解析失败后用什么。给一个默认值,如果留空就直接用默认的解析

Windows 虽然有多个 DNS,但超时时间会导致体验很差

@cxz66666
Copy link
Collaborator

cxz66666 commented Oct 22, 2023

打算用 secondary-dns-server 决定 ZJU DNS 解析失败后用什么。给一个默认值,如果留空就直接用默认的解析

那tun mode下ResolveWithLocal的逻辑,如果使用system default的dns会导致循环吗(

@Mythologyli
Copy link
Owner Author

打算用 secondary-dns-server 决定 ZJU DNS 解析失败后用什么。给一个默认值,如果留空就直接用默认的解析

那tun mode下ResolveWithLocal的逻辑,如果使用system default的dns会导致循环吗(

如果设置成空就会,但默认是用 114.114.114.114

@Mythologyli
Copy link
Owner Author

func (resolve *DnsResolve) Resolve(ctx context.Context, host string) (context.Context, net.IP, error) {
	if config.IsDnsRuleAvailable() {
		if ip, hasDnsRule := config.GetSingleDnsRule(host); hasDnsRule {
			ctx = context.WithValue(ctx, "USE_PROXY", true)
			log.Printf("%s -> %s", host, ip)
			return ctx, net.ParseIP(ip), nil
		}
	}
	var useProxy = false
	if config.IsZjuForceProxyRuleAvailable() {
		if isInZjuForceProxyRule := config.IsInZjuForceProxyRule(host); isInZjuForceProxyRule {
			useProxy = true
		}
	}
	if !useProxy && config.IsDomainRuleAvailable() {
		if _, found := config.GetSingleDomainRule(host); found {
			useProxy = true
		}
	}

	ctx = context.WithValue(ctx, "USE_PROXY", useProxy)

	if UseZjuDns {
		if cachedIP, found := GetDnsCache(host); found {
			log.Printf("%s -> %s", host, cachedIP.String())
			return ctx, cachedIP, nil
		} else {

这个地方现在的逻辑似乎是服务端下发的规则 > 自定义规则,因为 GetDnsCache 在后面

@cxz66666
Copy link
Collaborator

在macos上找到了这样的解决方法,并测试成功:

		if TunMode {
			if network == "tcp" {
				log.Printf("%s -> PROXY", addr)
				goDialer := &net.Dialer{
					Control: func(network, address string, c syscall.RawConn) error {
						return c.Control(func(fd uintptr) {
							iface, err := net.InterfaceByName("utun10")
							if err != nil {
								fmt.Println(err)
							}
							if err = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVIF, iface.Index); err != nil {
								fmt.Println(err)
							}
						})
					},
				}
				return goDialer.Dial("tcp", addr)

insomniacslk/dhcp#378
https://stackoverflow.com/questions/20616029/os-x-equivalent-of-so-bindtodevice/57013928#57013928
这两个里面都提到了使用IP_BOUND_IF这个flag,但是我使用这个flag并不行,设置成IP_RECVIF则可以在proxy mode下正确的访问内网外网

@cxz66666
Copy link
Collaborator

测试了一下macos上clash tun mode的情况,由于没有linux那样的网络子系统,clash是修改dns到8.8.8.8,然后路由写死,全部流量进clash,包括8.8.8.8:53,然后再用之前系统的dns进行解析。

感觉最后可以提供一个这样形式的dns server:

  1. tun mode下强制开始dns server,用户可以配置文件中选择绑定/不绑定到ip:port
  2. proxy mode下默认不开启dns server,用户可以配置开启/绑定到ip:port
  3. windows下用netsh dnsservers; linux下新建一张route table,劫持53到tun设备;macos下修改dns服务器到某个ip A,配置路由A到tun设备。

还有一个想法:初步实现中,使用配置文件中指定的SecondaryDns感觉没啥问题,但是后续改成可以自动使用用户机器原本的dns

@Mythologyli
Copy link
Owner Author

Mythologyli commented Oct 25, 2023

tun mode下强制开始dns server,用户可以配置文件中选择绑定/不绑定到ip:port

这个是为了在不配置系统代理的情况下正确访问 xxx.zju.edu.cn 吗。那我觉得后期还需要完善一下这个内置的 DNS,毕竟只实现了 A 和 AAAA 的解析

macos下修改dns服务器到某个ip A,配置路由A到tun设备。

macos 下 dns 如果可以走命令行设置的话,能不能像现在 Windows 的实现那样监听 0.0.0.0:53 然后设置为 127.0.0.1

还有一个想法:初步实现中,使用配置文件中指定的SecondaryDns感觉没啥问题,但是后续改成可以自动使用用户机器原本的dns

我也有这个想法。有什么方法获取用户之前使用的 DNS 嘛

@cxz66666
Copy link
Collaborator

这个是为了在不配置系统代理的情况下正确访问 xxx.zju.edu.cn 吗。那我觉得后期还需要完善一下这个内置的 DNS,毕竟只实现了 A 和 AAAA 的解析

是的,感觉这个需求在tun mode下是非常重要的,确实需要完善一下除了A和AAAA,不过也初步够用了

macos 下 dns 如果可以走命令行设置的话,能不能像现在 Windows 的实现那样监听 0.0.0.0:53 然后设置为 127.0.0.1

这个肯定是可以的,就是怕用户自己其他程序可能用了53端口,劫持dns毕竟还是更加的透明

我也有这个想法。有什么方法获取用户之前使用的 DNS 嘛

感觉可以后面研究一下,现在我只能想到mac上可以用scutil --dns命令,linux/macos下可以查看/etc/resolve.conf

@Mythologyli
Copy link
Owner Author

感觉 Clash 的 TUN 模式是没有做自动获取之前的 DNS 这个功能的,开启 TUN+DNS 后似乎必须要手动设置一个 nameserver

@cxz66666
Copy link
Collaborator

感觉 Clash 的 TUN 模式是没有做自动获取之前的 DNS 这个功能的,开启 TUN+DNS 后似乎必须要手动设置一个 nameserver

确实,clash for windows是默认了三个nameserver

@Mythologyli
Copy link
Owner Author

https://github.com/Mythologyli/zju-connect/tree/refactor

我在这个分支上做了一些重构。对于在路由器上跑 zju-connect 的用户,感觉最后的体积还是蛮重要的。现在这样重构比较方便未来在做一个只用 TUN 的版本,去掉 gVisor 体积应该能减少不小

我的 golang 纯纯野路子水平,有空的话希望帮忙看看整个结构设计的有没有问题(哭

@cxz66666
Copy link
Collaborator

过了一遍感觉水平很高 👍

现在感觉除了dns相关的,tun mode已经比较完善了(可以merge的感觉)。但linux和macos上的tun模式下dns我还是有点挠头

  • linux下经过测试,可以使用前面提到的dns拦截到tun网卡里,但是tun网卡现在的实现只能将流量原封不动送到rvpn server,达不到想要的效果,感觉这样不太好,而如果在tun实现中将dst 53的请求转发到dns server的ip:port上,又感觉多此一举了。想看看clash怎么实现的但是premium内核不开源 👎
  • mac下同理

所以一个可以接受的方案就是直接替换dns的配置文件/etc/resolve.conf,这就需要先实现优雅停机,程序结束前将/etc/resolve.conf.bak 复原。

anyway,后面可以新开一个pr做优雅停机和其他的改进,感觉这个tun mode已经基本可用了

@Mythologyli
Copy link
Owner Author

也许可以看看 clash meta 的实现?

@cxz66666
Copy link
Collaborator

cxz66666 commented Oct 27, 2023

看了一下clash meta的思路,他们是用这一套https://github.com/MetaCubeX/sing-tun ,同时基于tun配套实现了一个system的stack

我们这最简单的方法就是只管udp,把53端口的请求拿出来,单独resolve,然后把把返回的包的源ip和源port改成之前的目的ip和目的port。

我们感觉可以用这个tun替换现在的water.tun,这个tun对route和ip rule的支持也很完善

@Mythologyli
Copy link
Owner Author

看了一下clash meta的思路,他们是用这一套https://github.com/MetaCubeX/sing-tun ,同时基于tun配套实现了一个system的stack

我们这最简单的方法就是只管udp,把53端口的请求拿出来,单独resolve,然后把把返回的包的源ip和源port改成之前的目的ip和目的port。

我们感觉可以用这个tun替换现在的water.tun,这个tun对route和ip rule的支持也很完善

感觉不错!

@cxz66666
Copy link
Collaborator

cxz66666 commented Oct 28, 2023

现在分支dns-hijack已经可以正确拦截基于udp的dns请求

nslookup baidu.com 223.5.5.5

2023/10/28 19:14:56 hijack dns 10.190.68.144:55526 -> 223.5.5.5:53
2023/10/28 19:14:56 baidu.com -> 110.242.68.66
Server: 223.5.5.5
Address: 223.5.5.5#53
Non-authoritative answer:
Name: baidu.com
Address: 110.242.68.66

nslookup cspo.zju.edu.cn 223.5.5.5

2023/10/28 19:06:59 hijack dns 10.190.69.185:45533 -> 223.5.5.5:53
2023/10/28 19:06:59 cspo.zju.edu.cn -> 10.203.4.79
Server: 223.5.5.5
Address: 223.5.5.5#53
Non-authoritative answer:
Name: cspo.zju.edu.cn
Address: 10.203.4.79

但仍然还有一些问题:

  1. 需要手动设置一下路由表,linux下设置如下的ip rule。同时 使用ip route add default dev tun0 table 1234 添加route默认路由。
9500:   not from all dport 53 lookup main #注意这里不能用前缀抑制!!!!
9510:   not from all iif lo lookup 1234
9520:   from 0.0.0.0 iif lo uidrange 0-4294967294 lookup 1234
9530:   from 10.190.69.185 iif lo lookup 1234

考虑到后面如果换sing-tun或者其他tun实现,这里我就没用command写死

  1. 只能抓到目的ip是非localhost的包,因为localhost的优先级一般是最高的,如果像openwrt用户,非得用Dnsmasq这种,就只能用设置Dnsmasq上游dns,然后zju-connect监听10.a.b.c:53 解决

  2. 考虑dns超过mtu的情况,不过正常使用的话问题不大

@cxz66666
Copy link
Collaborator

分支dns-hijack已经将linux和macos的tun切换到sing-tun,支持了自动配置ip route 和 ip rule

linux下可以拦截非localhost下的dns请求,macos下只能在实现了优雅停机之后手动设置用户dns解析为一个fake ip,然后路由route这个fake ip到tun设备

macos下还存在问题: 使用proxy代理访问需要代理的、非10.0.0.0/8的网站现在发现还是报错no route to host,之前测试的有问题。我找了半天原因,最后看到这一篇
https://learn.microsoft.com/zh-cn/previous-versions/technet-magazine/cc137807(v=msdn.10)?redirectedfrom=MSDN

macos下使用的是强主机模型,

如果源接口上启用了强主机发送,IP 将在路由表中对数据包的目标地址执行受约束的查询。在受约束的查询中,仅考虑带有源接口的下一跃点接口的路由。根据所选目标路由,IP 确定下一跃点地址。IP 具有了源地址和目标地址、下一跃点接口和下一跃点地址。请注意,如果在源接口上启用强主机发送行为,下一跃点接口将始终与源接口相同。图 2 显示发送主机进程的普通 IP。

macos只能有一个默认路由,看这句话的样子就是说如果路由表中没有 ip为非10.0.0.0/8的、走tun设备的路由条目,则就算实用bindtodevice看起来也没法路由出去

使用proxy下测试访问cnki.net ,只有10.0.0.0/8的路由情况下报错 dial tcp4 10.190.65.184:0->115.31.65.10:80: connect: no route to host,添加 sudo route -n add 115.31.65.10 -interface utun10后,可以正常访问。

所以后面考虑开启tun模式下,macos上所有访问非10.0.0.0/8的可能得一律direct

@cxz66666
Copy link
Collaborator

或许后面可以考虑将windows的tun也切到sing-tun下,我fork了一份sing-tun,修改了部分设置优先级、路由策略的方法,如果windows需要修改里面的路由设置啥的我可以帮忙

@Mythologyli
Copy link
Owner Author

我记得 Windows 也是强主机模型,当时一开始测试的时候也会 no route to host,最后是通过加一条到 0.0.0.0/0 的低优先级路由解决的

@cxz66666
Copy link
Collaborator

我记得 Windows 也是强主机模型,当时一开始测试的时候也会 no route to host,最后是通过加一条到 0.0.0.0/0 的低优先级路由解决的

确实,我也以为这样可以,但是在sonoma14.0上面发现没法设置多个默认路由,netstat -rn 看不到跃点数,很烦

@Mythologyli
Copy link
Owner Author

要不要先不管 dns-hijack 了,先把 refactor 合并到 main,dns-hijack 单独开一个 PR

@cxz66666
Copy link
Collaborator

cxz66666 commented Nov 1, 2023

要不要先不管 dns-hijack 了,先把 refactor 合并到 main,dns-hijack 单独开一个 PR

合理,感觉改下readme就能合

@Mythologyli Mythologyli changed the title [WIP] feat: support tun mode feat: support tun mode Nov 1, 2023
@Mythologyli Mythologyli merged commit e0f46cd into main Nov 1, 2023
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants