orange723

orange723

5 posts

Terraform 使用

import

将已经创建的机器转换成 terraform 并管理

resource "google_compute_instance" "compute-00" {}
$ terraform import google_compute_instance.compute-00 projects/PROJECT_ID/zones/REGION/instances/compute-00

gcp lb

  • cloud func 和 cloud run 使用的 resource 是不同的,但是假如你有个 cloud func 它其实是会先创建一个 cloud run,在配置 lb 时写 cloud run 名称 这个 lb 也是能工作的。

根据路由区分 cloud func 两种写法

    path_rule {
      paths   = ["/terraform/*"]

      route_action {
        url_rewrite {
          path_prefix_rewrite = "/"
        }

      weighted_backend_services {
        backend_service = google_compute_backend_service.terraform.id
        weight          = 100
      }
      }
    }
    route_rules {
      priority = 1
      service  = google_compute_backend_service.terraform.id

      match_rules {
        prefix_match = "/terraform/"
      }

      route_action {
        url_rewrite {
          path_prefix_rewrite = "/"
        }
      }
    }
阅读全文 →

Docker 流量路径

环境 orbstack 两台 vm

d1: client

d2: docker + caddy

基础信息

d1

$ ip addr show eth0 | grep inet
    inet 192.168.139.208/24 metric 100 brd 192.168.139.255 scope global dynamic eth0
$ curl -s -v 192.168.139.184|head
*   Trying 192.168.139.184:80...
* Connected to 192.168.139.184 (192.168.139.184) port 80
> GET / HTTP/1.1
> Host: 192.168.139.184
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Content-Length: 18753
< Content-Type: text/html; charset=utf-8
< Etag: "dhu1h5rv30g0egx"
< Last-Modified: Wed, 15 Apr 2026 21:16:48 GMT
< Server: Caddy
< Vary: Accept-Encoding
< Date: Mon, 04 May 2026 07:21:36 GMT

d2

$ docker run -it --rm -p 80:80 caddy
$ ip addr show eth0 | grep inet
    inet 192.168.139.184/24 metric 100 brd 192.168.139.255 scope global dynamic eth0
    
$ ip addr show docker0 | grep inet
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
    
$ ip route
default via 192.168.139.1 dev eth0 proto dhcp src 192.168.139.184 metric 100
0.250.250.200 via 192.168.139.1 dev eth0 proto dhcp src 192.168.139.184 metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.139.0/24 dev eth0 proto kernel scope link src 192.168.139.184 metric 100
192.168.139.1 dev eth0 proto dhcp scope link src 192.168.139.184 metric 100
$ sudo docker inspect 47981fb7a43f|grep IPAddress
                    "IPAddress": "172.17.0.2",

filter

$ sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-BRIDGE
-N DOCKER-CT
-N DOCKER-FORWARD
-N DOCKER-INTERNAL
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-FORWARD
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER ! -i docker0 -o docker0 -j DROP
-A DOCKER-BRIDGE -o docker0 -j DOCKER
-A DOCKER-CT -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-FORWARD -j DOCKER-CT
-A DOCKER-FORWARD -j DOCKER-INTERNAL
-A DOCKER-FORWARD -j DOCKER-BRIDGE
-A DOCKER-FORWARD -i docker0 -j ACCEPT

nat

$ sudo iptables -S -t nat
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80

主要看这四条,filter允许 172.17.0.2 80端口访问,nat POSTROUTING 出网时做 DNAT

-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT

-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80

tcpdump/conntrack

d2

$ tcpdump -i eth0 -s0 -X -nn 'tcp port 80' -w docker/d2-eth0.pcap --print

eth0.pcap

$ tcpdump -i docker0 -s0 -X -nn 'tcp port 80' -w docker/d2-docker0.pcap --print

docker0.pcap

$ tcpdump -i veth4535c98 -s0 -X -nn 'tcp port 80' -w docker/d2-veth4535c98.pcap --print

veth4535c98.pcap

能看到每个网卡 Time 部分都不一样,从 veth -> docker0 -> eth0 是越来越大的,可以简单理解为转发损耗

再次访问,通过 conntrack 能看到是 src=172.17.0.2 就是容器的 ip

$ conntrack -L | grep 80
conntrack v1.4.8 (conntrack-tools): 1 flow entries have been shown.
tcp      6 119 TIME_WAIT src=192.168.139.208 dst=192.168.139.184 sport=59290 dport=80 src=172.17.0.2 dst=192.168.139.208 sport=80 dport=59290 [ASSURED] mark=0 use=1

通过抓包也能看到容器回包是 veth -> docker0 -> eth0

总结

放一个 ai 总结,会更加清楚

  完整路径还原

  容器内 eth0
    ↓  (回包发出,二层走 veth pair)
  veth4535c98 (host 端)         ← 第 1 次被抓到
    ↓  (这个 veth 本身就是 docker0 的一个 port)
  docker0 桥                      ← 第 2 次被抓到(同一包)
    ↓  (Linux 网桥决定:目的 IP 192.168.139.208 不在桥本地网段,
         走三层路由 → 路由表指向 eth0)
  内核路由 / POSTROUTING
    ↓  (在 POSTROUTING 链做反向 NAT:
         conntrack 还原 src 172.17.0.2:80 → 192.168.139.184:80)
  eth0 出去                       ← 第 3 次被抓到
    ↓
  vm-client

  你之前问的"veth 是直接回还是要经过 docker0"

  从抓包铁证:

  1. 数据链路层上看,veth 的 host 端就是 docker0 的一个端口——这俩在同一个 Linux bridge 上。包从容器侧 veth 出来,自动就在 docker0 桥上了。
  2. 但是包不会在 docker0 上"消失"或"反弹"。docker0 是一个二层桥,桥要做转发决策:
    • 目的 MAC 是不是桥上其他端口能到?→ 不是(vm-client 的 MAC 不在桥上)
    • 目的 IP 是不是 docker0 自己(172.17.0.1)?→ 不是
    • 那就走三层路由,交给内核的路由子系统
  3. 内核路由查表:192.168.139.208 → 走 eth0
  4. 在 eth0 出去之前,POSTROUTING 链触发,conntrack 反向 NAT 把 src 改回 vm-docker IP

  所以"veth → docker0 → eth0"这个顺序是对的,但要理解:
  • veth host 端和 docker0 是同一个桥域,包跨这两个抓包点其实是"桥内部转发",没有真正的物理跳跃
  • 真正的"跳"是从 docker0 上行到 IP 路由层 → 再下行到 eth0——这一步包做了 NAT 改写

  你看到时间戳几乎相同的现象

  ┌─────────┬────────┬───────┐
  │ 抓包点  │ 时间戳 │ Δ     │
  ├─────────┼────────┼───────┤
  │ veth    │ 676821 │ -     │
  │ docker0 │ 676834 │ +13µs │
  │ eth0    │ 676842 │ +8µs  │
  └─────────┴────────┴───────┘

  整个 veth → docker0 → eth0 全程 21 微秒。这说明三个抓包点在数据流上是串行的同一个包,不是分别独立产生的。

  还有一个有趣的细节

  请求方向(S 第一行):

  docker0:  192.168.139.208 > 172.17.0.2.80      ← DNAT 后 (dst 已改)
  veth:     192.168.139.208 > 172.17.0.2.80      ← DNAT 后 (dst 已改)
  eth0:     192.168.139.208 > 192.168.139.184.80 ← DNAT 前 (dst 还是 vm-docker)

  入向 DNAT 发生在 PREROUTING(在 eth0 之后、docker0 之前),所以你看到 eth0 抓到的包目的 IP 还是 vm-docker,而 docker0/veth 抓到时已经被改成容器 IP 了。

  回包方向(S. 第二行):

  veth:     172.17.0.2.80 > 192.168.139.208      ← 反向 NAT 前
  docker0:  172.17.0.2.80 > 192.168.139.208      ← 反向 NAT 前
  eth0:     192.168.139.184.80 > 192.168.139.208 ← 反向 NAT 后 (src 已改)

  回包反向 NAT 发生在 POSTROUTING(在 docker0/veth 之后、eth0 之前),所以 docker0 上看到的源 IP 还是容器 IP,eth0 上看到的源 IP 已经是 vm-docker IP 了。

  这跟我之前讲的 NAT 时机完美对应。你这个实验做得非常漂亮,三个抓包点正好把 NAT 的"前/中/后"三个状态都看到了。
阅读全文 →

uncloud 初体验

uncloud

安装

quick-start

注意项:
- mac 上使用 1password 的 ssh-agent,需覆盖 SSH_AUTH_SOCK 变量
- 使用私钥登录要写全路径,不然执行 uc 会提示找不到私钥
- 多集群可通过指定上下文区分 -c, --context string
- 因为域名和 dns 原因,并未直接安装 caddy 和 dns

$ export SSH_AUTH_SOCK=~/Library/Group\ Containers/com.1password/t/agent.sock

$ uc machine init root@example.com --name example-1 --no-caddy --no-dns

caddy ingress

  • 使用 ip ssl
  • caddy 仅为安装并配置 zerossl 签名证书
  • 路由划分放到 excalidraw,由于 ip ssl 的原因,个人测试不影响
$ cat certificate.crt ca_bundle.crt > fullchain.crt

caddy compose.yaml

services:
  caddy:
    image: docker.1ms.run/library/caddy:2.11
    command: caddy run -c /config/Caddyfile
    environment:
      CADDY_ADMIN: unix//run/caddy/admin.sock
    volumes:
      - /root/caddy:/root/caddy # 此处是在服务器上存放 ssl 证书文件的路径,可以根据需要修改
      - /var/lib/uncloud/caddy:/data
      - /var/lib/uncloud/caddy:/config
      - /run/uncloud/caddy:/run/caddy
    x-ports:
      - 80:80@host
      - 443:443@host
    deploy:
      mode: global

runme deploy

excalidraw

  • / 路径是 excalidraw
  • /message 是 leave-a-message 服务

excalidraw compose.yaml

services:
  excalidraw:
    image: docker.1ms.run/excalidraw/excalidraw
    x-caddy: |
      :443 {
          tls /root/caddy/fullchain.crt /root/caddy/private.key
          
          # excalidraw
          reverse_proxy {{upstreams}} {
              import common_proxy
          }
      
          # leave-a-message static
          handle /static/* {
              reverse_proxy {{upstreams "message" 3000}} {
                  import common_proxy
              }
          }

          # leave-a-message api
          handle /api/* {
              reverse_proxy {{upstreams "message" 3000}} {
                  import common_proxy
              }
          }
      
          # leave-a-message root pages
          handle_path /message/* {
              reverse_proxy {{upstreams "message" 3000}} {
                  import common_proxy
              }
          }
      
          log
      }

runme deploy

leave-a-message

leave-a-message compose-uncloud.yml

services:

  message:
    build:
      context: .
      platforms:
        - linux/amd64
    image: message:{{date "20060102-150405" "Local"}}
    configs:
      - source: app_config
        target: /app/.env
        mode: 0644

  db:
    image: docker.1ms.run/library/mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 12345678
      MYSQL_DATABASE: message
    x-ports:
      - 3306:3306/tcp@host
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

configs:
  app_config:
    content: |
      SERVER_HOST=0.0.0.0
      SERVER_PORT=3000

      ENABLE_LOGGER=true

      MYSQL_USER=root
      MYSQL_PASSWORD=12345678
      MYSQL_HOST=db
      MYSQL_DB=message

wireguard 组网测试

没用 uncloud 之前也没用过 wireguard 看了下 uncloud 的 blog connect-docker-containers-across-hosts-wireguard 不是很复杂,想要测试下 wireguard 组网效果

官方的图更直接

整体流程是(括号1代表第一台机器,括号2代表第二台)

docker bridge(1) -> iptables(1) -> wireguard(1) -> wireguard(2) -> iptables(2) -> docker bridge(2)

创建新的 bridge 网络,通过 iptables 将 bridge 网段流量转发到 wireguard,两台机器的 wireguard 之间做了 tunnel,另外一台机器的 wireguard 接收到流量通过 iptables 规则转发到 bridge 网络下的容器。

我的环境

docker-wireguard

先提前互 ping 下,免的访问不了白测试

# Machine 1
$ ping 74.48.78.11
PING 74.48.78.11 (74.48.78.11) 56(84) bytes of data.
64 bytes from 74.48.78.11: icmp_seq=1 ttl=48 time=189 ms
64 bytes from 74.48.78.11: icmp_seq=2 ttl=48 time=181 ms

# Machine 2
$ ping 101.200.150.26
PING 101.200.150.26 (101.200.150.26) 56(84) bytes of data.
64 bytes from 101.200.150.26: icmp_seq=1 ttl=50 time=193 ms
64 bytes from 101.200.150.26: icmp_seq=2 ttl=50 time=193 ms
# Machine 1
$ docker network create --subnet 10.200.1.0/24 -o com.docker.network.bridge.trusted_host_interfaces="wg0" multi-host
18ca4396c2977a987d3238f4a6b83d4dd3a0ceabad25c7c1f670a967c6b1cdef

# Machine 2
$ docker network create --subnet 10.200.2.0/24 -o com.docker.network.bridge.trusted_host_interfaces="wg0" multi-host
04c3ec0b2eaba837c5e974bdc6c76ed4dda8c10349fd3cb75af3506b0e2bac63
# 两边分别执行,确保云厂商方面安全组开通 51820
$ iptables -I INPUT -p udp --dport 51820 -j ACCEPT
# 安装 wireguard 生成配置
$ apt update && apt install wireguard
$ umask 077
$ wg genkey > privatekey
$ wg pubkey < privatekey > publickey

PrivateKey 填本机的,PublicKey 则要填对端的

原文:

PrivateKey = <replace with 'privatekey' file content from Machine 1>
PublicKey = <replace with 'publickey' file content from Machine 2>

# 生成 wg 配置
# Machine 1
$ cat /etc/wireguard/wg0.conf
[Interface]
ListenPort = 51820
PrivateKey = kKaGXES+s2mFVYTxEMN/KTJ0k/a0S9bv8JxeVz0bdnA=

[Peer]
PublicKey = Y8ENSMHYspa1x6Hfnj1YgZL0xSrOzBHeV7i6f6UZbhQ=
# IP ranges for which a peer will route traffic: Docker subnet on Machine 2
AllowedIPs = 10.200.2.0/24
# Public IP of Machine 2
Endpoint = 74.48.78.11:51820
# Periodically send keepalive packets to keep NAT/firewall mapping alive
PersistentKeepalive = 25

# Machine 2
$ cat /etc/wireguard/wg0.conf
[Interface]
ListenPort = 51820
PrivateKey = MBLrCQ2eVCga+unX3IIKPSnyzsyh1SwkmRIkIryX+0k=

[Peer]
PublicKey = TJOqJi0AuPP3GOOYFmq8jAsmak/hgKRfEpAH2nJNiVk=
# IP ranges for which a peer will route traffic: Docker subnet on Machine 2
AllowedIPs = 10.200.1.0/24
# Public IP of Machine 2
Endpoint = 101.200.150.26:51820
# Periodically send keepalive packets to keep NAT/firewall mapping alive
PersistentKeepalive = 25

# 分别启动
$ wg-quick up wg0

查看 wg 状态

# Machine 1
$ wg show
interface: wg0
  public key: TJOqJi0AuPP3GOOYFmq8jAsmak/hgKRfEpAH2nJNiVk=
  private key: (hidden)
  listening port: 51820

peer: Y8ENSMHYspa1x6Hfnj1YgZL0xSrOzBHeV7i6f6UZbhQ=
  endpoint: 74.48.78.11:51820
  allowed ips: 10.200.2.0/24
  latest handshake: 45 seconds ago
  transfer: 116.21 KiB received, 41.36 KiB sent
  persistent keepalive: every 25 seconds
  
# Machine 2
$ wg show
interface: wg0
  public key: Y8ENSMHYspa1x6Hfnj1YgZL0xSrOzBHeV7i6f6UZbhQ=
  private key: (hidden)
  listening port: 51820

peer: TJOqJi0AuPP3GOOYFmq8jAsmak/hgKRfEpAH2nJNiVk=
  endpoint: 101.200.150.26:51820
  allowed ips: 10.200.1.0/24
  latest handshake: 1 minute, 4 seconds ago
  transfer: 39.93 KiB received, 116.46 KiB sent
  persistent keepalive: every 25 seconds

配置 iptables 规则,允许 wg 到 bridge 网络

# Machine 1
$ docker network ls -f name=multi-host
NETWORK ID     NAME         DRIVER    SCOPE
18ca4396c297   multi-host   bridge    local
$ iptables -I DOCKER-USER -i wg0 -o br-18ca4396c297 -j ACCEPT

# Machine 2
$ docker network ls
NETWORK ID     NAME         DRIVER    SCOPE
e4613839f6f6   bridge       bridge    local
f1954f4aff63   host         host      local
04c3ec0b2eab   multi-host   bridge    local
7fb9f0ad47ba   none         null      local
$ iptables -I DOCKER-USER -i wg0 -o br-04c3ec0b2eab -j ACCEPT

最后配置下本机容器出网通过 wg 网卡的策略

# Machine 1
$ iptables -t nat -I POSTROUTING -s 10.200.1.0/24 -o wg0 -j RETURN

# Machine 2
$ iptables -t nat -I POSTROUTING -s 10.200.2.0/24 -o wg0 -j RETURN

测试下互通性

# Machine 2
$ docker run -d --name whoami --network multi-host traefik/whoami
$ docker inspect b992403e1e4f|grep 10.200
                    "Gateway": "10.200.2.1",
                    "IPAddress": "10.200.2.2",
                    
# Machine 1
$ docker run -it --rm --network multi-host busybox ping 10.200.2.2
PING 10.200.2.2 (10.200.2.2): 56 data bytes
64 bytes from 10.200.2.2: seq=0 ttl=62 time=194.198 ms
64 bytes from 10.200.2.2: seq=1 ttl=62 time=190.275 ms
64 bytes from 10.200.2.2: seq=2 ttl=62 time=198.435 ms
64 bytes from 10.200.2.2: seq=3 ttl=62 time=196.896 ms
64 bytes from 10.200.2.2: seq=4 ttl=62 time=197.444 ms
^C
--- 10.200.2.2 ping statistics ---
6 packets transmitted, 5 packets received, 16% packet loss
round-trip min/avg/max = 190.275/195.449/198.435 ms
$ docker run -it --rm --network multi-host alpine/curl http://10.200.2.2
Hostname: b992403e1e4f
IP: 127.0.0.1
IP: ::1
IP: 10.200.2.2
RemoteAddr: 10.200.1.2:60960
GET / HTTP/1.1
Host: 10.200.2.2
User-Agent: curl/8.17.0
Accept: */*

能看到互相通信没问题,就是延迟对比直 ping 会高一点点,毕竟本身两台机器就不近

在 1 机器把 iptables 规则导出看看

# filter
$ iptables -S -v
-P INPUT ACCEPT -c 608049 168734434
-P FORWARD DROP -c 0 0
-P OUTPUT ACCEPT -c 0 0
-N DOCKER
-N DOCKER-BRIDGE
-N DOCKER-CT
-N DOCKER-FORWARD
-N DOCKER-INTERNAL
-N DOCKER-USER
-A INPUT -p udp -m udp --dport 51820 -c 2385 192148 -j ACCEPT
-A FORWARD -c 270 22213 -j DOCKER-USER
-A FORWARD -c 147 12070 -j DOCKER-FORWARD
-A DOCKER ! -i docker0 -o docker0 -c 0 0 -j DROP
-A DOCKER ! -i br-18ca4396c297 -o br-18ca4396c297 -c 0 0 -j DROP
-A DOCKER-BRIDGE -o docker0 -c 0 0 -j DOCKER
-A DOCKER-BRIDGE -o br-18ca4396c297 -c 0 0 -j DOCKER
-A DOCKER-CT -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -c 0 0 -j ACCEPT
-A DOCKER-CT -o br-18ca4396c297 -m conntrack --ctstate RELATED,ESTABLISHED -c 0 0 -j ACCEPT
-A DOCKER-FORWARD -c 147 12070 -j DOCKER-CT
-A DOCKER-FORWARD -c 147 12070 -j DOCKER-INTERNAL
-A DOCKER-FORWARD -c 147 12070 -j DOCKER-BRIDGE
-A DOCKER-FORWARD -i docker0 -c 40 3360 -j ACCEPT
-A DOCKER-FORWARD -i br-18ca4396c297 -c 107 8710 -j ACCEPT
-A DOCKER-USER -i wg0 -o br-18ca4396c297 -c 123 10143 -j ACCEPT

# nat
$ iptables -t nat -S -v
-P PREROUTING ACCEPT -c 668150 42644933
-P INPUT ACCEPT -c 0 0
-P OUTPUT ACCEPT -c 7495 569953
-P POSTROUTING ACCEPT -c 7501 570385
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -c 667962 42630817 -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -c 0 0 -j DOCKER
-A POSTROUTING -s 10.200.1.0/24 -o wg0 -c 6 432 -j RETURN
-A POSTROUTING -s 10.200.1.0/24 ! -o br-18ca4396c297 -c 0 0 -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -c 2 168 -j MASQUERADE

关键是这三条

-A INPUT -p udp -m udp --dport 51820 -c 2385 192148 -j ACCEPT # 允许 wg 入网
-A DOCKER-USER -i wg0 -o br-18ca4396c297 -c 123 10143 -j ACCEPT # 允许 input 是 wg0 设备到 br-18ca4396c297 设备
-A POSTROUTING -s 10.200.1.0/24 -o wg0 -c 6 432 -j RETURN # 从容器出网走 wg 然后转发出去

原文还说 DOCKER-USER 规则会优先 DOCKER 链处理,所以添加在 DOCKER-USER,以后在自定义容器规则可以参考,查询确实如此

测试将 DOCKER-USER 的规则配置到 DOCKER rule 下也能正常工作,不过 docker 重启 DOCKER 里手动加的规则就会消失

$ iptables -I DOCKER -i wg0 -o br-18ca4396c297 -j ACCEPT
$ iptables -S
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-BRIDGE
-N DOCKER-CT
-N DOCKER-FORWARD
-N DOCKER-INTERNAL
-N DOCKER-USER
-A INPUT -p udp -m udp --dport 51820 -j ACCEPT
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-FORWARD
-A DOCKER -i wg0 -o br-18ca4396c297 -j ACCEPT
-A DOCKER ! -i docker0 -o docker0 -j DROP
-A DOCKER ! -i br-18ca4396c297 -o br-18ca4396c297 -j DROP

原文也说了局限性,容器互相之间通过 ip 访问 没有服务发现,无法通过名称互相访问

DNS resolution

DNS resolution
The main limitation of this setup is that containers can't find each other by name across machines. You need to use their IP addresses directly or implement a service discovery solution like Consul or CoreDNS.

For small deployments, you can assign static IPs to containers and use those IPs in your app configuration. But service discovery is essential for larger and more dynamic deployments.

总结

不用 Kubernetes 情况下,用 uncloud 可以平替(平替谈不上,uncloud只是个多机管理容器的服务)测试玩玩,多机器通过 wireguard 组网 每个机器都会部署 caddy 接收流量,在小流量的情况下单台机器也足够,对域名做 A 记录添加多个 IP 会分流,通过 caddy 内部的 upstreams 变量会负载到所有机器,这时 A 记录添加一个也是可以的,如果机器在不同地域通过 dns 做地域区分 cloudflare 也有此功能,做 cicd 时 build 和 deploy 阶段也可分开,是在本地 ci 机器上 build 后 push image 到远端机器,这种形式 github action 就能实现。

wireguard 这部分如果机器距离较远,该有的延迟不会变只是机器能互相通信,从稳定性上来说 服务器距离较远还是不建议组网,自己玩玩尚可,企业有需求不如直接用云厂商做 peer 或者像 gcp 那样各个区域本身就是通的,延迟低也不额外付费,只需规划好 ip 即可。

在更新 caddy 时会出现配置同步较慢问题,比如更新了 caddy config,重新 uc deploy 后 uc caddy config 会发现配置只会有默认 caddy 内容,路由信息不会更新的很快,或者 caddy config 写错了会导致 caddy 启动只有默认配置,由于只有一台机器并且还是 ip ssl,多域名暂未测试,可以借助 caddy 配置文件检测来规避文件编写错误问题,caddy 是支持零停机更新的 在多域名多服务情况下应该不会出现此问题。

另外相比 uncloud 我觉得 nomad 会更纯粹,只是负责容器管理 服务发现交给 consul 搭配 apisix 网关,我觉得从管理和稳定性方面会更好。

uncloud

阅读全文 →

文章链接

阅读全文 →

一个数据包的生命周期

实验流程来自:01|一个数据包的网络之旅:网络是如何工作的?

也可以阅读此文章:life-of-a-packet-in-the-linux-kernel

通过一个 HTTP 请求来观察数据包的旅程

$ sudo tcpdump -s0 -X -nn "tcp port 80" -w packet.pcap --print

packet.pcap

$ curl -o /dev/null -v http://example.com
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Host example.com:80 was resolved.
* IPv6: 2600:1406:5e00:6::17ce:bc1b, 2600:1408:ec00:36::1736:7f24, 2600:1406:bc00:53::b81e:94ce, 2600:1408:ec00:36::1736:7f31, 2600:1406:5e00:6::17ce:bc12, 2600:1406:bc00:53::b81e:94c8
* IPv4: 23.215.0.136, 23.192.228.80, 23.220.75.232, 23.220.75.245, 23.192.228.84, 23.215.0.138
*   Trying 23.215.0.136:80...
* Connected to example.com (23.215.0.136) port 80
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/8.5.0
> Accept: */*
>
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0< HTTP/1.1 200 OK
< Content-Type: text/html
< ETag: "bc2473a18e003bdb249eba5ce893033f:1760028122.592274"
< Last-Modified: Thu, 09 Oct 2025 16:42:02 GMT
< Cache-Control: max-age=86000
< Date: Fri, 28 Nov 2025 08:31:14 GMT
< Content-Length: 513
< Connection: keep-alive
<
{ [513 bytes data]
100   513  100   513    0     0    418      0  0:00:01  0:00:01 --:--:--   418
* Connection #0 to host example.com left intact

先是 dns 解析,知道 ip 后和 23.215.0.136:80 tcp 连接

* IPv6: 2600:1406:5e00:6::17ce:bc1b, 2600:1408:ec00:36::1736:7f24, 2600:1406:bc00:53::b81e:94ce, 2600:1408:ec00:36::1736:7f31, 2600:1406:5e00:6::17ce:bc12, 2600:1406:bc00:53::b81e:94c8
* IPv4: 23.215.0.136, 23.192.228.80, 23.220.75.232, 23.220.75.245, 23.192.228.84, 23.215.0.138
*   Trying 23.215.0.136:80...

连接成功后发送 GET / 请求

> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/8.5.0
> Accept: */*

example.com 回复 http 状态码 200,在通过抓包看本地机器 192.168.139.111 发起了 tcp 连接关闭

< HTTP/1.1 200 OK
< Content-Type: text/html
< ETag: "bc2473a18e003bdb249eba5ce893033f:1760028122.592274"
< Last-Modified: Thu, 09 Oct 2025 16:42:02 GMT
< Cache-Control: max-age=86000
< Date: Fri, 28 Nov 2025 08:31:14 GMT
< Content-Length: 513
< Connection: keep-alive

网络分层

图片来自:网络架构实战课

穿过客户端局域网

一句话总结:同局域网 arp 查询 mac 直接发送,不同局域网路由发送

计算下我的 ip 和 example.com 的 ip 在不在同一局域网

$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.139.111  netmask 255.255.255.0  broadcast 192.168.139.255
        inet6 fd07:b51a:cc66:0:a0db:deff:fea3:9cb5  prefixlen 64  scopeid 0x0<global>
        inet6 fe80::a0db:deff:fea3:9cb5  prefixlen 64  scopeid 0x20<link>
        ether a2:db:de:a3:9c:b5  txqueuelen 1000  (Ethernet)
        RX packets 46016  bytes 17701736 (17.7 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 330  bytes 29500 (29.5 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        
# 128 64 32 16 8 4 2 1
# 这里的对比是拿本机的子网掩码去和目的ip和本机ip对比,网络位相同则在同一网络
        
本机:192.168.139.111 子网掩码:255.255.255.0

IP:         11000000.10101000.10001011.01101111
子网掩码:     11111111.11111111.11111111.00000000
按位与运算:    11000000.10101000.10001011.00000000
网络位:        192.168.139.0

example.com:23.215.0.136

IP:          00010111.11010111.00000000.10001000
子网掩码:      11111111.11111111.11111111.00000000
按位与运算:     00010111.11010111.00000000.00000000
网络位:         23.215.0.0

既然不在一定会走路由规则,能看到走 192.168.139.1 网关设备是 eth0

$ ip route get 23.215.0.136
23.215.0.136 via 192.168.139.1 dev eth0 src 192.168.139.111 uid 501
    cache

网关一定是和主机在同一网络,观察下 arp 是怎么工作的

$ sudo arp -d 192.168.139.1

$ sudo tcpdump -s0 -X -nn "arp" -w arp.pcap --print

arp.pcap

$ arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.139.1            ether   da:9b:d0:54:e0:02   C                     eth0

traceroute 查看也是一样,虽然解析的 ip 不同但不影响

$ sudo traceroute -n -I example.com
traceroute to example.com (23.220.75.232), 30 hops max, 60 byte packets
 1  192.168.139.1  0.036 ms  0.014 ms  0.005 ms
 2  192.168.1.1  4.661 ms  4.647 ms  4.642 ms
 3  * 100.101.0.1  12.028 ms *
 4  * * *
 5  * * *
 6  * * *
 7  * * *
 8  * * *
 9  * * *
10  * * *
11  * * *
12  * * *
13  * * *
14  23.220.75.232  186.684 ms  186.577 ms  260.319 ms

推荐使用 NextTrace

在服务器上测试中间设备会响应 icmp 协议,可能是个人住址经过的设备屏蔽了 icmp

$ traceroute -I -n -m 50 example.com
traceroute to example.com (23.220.75.245), 50 hops max, 60 byte packets
 1  10.59.252.86  1.378 ms  1.446 ms  1.442 ms
 2  11.73.60.253  1.937 ms * *
 3  26.25.187.33  1.519 ms  1.529 ms  1.630 ms
 4  10.216.220.118  3.104 ms  3.179 ms  3.160 ms
 5  10.216.229.106  3.177 ms  3.178 ms  3.232 ms
 6  * * *
 7  * * *
 8  * * *
 9  * * *
10  * * *
11  * * *
12  219.158.5.174  178.130 ms  178.125 ms  178.145 ms
13  * * *
14  154.54.77.53  162.103 ms  162.088 ms  162.185 ms
15  154.54.63.70  157.864 ms  157.916 ms  157.911 ms
16  154.54.47.165  238.095 ms  238.125 ms  243.526 ms
17  154.54.169.178  260.650 ms  260.635 ms  260.638 ms
18  154.54.29.134  249.462 ms  249.394 ms  248.589 ms
19  154.54.40.249  249.505 ms  249.495 ms *
20  154.54.165.26  247.671 ms  249.877 ms  250.234 ms
21  154.54.166.58  251.909 ms  252.616 ms  252.628 ms
22  154.54.44.86  254.282 ms  254.289 ms  254.572 ms
23  154.54.27.118  250.352 ms  252.829 ms  252.919 ms
24  38.104.84.101  236.554 ms  236.499 ms  236.548 ms
25  218.30.54.6  242.952 ms  242.911 ms  242.916 ms
26  * * *
27  * * *
28  * * *
29  23.220.75.245  239.147 ms  236.590 ms  239.235 ms

推荐案例

分析了下此篇文章的问题,很有趣 0.01% 的概率超时问题

我的回答是:

两个包还有个区别

正常的:server 会给 client 发 zerowindow 随后又发 window update,server 处理的慢但节奏在 server 这里。

超时的:没看到窗口更新的包 都是 client 给 server 发送,2136 包到 2149包能看到重试 15次。

要说 server 处理的慢,只看到一次超时后面全部正常,你说中间设备处理的有问题吧 它还只有0.01的超时概率

作者给了回复:

zero window 在这里其实是一个好的现象。

数据进入的处理路径是:

NIC -> Kernel process -> tcp connection buffer -> 应用程序读取

正常的:

正是因为 kernel 处理的速度够快,才能填满 buffer,应用程序处理的不够快,导致 buffer 填满了,接收端发送 zero window 让发送端暂停发送。

超时的:

因为 kernel 处理的带宽(由于没有开启 LRO)变慢,导致无法填满 buffer,所以不会出现 zero window。同时,由于 NIC 收包比较快,很可呢是 kernel 处理不过来,导致了丢包。

当时看到这个分析又重新对比了两个包,确实是像关了网卡 Offload 功能,关于 Offload 之前测试时碰到过在这里 TCP 数据的发送和接收,同时又能看到数据包里都是 vxlan 封装了一层发送的数据,分析下来就会认为是 server 的处理能力不够导致,后面作者回复后又想了想,为什么 server 处理能力不够呢 其实并不是,因为之前没替换设备是正常的,所以是因为 NIC 收包快 kernal 处理变慢,才会以为是 server 处理能力不够。

借助 zero window 看到的现象,直接进行分析得出的结论还是太草率,不是根本原因。

阅读全文 →