Repository navigation
Expand file tree
/
Copy pathanyproxy.go
More file actions
725 lines (690 loc) · 32 KB
/
Copy pathanyproxy.go
File metadata and controls
725 lines (690 loc) · 32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
package main
import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
_ "net/http/pprof"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/keminar/anyproxy/config"
"github.com/keminar/anyproxy/grace"
"github.com/keminar/anyproxy/logging"
"github.com/keminar/anyproxy/nat"
"github.com/keminar/anyproxy/proto"
"github.com/keminar/anyproxy/tun"
"github.com/keminar/anyproxy/utils/conf"
"github.com/keminar/anyproxy/utils/daemon"
"github.com/keminar/anyproxy/utils/geo"
"github.com/keminar/anyproxy/utils/help"
"github.com/keminar/anyproxy/utils/rss"
"github.com/keminar/anyproxy/utils/systune"
"github.com/keminar/anyproxy/utils/tools"
)
var (
gListenAddrPort string
gProxyServerSpec string
gConfigFile string
gWebsocketListen string
gMode string
gHelp bool
gDebug int
gPprof string
gVersion bool
gGeoExtract bool
gGeoIn string
gGeoCat string
gGeoOut string
gCheck bool
gCheckFix bool
gGenKey bool
gGenConf bool
gWol string
gSend string
gSendTo string
gSendVia string
gRecv string
gParallel int
gConflict string
gDirectPlainUDP bool
)
func init() {
flag.Usage = help.Usage
flag.StringVar(&gListenAddrPort, "l", "", "listen address of socks5 and http proxy; comma-separated to bind multiple addresses at once (e.g. 192.168.1.5:3000,10.0.0.5:3000)")
flag.StringVar(&gProxyServerSpec, "p", "", "Proxy servers to use")
flag.StringVar(&gConfigFile, "c", "", "Config file path, default is router.yaml")
flag.StringVar(&gWebsocketListen, "ws-listen", "", "Websocket address and port to listen on")
flag.StringVar(&gMode, "mode", "", "Run mode: proxy (default) | tunnel | tun (build TUN NIC, needs admin/root) | bypass (bind physical NIC only, escape another process's TUN) | tcpcopy (forward every connection to tcpcopy.ip:port)")
flag.IntVar(&gDebug, "debug", 0, "debug mode (0, 1, 2, 3)")
flag.StringVar(&gPprof, "pprof", "", "pprof port, disable if empty")
flag.BoolVar(&gVersion, "v", false, "Show build version")
flag.BoolVar(&gHelp, "h", false, "This usage message")
// geo 数据集离线提取(提取完即退出, 不启代理): 从全量 geoip.dat/geosite.dat 提取
// 指定类别成小文件随发布携带。geoip 与 geosite 外层格式一致, 同一命令通用。
flag.BoolVar(&gGeoExtract, "geo-extract", false, "Extract categories from a geoip.dat/geosite.dat and exit")
flag.StringVar(&gGeoIn, "geo-in", "", "geo-extract: source geoip.dat/geosite.dat")
flag.StringVar(&gGeoCat, "geo-cat", "", "geo-extract: comma-separated categories to keep, e.g. cn,google")
flag.StringVar(&gGeoOut, "geo-out", "", "geo-extract: output .dat path")
// 系统调优检查/应用(仅 Linux): 对照建议的 sysctl/ulimit 报告, 或一键写入并应用。
flag.BoolVar(&gGenKey, "genkey", false, "Generate a websocket auth key pair (private for client, public for server) and exit")
// 配置模板生成: 新机器上不知道配置长什么样时, 先生成一份带注释的骨架再改。
// 模板按 -mode 裁剪, 写到 -c 指定的路径(默认程序目录 conf/router.yaml, 写 "-" 打到标准输出)。
flag.BoolVar(&gGenConf, "genconf", false, "Write a commented config template (tailored to -mode) and exit; -c sets the output path (\"-\" for stdout)")
// 网络唤醒(WOL): 广播魔术包唤醒开了 WOL 的机器。不带 -via 时纯本机 UDP 广播,
// 与 anyproxy 的隧道/中继能力无关; 带 -via EMAIL 时改成让那个订阅方在它自己的
// 局域网里广播——要唤醒的机器本来就没开机, 没法自己接这条命令, 只能请同一个
// 局域网里另一台已经开着的机器代劳。固定走服务端中继(不打洞): 魔术包只有 102
// 字节, 一来一回就结束, 用不上打洞那一套开销, 见 nat/wol_action.go 顶部说明。
// 复用 -send 的 -to/-via 两个 flag, 不单独开两个新名字: -to 在这里是广播地址
// (谁执行广播就用谁的), -via 在这里必须是订阅方 email(不认 "direct"/"relay"
// 这两个 -send/-recv 专用的关键字), 留空(即保持默认值 "direct")就是本机广播。
flag.StringVar(&gWol, "wol", "", "Send Wake-on-LAN magic packets to the given MAC address(es), comma-separated, and exit. Without -via, broadcasts from this machine (see -to for the broadcast address). With -via EMAIL, asks that subscriber (on the target machine's own LAN, since the target itself is powered off and cannot run this command) to broadcast instead -- that subscriber must list this email under its own websocket.client.receive.allow AND set wol: true on that entry (default false; being in receive.allow for file transfer does not by itself permit this) -- always relayed through the server B, no NAT punching")
flag.StringVar(&gSend, "send", "", "Send a file or directory to another subscriber and exit (extra paths may follow as arguments)")
flag.StringVar(&gSendTo, "to", "", "-send: the receiving subscriber's email, optionally scp-style with a :subdir suffix (e.g. user@example.com:/aaa/) to land files under receive.dir/aaa/. -recv: local directory to save into, default is the current directory. -wol: broadcast target ADDR[:PORT], default 255.255.255.255:9 (limited broadcast, local link only)")
flag.StringVar(&gSendVia, "via", nat.ViaDirect, "-send/-recv: \"direct\" (punch through NAT, fails closed if no path), \"relay\" (through the server B, no punching needed), or the email of a public VPS (needs directRelay enabled) to blindly relay the NAT punch through -- for when the two peers cannot punch to each other directly (e.g. both behind CGNAT) but can each reach that VPS; the QUIC/TLS session still ends end-to-end between the two peers, the VPS only forwards opaque UDP packets. All are end-to-end encrypted. See docs/direct-relay-design.md. -wol: the subscriber's email who should broadcast instead of this machine, default (\"direct\") means broadcast locally")
flag.StringVar(&gRecv, "recv", "", "Fetch a file or directory from another subscriber and exit, scp-style EMAIL:PATH (PATH is relative to that peer's websocket.client.receive.dir, and this machine must already be listed in its receive.allow)")
flag.IntVar(&gParallel, "parallel", 1, "-send/-recv: split each large file into up to N chunks and transfer them over N concurrent connections (default 1, today's single-connection behavior); small files are never split. N is capped at 3 regardless of what's passed -- independent connections beyond that stop paying off, start hurting NAT punch reliability, and make the traffic look more like a proxy/download tool. -via direct/VPS: each connection is its own independently NAT-punched QUIC connection with its own congestion window, which is the point -- a single QUIC flow on a lossy path can get stuck with a small window no matter how large the file is, and a few independent flows recover the throughput a lossy path is actually capable of. -via relay: chunks are still concurrent logical sessions but multiplexed over the one already-open connection to the server, so this mainly helps by overlapping round-trips rather than opening more raw connections")
flag.StringVar(&gConflict, "conflict", "", "-send/-recv: what to do about a same-name file or a leftover .part from an interrupted transfer. MODE is one of: ask (interactive, default on a terminal), rename (save under a new name, no probing; default when stdin is not a terminal), overwrite, skip, or resume (continue a matching .part automatically; skip if the name already holds an identical file, else save under a new name). Only MODE=ask additionally prompts at the terminal: for a same-name file, [r]ename/[o]verwrite(if identical)/[s]kip; for a matching .part, [c]ontinue/[r]estart/[s]kip -- those letters are answers to that prompt, not values for -conflict itself")
flag.BoolVar(&gDirectPlainUDP, "direct-plain-udp", false, "direct (NAT punch) connections: skip quic-go's batched/ECN fast path for the UDP socket, always falling back to plain per-packet I/O. Try this if a direct transfer shows high loss and a congestion window stuck near its floor even on an otherwise healthy link -- on some machines (seen on Windows, likely a NIC driver/virtual adapter quirk) that fast path itself corrupts or delays packets, which quic-go then mistakes for congestion. Unlike -debug, this does not log every packet, so it's fine to leave on")
flag.BoolVar(&gCheck, "check", false, "Check system tuning (sysctl/ulimit) against recommendations and exit")
flag.BoolVar(&gCheckFix, "check-fix", false, "Apply recommended sysctl tuning (needs root) and exit")
}
func main() {
// 尽早设置 GODEBUG=madvdontneed=1 并 exec 重启自身,使归还的内存立即扣 RSS。
// 必须在任何堆分配/goroutine 之前,保证 runtime 读到该开关(幂等,二次进入直接跳过)。
rss.Ensure()
flag.Parse()
if gHelp {
flag.Usage()
return
}
if gVersion {
help.ShowVersion()
return
}
// geo 数据集离线提取: 生成精简 .dat 后退出, 不启动代理。
if gGeoExtract {
if gGeoIn == "" || gGeoCat == "" || gGeoOut == "" {
log.Fatalln("geo-extract requires -geo-in -geo-cat -geo-out")
}
if err := geo.Extract(gGeoIn, strings.Split(gGeoCat, ","), gGeoOut); err != nil {
log.Fatalln("geo-extract:", err)
}
fmt.Printf("geo-extract: extracted categories [%s] from %s into %s\n", gGeoCat, gGeoIn, gGeoOut)
return
}
// 生成 websocket 鉴权密钥对: 私钥配订阅方 websocket.client.key, 公钥配服务端
// websocket.server.users[].key。与 user/pass 二选一, 好处是鉴权走挑战-应答,
// 不依赖两端时钟同步, 且服务端只存公钥。
if gGenKey {
priv, pub, err := nat.GenerateKeyPair()
if err != nil {
log.Fatalln("genkey:", err)
}
fmt.Printf("Private key (client, websocket.client.key): %s\n", priv)
fmt.Printf("Public key (server, websocket.server.users[].key): %s\n", pub)
return
}
// 网络唤醒(本机广播): 纯本机 UDP 广播, 不需要配置/凭证, 跑完即退。-via 复用
// -send 的 flag, 停在默认值("direct", 即没有显式传别的订阅方 email)时才算本地
// 场景; 显式给了 email 时改成让那个订阅方去广播(见下方配置加载之后的分支, 那条
// 路要用 websocket.client 的连接与凭证)。
if gWol != "" && gSendVia == nat.ViaDirect {
macs := splitMacs(gWol)
if err := nat.WakeOnLAN(macs, gSendTo); err != nil {
log.Fatalln("wol:", err)
}
fmt.Printf("wol: sent magic packet to %d mac(s)\n", len(macs))
return
}
// 生成配置模板后退出。放在 LoadAllConfig 之前 —— 这条命令存在的前提就是本机
// 还没有配置文件, 不能反过来要求先能加载配置。
if gGenConf {
if err := genConfig(); err != nil {
log.Fatalln("genconf:", err)
}
return
}
// 系统调优检查/一键应用(仅 Linux), 完成即退出。
if gCheckFix {
systune.Apply()
return
}
if gCheck {
systune.Check()
return
}
config.SetDebugLevel(gDebug)
config.DirectPlainUDP = gDirectPlainUDP
conf.LoadAllConfig(gConfigFile)
// 检查配置是否存在
if conf.RouterConfig() == nil {
time.Sleep(60 * time.Second)
os.Exit(2)
}
// 直连传文件: 一次性动作, 传完就退出, 不启动代理。
//
// 放在配置加载之后(要用 websocket.client 的连接与凭证), 但在日志目录初始化之前 ——
// 这是个前台命令, 输出该直接打在终端上, 而不是写进日志文件。
if (gSend != "" || gRecv != "") && gParallel < 1 {
log.Fatalln("-parallel must be at least 1")
}
if gSend != "" {
paths := append([]string{gSend}, flag.Args()...)
cfg, err := pickClientConfig("send")
if err != nil {
log.Fatalln("send:", err)
}
if err := nat.SendFiles(cfg, gSendTo, paths, gSendVia, gParallel, gConflict); err != nil {
// 打洞失败也走这里: 按约定不做中继回落, 一个字节都不传, 退出码非零。
log.Fatalln("send:", err)
}
return
}
// 一次性取文件: 跟 -send 反向, 取完就退, 不启动代理。
if gRecv != "" {
cfg, err := pickClientConfig("recv")
if err != nil {
log.Fatalln("recv:", err)
}
if err := nat.RecvFiles(cfg, gRecv, gSendTo, gSendVia, gParallel, gConflict); err != nil {
log.Fatalln("recv:", err)
}
return
}
// 网络唤醒(远端广播): -wol 配了非默认 -via(即一个订阅方 email)时, 到这里才
// 处理——本地场景(-via 留在默认值 "direct")已经在配置加载之前的分支里跑完
// 退出了。固定走服务端中继, gSendVia 这里必须是 email, 不是 "direct"/"relay"
// 这两个 -send/-recv 专用的关键字(nat.SendWol 会拒绝 "relay")。
if gWol != "" {
cfg, err := pickClientConfig("wol")
if err != nil {
log.Fatalln("wol:", err)
}
if err := nat.SendWol(cfg, gSendVia, splitMacs(gWol), gSendTo); err != nil {
log.Fatalln("wol:", err)
}
return
}
cmdName := "anyproxy"
defLogDir := fmt.Sprintf("%s%s%s%s", conf.AppPath, string(os.PathSeparator), "logs", string(os.PathSeparator))
logDir := config.IfEmptyThen(conf.RouterConfig().Log.Dir, defLogDir, "")
if _, err := os.Stat(logDir); err != nil {
log.Println(err)
time.Sleep(60 * time.Second)
os.Exit(2)
}
envRunMode := fmt.Sprintf("%s_run_mode", cmdName)
fd := logging.ErrlogFd(logDir, cmdName)
// 是否后台运行
daemon.Daemonize(envRunMode, fd)
gListenAddrPort = config.IfEmptyThen(gListenAddrPort, conf.RouterConfig().Listen, ":3000")
// listen 显式设为 off/none/- 时不起代理监听, 仅跑 websocket/tun 等后台服务
// (典型: 纯 websocket 裸TCP穿透, 不需要本机代理端口)。
listenOff := isListenOff(gListenAddrPort)
// listen 支持逗号分隔写多个地址, 用来同时绑定多个内网网段的具体 IP(比监听 0.0.0.0
// 更收敛, 又不必受限于只能绑一个 IP)。
var listenAddrs []string
if listenOff {
gListenAddrPort = ""
} else {
listenAddrs = parseListenAddrs(gListenAddrPort)
config.SetListenAddrs(listenAddrs)
}
var writer io.Writer
// 前台执行,daemon运行根据环境变量识别
if daemon.IsForeground(envRunMode) {
// 同时输出到日志和标准输出
writer = io.Writer(os.Stdout)
}
logging.SetDefaultLogger(logDir, fmt.Sprintf("%s.%d", cmdName, config.ListenPort), true, 3, writer)
// 设置代理。命令行 -p 为固定值(记入 ProxyCmdline, 优先于配置);
// 未指定 -p 时请求侧实时读取 default.proxy, 使其支持热加载。
config.ProxyCmdline = gProxyServerSpec
// 启动时按「-p > default.proxy」解析首个代理, 供 tun_windows 排除捕获与日志用
config.SetProxyServer(config.IfEmptyThen(config.ProxyCmdline, conf.RouterConfig().Default.Proxy, ""))
// 加载 geoip/geosite 数据集(配了才加载, 供 hosts 的 geoip:/geosite: 匹配)
loadGeo()
// 调试模式
if len(gPprof) > 0 {
go func() {
gPprof = tools.FillPort(gPprof)
//浏览器访问: http://:5001/debug/pprof/
log.Println("Starting pprof debug server ...")
// 这里不要使用log.Fatal会在平滑重启时导致进程退出
// 因为http server现在没办法一次平滑重启,会报端口冲突,可以通过多次重试来启动pprof
for i := 0; i < 10; i++ {
log.Println(http.ListenAndServe(gPprof, nil))
time.Sleep(10 * time.Second)
}
}()
}
// websocket 服务端
gWebsocketListen = config.IfEmptyThen(gWebsocketListen, conf.RouterConfig().Websocket.Server.Listen, "")
if gWebsocketListen != "" {
gWebsocketListen = tools.FillPort(gWebsocketListen)
go nat.NewServer(&gWebsocketListen)
// 服务端裸TCP端口转发入口(内网穿透)
go nat.StartForward(conf.RouterConfig().Websocket.Server.Forward)
// UDP 中继入口: 与上面的 TCP 入口同端口、各走各的, 只对 protocol: udp/both 生效。
go nat.StartRelayUDP(conf.RouterConfig().Websocket.Server.Forward)
}
// websocket 客户端: 可同时订阅多台 server(见 conf.Websocket.ClientList)
clientList := conf.RouterConfig().Websocket.ClientList()
for i, cfg := range clientList {
cfg.Connect = tools.FillPort(cfg.Connect)
go nat.ConnectServer(cfg, i)
}
// TUN 虚拟网卡全局代理
// 使用可取消的 context,在收到 SIGINT/SIGTERM 时关闭设备,
// 确保 wintun 虚拟网卡在进程退出前被清理。
tunCtx, tunCancel := context.WithCancel(context.Background())
var tunWG sync.WaitGroup
// 解析运行模式: 命令行 -mode > 配置 mode > proxy
mode := config.IfEmptyThen(gMode, conf.RouterConfig().Mode, "proxy")
switch mode {
case "tun":
// autoRoute 不配置时默认 true(自动加路由); 显式设 false 才关闭
autoRoute := true
if conf.RouterConfig().Tun.AutoRoute != nil {
autoRoute = *conf.RouterConfig().Tun.AutoRoute
}
tunCfg := tun.Config{
Name: conf.RouterConfig().Tun.Name,
Addr: conf.RouterConfig().Tun.Addr,
MTU: conf.RouterConfig().Tun.MTU,
AutoRoute: autoRoute,
ExcludeProcs: conf.RouterConfig().Tun.ExcludeProcs,
RedirectPorts: conf.RouterConfig().Tun.RedirectPorts,
InboundPorts: conf.RouterConfig().Tun.InboundPorts,
WindivertDir: conf.RouterConfig().Tun.WindivertDir,
// 所有以 IP 指定的上级代理默认并入 bypassIPs(直连例外/排除捕获),
// 避免 anyproxy→上级代理 的连接被自己的 TUN/WinDivert 再抓走成环路
BypassIPs: withProxyBypassIPs(conf.RouterConfig().Tun.BypassIPs),
// 仅 Windows(WinDivert): 私网/LAN/链路本地一律直连。不配默认 true(与
// linux/darwin 直连子网不进 TUN 的行为一致); 显式 false 才让私网 80/443 进引擎
BypassPrivate: conf.RouterConfig().Tun.BypassPrivate == nil || *conf.RouterConfig().Tun.BypassPrivate,
}
tunWG.Add(1)
go func() {
defer tunWG.Done()
if err := tun.Run(tunCtx, tunCfg); err != nil {
log.Println("tun run err:", err)
}
}()
case "bypass":
// 仅 Linux 支持: 绑定物理网卡绕行, 逃出同机另一个 TUN 进程的 0/1 路由。
// macOS/Windows 已移除该模式(见 tun/bypass_other.go)。
// bypass 复用 tun.linux 块的 excludeNics/device(applyOS 已把 tun.linux 压平进 Tun)
if err := tun.InitBypassOnly(tun.BypassConfig{
ExcludeNics: conf.RouterConfig().Tun.ExcludeNics,
Device: conf.RouterConfig().Tun.Device,
}); err != nil {
log.Printf("mode=bypass unsupported: %v; fallback proxy", err)
mode = "proxy"
break
}
// 退出/平滑重启前清理 bypass 加的 /32 例外路由(复用 tunCtx 取消信号 + tunWG 等待)
tunWG.Add(1)
go func() {
defer tunWG.Done()
<-tunCtx.Done()
tun.CleanupBypass()
}()
case "proxy", "tunnel":
// 不接管全局流量,仅按监听端口收流
case "tcpcopy":
// 端口转发: 不接管全局流量, 每个连接改投到 tcpcopy.ip:port(见 proto/request.go)。
// 命令行 -mode tcpcopy 时配置里可能没有 mode 字段, 这里补上归一(配置文件写 mode:
// tcpcopy 时已在 LoadRouterConfig 归一)。
conf.RouterConfig().TcpCopy.Enable = true
default:
log.Printf("unknown mode %q, expect proxy|tunnel|tun|bypass|tcpcopy, fallback proxy\n", mode)
mode = "proxy"
}
// tcp 同是监听IPv4 和 IPv6
// tcp4 仅监听使用IPv4
// tcp6 仅监听使用IPv6
network := "tcp"
if conf.RouterConfig().Network != "" {
network = conf.RouterConfig().Network
}
// tunnel 为服务端(tunneld); proxy/tun/bypass 均为客户端
handler := proto.ClientHandler
if mode == "tunnel" {
handler = proto.ServerHandler
}
if listenOff {
// 关闭了代理监听: 没有 grace server 阻塞主流程, 改为等退出信号,
// 收到后取消 TUN context 并等设备清理。websocket 后台 goroutine 随进程退出。
if gWebsocketListen == "" && len(clientList) == 0 && mode != "tun" && mode != "bypass" {
log.Println("warning: proxy listen is off, but no websocket/tun configured; process will idle")
}
log.Println("proxy listen is off; running background services only (websocket/tun etc.)")
if grace.IsChild() {
// listen 由端口改成了 off 后 SIGHUP 重启到这里: 旧进程还在
// grace.Server 里等子进程 bind 成功后发来的 SIGTERM 才退出, 但这里走不到
// ListenAndServe() 里那段握手, 需要自己补上, 否则旧进程会一直占着端口不退出。
notifyOldProcessExit()
}
grace.NotifyReady()
waitForShutdown(tunCancel, &tunWG)
return
}
// 逗号分隔的每个地址各起一个 grace.Server 并发监听; grace 包本身按地址分别
// 记录监听 fd(见 socketPtrOffsetMap), SIGHUP 平滑重启时会把所有 fd 一起交给新进程。
var serveWG sync.WaitGroup
for _, addr := range listenAddrs {
server := grace.NewServer(addr, handler, network)
registerTUNCleanup(server, tunCancel, &tunWG)
serveWG.Add(1)
go func(s *grace.Server) {
defer serveWG.Done()
if err := s.ListenAndServe(); err != nil {
log.Println(s.Addr, "listen err:", err)
}
}(server)
}
serveWG.Wait()
}
// isListenOff 判断监听地址是否被显式关闭(off/none/no/disable/-, 大小写不敏感)。
// 关闭后不起本机代理监听, 仅跑 websocket/tun 等后台服务。
func isListenOff(s string) bool {
switch strings.ToLower(strings.TrimSpace(s)) {
case "off", "none", "no", "disable", "disabled", "-":
return true
}
return false
}
// parseListenAddrs 把 -l/listen 配置解析成实际要监听的地址列表: 支持用逗号分隔写多个
// 完整地址(如 "192.168.1.5:3000,10.0.0.5:3000"), 用来同时绑定多个内网网段的具体 IP,
// 既不必监听 0.0.0.0(*) 那么宽, 也不受限于只能绑一个 IP。每个地址单独走 FillPort 补全
// (只写纯数字端口才会补成 ":端口" 通配地址), 不会跨地址互相补全 host/port。
func parseListenAddrs(spec string) []string {
var addrs []string
seen := make(map[string]bool)
for _, part := range strings.Split(spec, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
part = tools.FillPort(part)
if seen[part] {
continue
}
seen[part] = true
addrs = append(addrs, part)
}
return addrs
}
// waitForShutdown 在关闭代理监听时替代 grace server 的阻塞并处理信号:
// - SIGINT/SIGTERM: 取消 TUN context、等设备清理后退出;
// - SIGHUP: 平滑重启。listen off 时没有主监听 fd 可继承交接, 故以「先起新进程、
// 再退旧进程」实现——websocket 服务自带绑定重试(见 nat.NewServer), 旧进程退出
// 释放端口后新进程即接管, 订阅端会自动重连。
func waitForShutdown(cancel context.CancelFunc, wg *sync.WaitGroup) {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
for {
switch <-sig {
case syscall.SIGHUP:
log.Println(os.Getpid(), "Received SIGHUP (listen off): starting new process to take over, exiting current process")
grace.NotifyReloading()
pid, err := restartSelf()
if err != nil {
log.Println("restart err:", err, "(keeping current process running)")
continue // 起新进程失败就不退旧进程, 避免服务中断
}
// 这条路径不等子进程握手确认(见 restartSelf 上的注释), 拿到 pid 就直接
// 迁移 MAINPID: 否则老进程退出后 systemd 会误判服务停止, 清理掉新进程。
grace.NotifyMainPID(pid)
cancel()
wg.Wait()
return
default: // SIGINT / SIGTERM
cancel()
wg.Wait()
return
}
}
}
// restartSelf 用相同参数启动一个新进程(去掉 grace 内部的 -graceful 标志), 返回其 pid。
// 与 grace.fork 不同, 这里不继承任何监听 fd(listen off 无主监听); 依赖 websocket
// 服务的绑定重试来接管旧进程释放的端口, 也不等子进程握手确认, 起成功就算数。
func restartSelf() (int, error) {
var args []string
for _, a := range os.Args[1:] {
if a == "-graceful" {
continue
}
args = append(args, a)
}
cmd := exec.Command(os.Args[0], args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
if err := cmd.Start(); err != nil {
return 0, err
}
go cmd.Wait()
return cmd.Process.Pid, nil
}
// notifyOldProcessExit 在「端口 -> listen off」的 SIGHUP 重启里补上 grace.Server
// 原本在 ListenAndServe() 里做的握手: 关掉从旧进程继承来但用不上的监听 fd(从 fd 3 起
// 连续排列, 数量按 grace.InheritedFDCount() 来定, 旧进程配过几个 listen 地址就有几个,
// 不能只关 fd 3——否则 listen 配了多个地址时会漏关、泄漏 fd),
// 再给旧进程发 SIGTERM 让它退出、释放端口。不这样做旧进程会一直占着端口不退出。
func notifyOldProcessExit() {
for i := 0; i < grace.InheritedFDCount(); i++ {
if f := os.NewFile(uintptr(3+i), ""); f != nil {
f.Close()
}
}
ppid := os.Getppid()
if ppid <= 1 { // 安全检查, 避免误杀 init/被收养的孤儿进程
return
}
process, err := os.FindProcess(ppid)
if err != nil {
log.Println(os.Getpid(), "find old process err:", err)
return
}
if err := process.Signal(syscall.SIGTERM); err != nil {
log.Println(os.Getpid(), "notify old process err:", err)
}
}
// registerTUNCleanup 注册 SIGHUP/SIGINT/SIGTERM 的 PreSignal 钩子,
// 在 grace server fork 子进程或关闭 TCP 监听之前,先取消 TUN context 并等待设备关闭。
// - SIGHUP(平滑重启): fork 前主动释放 TUN 设备,让新进程能立即接管,
// 否则新进程创建同名设备会 EBUSY(TUN 设备独占)。
// - SIGINT/SIGTERM(退出): 确保 TUN 虚拟网卡在进程退出前被清理。
//
// PreSignal 钩子在 fork()/shutdown() 之前执行,保证释放先于接管/退出。
func registerTUNCleanup(server *grace.Server, cancel context.CancelFunc, wg *sync.WaitGroup) {
cleanup := func() {
cancel()
wg.Wait()
}
server.RegisterSignalHook(grace.PreSignal, syscall.SIGHUP, cleanup)
server.RegisterSignalHook(grace.PreSignal, syscall.SIGINT, cleanup)
server.RegisterSignalHook(grace.PreSignal, syscall.SIGTERM, cleanup)
}
// withProxyBypassIPs 把「所有以 IP 字面量指定的上级代理」并入 base(bypassIPs)后返回。
// 覆盖全局代理(-p / default.proxy, 取 config.ProxyServer)与各 hosts[].proxy(支持多代理
// 逗号分隔及 " last"/" deny" 后缀)。只收 IPv4 字面量; 域名指定的代理无法在此确定 IP, 跳过。
// 目的: 让 anyproxy 到上级代理的连接默认直连(不被自己的 TUN/WinDivert 抓走成环路)。
func withProxyBypassIPs(base []string) []string {
seen := map[string]bool{}
out := make([]string, 0, len(base))
for _, s := range base {
s = strings.TrimSpace(s)
if s != "" && !seen[s] {
seen[s] = true
out = append(out, s)
}
}
add := func(host string) {
host = strings.TrimSpace(host)
ip := net.ParseIP(host)
// 回环上级代理无需并入: WinDivert 对 loopback 一律直连, linux/darwin 的
// /32 直连路由对 127.x 也无意义, 加进去只是冗余噪音。
if ip == nil || ip.To4() == nil || ip.IsLoopback() || seen[host] {
return
}
seen[host] = true
out = append(out, host)
}
// 全局代理(命令行 -p / default.proxy 解析后的服务器地址)
add(config.ProxyServer)
// 各 host 的自定义代理
for _, h := range conf.RouterConfig().Hosts {
p := strings.TrimSpace(h.Proxy)
if p == "" {
continue
}
// 去掉尾部 " last" / " deny" 操作后缀
if i := strings.LastIndex(p, " "); i >= 0 {
if suf := p[i+1:]; suf == "last" || suf == "deny" {
p = p[:i]
}
}
for _, spec := range strings.Split(p, ",") {
spec = strings.TrimSpace(spec)
if spec == "" {
continue
}
if j := strings.Index(spec, "://"); j >= 0 { // 去 scheme
spec = spec[j+3:]
}
host := spec
if k := strings.LastIndex(spec, ":"); k >= 0 { // 去端口(IPv6 字面量会解析失败被跳过)
host = spec[:k]
}
add(host)
}
}
return out
}
// loadGeo 按配置加载 geoip/geosite 数据集(配了才加载)。加载失败只记日志、不中断启动;
// 若 hosts 用了 geoip:/geosite: 但对应数据没配/没加载, 该规则永不命中, 给出提示。
func loadGeo() {
// 顶层 geoip / geosite: 一个文件可多类别(cats 空=.dat 全部类别), 同文件只解析一次
for _, gf := range conf.RouterConfig().GeoIP {
path := strings.TrimSpace(gf.File)
if path == "" {
continue
}
if err := geo.LoadIPFile(path, gf.Cats); err != nil {
log.Printf("geo: failed to load geoip <- %s: %v", path, err)
}
}
for _, gf := range conf.RouterConfig().GeoSite {
path := strings.TrimSpace(gf.File)
if path == "" {
continue
}
if err := geo.LoadSiteFile(path, gf.Cats); err != nil {
log.Printf("geo: failed to load geosite <- %s: %v", path, err)
}
}
if ic, sc := geo.Stat(); ic > 0 || sc > 0 {
log.Printf("geo: loaded geoip categories=%d, geosite categories=%d", ic, sc)
}
// 用了 geoip:/geosite: 规则但数据未就绪时提示
for _, h := range conf.RouterConfig().Hosts {
if strings.HasPrefix(h.Name, "geoip:") && !geo.HasIP() {
log.Printf("geo: rule %q requires geo.ip to load geoip.dat, but it is not loaded; this rule will never match", h.Name)
}
if strings.HasPrefix(h.Name, "geosite:") && !geo.HasSite() {
log.Printf("geo: rule %q requires geo.site to load geosite.dat, but it is not loaded; this rule will never match", h.Name)
}
}
}
// genConfig 处理 -genconf: 生成一份按 -mode 裁剪的带注释配置模板。
//
// 输出位置取 -c(与启动时读配置用的是同一个参数, 生成完原样启动即可); 不带 -c 就写到
// 程序目录下的 conf/router.yaml —— 那正是 GetPath 的第一优先级, 生成后不带参数直接
// 启动就能被找到。-c - 打到标准输出, 方便先看一眼或自己重定向。
func genConfig() error {
mode := gMode
if mode == "" {
mode = "proxy"
}
if !conf.ValidGenMode(mode) {
return fmt.Errorf("unknown -mode %q, expect %s", mode, strings.Join(conf.GenModes, "|"))
}
if gConfigFile == "-" {
body, err := conf.GenerateConfig(mode)
if err != nil {
return err
}
fmt.Print(body)
return nil
}
path, err := conf.WriteConfigTemplate(mode, gConfigFile)
if err != nil {
return err
}
fmt.Printf("genconf: generated a %s-mode config template -> %s\n", mode, path)
// 日志目录不存在时程序启动即退出, 顺手建出来, 免得留个必踩的坑。
if dir, created, lerr := conf.EnsureLogDir(); lerr != nil {
fmt.Printf("genconf: failed to create log directory %s: %v (create it yourself before starting, or change log.dir in the config)\n", dir, lerr)
} else if created {
fmt.Printf("genconf: created log directory %s\n", dir)
}
fmt.Println("Next steps:")
for i, s := range conf.GenNextSteps(mode, path) {
fmt.Printf(" %d. %s\n", i+1, s)
}
return nil
}
// splitMacs 把 -wol 的逗号分隔 MAC 列表拆开并去掉两侧空白。
func splitMacs(s string) []string {
macs := strings.Split(s, ",")
for i := range macs {
macs[i] = strings.TrimSpace(macs[i])
}
return macs
}
// pickClientConfig 挑一条 websocket.client 配置给 -send/-recv 用。verb 只影响提示
// 文案("send"/"recv")。
//
// 多台 server 时不再默默取第一条: 目标订阅方连在哪台 server 上, 程序猜不出来
// (对端在哪要连上去问了才知道), 默默选错的话要么白跑一趟才报错、要么(更糟)真连
// 上了却把文件传去了错的地址段。改成把配置列出来, 交互式问一遍要用哪个——选错
// 的成本是重跑一次命令, 比默默用错一台强。
func pickClientConfig(verb string) (conf.WsClient, error) {
list := conf.RouterConfig().Websocket.ClientList()
if len(list) == 0 {
return conf.WsClient{}, errors.New("no websocket.client configured (need connect/user/email to reach the server)")
}
if len(list) == 1 {
return list[0], nil
}
fmt.Fprintf(os.Stderr, "%s: %d client blocks configured, choose which one to use:\n", verb, len(list))
for i, c := range list {
fmt.Fprintf(os.Stderr, " [%d] %s (user=%s email=%s)\n", i+1, c.Connect, c.User, c.Email)
}
fmt.Fprint(os.Stderr, "> ")
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return conf.WsClient{}, fmt.Errorf("read selection: %w", err)
}
line = strings.TrimSpace(line)
idx, err := strconv.Atoi(line)
if err != nil || idx < 1 || idx > len(list) {
return conf.WsClient{}, fmt.Errorf("invalid selection %q, expected a number from 1 to %d", line, len(list))
}
return list[idx-1], nil
}