Home

Zerotier 2.0 的愿景图

一直使用 Zerotier 作为我的 Homelab 内网穿透核心组网工具,配合各类安全上网工具,作为合格的社畜出差美滋滋。

但是一直不满意 Zerotier 不支持代理,不支持 TCP,不支持各种隧道, Moon 功能不直观不好用,私有化部署困难。

现在,Zerotier 团队规划了 2.0 版本,上面的大多数功能都有了对应的支持计划,我非常激动。

这里就大概讲述一下 ZT2 的 2.0 愿景。

使用 Go 重写非核心代码

是的,没错,你看到了,ZT2 的配置和控制部分代码使用 Go 完全重写。

在过去,Zerotier 使用21天才能精通的 C++:

C++ 21 Days

这导致了 Zerotier 的开发进度缓慢,而现在除核心网络 IO 路径代码以外,拥抱了新时代的 Go,相信会更快的开发。

去中心化的 lf 根服务器

Zerotier 团队实现了一个去中心化的 KV 数据库 lf,作为 ZT2 根服务器;由此可以容易实现私有化部署。

同时 ZT2 摒弃了旧有的 Moon/Planet 设计,支持通过域名动态查找根服务器,也不再使用固定端口。

对此我能预测到的是,当前云时代的复杂跨云及多云需求,Zerotier 可以很好的帮助我们解决这一问题。 以前我觉得也许某一个区块链的网络项目的私有链可以解决这个问题,现在也许 ZT2 是更好的方案。

智能组播复制

ZT2 支持点对点及星型组播,这对于公有云及复杂本地网络非常有意义。

其他改进

真正的多线程支持,全新的 UI 及 APP,支持新的加密算法及身份认证。

思考

一直有一个跨多云混合云的的虚拟网络的想法,寻找过许多工具,Zerotier 是最接近我的想法的,也许当 ZT2 面世的时候,会是让人惊喜的一天。

ZT2 的应用场景会有很多,物联网也是其中一种,甚至可以用于游戏服务器中实现跨区跨国。

以至于稍加修改整合,也会是一个区块链的虚拟云计算网络,希望这种价值,能早日得见。

View Comments

战略观

几日前,我在飞机上静思,也算是难得的安静时刻,重新概览了下我曾经玩过 700 多个小时的一个游戏【异星工厂】, 心神汇聚,突然有所领悟。

对战略、价值、产品、服务、交付、资源、渠道以及风险这些有了更高意义上的理解。

把这些高度抽象的概念一一思考,很多原本困惑的问题就通了,也瞬间看清了自己,乃至未来的模糊方向。

脑图整理如下(非完整版,如有意沟通,请与我联系):

Strategy MindMap

后面的鱼骨图表示了战略这个概念,可以用在产品设计、个人、家庭、公司、国家这个范畴;我这里统一称为实体。

何为战略

我把战略分为八个要点:

  • 资源
    资源可以是一般等价物,也可以是无形的资源。
  • 渠道
    渠道可以放大资源,或者扩展其他的战略要点。
  • 发展
    发展是战略健康演进的重要体现,始终贯穿战略的生命周期,引领全局。
  • 危机
    任何事物都会存在危机,在不同的战略阶段,危机所蕴含的威胁以及处理方式都有所不同。
  • 生产
    生产即我们进行生产任务的人或物。
  • 管理
    管理是流程,是标准,将一切混乱可控,也为其他战略要点服务。
  • 产品
    产品是最终产出物,也代表了价值,是实体对外的最终提现。
  • 债务
    任何事物都有两面性,必然产生债务,一切总会需要偿还。

战略,对一个实体而言,应仅仅低于基本生理需求,同时战略也可以优化这些基本诉求。

现在的很多人都是浑浑噩噩度日,过一天算一天,休息时间多沉醉在刷抖音玩手游等地方,缺少延迟满足能力,也是缺乏战略规划能力的体现。

所以对人的一生来说,明白自己想要什么,并不断演进自身战略,会是更有意义的事情。

理想的情况下,发展是一路顺风的,因此可以做宏大的规划,想要很多事情一步到位。 而现实情况是,无数的风险暗中窥伺,我们需要步步为营却又不能放弃合理适度的规划。

资源

现代文明都需要一般等价物进行交换,资源也就是对此的最大体现,但并非金钱就是资源。

它可以有形也可以无形,哪怕人情、出身、背景乃至种族都可以是资源。

如何最大化的发现和利用资源,也是一门高深的学问。

渠道

渠道可以放大资源,或者扩展其他的战略要点。

渠道可以从资源演化而来,甚至从危机演化而来。

发展

任何实体都需要成长,如何成长,如何健康成长,就是发展的要素。

发展应始终贯穿战略的生命周期,引领全局,是战略的核心体现,

发展应从基本生存出发,到明白实体自身的核心诉求,并以此为主延伸到方向与文化等等。

停止发展就是慢性自杀。

危机

危机从来都无处不在,内外均可,有意无意甚难猜,甚至是本身的方向问题。

发展管理的重要部分也包括危机处理和规避。

生产

生产,也就是生产者,生产者们协同工作,最终产出产品或部件。

所有生产都应该尽量标准化、自动化,以使其具备可复制性、规模扩展性。

你可以独特,但必须在规则之内,你可以非常独特,那可以为你专门改变,但必须有足够的价值或产出。

管理

管理是一门艺术,但是很多时候我们很难知道什么是管理水平的好坏。

管理的目的是驱动方向,并规范生产,使其保持高效率和健康。

大多数时候,可能体现为繁琐的流程。

产品

产品和服务就是目标,是一个实体的最终产生物,同时也是价值的承载和提现。 但是我们需要把目标转换为价值,这个过程是动态的,需要生产管理同时运作才能达成我们的目标。

一切为价值服务,而交付则是赋能和落地。

债务

任何事物都有两面性,必然产生债务,一切总会需要偿还。

债务也分为急性和慢性,慢性对于战略的影响是缓慢的,我们可以有更多的时间处理它,但并不能忽略。 而急性的债务则非常紧急,当下就会对造成损伤,必须立刻处理。

很多人都过于看重急性债务而忽略慢性债务,实际上慢性债务爆发时,也是实体生死存亡的时刻。

总结

兵者,国之大事,死生之地,存亡之道,不可不察也。

这里我们可以把“兵”作为战略的指代,《孙子兵法》不就通篇讲的什么是战略吗?

而《易经》更是直指本源,从阴阳五行到八卦,不正是我所抽象阐述的这些概念吗?

需深度学习之~

View Comments

在 K8S 中部署 MySQL 双主复制高可用服务器

之前因为某些需要,写了一个运行于 K8S 的 MySQL 双主复制高可用的编排。

核心使用 StatefulSet,通过 initContainers 初始化的方式完成初次启动配置及同步, 完全自动化。

当然,这只是用于测试环境的一个编排,因为缺少存储部分,因此不推荐用于生产。

编排代码如下:

mysql-mm-rbac.yml

RBAC 权限定义。

 1apiVersion: v1
 2kind: Namespace
 3metadata:
 4  name: mysql-ha
 5
 6---
 7apiVersion: v1
 8kind: ServiceAccount
 9metadata:
10  name: cluster-helper
11  namespace: mysql-ha
12
13---
14kind: Role
15apiVersion: rbac.authorization.k8s.io/v1
16metadata:
17  name: cluster-helper
18  namespace: mysql-ha
19rules:
20  - apiGroups: ["apps"]
21    resources: ["statefulsets"]
22    verbs: ["get", "list"]
23
24---
25kind: RoleBinding
26apiVersion: rbac.authorization.k8s.io/v1
27metadata:
28  name: cluster-helper
29  namespace: mysql-ha
30subjects:
31  - kind: ServiceAccount
32    name: cluster-helper
33roleRef:
34  kind: Role
35  name: cluster-helper
36  apiGroup: rbac.authorization.k8s.io

mysql-mm.yml

核心编排定义。

  1---
  2apiVersion: v1
  3kind: ConfigMap
  4metadata:
  5  name: mysql-scripts
  6  namespace: mysql-ha
  7data:
  8  common.sh: |
  9    #!/bin/bash
 10    update_data() {
 11      local svc=${HOSTNAME%-*}
 12      local token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
 13      local ns=${POD_NAMESPACE}
 14      local url=https://${KUBERNETES_SERVICE_HOST}/apis/apps/v1/namespaces/${ns}/statefulsets/${svc}
 15      curl -kfsSL -H "Authorization: Bearer ${token}" ${url} > /ha-info/state.json
 16    }
 17    get_data() {
 18      if [ ! -f /ha-info/state.json ]; then
 19        return
 20      fi
 21      cat /ha-info/state.json
 22    }
 23    get_replicas() {
 24      get_data | grep '"replicas":' | head -n 1 | awk '{print $2}' | tr -d ","
 25    }
 26    get_ready_replicas() {
 27      get_data | grep '"readyReplicas":' | awk '{print $2}' | tr -d ","
 28    }
 29    get_current_replicas() {
 30      get_data | grep '"currentReplicas":' | awk '{print $2}' | tr -d ","
 31    }
 32    sync_done() {
 33      echo "Sync has been done."
 34      # Infinite blocking
 35      while true; do
 36        sleep 3600
 37      done
 38    }
 39  init-cluster.sh: |
 40    #!/bin/sh
 41    if [[ -f /var/lib/mysql/CLUSTER_WAS_INIT ]]; then
 42      exit 0
 43    fi
 44    echo Initialize cluster configuration...
 45    source /scripts/common.sh
 46    SERVER_ID=$(hostname | grep -oE '[^-]+$')
 47    cat > /etc/mysql/conf.d/master-to-master.cnf <<EOF
 48    [mysql]
 49    no-auto-rehash
 50    [mysqld]
 51    skip-host-cache
 52    skip-name-resolve
 53    server_id=1${SERVER_ID}
 54    log-bin=mysql-bin
 55    auto_increment_increment=2
 56    auto_increment_offset=$((${SERVER_ID}+1))
 57    EOF
 58    echo Initialize cluster configuration successful.
 59  sync-ha.sh: |
 60    #!/bin/bash
 61    set -e
 62    if [ -f /var/lib/mysql/CLUSTER_WAS_INIT ]; then
 63      sync_done
 64    fi
 65    source /scripts/common.sh
 66    echo "Waiting all replicas up..."
 67    while [ `get_current_replicas` -ne `get_ready_replicas` ]
 68    do
 69      sleep 5
 70    done
 71    get_another_host() {
 72      local svc=${HOSTNAME%-*}
 73      local server_id=$(hostname | grep -oE '[^-]+$')
 74      local another_id
 75      if [[ $server_id == 0 ]]; then
 76        another_id=1
 77      else
 78        another_id=0
 79      fi
 80      echo "${svc}-${another_id}"
 81    }
 82    get_binlog_file() {
 83      local host=$1
 84      mysql -h $host -p"${MYSQL_ROOT_PASSWORD}" -e 'show master status\G;' | grep 'File' | awk '{print $2}'
 85    }
 86    get_binlog_pos() {
 87      local host=$1
 88      mysql -h $host -p"${MYSQL_ROOT_PASSWORD}" -e 'show master status\G;' | grep 'Position' | awk '{print $2}'
 89    }
 90    ensure_db() {
 91      while ! mysql -p"${MYSQL_ROOT_PASSWORD}" -e 'SELECT 1' > /dev/null; do
 92        sleep 5
 93      done
 94      mysql -p"${MYSQL_ROOT_PASSWORD}" -e "GRANT RELOAD, SUPER, REPLICATION SLAVE ON *.* TO 'root'@'%' identified by '${MYSQL_ROOT_PASSWORD}'"
 95    }
 96    wait_db() {
 97      local host=$1
 98      while ! mysql -h $host -p"${MYSQL_ROOT_PASSWORD}" -e 'SELECT 1' > /dev/null; do
 99        sleep 5
100      done
101    }
102    sync_core() {
103      local mainHost=$1
104      local targetHost=$2
105      local file=$3
106      local pos=$4
107      echo Synchionize "${mainHost}" "file=${file}" "pos=${pos}"
108      mysql -h "${mainHost}" -p"${MYSQL_ROOT_PASSWORD}" -e "CHANGE MASTER TO MASTER_HOST='${targetHost}', 
109        MASTER_USER='root',
110        MASTER_PASSWORD='${MYSQL_ROOT_PASSWORD}',
111        MASTER_LOG_FILE='${file}',
112        MASTER_LOG_POS=${pos};"
113      mysql -h "${mainHost}" -p"${MYSQL_ROOT_PASSWORD}" -e 'start slave;'
114    }
115    sync_db() {
116      local svc=${HOSTNAME%-*}
117      local right_fqdn=`get_another_host`.${svc}.${POD_NAMESPACE}.svc.cluster.local
118      local left_fqdn=${HOSTNAME}.${svc}.${POD_NAMESPACE}.svc.cluster.local
119      echo "Wait db $right_fqdn ..."
120      wait_db $right_fqdn
121      local left_file=`get_binlog_file ${left_fqdn}`
122      local left_pos=`get_binlog_pos ${left_fqdn}`
123      local right_file=`get_binlog_file ${right_fqdn}`
124      local right_pos=`get_binlog_pos ${right_fqdn}`
125      echo Sync right -> left ...
126      sync_core "$left_fqdn" "$right_fqdn" "$right_file" "$right_pos"
127      echo Sync left -> right ...
128      sync_core "$right_fqdn" "$left_fqdn" "$left_file" "$left_pos"
129    }
130    echo "Ensuring db..."
131    ensure_db
132    server_id=$(hostname | grep -oE '[^-]+$')
133    if [[ $server_id -eq 0 ]]; then
134      sync_db
135      echo "master-$server_id synchionize suffessful."
136    else
137      echo "master-$server_id are ready."
138    fi
139    touch /var/lib/mysql/CLUSTER_WAS_INIT
140    sync_done
141  sync-ha-metadata.sh: |
142    #!/bin/sh
143    if [ -f /var/lib/mysql/CLUSTER_WAS_INIT ]; then
144      sync_done
145    fi
146    apk add curl
147    source /scripts/common.sh
148    while [ ! -f /var/lib/mysql/CLUSTER_WAS_INIT ]
149    do
150      update_data
151      sleep 3
152    done
153    sync_done
154
155---
156apiVersion: apps/v1beta2
157kind: StatefulSet
158metadata:
159  name: mysql-mm
160  namespace: mysql-ha
161spec:
162  serviceName: mysql-mm
163  replicas: 2
164  selector:
165    matchLabels:
166      app: mysql-mm
167  template:
168    metadata:
169      labels:
170        app: mysql-mm
171    spec:
172      initContainers:
173        - name: init-cluster
174          image: alpine
175          command:
176            - /scripts/init-cluster.sh
177          volumeMounts:
178          - name: scripts
179            mountPath: /scripts
180            readOnly: true
181          - name: mysql-conf
182            mountPath: /etc/mysql/conf.d
183          - name: ha-metadata
184            mountPath: /ha-info
185            readOnly: true
186      containers:
187      - name: mysql
188        image: mysql:5.7
189        env:
190        - name: "MYSQL_ROOT_PASSWORD"
191          value: "123456!!"
192        - name: "MYSQL_ROOT_HOST"
193          value: "%"
194        volumeMounts:
195        - name: scripts
196          mountPath: /scripts
197          readOnly: true
198        - name: mysql-conf
199          mountPath: /etc/mysql/conf.d
200        - name: mysql-sock
201          mountPath: /var/run/mysqld
202        - name: mysql-data
203          mountPath: /var/lib/mysql
204        ports:
205        - containerPort: 3306
206      - name: sync-ha
207        image: mysql:5.7
208        command:
209          - /scripts/sync-ha.sh
210        env:
211        - name:  "MYSQL_ROOT_PASSWORD"
212          value: "123456!!"
213        - name: POD_NAMESPACE
214          valueFrom:
215            fieldRef:
216              fieldPath: metadata.namespace
217        volumeMounts:
218        - name: scripts
219          mountPath: /scripts
220          readOnly: true
221        - name: mysql-sock
222          mountPath: /var/run/mysqld
223        - name: ha-metadata
224          mountPath: /ha-info
225        - name: mysql-data
226          mountPath: /var/lib/mysql
227      - name: sync-ha-metadata
228        image: alpine
229        command:
230          - /scripts/sync-ha-metadata.sh
231        env:
232          - name: POD_NAMESPACE
233            valueFrom:
234              fieldRef:
235                fieldPath: metadata.namespace
236        volumeMounts:
237        - name: scripts
238          mountPath: /scripts
239          readOnly: true
240        - name: ha-metadata
241          mountPath: /ha-info
242        - name: mysql-data
243          mountPath: /var/lib/mysql
244      volumes:
245        - name: scripts
246          configMap:
247            name: mysql-scripts
248            defaultMode: 0755
249        - name: ha-metadata
250          emptyDir: {}
251        - name: mysql-conf
252          emptyDir: {}
253        - name: mysql-sock
254          emptyDir: {}
255        - name: mysql-data
256          emptyDir: {}
257      serviceAccountName: cluster-helper
258  # volumeClaimTemplates:
259  # - metadata:
260  #     name: pvc-mysql-data
261  #   spec:
262  #     accessModes: [ "ReadWriteOnce" ]
263  #     resources:
264  #       requests:
265  #         storage: 10Gi
266
267---
268# Service
269apiVersion: v1
270kind: Service
271metadata:
272  name: mysql-mm
273  namespace: mysql-ha
274spec:
275  ports:
276  - port: 3306
277    targetPort: 3306
278  selector:
279    app: mysql-mm
View Comments

Kubernetes Homelab 实验环境

Homelab 搭建也不能什么都没有,在之前解决了网络拓扑问题之后,便开始了 Kubernetes 在 Homelab 下的搭建运行。

和云环境之上的 Kubernetes 不同,还是踩到了一些小坑。

安装 Kubernetes

Kubernetes 在 1.7 版本时推出了 kubeadm,用于简化集群的安装和配置,这就解决了 Kubernetes
安装困难的一大痛点。

准备环境

Homelab 的好处就是可以开许多自己想要的虚拟机,这里我们为 Kubernetes 准备两台虚拟机:

  • K8S-MASTER-LAVA
    主节点
  • K8S-NODE-BEAT
    工作节点

安装 Kubeadm

这里我使用的是 Debian 9 环境,通过 Google 的官方源就可以安装 kubeadm

 1apt-get update
 2apt-get install -yq \
 3    apt-transport-https \
 4    ca-certificates \
 5    curl \
 6    gnupg2
 7apt-get update
 8
 9curl -fsSL  https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
10
11cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
12deb https://apt.kubernetes.io/ kubernetes-xenial main
13EOF
14
15apt-get update
16apt-get install -y \
17    ipvsadm ipset \ # for ipvs mode
18    kubelet kubeadm kubectl
19apt-mark hold kubelet kubeadm kubectl

初始化集群

官方安装指南的网络插件默认都使用 10.244.0.0/16 网段,这一网段和我的内网规划存在冲突,
因此自定义了一个网段 10.64.0.0/16 来代替默认设置。

登入主机 K8S-MASTER-LAVA,运行如下脚本:

1NETWORK_PLUGIN=https://raw.githubusercontent.com/coreos/flannel/a70459be0084506e4ec919aa1c114638878db11b/Documentation/kube-flannel.yml
2NETWORK_CIDR=10.64.0.0/16
3
4kubeadm init --pod-network-cidr=$NETWORK_CIDR
5
6export KUBECONFIG=/etc/kubernetes/admin.conf
7curl -sSL $NETWORK_PLUGIN | sed "s|10.244.0.0/16|${NETWORK_CIDR}|g" |
8  kubectl apply -f -

添加工作节点到集群

集群初始化完成后可以通过输出的一段命令来添加工作节点。

登入主机 K8S-NODE-BEAT,运行如下命令:

1kubeadm join K8S-MASTER-LAVA:6443 --token <YOUR_TOKEN> \
2  --discovery-token-ca-cert-hash sha256:<YOUR_CA_CERT_HASH>

遇到的问题

DNS 解析失败

在完成集群安装后,我部署了 gitlab-runner(k8s executor),在测试运行时却发生了问题,
在 gitlab-runner 中运行 Dind(Docker in Docker)时,产生的容器无法访问网络。

通过 tcpdump 等工具进一步排查后,发现网络是没有问题的,K8S 内的容器可以访问外网,但是无法解析域名

然后我找到了 /etc/resolv.conf,发现有一些小小的不同,search 字段的最后多了一个 lan:

1nameserver 10.96.0.10
2search default.svc.cluster.local svc.cluster.local cluster.local lan
3options ndots:5

关于 resolv.conf 的 search 和 ndots 这里不再详述,读者可以自行搜索。

正常情况下,这个 lan 的搜索域应该是可以正常处理的,但是通过 tcpdump 排查时,发现在查询一个域名时, 少发了一个查询请求,比如我们查询 example.com,正常情况下应有如下 FQDN 查询:

1example.com.example.com.default.svc.cluster.local.
2example.com.svc.cluster.local.
3example.com.cluster.local.
4example.com.lan.
5example.com.

但是 tcpdump 的结果少了最后一条 example.com. 这就导致我们无法访问外网。

最后问题的根源在于使用的基础 Docker 镜像为 alpine,这是一个精简镜像,使用 msul 作为 C 库。

msul 在 1.1.13 版本之前是不支持 search 和 ndots 选项的,之后的版本支持,
但是行为却和 glibc 不同

这里的问题在于搜索域列表达到四个时,不会查询根域 cert-manager#1161 这里也有相关的讨论。

解决方案有三种:

  • 修改 resolv.conf,使得列表域不超过三个
  • 不使用基于 msul 的镜像 alpine,使用 debian:9-slim
  • 部署 POD 时,自定义 dnsConfig,如下所示

custom-pod-dns.yml

 1apiVersion: v1
 2kind: Pod
 3metadata:
 4  namespace: default
 5  name: custom-pod-dns
 6spec:
 7  containers:
 8    - name: custom-pod-dns
 9      image: alpine
10      command:
11        - sleep
12        - "3600"
13  dnsPolicy: "None"
14  dnsConfig:
15    nameservers:
16      - 8.8.8.8
17    searches:
18      - lan
19    options:
20      - name: ndots
21        value: "2"

无法访问公网

同样是运行 DIND,发现集群 POD 的 DIND 运行的容器无法访问外网,但是 curl 拿到的错误提示为, Unknown SSL protocol error in connectioncurl -vv 查看发现证书握手到一半就停止了。

抓包诊断发现也是发包到一半就没了,开始以为是 NAT 问题,尝试了修改了宿主机的 docker 的 CIDR,但无法解决问题,后来想到可能是 MTU 导致的问题,Google 之找到了类似的问题。

分析原因,是因为 flannel 的集群网络默认 MTU 为 1450,由于 flannel 基于 vxlan,因此无法改变 MTU, 因此只能改自己的 MTU。

可以在 DIND 的运行参数上加上 --mtu=1450 即可解决此问题,但是同样存在连锁问题, 当使用 DIND 运行 docker-compose 时,docker-compose 并不会使用 dockerd 设置的 MTU,而是使用默认的 1500。

解决方法是配置 docker-compose 的网络使用指定 MTU:

1...
2networks:
3  default:
4    driver: bridge
5    driver_opts:
6      com.docker.network.driver.mtu: 1450

但这种方法入侵性太强,我更倾向于更干净的方式,比如考虑不使用 flannel 作为网络插件, 使用 Cilium 或者 Calico,Calico 的三层模式需要路由器 BGP 的支持,要求比较高和繁琐, 所以我选择了 Cilium 基于 BPF 的方案。

Cilium 支持三层平面网络,性能也不差,更拥抱 Service Mesh,紧密结合 Envoy,与 Calico 偏重 IDC 方向不同,Cilium 是偏重应用架构的容器网络方案。

Cilium 1.3 更是可以使用 Go 作为 Envoy 的扩展,可以自行编写 filter chain 扩展,有关这方面的内容可另写一篇文章详述,这里就此带过。

顺带一提,Cilium 也是 DigitalOcean Kubernetes Cluster 采用的网络方案,同时也被 Google 大力推广。

使用 Cilium 作为集群的网络方案后,docker-compose MTU 的问题也就此解决。

参考

View Comments

Homelab 手记

最近更新了家里的网关设备,也碰到了一堆问题,但也借由这个机会重新整理了网络拓扑,
并计划好好的利用 Kubernetes 部署内网的一堆应用与自动化工具。

网络拓扑

首先理清内网所有的主要网络设备,当前如图。

Network Topology

ESXi 以及 NAS,Printer 都是之前就配置好的,这里不再赘述。

主要涉及的是这次的新设备 ROS(RB450Gx4) 主网关以及 OpenWRT 虚拟路由,FreeGateway 用于科学上网,EdgeGateway 使用 Zerotier 作为边界网关,主要用于内网对等及穿透。

主网关

主网关使用 RB450Gx4,该设备属于有线路由,性能强劲,支持硬件转发,我在测速时可以跑满 200M,峰值270M,旧的路由器 RB951G 2HND 则只能跑到 170M。

ROS 主要配置的是策略路由,国外 IP 将 TCP 包路由到 FreeGateway,对等子网路由到 EdgeGateway,并配置 DNS 转发到 FreeGateway。

这里 ROS 配置时碰到了一个困扰很久的问题,路由转发到 FreeGateway 时,出现了大量丢包,导致无法完成请求。
使用 Wireshark 抓包分析发现有 ICMP Redirect 请求,之前的旧路由是同样的工作模式,没有出现问题,经过大量搜索与排查,没有在网上找到解决方案。
最后在配置防火墙时,无意发现禁用某条默认规则后再无丢包。

该条规则如下:

1/ip firewall filter
2add action=drop chain=forward connection-state=invalid

作用是丢掉无效连接状态的包,但是由于做了 marking-route 转发时产生了 ICMP Redirect,导致 tcp 状态失效,因此该规则就丢弃了部分正常包,下次记住配置 ROS 一定要默认的防火墙规则都删掉。

DNS

一个合格的 Homelab 网络理应有 DNS 提供内网的主机名映射,这里我使用的是 FreeGateway 提供的 chinadns 作为主 DNS 解析,并配置了内网 DNS 转发,内网主机应统一配置域名后缀 lan,或者由 DHCP 下发。

同时 ROS 配置了三条巧妙的 DNS 转发规则:

1/ip firewall nat
2add action=netmap chain=dstnat comment=DNS dst-address=10.0.0.1 dst-port=53 \
3    protocol=udp src-address=!10.0.1.1 to-addresses=10.0.1.1 to-ports=53
4add action=masquerade chain=srcnat dst-address=10.0.1.1 dst-port=53 protocol=\
5    udp src-address=!10.0.1.1
6add action=netmap chain=dstnat comment=HijackDNS dst-address=8.8.8.8 dst-port=\
7    53 protocol=udp src-address=!10.0.1.1 to-addresses=10.0.1.1 to-ports=53

主DNS为 ROS 10.0.0.1,也是 DHCP 下发的 DNS,这两条规则的作用是,当 DNS 请求源 IP 为 FreeGateway 10.0.1.1 时,DNS解析由 ROS 负责,当请求源 IP 为其他主机时,DNS 解析请求 NAT 转发到 10.0.1.1,而由于 FreeGateway 配置了内网域 lan 转发到 ROS,内网主机的解析就重新由 ROS 来解析。

最后劫持了对 8.8.8.8 的 DNS 请求,转发到 10.0.1.1,这样内网主机使用时,DHCP DNS 下发为 10.0.0.1, 手动配置 DNS 则为 8.8.8.8,都能达到同样的效果,并且对应用和配置毫无侵入。

而 ROS 则配置了一个脚本 schedule 来定时获取 DHCP 主机名并更新静态 DNS,以完成内网的解析,脚本如下。

Gist: dhcpHostToDNS.script

 1:local ttl "00:05:00"
 2:local zone "lan"
 3:local hostname
 4:local ip
 5:local dnsip
 6:local dhcpip
 7:local dnsnode
 8:local dhcpnode
 9 
10/ip dns static;
11:foreach i in=[find where name ~ (".*\\.".$zone) ] do={
12  :local nodettl
13  :set hostname [ get $i name ];
14  :set hostname [ :pick $hostname 0 ( [ :len $hostname ] - ( [ :len $zone ] + 1 ) ) ];
15  :set nodettl [get $i ttl];
16  /ip dhcp-server lease;
17  :set dhcpnode [ find where host-name=$hostname ];
18  :if ( [ :len $dhcpnode ] > 0) do={
19    :log debug ("Lease for ".$hostname." still exists. Not deleting.");
20  } else={
21# there's no lease by that name. Maybe this mac has a static name.
22    :local found false
23    /system script environment
24    :foreach n in=[ find where name ~ "shost[0-9A-F]+" ] do={
25       :if ( [ get $n value ] = $hostname ) do={
26         :set found true;
27       }
28    }
29    :if ( found ) do={
30      :log debug ("Hostname ".$hostname." is static");
31    } else={
32      :if ( $nodettl = $ttl ) do={
33        :log info ("Lease expired for ".$hostname.", deleting DNS entry.");
34        /ip dns static remove $i;
35      }
36    }
37  }
38}
39 
40/ip dhcp-server lease;
41:foreach i in=[find] do={
42  :set hostname ""
43  :local mac
44  :set dhcpip [ get $i address ];
45  :set mac [ get $i mac-address ];
46  :while ($mac ~ ":") do={
47    :local pos [ :find $mac ":" ];
48    :set mac ( [ :pick $mac 0 $pos ] . [ :pick $mac ($pos + 1) 999 ] );
49  };
50  :foreach n in=[ /system script environment find where name=("shost" . $mac) ] do={
51    :set hostname [ /system script environment get $n value ];
52  }
53  :if ( [ :len $hostname ] = 0) do={
54    :set hostname [ get $i host-name ];
55  }
56  :if ( [ :len $hostname ] > 0) do={
57    :set hostname ( $hostname . "." . $zone );
58    /ip dns static;
59    :set dnsnode [ find where name=$hostname ];
60    :if ( [ :len $dnsnode ] > 0 ) do={
61# it exists. Is its IP the same
62      :set dnsip [ get $dnsnode address ];
63      :if ( $dnsip = $dhcpip ) do={
64        :log debug ("DNS entry for " . $hostname . " does not need updating.");
65      } else={
66        :log info ("Replacing DNS entry for " . $hostname);
67        /ip dns static remove $dnsnode;
68        /ip dns static add name=$hostname address=$dhcpip ttl=$ttl;
69      }
70    } else={
71# it doesn't exist. Add it
72      :log info ("Adding new DNS entry for " . $hostname);
73      /ip dns static add name=$hostname address=$dhcpip ttl=$ttl;
74    }
75  }
76}

Kubernetes 集群

完成了以上基础网络和环境配置后,才有基本的条件建立 Kubernetes 集群,也是之后的主要目的,这里我使用了 kubeadmin。

由于搭建集群和配置过于复杂,应另起一文详叙。

集群运行的组件:

  • Gitlab Runner
    Gitlab Runner 的主要目的是提供一个特殊的 runner 供 gitlab-ci 使用,Gitlab 提供的 runner 虽然可以以 privlileged 模式运行,但是没有加载 nbd 内核模块,缺少我需要的构建条件,因此运行此 runner 来进行依赖 nbd 的特殊构建。
  • Bitcoin Daemon
    比特币全节点,主要用于开发和测试 API。
  • Geth
    以太坊全节点,同上。
  • go-filecoin
    Filecoin 节点,同上。

结语

网络知识是一个架构师所需的基本素质,无论是架构还是应用都需要,了解这些对于提升技术水平甚至架构水平很有帮助。
搭建这个实验环境的目的也是为了更好的学习和提升,未来 Kubernetes 也许是云上的应用操作系统,需要更多的了解和深入学习它,在云端的 Kubernetes 和本机搭建的集群始终有所限制,通过这个 Homelab 之上的集群,可以做很多事情。

参考

View Comments