Don't Starve 饥荒服务器部署(Macvlan)
背景
最近和朋友联机玩饥荒,需要部署一个 24 小时运行的游戏服务器,方便大家随时能加入游戏。 本文将记录搭建过程中遇到的各种问题,以及相关解决方案。
目标
我们的最终目标是:
- 支持 Steam 离线玩家加入
- 在 K8S 集群中部署饥荒游戏服务器
- 在 K8S 集群中部署多个饥荒游戏服务器,互相隔离
任务
对于上述目标,拆解成如下这些任务:
- 使用 Docker 运行饥荒在线服务器
- 使用 Docker 运行饥荒离线服务器
- 使用 Docker Host 网络模式运行饥荒服务器
- 使用 NAT 策略实现外部 IP 连接至内网饥荒服务器
- 使用 SNAT 策略实现外部 IP 映射至内网
- 使用 Docker Macvlan 实现多游戏服务器部署
- 使用 K8S Macvlan 实现游戏服务器多 POD 部署
- 使用 K8S Whereabouts 实现 POD IP 固化
术语
对下文提到的特定名词做些解释。
术语 | 含义 |
Steam 在线模式 | Steam 常规启动模式 |
Steam 离线模式 | 以离线模式启动 Steam , 运行游戏不会进行 Steam 官网校验 |
饥荒服务器在线模式 | 只允许 Steam 在线模式启动的玩家加入 |
饥荒服务器离线模式 | 允许 Steam 在线模式或 Steam 离线模式启动的玩家加入,同时要求处于相同局域网 |
Macvlan | 网卡虚拟化技术,将同一网卡虚拟出多张网卡 |
Whereabouts | 一个IP 地址管理(IP Address Management,IPAM)CNI 插件,用于分配整个集群的IP 地址。用于替换host-local。 |
环境信息
- Docker 服务器内网 IP: 192.168.1.100
- 我的饥荒客户端内网 IP: 192.168.1.77
- 我的公网 IP: 31.111.111.111
- 朋友公网 IP: 52.12.34.56
任务1 - 使用 Docker 运行饥荒在线服务器
饥荒服务器有两种模式:在线模式和离线模式
- 在线模式: 加入的玩家必须保持 Steam 在线用于校验正版信息。
- 离线模式: 玩家可以使用 Steam 离线模式启动饥荒并加入游戏,但要求所有玩家与游戏服务器都处于相同局域网。
在部署游戏服务器时,默认是在线模式。这里使用 Jamesits/docker-dst-server 的容器进行创建服务器。
docker run -d -v /mnt/dst:/data\
-p 10999-11000:10999-11000/udp\
-p 12346-12347:12346-12347/udp\
-e "DST_SERVER_ARCH=amd64"\
-it jamesits/dst-server:nightly
首次启动会有如下容器日志,表示服务器并未启动成功。
[00:00:10]: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! [00:00:10]: !!!! Your Server Will Not Start !!!! [00:00:10]: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! [00:00:10]: No auth token could be found. [00:00:10]: Please visit https://accounts.klei.com/account/game/servers?game=DontStarveTogether [00:00:10]: to generate server configuration files [00:00:10]: [00:00:10]: Alternatively generate a cluster_token you can [00:00:10]: open the console from a logged-in game [00:00:10]: client with the tilda key (~ / ù) and type: [00:00:10]: TheNet:GenerateClusterToken() [00:00:10]: This will create 'cluster_token.txt' in [00:00:10]: your client settings directory. Copy this [00:00:10]: into your cluster settings directory.
需要去日志中的官网链接生成一个 cluster token
,并将内容填入到 cluster_token.txt
。
本文中的路径为如下。
/mnt/dst/DoNotStarveTogether/Cluster_1/cluster_token.txt
这个 token 的格式类似 pds-gxxxxxxxxxxxxxxxa=
。
重新启动容器,显示如下日志表示运行成功。
[00:01:06]: [SyncWorldSettings] Resyncing master world option ghostsanitydrain = none to secondary shards. [00:01:06]: [SyncWorldSettings] Resyncing master world option portalresurection = always to secondary shards. [00:01:06]: [SyncWorldSettings] Resyncing master world option resettime = none to secondary shards. [00:01:06]: [SyncWorldSettings] Resyncing master world option basicresource_regrowth = always to secondary shards. [00:01:06]: [SyncWorldSettings] Resyncing master world option krampus = default to secondary shards. [00:01:06]: [SyncWorldSettings] recieved world settings from master shard. true [00:01:06]: [SyncWorldSettings] applying portalresurection = always from master shard. [00:01:06]: [SyncWorldSettings] applying krampus = default from master shard. [00:01:06]: [SyncWorldSettings] applying ghostsanitydrain = none from master shard. [00:01:06]: [SyncWorldSettings] applying resettime = none from master shard. [00:01:06]: [SyncWorldSettings] applying basicresource_regrowth = always from master shard. [00:01:07]: [Shard] secondary shard LUA is now ready! [00:01:07]: Sim paused
启动饥荒联机版,进入游戏后使用按键 `
启动内置控制台,输入指令并回车运行。
( 192.168.1.100
是 docker 服务器的 IP)
c_connect("192.168.1.100", 10999)
客户端连接成功,且服务器产生如下日志。
[00:02:32]: New incoming connection 192.168.1.77|65273 <781xxxx0> [00:02:32]: Client connected from 192.168.1.77|65273 <781xxxx0> [00:02:32]: ValidateGameSessionToken GUID<781xxxx0> [00:02:33]: Client authenticated: (KU_1xxxQ) xxx [00:02:33]: [Steam] Authenticated host '781xxxx0' [00:02:34]: There is no active event to validate against. [00:02:51]: [Join Announcement] xxx [00:03:06]: Resuming user: session/ExxxE/xxx
任务2 - 使用 Docker 运行饥荒离线服务器
我和朋友是通过 Steam 家庭模式进行游戏共享的。 Steam 家庭有个限制:Steam 在线模式下,共享的游戏只能家庭中 1 人玩。但使用 Steam 离线模式则可以突破这个限制。 基于此前提,我必须搭建离线模式的饥荒服务器,才能让我和朋友都正常加入游戏服务器。
离线模式饥荒只需修改 cluster.ini 文件的 offline_cluster
选项即可。
注:当 offline_cluster=true
时, lan_only_cluster
在服务器运行时会强制为 true
进行加载。
本文中将 /mnt/dst/DoNotStarveTogether/Cluster_1/cluster.ini
的 offline_cluster
改为 true
即可。
[NETWORK]
cluster_name = DST
cluster_description = DST
cluster_password = 12345678
offline_cluster = true
lan_only_cluster = false
重启容器,显示如下日志表示启动成功。
[00:01:20]: About to start a shard with these settings: [00:01:20]: ShardName: [SHDMASTER] [00:01:20]: ShardID: 1 [00:01:20]: ShardRole: MASTER [00:01:20]: MasterHost: (null) [00:01:20]: MasterBind: 127.0.0.1 [00:01:20]: MasterPort: 10998 [00:01:20]: [Shard] Starting master server [00:01:20]: [Shard] Shard server started on port: 10998 [00:01:20]: Telling Client our new session identifier: 3F8A53604F747599 [00:01:21]: Validating portal[1] <-> <nil>[1] (inactive) [00:01:21]: Validating portal[2] <-> <nil>[2] (inactive) [00:01:21]: Validating portal[3] <-> <nil>[3] (inactive) [00:01:21]: Validating portal[4] <-> <nil>[4] (inactive) [00:01:21]: Validating portal[5] <-> <nil>[5] (inactive) [00:01:21]: Validating portal[6] <-> <nil>[6] (inactive) [00:01:21]: Validating portal[7] <-> <nil>[7] (inactive) [00:01:21]: Validating portal[8] <-> <nil>[8] (inactive) [00:01:21]: Validating portal[9] <-> <nil>[9] (inactive) [00:01:21]: Sim paused
以 Steam 离线模式启动饥荒联机版,进入游戏后使用按键 `
启动内置控制台,输入指令并回车运行。
( 192.168.1.100
是 docker 服务器的 IP)
c_connect("192.168.1.100", 10999)
很不幸,在游戏内弹出了“这个服务器仅允许在相同局域网的玩家连接”的错误,同时游戏服务器也有如下相关日志。
[00:15:40]: Unconnected ping from 192.168.1.77|60674 [00:15:40]: New incoming connection 192.168.1.77|60674 <2395xxx3661> [00:15:40]: LAN server refusing connection from 192.168.1.77|60674 <2395xxx3661> [00:15:40]: CloseConnectionWithReason: ID_DST_SERVER_IS_LAN_ONLY
这是因为容器内的 IP 是 172.17.x.x
,而游戏客户端的 IP 是 192.168.1.77
,并不属于相同局域网,
所以被游戏服务器拒绝了(饥荒服务端的逻辑)。
接下来我们尝试解决这个问题。
任务3 - 使用 Docker Host 网络模式运行饥荒服务器
有个简单的处理方式是将该容器的网络模式改为 host
docker run -d -v /mnt/dst:/data --network=host -e "DST_SERVER_ARCH=amd64" -it jamesits/dst-server:nightly
这次同一局域网的客户端可以顺利进入游戏服务器了,并且服务器输出日志如下。
[00:01:02]: Unconnected ping from 192.168.1.77|60744 [00:01:02]: New incoming connection 192.168.1.77|60744 <879xxx174> [00:01:02]: Client connected from [LAN] 192.168.1.77|60744 <879xxx174> [00:01:02]: Client authenticated: (OU_xxx) xxx [00:01:02]: [Shard] Read save location file for (OU_xxx) [00:01:21]: [Join Announcement] xxx
任务4 - 使用 NAT 策略实现外部 IP 连接至内网饥荒服务器
为了让朋友连接到我的局域网游戏服务器,还需要在防火墙做 10999 的端口转发。类似如下规则:
31.111.111.111(wan_ip):57382 -> 192.168.1.100(lan_ip):10999
这样,朋友使用 c_connect("31.111.111.111", 57382)
即可连接到我的局域网游戏服务器。
但实际上,朋友仍然无法加入游戏服务器,日志显示如下:
[00:15:40]: Unconnected ping from 52.12.34.xx|60674 [00:15:40]: New incoming connection 52.12.34.xx|60674 <2395xxx3661> [00:15:40]: LAN server refusing connection from 52.12.34.xx|60674 <2395xxx3661> [00:15:40]: CloseConnectionWithReason: ID_DST_SERVER_IS_LAN_ONLY
52.12.34.xx
这是我朋友的公网出口 IP,饥荒服务器判定为不是一个局域网,所以拒绝连接。
任务5 - 使用 SNAT 策略实现外部 IP 映射至内网
为了解决上述公网请求的问题,需要在防火墙增加一个 SNAT 策略。
对于从 wan 接收到的请求,如果要转发至 lan,则在转发前将源 IP 修改为 lan 网关 IP, 即修改为 192.168.1.1,这样就能让饥荒服务器认为是来自相同局域网的连接。
饥荒服务端日志中可以看到有个来自网关(192.168.1.1)的请求。
[00:01:02]: Unconnected ping from 192.168.1.1|31202 [00:01:02]: New incoming connection 192.168.1.1|31202 <281xxx1> [00:01:02]: Client connected from [LAN] 192.168.1.1|31202 <281xxx1> [00:01:02]: Client authenticated: (OU_xxx) xxx [00:01:02]: [Shard] Read save location file for (OU_xxx) [00:01:21]: [Join Announcement] xxx
任务6 - 使用 Docker Macvlan 实现多游戏服务器部署
饥荒离线模式服务器还有个限制:无法修改监听端口
如果我们使用 host 网络模式部署,就会导致一台宿主机只能运行一个容器。 我们需要有个方案能在同一台宿主机上运行多个饥荒服务器,以提高资源利用率。
macvlan 可以解决上述问题。使用 macvlan 虚拟出多张网卡,并获得局域网内的真实 IP。
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 game_macvlan
使用 docker network ls
可以看到新建成功的 game_macvlan
网络
docker network ls
NETWORK ID NAME DRIVER SCOPE
de26baa4e09c bridge bridge local
bd5f6c20ce80 game_macvlan macvlan local
f28054695862 host host local
32f1f9ab8121 none null local
使用 game_macvlan
网络启动容器
docker run -d -v /mnt/dst:/data --network=game_macvlan -e "DST_SERVER_ARCH=amd64" -it jamesits/dst-server:nightly
查看运行状态 docker ps
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
1aad8d2208bc jamesits/dst-server:nightly "entrypoint.sh super…" 8 minutes ago Up 8 minutes (healthy)
查看容器获取的 IP, docker inspect 1aad8d2208bc
"Networks": {
"game_macvlan": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"1aad8d2208bc"
],
"NetworkID": "bd5f6c20ce8062a106c3b7e5dbf60f6c27584776884e92f17f99989bd83dc0dd",
"EndpointID": "a8c657ba59c8a7f8934756eb56618eaabc703efe0f06b46719541c557e7ec057",
"Gateway": "192.168.1.1",
"IPAddress": "192.168.1.2",
"IPPrefixLen": 24,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "01:32:c1:b8:19:21",
"DriverOpts": null
}
}
可以看到此时容器 IP 为 192.168.1.2
,为局域网内的真实 IP。
启动饥荒游戏,输入命令
c_connect("192.168.1.2", 10999)
游戏客户端可顺利加入,并且服务器日志显示如下
[00:01:02]: Unconnected ping from 192.168.1.77|60744 [00:01:02]: New incoming connection 192.168.1.77|60744 <879xxx174> [00:01:02]: Client connected from [LAN] 192.168.1.77|60744 <879xxx174> [00:01:02]: Client authenticated: (OU_xxx) xxx [00:01:02]: [Shard] Read save location file for (OU_xxx) [00:01:21]: [Join Announcement] xxx
接下来只要更新 NAT 规则即可。
31.111.111.111(wan_ip):57382 -> 192.168.1.2(lan_ip):10999
如果需要再创建一个饥荒服务器,再次使用 game_macvlan
网络创建容器即可。
至此,已经实现了在 docker 环境中部署多个饥荒离线服务器。接下来介绍 K8S 中的部署方案。
任务7 - 使用 K8S Macvlan 实现游戏服务器多 POD 部署
首先确保开启 multus 网络插件,用于实现给 pod 分配多个网络接口。
对于 Rancher,如果 UI 界面无法编辑,则可修改 Cluster Yaml,在 cni 中添加 multus。
machineGlobalConfig:
cni: multus,calico
disable-kube-proxy: false
etcd-expose-metrics: false
稍等片刻,确保 multus 正常启动
参考 https://github.com/k8snetworkplumbingwg/multus-cni/blob/master/docs/quickstart.md#storing-a-configuration-as-a-custom-resource
创建一个 NetworkAttachmentDefinition
注意:
- master 表示真实的网络接口,需按实际情况修改
- subnet 为子网范围,需按实际情况修改
cat <<EOF | kubectl create -f - apiVersion: "k8s.cni.cncf.io/v1" kind: NetworkAttachmentDefinition metadata: name: macvlan-conf spec: config: '{ "type": "macvlan", "master": "eth0", "mode": "bridge", "ipam": { "type": "host-local", "subnet": "192.168.1.0/24", "rangeStart": "192.168.1.200", "rangeEnd": "192.168.1.216", "gateway": "192.168.1.1" } }' EOF
如下表示创建成功
> kubectl get network-attachment-definitions
NAME AGE
macvlan-conf 3d2h
创建 Pod 进行测试 ,其中 k8s.v1.cni.cncf.io/networks: default/macvlan-conf
注解表示使用 macvlan-conf 该网络
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
name: samplepod
annotations:
k8s.v1.cni.cncf.io/networks: default/macvlan-conf
spec:
containers:
- name: samplepod
command: ["/bin/ash", "-c", "trap : TERM INT; sleep infinity & wait"]
image: alpine
EOF
查看 POD 获得的 IP,如下可知 POD 的 IP 为 192.168.1.200
> kubectl exec -it samplepod -- ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0@if38: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue state UP qlen 1000
link/ether 3e:2e:11:25:9a:d2 brd ff:ff:ff:ff:ff:ff
inet 10.42.57.112/32 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::4c6e:71ff:fe95:9ad2/64 scope link
valid_lft forever preferred_lft forever
3: net1@if5: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 12:cd:d6:5f:f9:a9 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.200/24 brd 192.168.1.255 scope global net1
valid_lft forever preferred_lft forever
inet6 fe80::80ad:c6ff:fe0f:f7e9/64 scope link
valid_lft forever preferred_lft forever
所以,使用注解 k8s.v1.cni.cncf.io/networks: default/macvlan-conf
即可让 POD 通过 macvlan 获取 IP。
但是我们前面创建的 NetworkAttachmentDefinition 有个缺点,即每次 POD 重建时,获取的 macvlan IP 都会变化,
对于饥荒服务器来说,期望是有固定的内网 IP,方便做稳定的端口映射。
接下来将使用 Whereabouts 来解决这问题。
任务8 - 使用 K8S Whereabouts 实现 POD IP 固化
whereabouts: 一个IP 地址管理(IP Address Management,IPAM)CNI 插件,用于分配整个集群的IP 地址。用于替换host-local。
它有个很好的特性是部署 StatefulSet 时 POD IP 可以保持不变。
首先需要在 K8S 集群中安装 whereabouts,我使用的是 Rancher,参考 Multus IPAM plugin options 启用 whereabouts。
# /var/lib/rancher/rke2/server/manifests/rke2-multus-config.yaml --- apiVersion: helm.cattle.io/v1 kind: HelmChartConfig metadata: name: rke2-multus namespace: kube-system spec: valuesContent: |- rke2-whereabouts: enabled: true
创建并编辑上述文件,重启 rke2-server
即可。
然后参考教程创建 NetworkAttachmentDefinition
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
name: whereabouts-conf
spec:
config: '{
"type": "macvlan",
"master": "eth0",
"mode": "bridge",
"ipam": {
"type": "whereabouts",
"range": "192.168.1.0/24",
"range_start": "192.168.1.200",
"range_end": "192.168.1.216"
}
}'
接下来创建一个 StatefulSet 的饥荒服务器
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: dst-server-test-headless
namespace: game
spec:
ports:
- name: port1
port: 10999
protocol: UDP
targetPort: 10999
type: ClusterIP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
annotations:
name: dst-server-test
namespace: game
spec:
replicas: 1
selector:
matchLabels:
app: dst-server-test
serviceName: dst-server-test-headless
template:
metadata:
annotations:
k8s.v1.cni.cncf.io/networks: default/whereabouts-conf
labels:
app: dst-server-test
spec:
containers:
- env:
- name: DST_SERVER_ARCH
value: amd64
image: jamesits/dst-server:nightly
imagePullPolicy: IfNotPresent
name: dst-server
volumeMounts:
- mountPath: /data
name: vol-data
subPath: data
volumeClaimTemplates:
- metadata:
name: vol-data
spec:
accessModes: [ "ReadWriteMany" ]
resources:
requests:
storage: 1Gi
EOF
可以从 POD 信息中看到获得的 IP 为 192.168.1.220
此时,即使重启该 StatefulSet,IP 仍能保持不变。后续操作则是 NAT + SNAT,与前面的 docker 相关操作逻辑类似。
结语
至此,通过 macvlan 技术实现了多容器同时部署饥荒服务器。 使用 whereabouts 固定 macvlan ip,使得防火墙转发规则更稳定。