周末一起联机玩一把文明6是多么快乐的事呀,但是坑爹的网络无情地摧毁了一切

同步回合慢就算了,时不时掉线真的是完全不能忍了

想来想去,咱这网络条件,肯定问题不在这里呀

花钱买了加速器还这么卡,问题八成也不在网络链路上呀

破案了,问题就是官方服务器垃圾垃圾垃圾~~

我们,必须要抗争,同这不合理的网络斗争


首先,咱肯定是要想办法摆脱官方服务器,因为只要网络要走官服,就肯定要卡

搜来搜去,只找到了国内某游戏平台,可惜的是,下载的时候浏览器报毒了

没办法,强烈洁癖忍不了,方案作废

没有路,那就开一条路出来

我们得先想想,我们联机时为什么要走官方服务器

显然是因为需要一个媒介来帮助两个异地小伙伴进行愉快的网络通信

因为 NAT 的流行,导致两个不同局域网的用户很难进行 p2p 通信

即使有着花样繁多的 p2p 方案,也没谁能保证稳定高可用

所以官方选择搭建一个服务器来帮助联机也是可以理解的

但是 !!!

我们还可以选择局域网联机,流量直达,稳定可靠

那么有什么能让异地小伙伴快乐的异地组网呢?

当然是 V ~ P ~ N ~

这里感谢 jintao 在使用 wireguard 搭 vpn 时的巨大贡献 :D

如果问题到此迎刃而解,那本文也未免太水了

接着我们就会遇到游戏联机史上最常见的问题:搜不到房间

其实这个问题以前上学时玩饥荒联机就遇到过,只要关掉多余的网卡就行

但是 wireguard,或者说几乎所有 vpn 软件组网时,都是使用了虚拟网卡

而这个虚拟网卡又干扰了游戏间正常的互相寻找流程

「关掉网卡,我连不上你;开启网卡,我找不到你」

当然我第一反应是把 wireguard 整到路由器上,这样就避免了本地网卡干扰了

但可惜的是,这个方案过于麻烦,并不是特别满意

所以我们就思考呀,为啥找不到房间呢?

找不到房间其实就是两端的进程无法发现对方,也就是「服务发现」失败

任何局域网联机都依赖于自己的服务发现,而且一般不会是 consul 这样复杂的方案

一般软件都会通过广播包来实现自己的简化版服务发现

那么八成就是这个广播包在虚拟网卡的干扰下,没有正确投递到对方机器咯

就没啥好说的,打开 Wireshark 抓个包,得到文明6发的一系列 udp 服务发现包

然后看地址是发到 255.255.255.255 的,查看路由表就一目了然了

默认的 255.255.255.255 地址是路由到了我的物理网卡上

所以对端的小伙伴时无法收到这个包的

接下来有两个办法

  1. 「路线纠正」修改路由表,让这个地址的广播包走到 wireguard 虚拟网卡上
  2. 「明确目标」抓包重发,把 udp 包内容修改目的地址后,直接发到小伙伴机器上

感觉方案1有点不太好,因为改路由表还是不靠谱,容易触发未知 bug

这里选择了方案2,简单写了个抓包重发工具

果然成功了~~~

知识改变游戏 ^-^


后记

其实感觉是可以把两地的网络配成真正互通的,这样才是最完美的方案

但是一直没想到比较好的办法,要是有网络大佬指教一下就好了 QAQ

为啥抓包重发后就能成功呢,因为最开始的服务发现包是 udp,是无连接的

也就是说我们可以手动构造发包来源和目的地址

那么,我们把来源端口设置为文明6进程使用的端口,目的地址修改为小伙伴机器的 ip

这个 udp 包自然能被顺利投递过去

而服务发现最重要的就是互相发现,当我们架上了这样一座桥后

对端就知道了这里有个进程在寻找房间,对面就会主动发房间信息过来

然后的然后,一切都刚刚好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Wirgurad Server Config
[Interface]
Address = 10.100.0.1/16
SaveConfig = false
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE
ListenPort = 10000
PrivateKey = Axxxxxxxxxxxxxxxxx

[Peer]
PublicKey = Bxxxxxxxxxxxxxxxx
AllowedIPs = 10.100.0.1/24

[Peer]
PublicKey = Cxxxxxxxxxxxxxxxx
AllowedIPs = 10.100.1.1/24
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
// 抓包重发小工具
package main

import (
"context"
"flag"
"log"
"net"
"os"
"os/signal"
"strings"
"syscall"
"time"

"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
)

var (
iface = flag.String("iface", "en0", "interface of network")
from = flag.String("from", "10.100.0.6", "copy udp packet from")
to = flag.String("to", "10.100.255.255", "copy udp packet to")
showIface = flag.Bool("show-iface", false, "show interface of network")

packets = make(chan *pkt, 100)
)

type pkt struct {
SrcPort int
DstPort int
Payload []byte
}

func capture(ctx context.Context, device string) {
var (
ethLyr layers.Ethernet
ip4Lyr layers.IPv4
ip6Lyr layers.IPv6
udpLyr layers.UDP
dnsLyr layers.DNS
ntpLyr layers.NTP
payload gopacket.Payload
)
parser := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, &ethLyr, &ip4Lyr, &ip6Lyr, &udpLyr, &payload, &dnsLyr, &ntpLyr)
decoded := make([]gopacket.LayerType, 0)
newHandler := func() *pcap.Handle {
inactive, err := pcap.NewInactiveHandle(device)
if err != nil {
log.Fatal(err)
}
defer inactive.CleanUp()
if err = inactive.SetPromisc(true); err != nil {
log.Fatal(err)
}
if err = inactive.SetSnapLen(4096); err != nil {
log.Fatal(err)
}
if err = inactive.SetTimeout(time.Minute); err != nil {
log.Fatal(err)
}
handler, err := inactive.Activate()
if err != nil {
log.Fatal(err)
}
return handler
}
handler := newHandler()
if err := handler.SetBPFFilter("udp"); err != nil {
log.Fatal("set bpf failed, ", err.Error())
}

log.Println("capture start")
for {
select {
case <-ctx.Done():
log.Println("capture stop")
return
default:
data, _, err := handler.ReadPacketData()
if err != nil {
log.Fatal(err)
}
if err := parser.DecodeLayers(data, &decoded); err != nil {
log.Println("decode error: ", err.Error())
continue
}
var packet pkt
for _, layer := range decoded {
switch layer {
case layers.LayerTypeIPv4:
if ip4Lyr.DstIP[3] != 0xff && !ip4Lyr.DstIP.IsMulticast() {
goto endPacket
}
case layers.LayerTypeIPv6:
if !ip6Lyr.DstIP.IsMulticast() {
goto endPacket
}
case layers.LayerTypeUDP:
packet.SrcPort = int(udpLyr.SrcPort)
packet.DstPort = int(udpLyr.DstPort)
packet.Payload = payload.LayerContents()
}
}
if packet.SrcPort > 0 && packet.DstPort != 53 && packet.DstPort != 5353 {
packets <- &packet
}
endPacket:
}
}
}

func send(ctx context.Context, srcIP, dstIP net.IP) {
log.Println("send start")
for {
select {
case <-ctx.Done():
log.Println("send stop")
return
case packet := <-packets:
laddr := &net.UDPAddr{IP: srcIP, Port: packet.SrcPort}
raddr := &net.UDPAddr{IP: dstIP, Port: packet.DstPort}
conn, err := net.DialUDP("udp", laddr, raddr)
if err != nil {
log.Printf("falied dial udp from %v to %v \n", laddr, raddr)
continue
}
if _, err := conn.Write(packet.Payload); err != nil {
log.Printf("send %d bytes from %v to %v failed \n", len(packet.Payload), laddr, raddr)
} else {
log.Printf("send packet from %v to %v success \n", laddr, raddr)
}
conn.Close()
}
}
}

func display() {
devs, err := pcap.FindAllDevs()
if err != nil {
log.Fatal("failed to find interface, ", err.Error())
}
str := func(addrs []pcap.InterfaceAddress) string {
var result []string
for _, v := range addrs {
result = append(result, v.IP.String())
}
return strings.Join(result, ", ")
}
for _, dev := range devs {
log.Printf("%s: [%v]", dev.Name, str(dev.Addresses))
}
}

func main() {
flag.Parse()
ctx, cancel := context.WithCancel(context.Background())
if *showIface {
display()
return
}
srcIP := net.ParseIP(*from)
dstIP := net.ParseIP(*to)
if len(dstIP) == 0 || len(srcIP) == 0 {
log.Fatal("invalid dst or src ip")
}
log.Printf("iface: %s, from: %s, to: %s", *iface, srcIP.String(), dstIP.String())
go capture(ctx, *iface)
go send(ctx, srcIP, dstIP)

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit
cancel()
time.Sleep(time.Second)
log.Println("exit.")
}