惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 热门话题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
K
Kaspersky official blog
Google Online Security Blog
Google Online Security Blog
Simon Willison's Weblog
Simon Willison's Weblog
Hacker News - Newest:
Hacker News - Newest: "LLM"
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
P
Privacy International News Feed
D
Docker
Cisco Talos Blog
Cisco Talos Blog
Spread Privacy
Spread Privacy
Jina AI
Jina AI
Last Week in AI
Last Week in AI
The Last Watchdog
The Last Watchdog
WordPress大学
WordPress大学
Project Zero
Project Zero
小众软件
小众软件
V
V2EX
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
L
Lohrmann on Cybersecurity
T
Troy Hunt's Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
P
Privacy & Cybersecurity Law Blog
AWS News Blog
AWS News Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
W
WeLiveSecurity
V
Vulnerabilities – Threatpost
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
Cloudbric
Cloudbric
罗磊的独立博客
TaoSecurity Blog
TaoSecurity Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google DeepMind News
Google DeepMind News
雷峰网
雷峰网
IT之家
IT之家
A
Arctic Wolf
The Hacker News
The Hacker News
T
Tailwind CSS Blog
Know Your Adversary
Know Your Adversary
博客园 - 司徒正美
腾讯CDC
S
SegmentFault 最新的问题
P
Palo Alto Networks Blog
NISL@THU
NISL@THU

豆逗子的小黑屋

GEMINI-CLI settings 参数详情 浅析 Claude Code 的执行与提示词 多模态模型是如何处理和理解图片的? 从配料表出发:便秘的猫应该怎么选主食 盘点开源的 DeepResearch 实现方案 浅谈 DeepSeek-R1 和 Kimi k1.5 论文中的思维链 + 强化学习 使用 TiDB Vector 构建 LightRAG 知识库 从论文到源码:详解 RAG 算法 云南之行——游在大理食在昆明 浅入浅出 Rerank 模型 一年同行:我的TiDB社区之旅 读书笔记《大语言模型》 TiDB Vector + Dify 快速构建 AI Agent 基于 LLM 推动游戏叙事 HTTP/2 和 CONTINUATION Flood 混合专家模型 (MoE) 笔记 报告分享: IMF第四次磋商报告 和 美联储研究笔记 使用 Coze 搭建 TiDB 助手 2023年总结 读书笔记《大规模语言模型:从理论到实践》 TiDB知识点梳理 (PCTA 笔记) 向量相似性检索方法 Java & Go 线程模式对比 Hugo + umami 博客统计面板 资产配置 101 探究 Spring-Boot 内置Server 搭建Go版本Kubernetes微服务示例 为什么Spring可以“自己注入自己” Go语言指针性能 从固定走向浮动 ——《时运变迁》读书笔记 Netty 源码分析及内存溢出思路 博客搭建简述 推荐 笔记 看得见的手——《置身事内》读书笔记 2022年初读书回顾 荐书:《走出唯一真理观》 关于 nintendo switch 邮箱
使用 sing-box Tun 模式实现 V2rayU 的透明代理
Weaxs · 2025-11-09 · via 豆逗子的小黑屋

使用 sing-box Tun 模式实现 V2rayU 的透明代理

前言 #

因为最近一直在用 gemini-cli,在家里使用的时候一直有登录不上的问题。

自己用的是 mac 的 V2rayU 客户端的代理,在浏览器上登录 Gemini 肯定是没啥问题的,但 gemini-cli 一直不行,就花了些功夫解决这个问题。

技术上其实没什么难的,只是梳理一下思路,总结一下代理方式。

问题分析 #

首先我肯肯定需要分析一下 gemini-cli 在本地鉴权的过程:

  1. gemini-cli 开启一个本地端口作为 oauth 的 callback 地址,端口号是随机的
  2. 请求浏览器打开 google account 已向 oauth 服务器发送
  3. 登录自己的 google 账号
  4. 浏览器验证通过后,会返回一个 access token 给到本地的 callback 地址
  5. 本地的 callback 地址最终请求 google 服务器已确认token 的准确性并返回凭证

上面终端需要注意的就是,gemini-cli 会在本地随机开一个端口

我们前面说到本人本地用的代理是 V2RayU。

V2rayU 的高级设置了其实写了其监听和代理的接口,如下图

V2rayU.png

至此就可以很明显地发现,gemini-cli 开启的随机端口和 V2rayU 所监听的端口肯定不一样。

所以在 gemini-cli 鉴权的最后一步自然也无法获取到登录凭证。

当你把这个问题给到 ChatGPT 或者 Gemini 的时候,这时它会推荐给以一个 系统层面全局流量代理 —— Tun 模式、虚拟网卡模式。

模型会先推荐你去启用 V2rayU 的 Tun 代理,但是仔细找找你会发现,V2rayU 根本没有 Tun 代理模式 !

去 V2rayU 的 issue 里也能看到相关的 feature

V2rayU-issues.png

这个时候如果你追问模型,并让他给你推荐一些有 GUI 的工具,那么它会推荐你 sing-boxClash Verge RevHiddify

于是笔者挨个下了这三个 app,并解锁了新的问题:当你把 V2rayU 中的 v2fly 客户端 config.json 尝试导入进来的时候会报错。这三个 app 中的代理的 config 和 v2fly 中的并不完全对齐,需要改配置。

这个问题的解决办法当然也是扔给模型,让他给我产出 Clash Verge RevHiddify 需要的导入格式。

在模型改了 N 遍之后,导入的配置终于可以代理了。

于是我兴冲冲地准备开启 Clash Verge RevHiddify 的 Tun 模式,app 中告诉我此模式需要管理员的权限。这次轮到 mac 掉链子了——mac 死活不弹出需要我确认授权的入口,模型给了几种方式像是完全退出重启、重装等等试了都不行。除此之外 sing-box 的 GUI 会要求授权安装一个网络拓展,也是无法弹出授权入口。我甚至更新了 mac 系统还是不行。

尝试了很多次之后累了,最终选择了非 GUI 的方法。

下载 sing-box 的可执行程序压缩包,并在 console 控制台中开启监听。

sing-box 转发到 V2rayU #

我们前面说过 V2rayU 本身是好使的,完全没啥问题。

我们既然用了 sing-box ,那是不是可以直接用 sing-box 的 TUN 模式全局监听本地,然后将请求都转发到 V2rayU 的 sock 端口。

和 Gemini 对了一下这个思路和方案之后发现完全可行,那么剩下的问题就是 sing-boxconfig.json 怎么写了。

经过二十多版的修改,Gemini 终于给我写出了可以用的config.json,有了 config.json 之后我们只需要在控制台执行类似下面这个命令就行

sudo ./sing-box run -c "config.json"

需要注意的是执行这个命令之前需要先开启 V2rayU。

最后贴一下 Gemini 给我的最终版的 config.json 作为参考。

{
    "log": {
        "level": "info",
        "timestamp": true
    },
    "dns": {
        "servers": [
            {
                "tag": "google-dns",
                "address": "udp://8.8.8.8"
            },
            {
                "tag": "cf-dns",
                "address": "udp://1.1.1.1"
            }
        ],
        "strategy": "ipv4_only"
    },
    "inbounds": [
        {
		        ## inbound 定义为 Tun 模式
            "type": "tun",
            "tag": "tun-in",
            "interface_name": "utun8",
            "address": "198.18.0.1/16",
            "auto_route": true,
            "strict_route": true,
            "sniff": false
        }
    ],
    "outbounds": [
        {
		        ## 所有出站转发到 v2rayu-proxy 对应的 sock 端口 (1080)
            "type": "socks",
            "tag": "v2rayu-proxy",
            "server": "127.0.0.1",
            "server_port": 1080
        },
        {
	          ## localhost 和 代理服务器的 ip 需要直接走 direct 模式
            "type": "direct",
            "tag": "direct",
            "bind_interface": "en0"
        }
    ],
    "route": {
        "rules": [
            {
                "ip_is_private": true,
                "outbound": "direct"
            },
            {
		            ## 本地域名
                "domain": [
                    "localhost"
                ],
                "outbound": "direct"
            },
            {
		            ## 本地 ip
                "ip_cidr": [
                    "127.0.0.1/32"
                ],
                "outbound": "direct"
            },
            {
		            ## 代理服务器的 ip
                "ip_cidr": [
                    "3.xxx.xx.82/32"
                ],
                "outbound": "direct"
            },
            {
                "network": "tcp,udp",
                "outbound": "v2rayu-proxy"
            }
        ]
    }
}

Tun 原理 #

最后我们从代码层面简单地看看 Tun 模式是如何实现的。

sing-box 的 tun 实现实际上是在 sing-tun 库中。

sing-box 中只需要判断 inbound 入站,当发现类型是 tun 时,所有都走sing-tun

Tun 定义 #

sing-tuntun.go 分别定义了 Tun 的接口,并定义了 Windows、Linux 和 Mac 中的 interface 层。

type Tun interface {
	io.ReadWriter
	Name() (string, error)
	Start() error
	Close() error
	UpdateRouteOptions(tunOptions Options) error
}

type WinTun interface {
	Tun
	ReadPacket() ([]byte, func(), error)
}

type LinuxTUN interface {
	Tun
	N.FrontHeadroom
	BatchSize() int
	BatchRead(buffers [][]byte, offset int, readN []int) (n int, err error)
	BatchWrite(buffers [][]byte, offset int) (n int, err error)
	TXChecksumOffload() bool
}

type DarwinTUN interface {
	Tun
	BatchRead() ([]*buf.Buffer, error)
	BatchWrite(buffers []*buf.Buffer) error
}

Tun 创建 #

下面我们只看看 Mac 中的实现,这部分代码在 tun_darwin.go

utun 的创建是在 NativeTunNew 方法中 :

  1. 通过 tunFd, err = unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, 2) 开启一个 Socket 连接 unix.AF_SYSTEM System 的地址簇,这个地址是 macOS 内核特有的
  2. 通过 err = create(tunFd, ifIndex, [options.Name](http://options.name/), options) 向 macOS 发送监听的指令,具体我们看看这个方法,这个方法使用了三次 useSocket ,具体看这部分就行
    1. 先调用 useSocket 通过 unix.IoctlSetIfreqMTU(socketFd, &ifr) 添加 MTU (Maximum Transmission Unit, 最大传输单元)
    2. 调用 useSocket 通过 unix.Syscallunix.AF_INETunix.SIOCAIFADDRutun 配置 IP 和 子网掩码,和我们上面的 config.json 对齐,那这里配置的就是 198.18.0.1/16
    3. 和 上一个步骤类似,只不过上一个步骤是给 utun 配置 ipv4 的 IP 和子网掩码,这一步是通过 unix.Syscallunix.AF_INET6 和 unix.SIOCAIFADDR_IN6utun 配置 ipv6

至此我们已经创建了 utun,并给他分配了一个本地的 ip 和 掩码,但是我们还需要把本地的请求全都打到这个 ip 上去

// https://github.com/SagerNet/sing-tun/blob/main/tun_darwin.go

var _ DarwinTUN = (*NativeTun)(nil)

const PacketOffset = 4

type NativeTun struct {
	tunFd               int
	tunFile             *os.File
	batchSize           int
	iovecs              []iovecBuffer
	iovecsOutput        []iovecBuffer
	iovecsOutputDefault []unix.Iovec
	msgHdrs             []rawfile.MsgHdrX
	msgHdrsOutput       []rawfile.MsgHdrX
	buffers             []*buf.Buffer
	stopFd              stopfd.StopFD
	options             Options
	inet4Address        [4]byte
	inet6Address        [16]byte
	routeSet            bool
	sendMsgX            bool
}

func New(options Options) (Tun, error) {
	var tunFd int
	batchSize := ((512 * 1024) / int(options.MTU)) + 1
	if options.FileDescriptor == 0 {
		ifIndex := -1
		_, err := fmt.Sscanf(options.Name, "utun%d", &ifIndex)
		if err != nil {
			return nil, E.New("bad tun name: ", options.Name)
		}

		tunFd, err = unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, 2)
		if err != nil {
			return nil, err
		}

		err = create(tunFd, ifIndex, options.Name, options)
		if err != nil {
			unix.Close(tunFd)
			return nil, err
		}
		err = configure(tunFd, options.EXP_MultiPendingPackets, batchSize)
		if err != nil {
			unix.Close(tunFd)
			return nil, err
		}
	} else {
		tunFd = options.FileDescriptor
		err := configure(tunFd, options.EXP_MultiPendingPackets, batchSize)
		if err != nil {
			return nil, err
		}
	}
	nativeTun := &NativeTun{
		tunFd:         tunFd,
		tunFile:       os.NewFile(uintptr(tunFd), "utun"),
		options:       options,
		batchSize:     batchSize,
		iovecs:        make([]iovecBuffer, batchSize),
		iovecsOutput:  make([]iovecBuffer, batchSize),
		msgHdrs:       make([]rawfile.MsgHdrX, batchSize),
		msgHdrsOutput: make([]rawfile.MsgHdrX, batchSize),
		stopFd:        common.Must1(stopfd.New()),
		sendMsgX:      options.EXP_SendMsgX,
	}
	for i := 0; i < batchSize; i++ {
		nativeTun.iovecs[i] = newIovecBuffer(int(options.MTU))
		nativeTun.iovecsOutput[i] = newIovecBuffer(int(options.MTU))
	}
	if len(options.Inet4Address) > 0 {
		nativeTun.inet4Address = options.Inet4Address[0].Addr().As4()
	}
	if len(options.Inet6Address) > 0 {
		nativeTun.inet6Address = options.Inet6Address[0].Addr().As16()
	}
	return nativeTun, nil
}

func create(tunFd int, ifIndex int, name string, options Options) error {
	ctlInfo := &unix.CtlInfo{}
	copy(ctlInfo.Name[:], utunControlName)
	err := unix.IoctlCtlInfo(tunFd, ctlInfo)
	if err != nil {
		return os.NewSyscallError("IoctlCtlInfo", err)
	}

	err = unix.Connect(tunFd, &unix.SockaddrCtl{
		ID:   ctlInfo.Id,
		Unit: uint32(ifIndex) + 1,
	})
	if err != nil {
		return os.NewSyscallError("Connect", err)
	}

	err = useSocket(unix.AF_INET, unix.SOCK_DGRAM, 0, func(socketFd int) error {
		var ifr unix.IfreqMTU
		copy(ifr.Name[:], name)
		ifr.MTU = int32(options.MTU)
		return unix.IoctlSetIfreqMTU(socketFd, &ifr)
	})
	if err != nil {
		return os.NewSyscallError("IoctlSetIfreqMTU", err)
	}
	if len(options.Inet4Address) > 0 {
		for _, address := range options.Inet4Address {
			ifReq := ifAliasReq{
				Addr: unix.RawSockaddrInet4{
					Len:    unix.SizeofSockaddrInet4,
					Family: unix.AF_INET,
					Addr:   address.Addr().As4(),
				},
				Dstaddr: unix.RawSockaddrInet4{
					Len:    unix.SizeofSockaddrInet4,
					Family: unix.AF_INET,
					Addr:   address.Addr().As4(),
				},
				Mask: unix.RawSockaddrInet4{
					Len:    unix.SizeofSockaddrInet4,
					Family: unix.AF_INET,
					Addr:   netip.MustParseAddr(net.IP(net.CIDRMask(address.Bits(), 32)).String()).As4(),
				},
			}
			copy(ifReq.Name[:], name)
			err = useSocket(unix.AF_INET, unix.SOCK_DGRAM, 0, func(socketFd int) error {
				if _, _, errno := unix.Syscall(
					syscall.SYS_IOCTL,
					uintptr(socketFd),
					uintptr(unix.SIOCAIFADDR),
					uintptr(unsafe.Pointer(&ifReq)),
				); errno != 0 {
					return os.NewSyscallError("SIOCAIFADDR", errno)
				}
				return nil
			})
			if err != nil {
				return err
			}
		}
	}
	if len(options.Inet6Address) > 0 {
		for _, address := range options.Inet6Address {
			ifReq6 := ifAliasReq6{
				Addr: unix.RawSockaddrInet6{
					Len:    unix.SizeofSockaddrInet6,
					Family: unix.AF_INET6,
					Addr:   address.Addr().As16(),
				},
				Mask: unix.RawSockaddrInet6{
					Len:    unix.SizeofSockaddrInet6,
					Family: unix.AF_INET6,
					Addr:   netip.MustParseAddr(net.IP(net.CIDRMask(address.Bits(), 128)).String()).As16(),
				},
				Flags: IN6_IFF_NODAD | IN6_IFF_SECURED,
				Lifetime: addrLifetime6{
					Vltime: ND6_INFINITE_LIFETIME,
					Pltime: ND6_INFINITE_LIFETIME,
				},
			}
			if address.Bits() == 128 {
				ifReq6.Dstaddr = unix.RawSockaddrInet6{
					Len:    unix.SizeofSockaddrInet6,
					Family: unix.AF_INET6,
					Addr:   address.Addr().Next().As16(),
				}
			}
			copy(ifReq6.Name[:], name)
			err = useSocket(unix.AF_INET6, unix.SOCK_DGRAM, 0, func(socketFd int) error {
				if _, _, errno := unix.Syscall(
					syscall.SYS_IOCTL,
					uintptr(socketFd),
					uintptr(SIOCAIFADDR_IN6),
					uintptr(unsafe.Pointer(&ifReq6)),
				); errno != 0 {
					return os.NewSyscallError("SIOCAIFADDR_IN6", errno)
				}
				return nil
			})
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func (t *NativeTun) Start() error {
	t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name)
	return t.setRoutes()
}

Tun 监听 #

下面就是需要把请求都路由到这个 198.18.0.1/16 上去,这部分主要是在 NativeTunStart 方法中

这里其实核心的只有 setRoutes 方法 和 err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, interfaceIndex, destination, gateway)

这里的改动会将所有的请求都转到 utun8 接口(198.18.0.1),并将 config.json中的一些额外的规则也添加进来,例如路由表改成了

目的地 (Destination) 网关 (Gateway) 接口 (Interface)
0.0.0.0/1 198.18.0.1 utun8
128.0.0.0/1 198.18.0.1 utun8
3.xxx.xx.82/32 192.168.1.1 en0
func (t *NativeTun) Start() error {
	t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name)
	return t.setRoutes()
}

func (t *NativeTun) setRoutes() error {
	if t.options.FileDescriptor == 0 {
		routeRanges, err := t.options.BuildAutoRouteRanges(false)
		if err != nil {
			return err
		}
		if len(routeRanges) > 0 {
			gateway4, gateway6 := t.options.Inet4GatewayAddr(), t.options.Inet6GatewayAddr()
			for _, destination := range routeRanges {
				var gateway netip.Addr
				if destination.Addr().Is4() {
					gateway = gateway4
				} else {
					gateway = gateway6
				}
				var interfaceIndex int
				if t.options.InterfaceScope {
					iff, err := t.options.InterfaceFinder.ByName(t.options.Name)
					if err != nil {
						return err
					}
					interfaceIndex = iff.Index
				}
				err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, interfaceIndex, destination, gateway)
				if err != nil {
					if errors.Is(err, unix.EEXIST) {
						err = execRoute(unix.RTM_DELETE, false, 0, destination, gateway)
						if err != nil {
							return E.Cause(err, "remove existing route: ", destination)
						}
						err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, interfaceIndex, destination, gateway)
						if err != nil {
							return E.Cause(err, "re-add route: ", destination)
						}
					} else {
						return E.Cause(err, "add route: ", destination)
					}
				}
			}
			flushDNSCache()
			t.routeSet = true
		}
	}
	return nil
}