知识广场

按学科筛选:计算机科学 / 计算机网络
清除筛选

«计算机科学 / 计算机网络» 分类下共 13 篇帖子

给 nginx 启用 Brotli 压缩(含验证步骤)

Brotli 是 Google 开源的压缩算法,对 HTML / CSS / JS 的压缩率通常比 gzip 高 15-25%。现代浏览器(Chrome 50+ / Firefox 44+ / Safari 11+)全支持, 所以现在没有理由不开。 需要源里有 `libnginx-mod-brotli`(Debian 12+ / Ubuntu 22.04+ 已带)。 官方仓库的 nginx 不带,要么从 nginx.org 仓库装 `nginx-mod-brotli` 要么编译时加 `--add-dynamic-module=ngx_brotli`。 ## 启用 `/etc/nginx/nginx.conf` 顶部 `events {}` 之前加: ```nginx load_module modules/ngx_http_brotli_filter_module.so; load_module modules/ngx_http_brotli_static_module.so; ``` `http {}` 里: ```nginx # Brotli for dynamic responses brotli on; brotli_comp_level 5; brotli_min_length 1024; brotli_types application/atom+xml application/javascript application/json application/rss+xml application/xml application/xml+rss application/x-font-ttf application/vnd.ms-fontobject application/x-web-app-manifest+json font/opentype image/svg+xml image/x-icon text/css text/javascript text/plain text/x-component text/xml; # Pre-compressed .br files (built by your asset pipeline) brotli_static on; # gzip still on for legacy clients gzip on; gzip_comp_level 5; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css application/json application/javascript application/xml text/xml application/xml+rss image/svg+xml; ``` 为什么 level 5:测试下来 5 是 CPU / 压缩比的甜点。9 会把 CPU 拉满但压缩 比只多 2-3%。 ## 校验 ```bash sudo nginx -t && sudo systemctl reload nginx # 客户端模拟 Brotli 支持 curl -H 'Accept-Encoding: br' -I https://example.com/main.css # 看返回头里有 content-encoding: br ``` 完整对比: ```bash echo '--- raw ---' curl -s -H 'Accept-Encoding: identity' https://example.com/main.css | wc -c echo '--- gzip ---' curl -s -H 'Accept-Encoding: gzip' --compressed https://example.com/main.css | wc -c # wc -c 是解压后的字节,看不出区别。改用 -w 看下载字节: curl -s -H 'Accept-Encoding: gzip' -o /dev/null -w 'gzip: %{size_download}\n' https://example.com/main.css curl -s -H 'Accept-Encoding: br' -o /dev/null -w 'br: %{size_download}\n' https://example.com/main.css ``` ## Brotli static —— 预压缩资源 构建期生成 `.br` 文件,nginx 命中后直接发文件,省运行时 CPU: ```bash # 给所有 css/js 生成 .br find /var/www/static -type f \( -name '*.css' -o -name '*.js' \) -print0 \ | xargs -0 -n 1 -P 4 brotli --keep --best ``` 然后 `brotli_static on;` 会优先发 `foo.css.br`。 ## 踩过的坑 - 别忘了 gzip 留着,Bot / 旧浏览器不发 `br` 头时 fallback 用。 - 如果前面有 Cloudflare 等 CDN,**它已经做了 Brotli**,源站没必要再做。 反而源站 Brotli + CDN gzip 链路上会多两次解压压缩。可以关掉源站的 brotli 只留 brotli_static 节省 CPU。 - 静态预压缩别忘了同步部署:CI 在压缩 `app.js` 后没把 `app.js.br` 推到 目录,nginx 走的还是动态压缩,毫无收益。

给纯 IPv4 服务器开 IPv6(含 nginx / Docker / 防火墙的几个坑)

## 起因 VPS 厂商给的 IP 默认是 IPv4。但越来越多用户网络只走 IPv6(移动 5G、 家宽 IPv6-only),网站不支持 IPv6 会被这些用户判为"无法访问"。 另外 SEO(Google)也把 IPv6 当作 ranking signal。 许多 VPS 厂商免费送 IPv6 /64 段,不开白不开。 ## 解决方案 ### 1. 拿到 IPv6 配置 VPS 厂商控制台 → Network → 看 IPv6 段(如 `2001:db8::/64`)+ gateway。 有些厂商默认开了,`ip -6 addr` 能看到;有些要在控制台手动启用。 ### 2. 配置接口 `/etc/netplan/01-netcfg.yaml`(Ubuntu): ```yaml network: version: 2 ethernets: eth0: addresses: - 192.0.2.10/24 - 2001:db8::a/64 gateway6: 2001:db8::1 nameservers: addresses: - 1.1.1.1 - 2606:4700:4700::1111 # 同时配 v4 + v6 DNS ``` ```bash sudo netplan apply ip -6 addr show eth0 # 应该看到 2001:db8::a/64 ``` 老 Debian 用 `/etc/network/interfaces`: ``` iface eth0 inet6 static address 2001:db8::a/64 gateway 2001:db8::1 ``` ### 3. 校验 ```bash ping6 ipv6.google.com # PING ipv6.google.com(... 2607:f8b0:...) ... curl -6 https://ifconfig.co # 你的 IPv6 地址 ``` ### 4. DNS 加 AAAA 记录 DNS 控制面板加: ``` example.com. AAAA 2001:db8::a www.example.com. AAAA 2001:db8::a ``` A 记录保留给 v4;AAAA 加给 v6。客户端按需选。 ```bash dig AAAA example.com # example.com. 300 IN AAAA 2001:db8::a ``` Cloudflare 用户:A + AAAA 都加上让 Cloudflare 处理 dual stack。 ### 5. nginx listen on IPv6 ```nginx server { listen 80; listen [::]:80; # IPv6 listen 443 ssl http2; listen [::]:443 ssl http2; server_name example.com; ... } ``` `[::]` 是 IPv6 通配地址。重启: ```bash sudo systemctl reload nginx ss -tlnp | grep nginx # 同时看到 *:80 和 :::80 ``` ### 6. 防火墙 IPv6 规则 `ufw` 默认同时管 v4 + v6(看 `/etc/default/ufw` 里 `IPV6=yes`): ```bash sudo ufw allow 22 sudo ufw allow 80 sudo ufw allow 443 # 自动同时管 v4 + v6 ``` `iptables` / `nftables` 要分别管 v4 / v6: ```bash # nftables(推荐:table inet 一管两种) table inet filter { chain input { type filter hook input priority filter; policy drop; ct state established,related accept iif lo accept ip protocol icmp accept meta nfproto ipv6 icmpv6 accept # IPv6 ICMP(必须开,PMTU 用) tcp dport { 22, 80, 443 } accept } } ``` **v6 ICMP 必须放行**——v6 路径 MTU 发现完全依赖 ICMP。封了 → 大包不通 → 间歇性页面打不开。 ### 7. systemd / 应用监听 v6 很多 service 默认只听 v4: ```nginx listen 0.0.0.0:8080; # 只 v4 # vs listen [::]:8080; # 既 v6 也 v4(Linux 默认开 IPV6_V6ONLY=0) listen [::]:8080 ipv6only=on; # 仅 v6 ``` Python: ```python # 单一 socket 同时 v4 + v6 import socket s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) s.bind(('::', 8080)) # accept v4 client 时收到 ::ffff:1.2.3.4 形式 ``` Node: ```js server.listen({ host: '::', port: 8080 }) ``` Go: ```go http.ListenAndServe(":8080", nil) // Go 默认双栈 ``` ### 8. Docker 容器开 v6 默认 Docker 容器只有 v4 网络。开 v6: `/etc/docker/daemon.json`: ```json { "ipv6": true, "fixed-cidr-v6": "fd00::/80", "experimental": true, "ip6tables": true } ``` ```bash sudo systemctl restart docker docker run --rm alpine ip -6 addr # 看到 inet6 ``` 或者用 host network 模式: ```bash docker run --network host nginx # 容器直接用 host 的网络栈 + v6 ``` Docker Compose: ```yaml networks: default: enable_ipv6: true ipam: config: - subnet: 'fd00:dead:beef::/64' ``` ### 9. systemd-resolved 配 v6 ```bash sudo nano /etc/systemd/resolved.conf # DNS=1.1.1.1 1.0.0.1 2606:4700:4700::1111 # FallbackDNS=8.8.8.8 8.8.4.4 2001:4860:4860::8888 sudo systemctl restart systemd-resolved ``` 或者直接 `/etc/resolv.conf` 加 v6 nameserver。 ### 10. 监控两栈连通 写一个 cron: ```bash #!/usr/bin/env bash # /usr/local/sbin/dual-stack-check.sh set -e curl -sf -4 https://example.com/health || echo "v4 down" | mail -s alert ops@ curl -sf -6 https://example.com/health || echo "v6 down" | mail -s alert ops@ ``` 或者用 Uptime Kuma 配两个 monitor(v4 + v6)。 ## 实测一些坑 ### A. `ping6` ICMP 被防火墙挡 ```bash ping6 ipv6.google.com # ping: connect: Network is unreachable ``` 99% 是 ICMPv6 被防火墙误封。`nft list ruleset` 看 `meta nfproto ipv6 icmpv6 accept` 是否在。 ### B. v4 通 v6 不通 ```bash curl example.com # OK curl -6 example.com # timeout ``` 检查: 1. AAAA 记录有没有:`dig AAAA example.com` 2. nginx 是否 listen :::80:`ss -tlnp | grep nginx` 3. 防火墙 v6 允许 80/443:`ip6tables -L` / `nft list` ### C. 部分客户端"happy eyeballs" 走 v6 失败 浏览器有 happy eyeballs 算法:v4/v6 都试,谁先连上用谁。 如果 v6 路径慢(你 ISP 链路差),happy eyeballs 会让浏览器走 v4, 看起来一切正常。但 v6-only 用户仍打不开 → 监控里抓不到。 专门做 v6-only 测试机定期 check。 ### D. CDN / WAF v6 支持 Cloudflare / Fastly 自动双栈。 某些小 CDN 不支持 v6 → 源站直连 v6 用户慢。问清楚。 ### E. SSH brute-force 在 v6 上 v6 地址空间大,扫不动。所以 v6 上 SSH 攻击罕见。 不过 fail2ban 默认只管 v4,要专门启 v6 支持: ```ini [sshd] banaction = ufw # 或 nftables-multiport ipv6 friendly ``` ### F. AAAA 记录加错让网站慢 如果 AAAA 指向不可达 v6(你机器没真开 v6),浏览器会先试 v6 失败 + timeout + 再试 v4。**首屏慢 1-3 秒**。 要么真开 v6,要么删 AAAA。 ## 效果 我们站点开 IPv6 后: - 移动 5G 用户访问稳定(之前部分运营商 v4 NAT 偶尔丢包) - Google PageSpeed Insights "Modern web" 那条满分 - 全球访问质量轻微改善(v6 路径在某些链路上更直) - 部分 API 请求量上升(v6-only 客户端被 unblock) ## 测试工具 - [https://test-ipv6.com](https://test-ipv6.com):浏览器跑测试看双栈情况 - [https://ipv6-test.com/validate.php?url=example.com](https://ipv6-test.com):你的网站 v6 评分 ## 踩过的坑(再总结) 1. **AAAA 记录加了但服务器没真 listen v6** → 用户访问慢 2. **ICMPv6 防火墙封死** → MTU 探测失败 → 大包不通 3. **systemd service 只 bind 0.0.0.0** → v6 客户端连不到 4. **Docker 容器忘开 v6** → 容器内服务对 v6 client 不响应 5. **fail2ban 只防 v4** → v6 上 attacker 不受限(虽然少见但仍要防)

Traefik v3:用 Docker label 自动发现 + 自动 HTTPS 的反代

## 起因 服务器上跑了 8 个 Docker 容器,每个都想用自己的子域名访问 + HTTPS。 nginx 每加一个服务要改 conf + 申证书 + reload;Caddy 也要改 Caddyfile。 有没有"加新容器自动注册"的方案? Traefik 是 Go 写的反代,原生支持"从 Docker / K8s / Consul 等动态发现 服务" + Let's Encrypt 自动签证书。加一个容器 = 加一组 label,零配置改动。 ## 解决方案 ### 1. Traefik 容器本身 ```yaml # /srv/traefik/docker-compose.yml services: traefik: image: traefik:v3.1 restart: unless-stopped ports: - "80:80" - "443:443" # Traefik dashboard (内网可见) - "127.0.0.1:8080:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./letsencrypt:/letsencrypt - ./traefik.yml:/etc/traefik/traefik.yml:ro networks: - web networks: web: external: true ``` 创建共享网络: ```bash docker network create web ``` ### 2. traefik.yml ```yaml api: dashboard: true insecure: true # 仅 127.0.0.1 暴露所以可以 true entryPoints: web: address: ':80' http: redirections: entryPoint: to: websecure scheme: https permanent: true websecure: address: ':443' providers: docker: exposedByDefault: false # 容器必须显式 enable 才被 routing network: web certificatesResolvers: le: acme: email: [email protected] storage: /letsencrypt/acme.json httpChallenge: entryPoint: web # 想用 DNS-01 + Cloudflare: # dnsChallenge: # provider: cloudflare # delayBeforeCheck: 0 log: level: INFO accessLog: {} ``` 启动: ```bash docker compose up -d ``` ### 3. 加业务容器:只加 label,不改 traefik `/srv/myapp/docker-compose.yml`: ```yaml services: myapp: image: myorg/myapp:latest restart: unless-stopped networks: - web labels: - 'traefik.enable=true' - 'traefik.http.routers.myapp.rule=Host(`myapp.example.com`)' - 'traefik.http.routers.myapp.entrypoints=websecure' - 'traefik.http.routers.myapp.tls.certresolver=le' - 'traefik.http.services.myapp.loadbalancer.server.port=3000' networks: web: external: true ``` ```bash docker compose up -d ``` Traefik 一秒内: - 发现新容器 - 加路由 `myapp.example.com → myapp:3000` - 通过 HTTP-01 challenge 自动签 Let's Encrypt 证书 - 80 自动跳 443 `curl https://myapp.example.com/` 立刻可用。 ### 4. 多服务批量 ```yaml services: api: image: api:latest networks: [web] labels: - traefik.enable=true - traefik.http.routers.api.rule=Host(`api.example.com`) - traefik.http.routers.api.entrypoints=websecure - traefik.http.routers.api.tls.certresolver=le - traefik.http.services.api.loadbalancer.server.port=8000 blog: image: ghost:5 networks: [web] labels: - traefik.enable=true - traefik.http.routers.blog.rule=Host(`blog.example.com`) - traefik.http.routers.blog.entrypoints=websecure - traefik.http.routers.blog.tls.certresolver=le - traefik.http.services.blog.loadbalancer.server.port=2368 uptime: image: louislam/uptime-kuma:1 networks: [web] labels: - traefik.enable=true - traefik.http.routers.up.rule=Host(`up.example.com`) - traefik.http.routers.up.entrypoints=websecure - traefik.http.routers.up.tls.certresolver=le - traefik.http.services.up.loadbalancer.server.port=3001 ``` 3 个服务,3 套 label,3 个域名。零修改 traefik 配置。 ### 5. 加中间件:basic auth / IP 白名单 / rate limit label 里声明 + 应用: ```yaml labels: - traefik.enable=true - traefik.http.routers.admin.rule=Host(`admin.example.com`) - traefik.http.routers.admin.entrypoints=websecure - traefik.http.routers.admin.tls.certresolver=le - traefik.http.routers.admin.middlewares=auth,ratelimit - 'traefik.http.middlewares.auth.basicauth.users=alice:$$apr1$$abc...' - 'traefik.http.middlewares.ratelimit.ratelimit.average=10' - 'traefik.http.middlewares.ratelimit.ratelimit.burst=20' - traefik.http.services.admin.loadbalancer.server.port=8000 ``` basic auth password 用 `htpasswd -nb alice secret` 生成(注意 `$` 在 YAML 里 escape 成 `$$`)。 ### 6. dashboard 打开 SSH tunnel: ```bash ssh -L 8080:localhost:8080 server # 浏览器:http://localhost:8080 ``` 看到所有 router / service / middleware,状态可视化。 ## 效果 - 加新容器从"改 nginx + certbot + reload + DNS 配 + …" 收敛到 "docker-compose up,加 5 行 label" - 8 个服务的反代配置文件总长 < 100 行(之前 nginx 200+ 行 / 4 个 vhost 文件) - 证书自动续期,不再有 "忘续期 → 网站红框" 事故 - 加 / 删服务无 traefik 重启 - dashboard 一眼看到所有 route 状态 ## 与 nginx / Caddy / HAProxy 对比 | | Traefik | nginx | Caddy | HAProxy | |---|---|---|---|---| | Docker 自动发现 | ✅ 原生 | ❌(需第三方) | 一定程度 | ❌ | | 自动 HTTPS | ✅ | ❌(需 certbot) | ✅ | ❌ | | K8s ingress | ✅ | ✅ | ✅ | ✅ | | 性能 | 中 | 极高 | 高 | 极高 | | 配置 | YAML / labels | nginx 语法 | Caddyfile | HAProxy 语法 | | 学习曲线 | 中 | 高 | 低 | 高 | 容器化场景选 Traefik;静态站 / 极致性能选 nginx;混合简单场景 Caddy。 ## 踩过的坑 1. **`network: web` 不指定 Traefik 不知道连哪个 net**:容器在多个网络 时 Traefik 不确定走哪个。一定在 traefik.yml 里 `providers.docker.network: web` 或每个服务 label 写 `traefik.docker.network=web`。 2. **acme.json 权限不对**:Traefik 要求 600。`chmod 600 letsencrypt/acme.json`。 3. **HTTP-01 challenge 失败**:80 端口被占 / 路由器没转发。检查 `curl -I http://myapp.example.com/.well-known/acme-challenge/test` 能不能到 Traefik 容器。 4. **生产 dashboard 暴露公网**:`api.insecure=true` 千万别开公网。 要外网访问 dashboard 加 router + auth middleware 包起来。 5. **Let's Encrypt 频率限制**:测试期间反复重启 + 反复申证书会被 LE rate limit。dev 用 `acme.caServer: https://acme-staging-v02.api.letsencrypt.org/directory` 测试,跑通后切回生产 CA。

SSH config 写好 jump host 跳板(一行 ProxyJump 干掉所有手动 tunnel)

很多生产环境的机器不直接对外,要通过堡垒机(jump host)跳进去。 没配置 SSH config 时每次都要: ```bash ssh -L 2222:internal:22 user@bastion # 另开终端 ssh -p 2222 user@localhost ``` 或者: ```bash ssh -t user@bastion ssh user@internal ``` `~/.ssh/config` 里写好 `ProxyJump`,之后直接: ```bash ssh internal # 就完事了 ``` ## 最小可用 `~/.ssh/config` ``` # 公开堡垒机 Host bastion HostName bastion.example.com User opsdude Port 22 IdentityFile ~/.ssh/id_ed25519_company IdentitiesOnly yes # 内网机器,通过 bastion 跳 Host db1 HostName 10.0.0.11 User dbadmin ProxyJump bastion IdentityFile ~/.ssh/id_ed25519_company Host app1 app2 app3 HostName %h.internal.example.com User app ProxyJump bastion IdentityFile ~/.ssh/id_ed25519_company ``` `%h` 是当前 `Host` 名字(app1 / app2 / app3)。整段不需要重复 3 遍。 之后: ```bash ssh bastion # 直接进堡垒 ssh db1 # 自动通过 bastion 进 db1 ssh app1 # 同上 scp file.tar app1:/tmp/ # scp / rsync 同样自动跳板 rsync -avz ./dist/ app1:/srv/myapp/ ``` ## 多层跳板 ``` Host very-deep HostName 192.168.99.5 User admin ProxyJump bastion,intermediate # 等价于: # bastion → intermediate → very-deep ``` ## 常用配置项速查 ``` Host name HostName actual.host # 真实主机名 / IP User myname # 登录用户 Port 22 # 端口 IdentityFile ~/.ssh/key # 指定密钥 IdentitiesOnly yes # 只用上面这个 key,不用 ssh-agent 里的其它 ProxyJump bastion # 跳板 ProxyCommand cmd %h %p # 自定义代理命令(少用了,被 ProxyJump 替代) LocalForward 9000 localhost:9000 # ssh -L 等价 RemoteForward 9001 localhost:80 # ssh -R 等价 ServerAliveInterval 60 # 60 秒发一个 keepalive 心跳 ServerAliveCountMax 3 # 3 次没回应就断 ControlMaster auto # 复用 TCP 连接 ControlPath ~/.ssh/cm-%r@%h:%p ControlPersist 10m StrictHostKeyChecking accept-new UserKnownHostsFile ~/.ssh/known_hosts ``` ## 关键技巧:ControlMaster 复用连接 ``` Host * ControlMaster auto ControlPath ~/.ssh/cm-%r@%h:%p ControlPersist 10m ``` 意思是:每个 (user, host, port) 组合的第一次 ssh 建立真实 TCP; 后续 ssh / scp / rsync / git 全部复用这个 TCP, 不再重新握手 / 不再输密码(如果原先要的话)。 效果: - 第一次 `ssh server` 慢一点 - 第二次 / scp / rsync / git push 几乎瞬开 `/tmp/` 可能不够安全(多用户系统),放 `~/.ssh/cm-...` 私有。 ## 跳板的端口转发 ``` Host db-via-bastion HostName 10.0.0.11 User dbadmin ProxyJump bastion LocalForward 5432 localhost:5432 ``` ```bash ssh -fN db-via-bastion # 后台开 tunnel psql -h localhost -p 5432 -U user ``` `-fN` 表示后台 + 不开 shell,纯做 tunnel。 ## SSH agent + agent forwarding 跳到中间机后再要去第三跳,不想把 key 拷到中间机: ``` Host bastion ForwardAgent yes ``` ```bash eval $(ssh-agent) ssh-add ~/.ssh/id_ed25519 ssh bastion # 在 bastion 上 ssh other —— 用的是你本地的 key ``` agent forwarding 有 root 风险(堡垒机 root 能用你的 key), 所以 **只对自己完全信任的机器开**。生产堡垒机用 ProxyJump 替代, 不让 key 真的暴露给堡垒机。 ## 别名 + 子目录配置 ``` # ~/.ssh/config Include ~/.ssh/config.d/*.conf ``` ``` # ~/.ssh/config.d/work.conf Host work-* User worker IdentityFile ~/.ssh/work_key ``` 把不同项目 / 客户的配置拆开,避免单个文件几百行。 ## known_hosts + 自动接受新主机 在脚本里第一次连陌生机器: ```bash ssh -o StrictHostKeyChecking=accept-new server ``` `accept-new` 比 `no` 安全——只接受新主机;已存在 host key 改变了仍报错 (中间人攻击信号)。 ## 密钥类型 ```bash # 推荐:Ed25519,短、快、安全 ssh-keygen -t ed25519 -C '[email protected]' # 老兼容:RSA 4096 ssh-keygen -t rsa -b 4096 ``` 服务端要支持 Ed25519(OpenSSH 6.5+ / 2014 后基本都行)。 ## 安全:禁用密码登录 服务端 `/etc/ssh/sshd_config`: ``` PasswordAuthentication no PubkeyAuthentication yes PermitRootLogin prohibit-password ``` ```bash sudo systemctl reload ssh ``` 之后任何密码登录都被拒;只能凭密钥。结合 fail2ban / 改端口效果叠加。 ## 踩过的坑 - `IdentityFile` 没设 `IdentitiesOnly yes` → ssh-agent 里的其它 key 会被一一尝试,可能因连续失败把你 ban 了。配同事的 git host 时尤其 容易踩。 - ProxyJump 在很老的 OpenSSH(< 7.3)不支持,要用 ProxyCommand: `ProxyCommand ssh -W %h:%p bastion`。 - ControlPersist 时长不要无限("yes"),机器一直长连接,重启后状态 乱。10m / 1h 比较合理。 - known_hosts 里的旧 host key 在机器重装后失效,连接报 MITM 警告。 `ssh-keygen -R hostname` 清掉那条。

用 Cloudflare Tunnel 把本地服务暴露到公网(不开任何端口)

家宽 / NAT 后面的开发机想暴露一个服务出去,传统方案是路由器端口转发 + 动态 DNS。Cloudflare Tunnel(前身 Argo Tunnel)走反向通道:本地 `cloudflared` 守护进程主动连出,所有入站流量走 Cloudflare 边缘转发回来。 好处: - 公网完全看不到你的源 IP - 不需要在路由器 / 防火墙打洞 - 免费层支持自定义域名 + HTTPS 自动签发 下面把本地 `http://127.0.0.1:8080` 暴露成 `myapp.example.com`。 前提:`example.com` 已经托管在 Cloudflare。 ## 1. 安装 cloudflared ```bash # Debian/Ubuntu curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \ | sudo tee /etc/apt/trusted.gpg.d/cloudflare-main.gpg >/dev/null echo "deb https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" \ | sudo tee /etc/apt/sources.list.d/cloudflared.list sudo apt update && sudo apt install -y cloudflared ``` ## 2. 登录 + 创建隧道 ```bash cloudflared tunnel login # 浏览器里选择 example.com,授权后会下载 cert.pem 到 ~/.cloudflared/ cloudflared tunnel create myapp # 输出隧道 UUID 和凭据文件路径 ~/.cloudflared/<uuid>.json ``` ## 3. 写配置 ```yaml # ~/.cloudflared/config.yml tunnel: <这里粘上面的 UUID> credentials-file: /home/yourname/.cloudflared/<uuid>.json ingress: - hostname: myapp.example.com service: http://127.0.0.1:8080 - service: http_status:404 ``` 最后一条 `http_status:404` 是兜底必填项,否则 cloudflared 拒绝启动。 ## 4. 配 DNS ```bash cloudflared tunnel route dns myapp myapp.example.com # 在 Cloudflare DNS 表里自动写入一条 CNAME 指向 <uuid>.cfargotunnel.com ``` ## 5. 跑起来(先前台测试) ```bash cloudflared tunnel run myapp ``` 另开终端 `curl https://myapp.example.com/`,能拿到本地服务的响应就成功。 ## 6. 转为 systemd 服务 ```bash sudo cloudflared service install sudo systemctl enable --now cloudflared sudo journalctl -u cloudflared -f ``` `service install` 会把 `~/.cloudflared/` 整个复制到 `/etc/cloudflared/`, 并生成 unit。后续改配置改 `/etc/cloudflared/config.yml` 然后 `restart`。 ## 多服务复用同一隧道 `ingress:` 是个列表,可以挂多个: ```yaml ingress: - hostname: app.example.com service: http://127.0.0.1:8080 - hostname: api.example.com service: http://127.0.0.1:3000 - hostname: ssh.example.com service: ssh://127.0.0.1:22 # 配合 cloudflared access ssh 用 - service: http_status:404 ``` ## 踩过的坑 - 配置里 `credentials-file` 必须是 **绝对** 路径,相对路径在 systemd 下找不到。 - DNS 那条 CNAME 是 "proxied" 状态(橙云)才会走 tunnel;如果手动改成 灰云(DNS only),整个 tunnel 立刻 404。 - 隧道凭据 `.json` 文件等于密码,泄露后任何人都能冒充你的隧道。 `/etc/cloudflared/` 权限默认 700,不要随便改。

用 tcpdump 抓包 + Wireshark 复现一个 TCP RST 问题

应用日志里出现 "connection reset by peer",应用端代码看着没问题, 对端代码看着也没问题。这类问题 70% 是中间链路(LB / 防火墙 / 反代) 干的。抓包是唯一直接定位的办法。 ## 1. 收集足够的现场信息 before 开抓之前,先列清楚四元组: ```bash # 客户端 ss -tan state established '( dport = :8080 or sport = :8080 )' # 服务端 ss -tan state established '( sport = :8080 )' ``` 记下 src / dst IP + port。 ## 2. tcpdump 命令 服务端: ```bash sudo tcpdump -i any \ -s 0 \ -w /tmp/svr.pcap \ 'tcp port 8080' ``` 客户端(如果可以同时抓): ```bash sudo tcpdump -i eth0 -s 0 -w /tmp/cli.pcap 'host <server-ip> and tcp port 8080' ``` - `-s 0` 不截断 payload(默认 96 字节,分析 HTTP / TLS 不够看) - `-i any` 在 Linux 上抓所有接口(不知道走哪个就用这个) - 写文件比直接终端打印好 100 倍 —— 终端文本格式会丢字段、丢顺序 跑出问题后 Ctrl-C 停。 ## 3. 快速浏览 ```bash # 文本看一眼 tcpdump -r /tmp/svr.pcap -n -nn -c 20 # 只看 RST 包 tcpdump -r /tmp/svr.pcap -n 'tcp[tcpflags] & tcp-rst != 0' # 看哪些会话有 RST tshark -r /tmp/svr.pcap -Y 'tcp.flags.reset == 1' \ -T fields -e tcp.stream -e ip.src -e ip.dst | sort -u ``` `tcp.stream` 是 Wireshark 给每个 TCP 会话的整数 ID。找到出问题的 stream 号 (比如 7): ```bash tshark -r /tmp/svr.pcap -Y 'tcp.stream == 7' -V | less ``` ## 4. 用 Wireshark 看 ```bash scp server:/tmp/svr.pcap . wireshark svr.pcap ``` 经验流程: 1. **Statistics → Conversations → TCP**,看哪些会话异常短或有 RST 2. 右键问题会话 **Follow → TCP Stream**:看应用层 payload,能判断 RST 是发生在哪条请求 / 哪个字节边界 3. **Expert Information**(左下角的⚠图标):Wireshark 自动标注的 异常(previous segment lost, dup ACK, RST 等) ## 5. RST 的常见根因 | 现象 | 根因 | |---|---| | 服务端发的 RST,紧跟在 FIN 之后 | 应用没读完 socket 缓冲就 close(HTTP body 没消费) | | 客户端发的 RST,应用层没异常 | 客户端 OS RST:socket 被强制回收(fd 泄露后被 GC) | | 中间设备发的 RST,TTL 远小于真实端 | 防火墙 / 流量清洗设备主动断 | | 握手后立刻 RST | TCP wrappers / hostsdeny;或对端只接受 IPv4 而你发的 IPv6 | | 大块上传中途 RST | LB 的 buffer overflow / 限流;或 MTU 不匹配(Path MTU Black Hole) | ## 6. MTU 问题的快速验证 如果怀疑 MTU: ```bash # 强制最大 1500 - 28 = 1472 字节 payload,不允许分片 ping -M do -s 1472 <server> # 二分缩小直到能通 ping -M do -s 1400 <server> ``` `ping ... fragmentation needed` 就是被某段链路要求分片但你设了 DF(不分片)。 通常出现在 PPPoE / VPN / 部分隧道场景。 ## 7. 持续抓包(环形缓冲) 排查间歇性问题时,连续抓不停但不要把磁盘塞满: ```bash sudo tcpdump -i any -s 0 \ -w /tmp/sniff-%Y%m%d-%H%M%S.pcap \ -G 600 \ -W 24 \ 'tcp port 8080' ``` `-G 600` 每 10 分钟切新文件;`-W 24` 最多保留 24 个,循环覆盖。 ## 踩过的坑 - 抓包文件别忘了清理 —— `-s 0` 在繁忙服务上一小时能写 GB 级。 - tshark `-Y` 是显示过滤器(capture 后筛选),`-f` 是 capture 过滤器 (抓的时候用)。语法不一样,别混。`-Y 'tcp.port == 8080'` vs `-f 'tcp port 8080'`。 - TLS 流量看不到明文,配合 SSLKEYLOGFILE 让浏览器/curl 把会话 key 落地, Wireshark 用这个 key 文件就能解密(Edit → Preferences → Protocols → TLS)。 - 抓本机环回流量在 Linux 上需要 `-i lo`,`-i any` 偶尔会漏。

Caddy 一行配置自动 HTTPS(含证书自动续期与多站点反代)

Caddy 把"自动签 Let's Encrypt 证书 + HTTP/2 + 自动续期"做成了默认行为。 nginx 配同样的事情至少 30 行配置 + certbot crontab,Caddy 是 1 行: ``` example.com { reverse_proxy localhost:3000 } ``` ## 安装 ```bash # 官方 apt 仓库(也有 yum / 二进制下载) sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ | sudo tee /etc/apt/sources.list.d/caddy-stable.list sudo apt update && sudo apt install -y caddy ``` ## 最小可工作 Caddyfile `/etc/caddy/Caddyfile`: ``` example.com { encode zstd gzip reverse_proxy 127.0.0.1:3000 header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options nosniff Referrer-Policy strict-origin-when-cross-origin } } api.example.com { reverse_proxy 127.0.0.1:8000 log { output file /var/log/caddy/api.log format json } } static.example.com { root * /var/www/static file_server encode zstd gzip } ``` `encode` 自动启用 Brotli / zstd / gzip 链路压缩;`reverse_proxy` 自动透传 `X-Forwarded-*`;TLS 证书 80 端口 ACME HTTP-01 自动签 + 续期。 ## 启用 ```bash sudo caddy validate --config /etc/caddy/Caddyfile sudo systemctl reload caddy journalctl -u caddy -n 50 ``` `reload` 是热重载,不会断现有连接。 ## 防火墙 ```bash sudo ufw allow 80/tcp # ACME challenge 需要 80 sudo ufw allow 443/tcp ``` 80 必须开 —— Let's Encrypt 通过 80 端口验证域名所有权。 ## 进阶:通配符证书 + DNS-01 如果你想签 `*.example.com`,必须用 DNS-01 而不是 HTTP-01。Caddy 需要装 带 DNS 插件的版本: ```bash sudo caddy add-package github.com/caddy-dns/cloudflare ``` 然后: ``` *.example.com { tls { dns cloudflare {env.CF_API_TOKEN} } reverse_proxy 127.0.0.1:3000 } ``` 环境变量在 systemd drop-in 里设: ```bash sudo systemctl edit caddy # [Service] # Environment=CF_API_TOKEN=xxxx ``` ## 与 nginx 对比 | 维度 | Caddy | nginx + certbot | |---|---|---| | 配置行数 | 1-3 行 | 20-30 行 | | 自动续期 | 默认 | crontab + reload hook | | HTTP/3 | 默认 | 编译时 `--with-http_v3_module` | | 性能 | 略低 | 略高(C 写的) | | 配置复杂度 | 低 | 高,但灵活 | 小规模 / 个人项目用 Caddy;流量上去后或者需要复杂 rewrite 用 nginx。 ## 踩过的坑 - 第一次签证书会卡几十秒,是正常 ACME 流程;不要看到日志没动静就 restart, 会把证书签到一半的 nonce 弄丢,触发 LE 频率限制。 - LE 每周每域名最多签 50 张;测试时用 `tls internal` 走 Caddy 内置 CA, 浏览器手动信任即可。 - Caddy 默认证书 / 凭据存 `/var/lib/caddy/.local/share/caddy/`,备份时别漏。

WireGuard 部署一条点对点 VPN(含 IPv4/IPv6 双栈)

WireGuard 是当前最容易理解 + 最快的 VPN 协议:4000 行内核代码, 配置就一个 `.conf` 文件。比 OpenVPN / IPsec 维护成本低一个数量级。 下面把两台公网服务器 `gate.example.com`(公网 IP `203.0.113.10`) 和家里的 NAT 后机器接到同一虚拟网段 `10.7.0.0/24`。 ## 1. 安装 ```bash # Debian 11+ / Ubuntu 20.04+ sudo apt install -y wireguard wireguard-tools ``` ## 2. 生成密钥(两端各一份) ```bash cd /etc/wireguard umask 077 wg genkey | tee privatekey | wg pubkey > publickey cat privatekey publickey ``` 下面用占位符: - `SERVER_PRIV` / `SERVER_PUB` - `PEER_PRIV` / `PEER_PUB` ## 3. 服务端 `gate` 上 `/etc/wireguard/wg0.conf` ```ini [Interface] Address = 10.7.0.1/24, fd00:7::1/64 ListenPort = 51820 PrivateKey = SERVER_PRIV # NAT 出口:让 peer 通过本机访问公网 PostUp = nft add table inet wg-nat; \ nft add chain inet wg-nat postrouting { type nat hook postrouting priority 100\; }; \ nft add rule inet wg-nat postrouting ip saddr 10.7.0.0/24 oifname "eth0" masquerade PostDown = nft delete table inet wg-nat [Peer] PublicKey = PEER_PUB AllowedIPs = 10.7.0.2/32, fd00:7::2/128 PersistentKeepalive = 25 ``` ## 4. 客户端 `/etc/wireguard/wg0.conf` ```ini [Interface] Address = 10.7.0.2/24, fd00:7::2/64 PrivateKey = PEER_PRIV DNS = 1.1.1.1, 2606:4700:4700::1111 [Peer] PublicKey = SERVER_PUB Endpoint = 203.0.113.10:51820 AllowedIPs = 0.0.0.0/0, ::/0 # 全量流量走 VPN;只走内网就改 10.7.0.0/24 PersistentKeepalive = 25 ``` `PersistentKeepalive = 25` 是 NAT 穿透的关键:每 25 秒发一个空包让上游 路由器 keep mapping。漏写 NAT 后端 5 分钟就掉。 ## 5. 启用 两端都: ```bash # 开 IP 转发(服务端必须) echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-wg.conf echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-wg.conf sudo sysctl --system sudo systemctl enable --now wg-quick@wg0 sudo wg show ``` ## 6. 校验 ```bash # 在 peer 上 ping 10.7.0.1 ping fd00:7::1 curl ifconfig.me # 应当返回 gate 的公网 IP(说明全量走 VPN 了) # 在 gate 上 sudo wg show # peer: ... # latest handshake: 5 seconds ago # transfer: 10 KiB received, 12 KiB sent ``` `latest handshake` 没出现说明握手没成功;先排查防火墙是否放行 UDP/51820。 ## 7. 加更多 peer 服务端 wg0.conf 追加 `[Peer]` 段;不需要 restart,热加: ```bash sudo wg set wg0 peer NEW_PEER_PUB allowed-ips 10.7.0.3/32 persistent-keepalive 25 # 然后改 conf 保存让重启后还在 ``` ## 踩过的坑 - `AllowedIPs` 不是 "允许这个 peer 访问哪里",而是 **路由表**——告诉 内核"目标是这些 IP 的包应该走这个 peer"。客户端写 `0.0.0.0/0` 就把 默认路由抢了。 - 服务端 `AllowedIPs = 10.7.0.2/32` 不能写 `/24`,否则只能挂一个 peer —— 后加的 peer 路由会被前者抢掉。 - `wg-quick` 的 `PostUp` / `PostDown` 行如果失败,接口照常起来但 NAT 没工作,表现为"能 ping 服务端但 ping 不到外网"。用 `sudo journalctl -u wg-quick@wg0` 看清楚 nft 报错。 - IPv6 加进来后 DNS resolver 必须双栈,否则会出现 IPv4 通、IPv6 通但 解析慢的"假掉线"现象。

申 Let's Encrypt 通配符证书(DNS-01 + Cloudflare API)

## 起因 要给 `*.example.com` 申通配符证书(一个证书管所有子域名)。 普通 `--nginx` / `--apache` certbot 用 HTTP-01 challenge, **不支持通配符** —— LE 强制通配符必须用 DNS-01 challenge。 DNS-01:在 DNS 加一条 `_acme-challenge.example.com TXT` 记录, LE 来查这条 → 验证你控制这个域。 手动加 TXT 记录烦 + 续期时还要再加 → 自动化 DNS API 是必经之路。 ## 用 certbot + Cloudflare API ### 1. 拿 Cloudflare API token Cloudflare 控制台 → 我的个人资料 → API 令牌 → 创建令牌: 权限: - Zone:Zone:Read - Zone:DNS:Edit 资源:包括 → 特定区域 → example.com 复制 token。 ### 2. 装 certbot + cloudflare 插件 ```bash sudo apt install -y certbot python3-certbot-dns-cloudflare ``` ### 3. 配置 token 文件 ```bash sudo mkdir -p /etc/letsencrypt sudo nano /etc/letsencrypt/cloudflare.ini ``` ``` dns_cloudflare_api_token = your-token-here ``` ```bash sudo chmod 600 /etc/letsencrypt/cloudflare.ini ``` ### 4. 申请通配符证书 ```bash sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ --dns-cloudflare-propagation-seconds 30 \ -d '*.example.com' \ -d 'example.com' \ --agree-tos --no-eff-email \ -m [email protected] ``` 要点: - `-d '*.example.com' -d 'example.com'`:通配符 + 裸域名要分别 list (通配符不 cover 裸域名 + apex domain) - `--dns-cloudflare-propagation-seconds 30`:让 LE 查询前等 30 秒 让 DNS 传播 成功输出: ``` Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem ``` ### 5. nginx 配置 ```nginx server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name app.example.com api.example.com blog.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # 加强配置 ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; ssl_dhparam /etc/ssl/dhparam.pem; # openssl dhparam -out ... 4096 # OCSP stapling ssl_stapling on; ssl_stapling_verify on; location / { proxy_pass http://localhost:8080; } } ``` ```bash sudo nginx -t && sudo systemctl reload nginx ``` ### 6. 自动续期 certbot 装好后默认 systemd timer: ```bash sudo systemctl list-timers certbot.timer # certbot.timer active next run in ~12h ``` 每 12 小时检查证书;< 30 天到期时续。 确认续期能跑通: ```bash sudo certbot renew --dry-run ``` 成功输出说明真续期会 work。 ### 7. 续期后 reload nginx certbot hook: ```bash sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy sudo tee /etc/letsencrypt/renewal-hooks/deploy/nginx-reload <<'EOF' #!/bin/bash systemctl reload nginx EOF sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/nginx-reload ``` 证书续期成功后自动 reload nginx,新证书生效。 ## 其它 DNS provider `certbot-dns-*` 插件覆盖大多数: ```bash sudo apt install -y \ python3-certbot-dns-cloudflare \ python3-certbot-dns-route53 \ python3-certbot-dns-google \ python3-certbot-dns-digitalocean \ python3-certbot-dns-rfc2136 # 用于 bind 等支持 RFC2136 的服务器 ``` 完整列表 see certbot docs。 ### Route53 / AWS ```bash sudo certbot certonly \ --dns-route53 \ -d '*.example.com' -d 'example.com' ``` 凭据从 `~/.aws/credentials` 或 instance role。 ### Google Cloud DNS ```bash sudo certbot certonly \ --dns-google \ --dns-google-credentials /etc/letsencrypt/gcloud-svc.json \ -d '*.example.com' ``` 需要 service account JSON。 ## 用 acme.sh(轻量替代) ```bash curl https://get.acme.sh | sh export CF_Token="..." export CF_Zone_ID="..." acme.sh --issue --dns dns_cf -d '*.example.com' -d 'example.com' ``` acme.sh 是 shell 写的,比 certbot 轻量 + 不依赖 Python。 内置上百个 DNS provider。功能等价。 ## Caddy 一行搞定 ``` *.example.com { tls { dns cloudflare {env.CF_API_TOKEN} } reverse_proxy localhost:8080 } ``` `sudo caddy add-package github.com/caddy-dns/cloudflare` 装好插件后 这就够了。Caddy 自动 ACME + DNS-01 + 续期,零额外配置。 如果选 Caddy 作反代,完全不用 certbot。 ## DNS-01 vs HTTP-01 | | DNS-01 | HTTP-01 | |---|---|---| | 通配符 | ✅ | ❌ | | 需要 80 端口 | ❌ | ✅ | | DNS provider API | ✅ | ❌ | | 域名 ACME 验证 | 通过 DNS | 通过 HTTP | | 内网 / 防火墙后服务器 | ✅(DNS 在云端) | ❌(需公网 80) | | 实施复杂度 | 中 | 简单 | 总结: - 普通单域名 + 公网 → HTTP-01 - 通配符 / 内网 / 多服务器 → DNS-01 ## Let's Encrypt 限制 - 每周每证书 5 次重复申请(同样 SAN 列表) - 每周 50 个不同 SAN 组合 - 失败计数:连续 5 次 fail 后 1 小时锁 - 全球每周每注册域名 300 张新证书 正常使用碰不到。**测试时用 staging server** 避免被限: ```bash certbot ... --staging ``` 或者用 ZeroSSL / BuyPass / Google CA 等其它 ACME provider 分散。 ## 监控证书到期 ```bash # 看到期时间 echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate # notAfter=Jul 15 12:00:00 2024 GMT ``` Prometheus blackbox_exporter: ```yaml - name: tls_cert_not_after url: https://example.com threshold_days: 14 # < 14 天告警 ``` 监控让你看 certbot 是否真的在续。一次因 cron 没跑而过期 → 客户全报错 红色警告,痛。 ## 踩过的坑 1. **通配符不 cover 二级**:`*.example.com` 不包含 `example.com` 本身(apex), 也不包含 `*.sub.example.com`。第二级通配符要单独申 `*.api.example.com`。 2. **API token 权限不够**:缺 DNS:Edit → 加 TXT 失败 → certbot 报 "rate limit" 误导。先 `dig _acme-challenge.example.com TXT` 验证。 3. **DNS propagation 时间**:cloudflare 一般 30s 内全球同步。 AWS / DigitalOcean 偶尔慢,调 `--propagation-seconds 60`。 4. **renew dry-run 成功 → 真 renew 失败**:dry-run 用 staging API, 生产可能 rate limit。看 `journalctl -u certbot.service` 真实日志。 5. **fullchain vs cert**:nginx 用 `fullchain.pem`(含中间证书)。 写成 `cert.pem` 浏览器报"untrusted intermediate"。

headscale:自托管 tailscale control plane(mesh VPN 不依赖第三方)

## 起因 Tailscale 是 WireGuard-based 的 mesh VPN,体验绝佳:所有设备通过 控制平面"发现"互通,零端口转发。但: - 控制平面是 Tailscale 公司的(数据不出公司是大忌行业) - 免费层 100 设备限制 - 隐私敏感场景不想依赖商业服务 `headscale` 是开源的 Tailscale control plane 实现,自己跑一台 机器就能成为"自己的 Tailscale 公司"。客户端继续用 Tailscale 官方 app(功能完整)+ 连你的 headscale server。 ## 架构 ``` ┌──────────────┐ │ headscale │ ← 自己跑,public 可达 │ (control) │ └──┬──────┬────┘ │ │ ┌───────┘ └────────┐ │ │ [laptop] ←── mesh ──→ [server] │ WireGuard │ └─── 互联 ──────────────┘ ``` 控制平面只协调 peer discovery + key exchange,**不路由数据流量**。 节点间 P2P 直连(NAT 穿透)或者 P2P 失败时 fallback 到 DERP relay。 ## 装 headscale 需要一台公网可达的服务器(VPS)+ HTTPS(必须)。 ```bash # 二进制安装 HEADSCALE_VERSION=0.23.0 curl -fsSL https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64.deb \ -o headscale.deb sudo dpkg -i headscale.deb ``` 或 Docker: ```yaml # docker-compose.yml services: headscale: image: headscale/headscale:0.23.0 command: serve restart: unless-stopped ports: ["8080:8080", "9090:9090"] volumes: - ./config:/etc/headscale - ./data:/var/lib/headscale ``` ## 配置 `/etc/headscale/config.yaml`(关键字段): ```yaml server_url: https://hs.example.com listen_addr: 0.0.0.0:8080 metrics_listen_addr: 0.0.0.0:9090 ip_prefixes: - 100.64.0.0/10 # tailscale 默认 CGNAT 段 - fd7a:115c:a1e0::/48 derp: server: enabled: false # 自己跑 DERP 可选;先用官方 DERP urls: - https://controlplane.tailscale.com/derpmap/default database: type: sqlite sqlite: path: /var/lib/headscale/db.sqlite log: level: info oidc: # 可选:用 GitHub / Google / Authelia 等 SSO 登录 # issuer: https://auth.example.com # client_id: ... ``` ## 用 systemd ```bash sudo systemctl enable --now headscale sudo systemctl status headscale ``` 需要 HTTPS 反代(nginx / Caddy)。`hs.example.com` 指向这台机器 + TLS 证书。 ## 创建 user + 设备 enroll ```bash # 创建 user (一个 user 一组设备) sudo headscale users create alice # 客户端那边(macOS / Linux / iOS / Android / Windows)装 Tailscale, # 但通过 --login-server 指向你的 headscale sudo tailscale up --login-server https://hs.example.com # 输出一个授权 URL,但 headscale 不能 auto-auth,要手动 register key: # 看到 URL: # To authenticate, visit: https://hs.example.com/register/abc123... # 复制 nodekey 到 server 上: sudo headscale nodes register --user alice --key nodekey:abc123... ``` 或者用 pre-auth key(推荐脚本化): ```bash sudo headscale preauthkeys create --user alice --reusable --expiration 24h # 输出:xxx-yyy-zzz # 客户端 sudo tailscale up --login-server https://hs.example.com --auth-key xxx-yyy-zzz # 自动 enroll,无需手动 register ``` 成功后: ```bash sudo headscale nodes list # ID | Name | User | IP | Online # 1 | laptop | alice | 100.64.0.1 | yes # 2 | server | alice | 100.64.0.2 | yes ``` ## 节点间访问 ```bash ssh [email protected] # 通过 mesh VPN 访问 ping 100.64.0.2 ``` 或用 MagicDNS: ```bash ssh user@server # tailnet 内 hostname 自动解析 ``` 要开 MagicDNS: ```yaml # config.yaml dns: magic_dns: true base_domain: tailnet.example.com override_local_dns: true nameservers: global: - 1.1.1.1 ``` 每节点 `tailscale.tailnet.example.com` 自动可解析。 ## ACL(谁能访问谁) ```hujson // /etc/headscale/acl.hujson { "acls": [ // alice 的设备能互访 {"action": "accept", "src": ["alice"], "dst": ["alice:*"]}, // bob 只能访问 server {"action": "accept", "src": ["bob"], "dst": ["alice:server:22"]}, // 默认 deny(不写 accept 的都不通) ], "groups": { "group:admins": ["alice"], }, "tagOwners": { "tag:prod-server": ["group:admins"], }, } ``` ```bash sudo headscale policy set -f /etc/headscale/acl.hujson ``` 类似 K8s NetworkPolicy 但更简单。 ## subnet routes(让 VPN 节点暴露内网) server 节点 advertise 内网: ```bash sudo tailscale up --login-server https://hs.example.com \ --advertise-routes=192.168.1.0/24 ``` headscale 上 approve: ```bash sudo headscale nodes routes list sudo headscale nodes routes enable -r 1 ``` 之后所有 mesh 节点能通过这个 server 访问 192.168.1.x(家庭内网)。 等于"VPN 入口"。 ## exit nodes(用某节点作出口) ```bash # server 节点 advertise 为 exit node sudo tailscale up --advertise-exit-node # headscale approve sudo headscale nodes routes enable -r 1 --all # 客户端用: sudo tailscale up --exit-node=server ``` 客户端流量全部走 server 出公网。"自建 VPN" 替代商业 VPN。 ## 跟 cloud Tailscale 对比 | | Tailscale Cloud | headscale | |---|---|---| | 控制平面 | Tailscale 公司 | 自己 | | 数据流量 | 不经控制平面 | 不经控制平面 | | 客户端 app | 一样 | 一样(开源 tailscaled) | | ACL UI | Web 仪表盘 | CLI / hujson 文件 | | OIDC | 完善 | 基础支持 | | MagicDNS | ✅ | ✅ | | DERP relay | 全球 + 免费 | 用官方 DERP 或自建 | | 价格 | 免费 100 节点 / 团队收费 | 完全免费 | | 适合 | 普通团队 | 大规模 / 隐私敏感 | 非企业 + 100 节点以内 → 用 cloud Tailscale 省心。 > 100 设备 / 隐私敏感 / 跑公司基础设施 → headscale 自托管。 ## 与 WireGuard 直接配相比 裸 WireGuard: - 每加设备改所有节点 conf - 没 NAT 穿透(双 NAT 后设备难互联) - 没 MagicDNS - 维护 100 节点想哭 Tailscale / headscale: - 自动 P2P discovery + NAT 穿透 - 加设备一行命令 - ACL 中央管理 - MagicDNS mesh 规模上去后 headscale > 裸 WireGuard 远不止一个量级。 ## 实际效果 我家庭 + 公司 + VPS 一共 12 个设备: - 手机 / 笔记本 在外能直连家里 NAS(之前要开 OpenVPN 客户端) - 公司 / 家里互通(VPC peering 替代品) - 一台 VPS 当 exit node:手机走 VPN 翻墙 - ACL 让"工作笔记本只能访问 work server" 严格隔离 - headscale 自己跑在一台 $5 / 月 VPS 上,零成本控制平面 完全替代了我之前的 OpenVPN + 维护 WireGuard config。 ## 踩过的坑 1. **server_url 错** → 客户端连不上。必须包含 https:// 和正确域名, 且证书有效。 2. **客户端不接受 `--login-server` 指向 IP**:必须用域名 + 证书。 3. **NAT 双层穿不过** → fallback 到 DERP relay。自建 DERP server 加速。 4. **OIDC 配 SSO 时 callback URL 错**:headscale callback 是 `/oidc/callback`,注册 SSO 时填对。 5. **0.22 → 0.23 数据库迁移**:headscale 大版本升级偶尔需 migrate 命令。release notes 仔细看。

nftables 替代 iptables 写防火墙(统一 IPv4/IPv6 + 现代语法)

iptables 用了 20 年但有几个老问题:IPv4 / IPv6 / arptables / ebtables 四套不同命令、规则插入慢、语法古老。nftables 是它的现代继任者: - 单一 `nft` 命令统一所有协议 - 表达式 + 集合(set / map)让规则更短 - 增量加规则不重建整张表,性能好 - 类似 iptables 的 chains / rules,迁移有学习成本但不大 ## 装 + 启用 ```bash sudo apt install -y nftables sudo systemctl enable --now nftables ``` Debian 11+ / Ubuntu 22.04+ 默认就有。 ## 一个完整的服务器防火墙 `/etc/nftables.conf`: ``` #!/usr/sbin/nft -f flush ruleset table inet filter { # 允许 SSH 的源(管理 IP 段) set admin_ips { type ipv4_addr elements = { 192.0.2.10, 198.51.100.0/24 } } chain input { type filter hook input priority filter; policy drop; # 1. 已建立连接 / 相关连接放行 ct state established,related accept ct state invalid drop # 2. 本地 loopback iif lo accept # 3. ICMP / ICMPv6(ping、路径 MTU 探测等) ip protocol icmp accept meta nfproto ipv6 icmpv6 accept # 4. SSH 仅允许 admin_ips tcp dport 22 ip saddr @admin_ips accept # 5. 公开服务 tcp dport { 80, 443 } accept # 6. WireGuard udp dport 51820 accept # 7. 日志后丢弃(限速避免刷屏) limit rate 5/minute log prefix "nft drop input: " counter drop } chain forward { type filter hook forward priority filter; policy drop; } chain output { type filter hook output priority filter; policy accept; } } ``` 启用: ```bash sudo nft -c -f /etc/nftables.conf # 语法检查 sudo systemctl restart nftables sudo nft list ruleset ``` ## 关键语法点 - `table inet ...` 中 `inet` 表示同时处理 IPv4 + IPv6(这是 nftables 最大优势) - `policy drop` = 默认拒绝(白名单模式) - `ct state established,related accept` = 连接跟踪放行 - `set admin_ips` = 命名集合,规则里 `@admin_ips` 引用, 改 IP 不动规则 - `dport { 80, 443 }` = 内联集合 - `limit rate 5/minute log ...` = 限速日志 ## 命令行操作(运行时) ```bash # 看现有规则 sudo nft list ruleset sudo nft list table inet filter # 给集合加 IP(不需要重写整套规则) sudo nft add element inet filter admin_ips '{ 203.0.113.5 }' sudo nft delete element inet filter admin_ips '{ 198.51.100.0/24 }' # 加一条临时规则 sudo nft add rule inet filter input tcp dport 8080 accept # 删某条规则 sudo nft -a list ruleset # -a 显示 handle 编号 sudo nft delete rule inet filter input handle 12 ``` ## NAT 表(让内网通过本机出公网) ``` table inet nat { chain prerouting { type nat hook prerouting priority dstnat; } chain postrouting { type nat hook postrouting priority srcnat; # 内网 10.0.0.0/24 出口走 eth0 做 masquerade ip saddr 10.0.0.0/24 oifname "eth0" masquerade } } # 端口转发:把 80 转给内网 10.0.0.5 table inet nat { chain prerouting { type nat hook prerouting priority dstnat; iifname "eth0" tcp dport 80 dnat to 10.0.0.5:80 } } ``` ## 限速 / 防 DDoS ``` chain input { type filter hook input priority filter; policy drop; # 新 SSH 连接限速:每分钟同 IP 最多 6 次 tcp dport 22 ct state new \ limit rate over 6/minute \ counter drop # SYN flood 防护 tcp flags syn tcp option maxseg size 1-535 drop tcp flags & (syn|rst|ack) == syn \ limit rate 100/second burst 50 packets accept # ...其它规则 } ``` ## 集合 + map 高级用法 ``` # 不同源 IP → 不同处理 map verdict_map { type ipv4_addr : verdict elements = { 192.0.2.10 : accept, 198.51.100.20 : drop, } } chain input { ip saddr vmap @verdict_map } ``` ## 持久化 ```bash # 当前规则保存到 /etc/nftables.conf sudo nft list ruleset > /etc/nftables.conf # 或者用 systemd sudo systemctl restart nftables # 会读 /etc/nftables.conf ``` 服务重启 / 机器重启后 `/etc/nftables.conf` 被加载。 ## 从 iptables 迁移 ```bash # 看现有 iptables 规则 sudo iptables-save > /tmp/v4.rules sudo ip6tables-save > /tmp/v6.rules # 转 nftables sudo iptables-restore-translate -f /tmp/v4.rules > /tmp/v4.nft sudo ip6tables-restore-translate -f /tmp/v6.rules > /tmp/v6.nft # 看生成的,决定要不要手动整理(自动转出来语法生硬) ``` 实际生产建议手写 nftables,不直接转。iptables 规则积累的"历史包袱" 不该带进来。 ## ufw / firewalld 怎么办 它们底层会用 iptables 或 nftables(取决于 distro 版本)。 不要 ufw / firewalld + 手写 nftables 混用 —— 不同工具会互相覆盖。 选一个: - 简单 / 不需要进阶规则:ufw(前面那篇) - 需要 NAT / 限速 / map / 大规模规则:直接 nftables ## 调试 ```bash # 实时看哪条规则被命中(packet/byte count) sudo nft list ruleset | grep counter # 看 drop 日志 sudo journalctl -k -f | grep 'nft drop' # tcpdump 抓没通过的包 sudo tcpdump -i eth0 -nn 'host 1.2.3.4' ``` ## 踩过的坑 - 把自己 ban 了:从 console / out-of-band 进,`nft flush ruleset` 清空, 重新写规则。提前准备一个 5 分钟自动恢复脚本: ```bash (sleep 300 && sudo nft flush ruleset) & ``` - iptables-nft(在 RHEL 8+ / Debian 11+)让 iptables 命令实际写 nftables。这是过渡兼容;新写规则直接用 nft。 - `inet` 表的规则对 IPv4 和 IPv6 都生效;如果你 IPv6 没用,规则也会 消耗 conntrack。可以分别建 `ip filter` + `ip6 filter` 而不是 `inet`。 - container(Docker / K8s)默认绕过 nftables。Docker 会自己写 iptables / nftables 规则,与你的规则可能冲突。生产容器集群单独 规划网络策略(Calico / Cilium)。

nginx limit_req + limit_conn:按 IP / API key 限速

## 起因 API 上线后两天,账单显示某个客户调用量是其它人的 50 倍。 查 log:他用 curl 写了个无 sleep 的循环,1000 QPS 持续 12 小时。 正常使用 < 1 QPS。 应用层加限流来不及,先在 nginx 层挡住才是第一道闸。 ## 解决方案 ### 1. 按 IP 限速:limit_req ```nginx # /etc/nginx/conf.d/limits.conf # 共享内存区 zone:用 10MB 存 IP -> 计数器 limit_req_zone $binary_remote_addr zone=ip_lim:10m rate=10r/s; server { listen 443 ssl http2; server_name api.example.com; location /v1/ { # burst=20: 允许短期突发 20 个请求超出 10r/s # nodelay: 突发请求立刻处理(不排队延迟) limit_req zone=ip_lim burst=20 nodelay; # 超出限制返回 429(默认 503,改 429 更标准) limit_req_status 429; proxy_pass http://app; } } ``` `$binary_remote_addr` 是把 IP 压缩成 4 字节(IPv4)/ 16 字节(IPv6), 比 `$remote_addr` 字符串省内存。10MB 能存约 16 万个 IP。 `rate=10r/s` 等价 `1r per 100ms`。"突发 20 + 持续 10r/s"是常见配置。 ### 2. 按 API key 限速:用 map + limit_req_zone ```nginx # 把 Authorization header / query param 抽出 key map $http_authorization $api_key { ~^Bearer\s+(?<token>.+) $token; default ''; } # 优先用 API key 限流,没 key 才用 IP map $api_key $rate_key { '' $binary_remote_addr; default $api_key; } limit_req_zone $rate_key zone=api_lim:20m rate=20r/s; ``` 不同 key 独立计数,恶意 key 不影响别人。 ### 3. 按用户等级限流(基于 key 的差异化) ```nginx # /etc/nginx/conf.d/api_tiers.conf # 假设你有个文件 /etc/nginx/api_keys.conf 维护 key → tier 映射 # 用 njs 或外部 lookup(auth_request)拿 tier 信息 # 用 split_clients 或 geo / map 决定走哪个 zone map $api_key $rate_zone { "free-key-abc" "free"; "free-key-def" "free"; "pro-key-xyz" "pro"; "ent-key-mno" "ent"; default "free"; } limit_req_zone $api_key zone=free:10m rate=1r/s; limit_req_zone $api_key zone=pro:10m rate=10r/s; limit_req_zone $api_key zone=ent:10m rate=100r/s; server { location /v1/ { # 按 tier 选 zone(nginx 不能动态选 zone,用 if + return) # 实际生产用 auth_request + 后端决定 ... } } ``` 复杂 tier 路由通常上 nginx + Lua(OpenResty)或 Kong / Tyk API gateway。 ### 4. 按连接数限:limit_conn 防"单 IP 大量并发长连接占资源"(如 download 慢慢传): ```nginx limit_conn_zone $binary_remote_addr zone=conn_lim:10m; server { location /downloads/ { limit_conn conn_lim 5; # 同 IP 最多 5 个并发连接 limit_rate 1m; # 每连接限速 1 MB/s } } ``` `limit_rate` 控制响应字节速率;适合大文件下载 / 视频流。 ### 5. 看效果 / 监控 ```bash # 看被 429 拒绝的请求 sudo tail -f /var/log/nginx/access.log | awk '$9 == 429' # 统计 sudo awk '$9 == 429 {print $1}' /var/log/nginx/access.log \ | sort | uniq -c | sort -rn | head ``` 把 nginx access log status 暴露给 Prometheus: ```nginx # 用 nginx-prometheus-exporter 抓 stub_status location /metrics { allow 127.0.0.1; deny all; stub_status; } ``` PromQL: ``` rate(nginx_429_total[5m]) ``` ### 6. 给客户友好的 429 响应 ```nginx limit_req_status 429; # 自定义 429 页面 error_page 429 /errors/429.json; location = /errors/429.json { internal; add_header Content-Type 'application/json' always; return 429 '{"error": "rate_limit_exceeded", "retry_after": 60}'; } # 加 Retry-After header location /v1/ { limit_req zone=ip_lim burst=20 nodelay; add_header Retry-After 60 always; add_header X-RateLimit-Limit 10 always; add_header X-RateLimit-Remaining 0 always; proxy_pass http://app; } ``` ## 效果 - 滥用 client 在 nginx 层立刻被 429 挡住,application 不再被打扰 - 账单回归正常水平 - 正常用户感知 0(10 r/s 远超 UI 触发的频率) - 监控 dashboard 上有 429 趋势曲线,能看到攻击 / 滥用模式 ## limit_req 几个 trap **burst vs nodelay**: - `burst=20`(无 nodelay):允许 20 个排队,按 10r/s 速度处理 → 第 20 个 请求要等 2 秒才被响应(用户感受很差) - `burst=20 nodelay`:20 个突发请求立刻处理,之后超限的直接 429 → 用户感受好,但服务端瞬时压力大 - `burst=20 delay=5`:前 5 个突发请求 nodelay,第 6-20 个排队,超过 20 拒绝 API 服务建议 `burst+nodelay`;下载服务建议 `burst` 排队(保护后端)。 **`$binary_remote_addr` 在 CDN 后**:所有请求源 IP 都是 CDN,限流没意义。 用 `$http_x_forwarded_for` 或 `$http_cf_connecting_ip`: ```nginx limit_req_zone $http_cf_connecting_ip zone=ip_lim:10m rate=10r/s; ``` ## 与 application 层限流配合 nginx 层适合"按 IP / key 限速防滥用"。 应用层适合"按业务逻辑限流"(如"每用户每天 100 个 AI 生成请求")。 两者互补:nginx 挡明显异常,应用 enforce 业务规则。 ## 踩过的坑 1. **zone 写错放在 location 里**:`limit_req_zone` 必须在 `http {}` 段 定义;`limit_req` 才在 `server` / `location` 里用。 2. **rate=10r/m 不是 10r/min**:nginx 写法是 `10r/m` 表示 10 requests per minute;`10r/s` 是每秒。别混。 3. **测试时永远 429**:自己开发用浏览器刷页面 + 1r/s 限流自然超。 测试环境配宽点(如 100r/s)或者 dev 走不同 server block。 4. **`X-Forwarded-For` 被伪造**:如果你直接 trust `X-Forwarded-For[0]`, 攻击者可以伪造一个 IP 在 header 里,绕过限流。 `real_ip_header` + `set_real_ip_from` 限定可信代理范围才安全。 5. **zone 太小**:10MB zone 满了 → 老条目被新条目挤出 → 老 IP 一段时间 没访问 + 现在又来访问会被当作"新连接"立刻消耗 burst。生产至少 50MB per zone。

Linux Network Namespace:在一台机器上模拟多个网络环境

Network namespace(netns)是 Linux 内核的网络隔离机制。每个 netns 有独立的网卡、路由表、防火墙规则、socket 列表。 container(Docker / podman / K8s pod)的网络隔离底层就是这个。 理解 netns 后调试容器网络就直观了;还能直接用 netns 跑测试 / 模拟复杂拓扑。 ## 1. 创建 netns ```bash sudo ip netns add red sudo ip netns add blue ip netns list # blue # red ``` 每个 netns 默认只有一个 loopback(甚至没启): ```bash sudo ip netns exec red ip link # 1: lo: <LOOPBACK> mtu 65536 ... state DOWN # 启 lo sudo ip netns exec red ip link set lo up sudo ip netns exec red ip addr show ``` ## 2. 在 netns 里执行命令 ```bash sudo ip netns exec red bash # 进了一个新 shell,网络环境完全隔离 ip addr # 只有 lo ip route # 空路由表 curl google.com # 当然不通 exit ``` ## 3. 用 veth pair 把两个 netns 连起来 veth 是"虚拟以太网网卡对"——两端一对,一端发送的包另一端立刻收到, 像一根虚拟网线: ```bash # 创建 veth pair sudo ip link add veth-red type veth peer name veth-red-host sudo ip link add veth-blue type veth peer name veth-blue-host # 把一端塞进 netns sudo ip link set veth-red netns red sudo ip link set veth-blue netns blue # host 端启接口 sudo ip link set veth-red-host up sudo ip link set veth-blue-host up # netns 内端启接口 + 配 IP sudo ip netns exec red ip link set veth-red up sudo ip netns exec red ip addr add 10.10.0.2/24 dev veth-red sudo ip netns exec red ip route add default via 10.10.0.1 sudo ip netns exec blue ip link set veth-blue up sudo ip netns exec blue ip addr add 10.10.1.2/24 dev veth-blue sudo ip netns exec blue ip route add default via 10.10.1.1 ``` ## 4. 用 bridge 让多个 netns 共网段 ```bash # host 上建网桥 sudo ip link add br0 type bridge sudo ip link set br0 up sudo ip addr add 10.10.0.1/24 dev br0 # veth host 端挂到 bridge sudo ip link set veth-red-host master br0 sudo ip link set veth-blue-host master br0 ``` bridge 像一个虚拟交换机:red / blue netns 互通。 Docker 默认网络模型就是这套:所有容器 veth 一端在容器 netns,另一端 挂在 `docker0` bridge。 ## 5. 让 netns 访问外网 ```bash # host 上开 IP forwarding sudo sysctl -w net.ipv4.ip_forward=1 # 配 NAT:netns 流量从 eth0 出去时做 SNAT sudo iptables -t nat -A POSTROUTING \ -s 10.10.0.0/16 -o eth0 -j MASQUERADE # 测试 sudo ip netns exec red curl ifconfig.me # 公网 IP(你的 host 的) ``` ## 6. 给 netns 分配独立 DNS netns 内的 `/etc/resolv.conf` 默认还是 host 的。改: ```bash sudo mkdir -p /etc/netns/red echo 'nameserver 1.1.1.1' | sudo tee /etc/netns/red/resolv.conf ``` `ip netns exec red ...` 会自动 bind-mount `/etc/netns/red/resolv.conf` 到 `/etc/resolv.conf`。 ## 7. 实战用例:用 VPN 跑某个进程,不影响系统 ```bash sudo ip netns add vpn # WireGuard 接口 wg0 起好之后丢进 netns sudo ip link set wg0 netns vpn sudo ip netns exec vpn ip addr add 10.0.0.2/24 dev wg0 sudo ip netns exec vpn ip link set wg0 up sudo ip netns exec vpn ip route add default via 10.0.0.1 # 跑 firefox(流量走 VPN) sudo ip netns exec vpn sudo -u $USER firefox ``` 系统的其它进程走正常网络,firefox 走 VPN,零干扰。 ## 8. 删 netns ```bash sudo ip netns del red # veth 一端跟着 netns 消失,host 端 veth-red-host 自动被删 ``` ## 9. 跨重启持久化 iproute2 不持久化 netns。要持久化用 systemd-networkd 或 NetworkManager 配 dispatcher script。容器项目(Docker、systemd-nspawn)自动管 netns 生命周期。 ## 10. 调试技巧 ```bash # 看 netns 内的 socket sudo ip netns exec red ss -tan # 看 netns 内的 conntrack sudo ip netns exec red conntrack -L # 在 netns 内抓包 sudo ip netns exec red tcpdump -i veth-red -nn # 从 host 看进程在哪个 netns sudo lsns -t net # NS TYPE NPROCS PID USER COMMAND # 4026531992 net 100 1 root /sbin/init # 4026532145 net 1 1234 root /usr/bin/firefox ``` ## 11. 容器视角 Docker / podman / K8s 创建容器时: 1. `ip netns add <container-id>` 2. 创建 veth pair,一端塞进 netns 3. 另一端挂到 `docker0` bridge / CNI 网桥 4. 给容器 netns 内的 veth 配 IP / 默认路由 5. `nsenter --net=...` 在容器 netns 内启进程 `docker exec` 等价于 `nsenter --target $PID --net --pid --mount`。 理解了 netns,调试 "我的容器没网络" / "K8s pod 间不通" 等问题 就有了底层视角。 ## 踩过的坑 - 用 `ip netns exec X command` 但 `command` 是 bash 的 builtin(如 `cd`)—— 不是命令。改成 `bash -c "..."` 包一下。 - veth 的 MTU 默认 1500,套 VPN 后 MTU 减小,netns 内 MTU 不调整会 fragment 频繁。`ip link set veth-xxx mtu 1420`。 - conntrack:每个 netns 有独立 conntrack 表。host 防火墙规则不自动 影响 netns。 - 用 sudo 进 netns 后丢失了原 user 环境变量:`ip netns exec X sudo -u $USER bash` 能切回普通用户但要小心 PATH / DISPLAY 等。