MoreRSS

site iconVincent Yang修改

DeepLX作者。2000年,天秤座,美国计算机科学硕士。篮球爱好者,金州勇士队粉丝,喜欢旅行,尤其是海边,Golang编程。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Vincent Yang的 RSS 预览

Your Flight Almost Never Flies the Great Circle

2026-07-08 01:00:00

Here's a fact that sounds wrong the first time you hear it: the great-circle route — the provably shortest path between two airports on a sphere — is almost never what your flight actually flies.

Every flight tracker, every mileage program, every "distance" you've ever been shown for a flight is a great-circle number. It's the aviation industry's official fiction. The real path is longer. Usually much longer.

I can put numbers on this, because I recently rebuilt my flights page to compute distances from actual ADS-B position data instead of airport coordinates. For 107 of my 113 lifetime flights I now have the real flown track. The result:

  • On average, my flights flew 12.9% farther than the great circle.
  • Across everything, that's 11,400 extra miles my mileage apps never told me about — the gap between 155,589 great-circle miles and 167,015 actually flown.
  • The worst offender flew 44% over. The best was just 0.4% over — and where those two flights happened tells you almost the whole story.

The extremes

Here are my ten biggest offenders — every flight ranked by how far its actual ADS-B track exceeded the great-circle distance:

# Flight Date Route Great circle Flown (ADS-B) Over
1 NH32 2025-06-24 ITM → HND 251 mi 362 mi +44.2%
2 CX961 2025-07-24 HGH → HKG 680 mi 940 mi +38.2%
3 AA4568 2025-03-02 PIT → JFK 339 mi 458 mi +35.1%
4 GJ8738 2023-04-15 SZX → HGH 665 mi 879 mi +32.2%
5 CX302 2026-07-06 HKG → PVG 780 mi 1,027 mi +31.7%
6 DL5794 2024-01-12 JFK → PIT 339 mi 440 mi +29.8%
7 9C8921 2023-06-07 PVG → HKG 780 mi 1,008 mi +29.2%
8 UA408 2025-05-22 EWR → IAD 212 mi 273 mi +28.8%
9 AA4734 2025-03-07 JFK → PIT 339 mi 436 mi +28.6%
10 KE882 2026-03-31 PVG → ICN 511 mi 656 mi +28.4%

Two patterns jump out immediately: short hops (Osaka–Tokyo, the New York–Pittsburgh cluster, Newark–Washington) and Chinese coastal routes (Hong Kong, Shanghai, Hangzhou, Shenzhen in every combination). Both are explained below.

The record: NH32, Osaka Itami → Tokyo Haneda, +44.2%. Great circle: 251 mi. Actually flown: 362 mi. This is a 250-mile hop, and that's exactly why the percentage explodes — departure and arrival procedures are a roughly fixed overhead, and on a short flight they are the flight. You don't take off from Itami and point at Haneda; you fly a published departure, get vectored around Osaka's terminal airspace, cruise for twenty minutes, then join an arrival stream that snakes across Tokyo Bay. The overhead that disappears into rounding error on a long-haul is nearly half the trip here.

The runners-up are all Chinese coastal routes. Hangzhou → Hong Kong at +38%, my Hong Kong → Shanghai flights clustered between +27% and +32%. These aren't weather anomalies — the same routes come in long every single time, because in Chinese airspace you fly published airways between fixed waypoints, full stop. The direct line from Hong Kong to Shanghai crosses airspace you simply don't get to use. Every HKG–PVG flight dutifully tracks up the coast, and every one of them logs roughly 1,000 miles for a 780-mile city pair.

The exception that proves the rule: TN102, Auckland → Papeete, +0.4%. Over the empty South Pacific there are no airways to follow, no military zones to dodge, no arrival queues. The 787 simply pointed at Tahiti and flew the sphere's geodesic, 2,543 miles of it, four miles over the theoretical minimum. When nothing stops a plane from flying the great circle, it does.

So why doesn't everyone fly like TN102?

1. The sky is a road network, not an open field. Most of the world's airspace is organized into airways — published corridors between named waypoints. Your flight plan is a sequence of these, filed and approved before departure. Some regions (the North Atlantic, parts of the US) have loosened into "free route" airspace; much of Asia has not. If the airway bends, you bend.

2. Wind beats distance. The shortest path is not the fastest or cheapest flight. A jet stream tailwind of 100+ knots is worth far more than a few dozen miles of track. Eastbound trans-Pacific flights deliberately swing south or north to ride the jet; westbound flights detour to avoid it. Dispatchers optimize a cost function of time and fuel — distance is just one input. A longer route that lands earlier wins.

3. Procedures bracket every flight. SIDs and STARs — standard instrument departures and arrivals — exist so that hundreds of flights an hour don't converge on an airport from random directions. They add miles by design, and runway direction adds more: land to the south on a field north of the city and you'll overfly, turn back, and line up. Fixed overhead, every flight, both ends.

4. Weather, the day-of tax. Thunderstorm cells, typhoons, turbulence reports — all bought with track miles. My own record-setting CX302 into Shanghai carried a remark about bad weather at Chek Lap Kok and paid +31.7% that day.

5. Politics and money. Overflight permissions, country-by-country navigation fees, and sanctions redraw routes wholesale — the closure of Russian airspace added hours to Europe–Asia flights, no meteorology involved. Airlines also route around expensive airspace when fuel is cheaper than fees.

The six flights I couldn't measure

Of my 113 flights, six have no usable track at all and are counted at their great-circle minimum. All six are mainland-China domestic flights, flown between May 2021 and August 2023 — four Shanghai–Chongqing legs, one Shenzhen–Hangzhou, one Guangzhou–Hangzhou. That clustering is not a coincidence.

Community trackers like ADS-B Exchange depend on volunteers running ground receivers, and through 2020 mainland coverage was genuinely good — both of my July 2020 domestic flights came back with essentially complete tracks (one at 100% coverage, one at 91%). Then, around 2021, Chinese authorities cracked down on hobbyist ADS-B receivers on national-security grounds, and crowd-sourced coverage over the mainland collapsed. My 2021 and 2022 flights exist in the archives only as scraps: a distant receiver catching a few minutes of cruise, then nothing. One flight I checked appears as 58 position reports covering thirteen minutes of a two-hour trip.

I tried hard to salvage these. For a couple of them I could stitch fragments into a partial reconstruction, but they failed my own sanity checks — one came out 48% over the great circle, which might even be true (a typhoon was active in East China that week), but "might" isn't a number I'll put in the dataset. The alternatives dried up too: Flightradar24 does have these flights — it never depended on the volunteer receivers — but its history is subscription-tiered and my plan reaches back one year, not five. adsb.lol's open archive only begins around 2024. Coverage over the mainland has since partially recovered through satellite ADS-B and new receivers, which is why my recent Chinese flights track fine; the 2021–2023 gap, though, is simply lost history.

So those six are honest floors: the plane certainly flew farther than the number I'm using.

The slightly unfortunate part

Every frequent-flyer program on earth credits you great-circle miles. Flighty, Umetrip, your airline's app — all display great-circle distance (I checked Umetrip against my own tracks: its kilometers match the great circle almost exactly). By the industry's own bookkeeping, I've flown 155,589 miles. My actual tracks say 167,015 and counting, and that number is a floor — the six flights I couldn't recover tracks for are counted at their great-circle minimum, and coverage gaps inside tracked flights are bridged with straight lines. Eleven thousand miles, genuinely flown, that no ledger will ever credit.

So the shortest-path number everyone quotes is really a lower bound nobody achieves — except, apparently, one 787 alone over the South Pacific with nowhere to be vectored and nothing in the way.

The great circle isn't a route. It's a promise the sky can't keep.

Goodbye Hugo: I Rebuilt My Blog with Astro Overnight

2026-07-07 18:30:00

This blog ran on Hugo with the PaperMod theme for four or five years. On June 15 I threw the whole thing away and rewrote it in Astro.

The interesting part isn't the framework choice. It's how the rewrite happened: I typed /goal in Claude Code, described what I wanted, and went to bed. The next morning, a working Astro site was sitting in a new repository. The migration commit tells the story in one line: 184 files changed, 95,175 insertions — every published post carried over, every URL intact, every comment thread still attached.

Why leave Hugo at all

Hugo itself was never the problem — it's fast and it's stable. The problem was living on top of PaperMod. Updates slowed to a crawl, known bugs sat unfixed for months, and every release I did pull was a small gamble on what my overridden partials would collide with. And fixing things myself meant fighting Hugo's template DSL — which isn't really Go, even if you write Go: logic lives inside markup, there's no component model, composition happens through partials and dict gymnastics, and when something breaks the error points into the theme's internals instead of your code. Over the years my workarounds accumulated as a pile of overridden partials I was afraid to touch.

The honest reason I stayed so long: a full rewrite was a multi-weekend project, and the blog worked. That math changed.

The overnight migration

I started from AstroPaper (v6 at the time, since upgraded to Astro 7) rather than a blank page — a solid, minimal base to mutate. The overnight /goal run handled the tedious parts that used to make these projects die in a branch:

  • 80 published posts (plus two dozen drafts) with frontmatter mapped from Hugo's format to Astro content collections — datepubDatetime, AI summaries → description, tags and drafts preserved
  • Filenames kept as slugs, so every URL stays /posts/<slug>/ — nothing 301s, nothing breaks
  • Comment threads preserved by pinning each page's comment key to the old Hugo permalink, trailing slash and all — a one-line detail that would have silently orphaned years of comment history
  • KaTeX, RSS, sitemap, Pagefind search, and OG image generation (Satori, with a CJK font embedded so Chinese titles render) re-wired

I reviewed the result over coffee and filed nitpicks. That was the migration.

Astro 7 is quietly very fast

I started on Astro 6 and upgraded to 7 mid-rewrite — a single commit. It's worth a section, because the toolchain got noticeably better underneath me:

  • The compiler is now Rust. Astro 7 ships @astrojs/compiler-rs, a Rust rewrite of the old Go-to-WASM compiler, with Rolldown in the dependency tree. A full production build of this site — astro check type-checking every component, building 80+ posts, generating OG images, and indexing everything for Pagefind search — finishes in about half a minute on my laptop.
  • astro dev is a daemon now. The dev server detaches and keeps running with astro dev stop / status / logs to manage it, instead of holding a terminal hostage.
  • One sharp edge: Astro 7 collapses HTML whitespace more aggressively than 6. A space between a text node and an inline element on the next line silently disappears — I shipped a "labeledGreat Circle" to production before catching it. Use explicit {" "} at those boundaries.

Deployment matches the pace. The repo pushes to GitHub, and Cloudflare Workers picks up every commit, builds it with bun, and has it live on the edge about a minute later. My publishing flow is literally /ship — Claude writes the conventional commit, pushes, and by the time I've switched tabs to check, the deploy is usually already green. The old Hugo site deployed fast too, to be fair; the difference is that now everything else is this fast.

Three weeks of taste, one sentence at a time

The real payoff came after. The git log since June 15 is ~50 small commits, and almost every one of them was a sentence to Claude rather than an evening of my own:

Typography took four iterations to get right — and that's the point: iterating was cheap. System fonts first, then Berkeley Mono as the Latin face, then Space Grotesk for everything, and finally where it landed today: Inter for body text, Space Grotesk for headings, Berkeley Mono for code — with CJK always falling through to system fonts so Chinese readers never download a webfont.

The palette went through the same loop. I started with AstroPaper's colors, tried a blue/teal pair that never felt right, studied a design I admire, and ended with warm stone neutrals and pure ink accents — black on paper in light mode, white on warm black in dark. No borrowed blues.

Code blocks became macOS windows via a custom Shiki transformer — traffic-light dots, the filename in the title bar, click a line to copy that line, click the header to copy the block.

Posts got infrastructure Hugo never had:

  • Every post serves a raw .md endpoint with a copy button, because half of us are pasting articles into an LLM anyway
  • An AI summary streams in at the top with a typewriter reveal
  • A copyright card at the end with the license, permalink, and share links
  • KaTeX CSS loads only on pages that actually contain math; the homepage CSS got cut by a third

The flights page became a real data product. It started as a static table; now a pipeline in the repo computes my actual flown distances from ADS-B position data — 107 of my 113 flights show genuine track miles instead of great-circle estimates, each labeled with its data source, with rankings and an AI-written analysis of my flying habits.

Even the chores automate themselves: a GitHub Action refreshes my U.S. visa wait-times post from travel.state.gov every three hours. It has made more commits than I have.

The comments are mine, all the way down

My comment system is now entirely my own implementation. The backend is Cloudflare Workers + D1 with Telegram moderation — I wrote about building it in March. During this rewrite the frontend became a native Astro component in my own codebase: no embedded widget, no iframe, no third-party JavaScript. The git log shows what that ownership buys — four styling passes on reply indentation and the input area until it felt right, each a ten-minute conversation instead of a fork of someone else's plugin.

The point

None of the individual features here are remarkable. What's remarkable is the cost curve. A bespoke blog used to be a tax paid in weekends; now the expensive part is knowing what you want. I spent three weeks deciding — fonts, colors, how a code block should feel — and almost no time implementing. The implementation happened while I slept, or while I was doing something else, one sentence at a time.

Hugo is still a great tool, and I'd still recommend it to anyone who wants a blog that just works. But if you've been putting off the rewrite that makes the site truly yours — the excuse is gone.

Give Your Camera Real GPS: A No-Internet Hardware Fix with furble

2026-06-17 22:30:00

我一直有个执念:希望每一张照片的 EXIF 里都带上准确的 GPS 位置。这样以后在地图上回看,哪年哪月在哪儿按下的快门一目了然。可惜大部分相机并不内置 GPS,为了这件小事我折腾了一圈方案,从手表轨迹回填、低功耗 App,到自己写了一款 App,最后才找到一个几乎完美的硬件解法。这篇就把整个过程和最终方案完整记录一下。

现状与痛点

大部分数码相机,尤其是无反,还是没有内置 GPS。照片导出来以后,EXIF 里少的就是经纬度。

厂商一般会把这个问题丢给手机 App:手机通过蓝牙连相机,把当前位置同步过去,相机按下快门时顺手写进 EXIF。这个设计在纸面上没什么毛病,但一到旅行场景就很烦。相机不可能一直开着,走几步拍几张,关机、休眠、再唤醒,蓝牙连接很容易就断了。等它慢吞吞重新连上时,刚才那几张已经拍完了。

手机这边也不省心。App 放到后台久了以后,系统随时可能把它收掉,iOS 尤其明显。定位一停,后面拍出来的照片就又变成没有坐标的状态。最难受的是,拍的时候你通常还以为它在正常工作,回家导图才发现 GPS 信息有一段没一段。

先试过后期回填

我一开始想的是,不走相机厂商这套蓝牙同步,改成后期补。

一种办法是用 Garmin 手表全程记录 GPS 轨迹,导出 GPX。回来以后写个脚本,按照片的拍摄时间去轨迹里查坐标,再写回 EXIF。

另一种是 Lakr233 之前推荐过的「一生足迹」。让手机低功耗记录轨迹,最后再用他写的程序回填照片位置。

这两条路本质上都是先录轨迹,再按时间对齐。技术上当然可行,精度也够。但我后来发现,这种方案很吃「自觉」。

出门前要记得开记录。忘了开,这一天就没轨迹可用。回来以后还要导轨迹、跑脚本、检查时区。刚开始觉得还好,多来几次就懒了。它解决的是「照片里最终能不能有 GPS」的问题,没有解决「我拍完就希望它已经有 GPS」的问题。

我写过一个 App:Koko

后来我在 GitHub 上看到 gkoh/furble。这个项目很厉害,作者把不少相机厂商的蓝牙协议都逆向出来了。也就是说,开发者可以绕过官方 App,直接给相机发送 GPS 坐标。

我顺着这个项目写了一款 iOS App,叫 Koko。名字来自日语「ここ(koko)」,意思是「这里」。它做的事情也很直接:配对相机以后,在按下快门那一刻,把手机当前的位置写进 RAF / JPEG 的 EXIF。

Koko 集成了 furble 里已有的协议,理论上支持富士 X / GFX 系列、索尼 Alpha / ZV 系列、佳能 EOS / PowerShot 系列等机型。

另外,Ricoh GR 这边也可以稍微期待一下。furble 的 Issue #267 里已经有人在做 GR 的蓝牙支持,后面还提交了 PR #268。目前这个 PR 主要是连接、配对、重连和基础快门控制,作者说已经在 Ricoh GR III HDF 真机上测过。GPS 写入还没一起做进去,只是先留了 stub,等快门这部分稳定以后再单独补。所以它还不是「今天刷完就能给 GR 写 GPS」的状态,但至少这条路已经有人在推了,GR 用户可以先关注一下。

顺便说一句:Koko 支持的相机型号是按协议推的,但我手头只有富士 X-S20 一台机器,所以只在 X-S20 上实测过。其他型号理论上都能用,但因为没有设备,我没办法逐一验证,介意的话请谨慎尝试。

Koko 比官方 App 清爽很多,后台常驻也更稳一些。但它最后还是没能把问题彻底解决掉。原因很简单:不管 App 怎么写,它仍然走的是「手机 ↔ 相机」这条蓝牙链路。相机休眠会断,唤醒后不一定马上重连,手机系统也可能杀后台。这些不是写几行代码就能绕开的。

后来发现硬件方案

真正让我觉得可以不用再折腾的,是 furble README 里提到的一套硬件玩法。

思路反而很朴素:既然手机不适合当中间人,那就换一个只做这件事的小设备。这个设备自己接 GPS 模块定位,再通过蓝牙直接连相机。它不需要联网,也不需要手机参与。配对好以后,开机就连,相机醒着的时候它就一直给坐标。

有意思的是,这套东西不是需要自己画板焊接,而是可以直接买 M5Stack 的现成模块。我已经在他们官方店下单了。

需要买的两样东西

硬件 作用 美元价 国内价(淘宝官方旗舰店)
M5StickS3(ESP32-S3 Mini IoT Dev Kit) 主控 + 屏幕 + 电池 + 蓝牙 US$21.50 ¥149
M5Stack GPS Unit v1.1(AT6668) GPS / 北斗定位模块 US$9.95 ¥65

两样加起来三十多美元,国内官方店两百出头人民币。比起为了 GPS 去换一台带定位的机身,这个成本基本可以忽略。GPS 模块尤其便宜,AT6668 还是 GPS + 北斗双模。

GPS Unit 用 HY2.0-4P 线接到 StickS3 上就行。组装完大概就是这么一小坨,揣兜里没什么压力:

M5StickS3 + GPS Unit 组装完成

为什么选 M5StickS3

furble 的 README 里推荐了好几款主控,比如 M5StickC Plus、M5StickC Plus2、M5Core2、M5Core Basic 等。我最后选 M5StickS3,主要是两个原因。

第一,它是新款,芯片是 ESP32-S3,内置电池 250mAh,比常被推荐的 StickC Plus2(200mAh)还大一点。第二,README 里那些旧型号现在有些已经停产,或者不太好买。与其到处淘库存,不如买一个还在正常销售的新款。

StickS3 这块小东西其实性能挺夸张,双核 LX7 @ 240MHz、8MB Flash、1.14 寸彩屏、USB-C。拿来只当相机 GPS 模块,确实有点大材小用。

250mAh 到底能撑多久?

这是我下单前最关心的一点。

furble 作者给过一个参考:ESP32 保持蓝牙连接时功耗大约 30mA。单看这个数字:

  • M5StickC Plus2(200mAh):约 6 小时
  • M5StickC Plus(120mAh):约 4 小时

StickS3 是 250mAh,如果只是纯蓝牙遥控,不挂 GPS,按比例估算大概能到 7 到 8 小时。但这套方案还要外接 GPS Unit,AT6668 会从 StickS3 的电池里取电,屏幕如果常亮也会继续耗电,所以不能按纯蓝牙的数字算。

综合现有反馈,带 GPS 连续工作大概按 4 到 5 小时预期比较稳。小半天够用,拍一整天就得补电。

好在 StickS3 是 USB-C,可以边充边用。真要从早拍到晚,带个小充电宝就行。再把屏幕超时、亮度这些设置调一下,还能再省一点。

操作流程

整套东西到手以后,刷机和配置都不复杂:

  1. 把 GPS Unit 用 HY2.0-4P 线接到 StickS3 的 PORT.A。
  2. 用 Chrome / Edge 打开 furble 官方刷机网站:web-installer.furble.net
  3. StickS3 用 USB-C 连电脑,在网页上选择对应型号,一键刷入官方固件。全程在浏览器里完成,不用装 IDE 或 SDK。
  4. 进设备菜单打开 GPS,开启自动连接、自动重连之类的选项,再和相机配一次对。

设置好以后,每次开机,StickS3 + GPS Unit 都会自动连相机并开始发送坐标。相机休眠以后再唤醒,它也会自己重连。至少从机制上看,这比手机 App 靠谱太多。

小提示:furble 的 GPS 地理标记目前主要支持富士和索尼机型(快门遥控、对焦、间隔拍摄等功能覆盖更广)。下单前最好先去 README 的支持列表里确认下自己的机器。

后期考虑

现在这套东西还是有点散装。StickS3 和 GPS Unit 靠一根线连着,拍的时候只能揣兜里或者拿在手上,能用,但不够舒服。

我后面打算给它做一个 3D 打印支架,把这两个小东西固定成一体,然后直接卡到相机热靴口上。这样 GPS 模块朝天,信号应该更好;线也能收进支架里,看起来不会那么狼狈。等做完了再单开一篇,把结构和打印文件一起放出来。

小结

为了让每张照片都带 GPS,我从 Garmin 轨迹回填、低功耗记录 App,一路折腾到自己写 Koko。绕了一圈之后,结论反而很简单:只要还让手机夹在相机和 GPS 中间,这件事就很难稳定。

后期回填能用,但每次出门和回来都要多做一步。Koko 可以把官方 App 的体验改善一些,但它还是逃不出手机后台和蓝牙重连的问题。furble + M5StickS3 这套方案最吸引我的地方,就是把手机从链路里拿掉了。

三十多美元,一个小盒子,自己定位,自己连相机,拍完照片坐标已经在 EXIF 里。对我这种相机 GPS 强迫症来说,这大概就是目前最接近「不用管它」的解法。

The Right Way to Use WebArena Indigo

2026-06-17 20:00:00

最近在折腾日本落地节点,用的是 WebArena Indigo(下文简称 Indigo)。这家日本 VPS 便宜、走 NTT 线路,拿来做落地很合适。但我在「中转机 + Indigo 落地」这套架构上踩了个不小的坑,折腾了大半天才定位清楚,这篇就记录一下整个过程和最后的解法。

架构与需求

需求很简单:用 Indigo 当日本落地,但 Indigo 的直连 IP 从国内访问的路由一般,所以前面再挂一台中转机(线路对国内友好),客户端连中转机、中转机把流量送到 Indigo 落地。

客户端 → 中转机 → (隧道) → Indigo 落地(snell)

落地这台 Indigo 上跑的是 snell,中转机负责把入口端口的流量转发过去。问题就出在「中转机 → Indigo」这一段该怎么走。

为什么没直接用 realm 公网转发

最省事的做法当然是 realm(或 gost 之类)做个普通的公网 TCP 转发,中转机监听一个端口,直接转到 Indigo 的公网 IP。

但在 Indigo 上,走公网转发是百分之百会被限速的——把流量从中转机公网直接灌到落地的公网 IP,必然撞限速。所以 realm 这条路直接 pass,我给两台机器之间建了一条 GRE 隧道,想着把流量「包」进点对点的隧道里、看起来像内网互联,应该能躲过针对公网中转的限速。

剧透一下:隧道这个方向是对的,但只对了一半——IPv4 GRE 照样会被限速,一上量就崩;最后只有 IPv6 的 GRE 才真正躲过去。

IPv4 GRE 的坑:一跑测速就崩

隧道建好后,节点能连、能上网,平时刷网页也没问题。但只要一测速,现象非常稳定:

  • 第一波数据能冲出去(一两秒、十几 MB),
  • 紧接着直接 Socket closed by remote peer,测速被打断,
  • 换任何别的节点都正常,唯独这条经 Indigo 落地的会崩。

把 snell 摘掉、直接在隧道内裸跑 iperf3,问题原形毕露:

方向 IPv4 GRE
落地 → 中转(即下行/测速方向) ~198 Mbps,一路下滑,重传 2.8 万+
中转 → 落地 ~475 Mbps,重传几千

下行方向 8 秒重传两万八,吞吐从 235 一路掉到 154——典型的「一上量就被狠丢包、TCP 雪崩、连接被 reset」。这就是测速「第一下能跑、马上断」的根源。

排查:把嫌疑一个个排除

为了不冤枉好人,我把可能的原因逐个排掉:

  • 不是 snell:直接连中转机本机的 snell(不经隧道)完全正常,说明协议和版本没问题。
  • 不是 MTU:两个方向用 DF 大包打满 1500 都能过,隧道内 1450 也能过,简单的 GRE 黑洞排除。
  • 不是 DNS / 出网:在落地机本机 curl 直连测速源,5MB 秒下,出口带宽、源站、解析全都好的。
  • 不是 qdisc 丢的:落地机网卡用的是 cake,但它 bandwidth unlimited,整个生命周期只丢了几百个包,根本没在限速。

也就是说:包不是在落地机的队列里丢的,是出了落地机之后、在到中转机的这段公网链路上丢的。 落地机本机直连测速源又是满速的(出网和源站都没问题),所以矛头指向:这条路径对 GRE(协议 47)这种隧道流量,在高速率下做了限速 / 丢包。

更有意思的是,后来我拿另一台 Indigo(同机房、邻近子网)裸测不带任何 GRE 的 IPv6 直连,重传也高达四万多。这进一步说明:底层路径本身对大流量就一般,而 IPv4 + GRE 是被额外「针对」的那一层,叠起来就直接雪崩了。

解决:改用 IPv6 GRE(ip6gre)

两台机器都有 global IPv6,而且 v6 之间互 ping 延迟低、空载零丢。运营商的 QoS / policing 规则又常常只配了 IPv4,IPv6 那条路往往更干净、没人管。于是我把隧道的外层从 IPv4 GRE 换成 IPv6 GRE(ip6gre)——内层地址(10.0.0.x)和上面的转发规则一个字都不用动,只换「管子外面」。

换完立刻见效:

方向 IPv4 GRE IPv6 ip6gre
落地 → 中转(测速方向) ~198 Mbps,持续塌 ~470 Mbps,稳定还上扬
中转 → 落地 ~475 Mbps ~460 Mbps

最关键的是:不再雪崩了。测速方向稳稳跑到带宽上限附近,Surge 那边也不再 closed by peer。残余还是有一些重传(这条路底子就一般),但 TCP 能正常「骑」过去,不影响使用。

所以这篇的核心结论就一句话:在 Indigo 上做隧道中转,只有 IPv6 的 GRE 才能保证不被限速。

配置要点

建隧道(两端对称,把 local / remote 和内层地址对调即可):

# 中转机一端
ip link add tun-gre type ip6gre \
  local <中转机 IPv6> remote <Indigo IPv6> \
  ttl 255 encaplimit none
ip addr add 10.0.0.2/30 dev tun-gre        # 落地机用 10.0.0.1/30
ip link set tun-gre mtu 1400 up

几个容易忽略的点:

  • MTU 要降下来。IPv6 外层比 IPv4 多 20 字节开销(IPv6 头 40 + GRE 4 = 44),1500 的底层算下来内层最多 1456。我直接设成 1400,留足余量,反正丢包不是 MTU 问题。
  • encaplimit noneip6gre 默认会塞一个 IPv6 封装限制扩展头,多 8 字节还可能被某些路径丢,关掉更干净。
  • 记得做 MSS clamp。在隧道接口上把 SYN 的 MSS 按隧道 MTU 夹一下(nftables tcp option maxseg size set rt mtu),不然某些方向的大包还是会顶破隧道。
  • 持久化别忘。我两台一台用 systemd-networkd.netdevKind=ip6gre),一台用 ifupdown/etc/network/interfaces.d/),改完都要落盘,不然重启就回去了。
  • 内层还是 IPv4 的 10.0.0.x,所以日常验证隧道通不通还是 ping 10.0.0.1(穿隧道、外层用 v6 封装);ping 那串 v6 地址只是测两台公网底层,跟隧道本身没关系。

选机器:500M 四核 vs 1000M 六核共享

顺带说下套餐。我手上两台 Indigo:

  • A:500M 带宽 / 四核 —— 日常落地中转,完全够用,能稳定跑到带宽上限附近。
  • B:六核 / 1000M「共享」带宽 —— 理论规格更高,但实测没必要

原因有两个:一是那 1000M 是共享带宽,不保证;二是这套架构真正的瓶颈在中转那段链路和路径质量,不在落地机的标称带宽——换上 B 之后单条隧道也跑不出比 A 更高的有效吞吐。所以为了这个用途,500M 四核那档就够了,没必要上 1000M 共享,省下的钱多开一台或者升级中转机更划算。

IP 质量

顺手也跑了下这几个 IP 的质量测试,贴出来给个参考:

WebArena Indigo IP 质量测试 3

WebArena Indigo IP 质量测试 2

WebArena Indigo IP 质量测试 1

小结

  • Indigo 做隧道中转,IPv4 GRE 会被限速到雪崩,IPv6 GRE(ip6gre)才稳——这是这次最大的收获。
  • 公网转发在 Indigo 上是百分之百会被限速的,所以必须走隧道;但别以为建了隧道就万事大吉——IPv4 GRE 一样会被限,只有 IPv6 GRE 能稳。
  • 隧道丢包先别急着怪 GRE「性能差」:GRE 本身是很薄的内核封装,能跑千兆。先用 iperf 在隧道内裸跑、再和普通 TCP 直连对比,很快就能区分是「协议被限」还是「路径本身烂」。
  • 套餐别被规格忽悠,500M 四核足矣,1000M 共享对这个场景没意义

Tinkering with Home Network: N100 + PVE + iKuai + sing-box

2026-04-21 11:00:19

最近搬了新家,顺手开了一条 2000M 的宽带,家里的网络设备也正好趁这个机会重新折腾一遍。这篇就当是一次记录,也顺便给以后留个备忘。

为什么没用 BE7200 当主路由

一开始为了省事,我先买了一台 TP-LINK 的 Wi-Fi 7 路由器 BE7200,四个 2.5G 口加四个 1G 口。单看硬件规格其实挺能打,拨号、测速都没什么问题,2000M 的带宽基本也能跑满。

但系统功能就不太够了。我想要的是一个「能自己折腾」的主路由:能自动分流,能屏蔽一些广告和追踪域名,最好还能把代理直接放到网关上,LAN 里的设备不用单独配置就能自动分流。TP-LINK 的固件明显还是面向普通用户的,这些需求基本没法满足。

架构:N100 + PVE + iKuai + Debian

正好手头还有一台几年前买的 N100 小主机,四口 2.5G,拿来跑软路由绰绰有余。最后定下来的架构是这样:

  • 底层装 PVE 9.0 作为虚拟化平台;
  • 虚拟机 1:iKuai,负责 PPPoE 拨号、DHCP Server、NAT;
  • 虚拟机 2:Debian 13,跑 sing-box 作为旁路由网关,负责透明代理和分流。

这样拆开之后,职责会比较清楚:iKuai 只负责「稳定的基础网络」,Debian 负责「很可能被我反复改配置」的代理部分。就算哪天代理被我搞挂了,把 Debian 关掉,家里的基础网络也还能照常用。

网口分配与 PVE 管理口的小技巧

N100 的四个 2.5G 网口分别是 ETH0 到 ETH3。分配方案是:

  • ETH0:留给 PVE 管理;
  • ETH1、ETH2、ETH3:全部通过 PCI passthrough 直通给 iKuai,其中 ETH1 作 WAN,ETH2/ETH3 作 LAN。

这么做马上会遇到一个问题:iKuai 独占这三个口之后,PVE 的管理口 ETH0 和 iKuai 的 LAN 在物理上就是隔开的。即使两边都配在 10.10.10.0/24 这个网段里,也还是互相访问不了。总不能再浪费一个口,专门把 ETH0 接回 LAN 吧。

解决办法其实也简单:在 PVE 里给 iKuai 虚拟机额外挂一张 虚拟网卡(virtio net),然后在 iKuai 里把这张虚拟网卡和 LAN 口桥接起来。这样相当于在 iKuai 内部搭了一座桥,PVE 的管理网络就能通过这张虚拟网卡接到 LAN 里。ETH0 这个物理口反而可以完全空着,数据只走虚拟网卡。

配完之后,所有端口协商都在 2.5G,基础网络这一层就算理顺了。

Debian 侧:从 Surge 到 dae 再到 sing-box

旁路由这边,我花的时间就多一些。

最想用的其实是 Surge VM Gateway。规则语法熟,日常 Mac 上也一直在用。但现在手头已经没有闲置的 Mac mini 可以拿来当网关了(一台在日本,另一台在另一个家里),所以只能看 Linux 方案。

第二选择是 dae。dae 的设计很简洁,基于 eBPF,功能不多但刚好够用,本来挺合我胃口。结果试了一下发现它不支持 Shadowsocks 2022,而我用的大部分都是 ss2022 和 snell,没办法,只能放弃。

最后还是落到了 sing-box 上。sing-box 的协议支持很全,但配置复杂度也确实有名,官方文档又分散在一堆子页面里,手写 JSON 会很费劲。我比较懒,直接把 ssh 交给 Claude Code(Opus 4.7),再把我 Surge 的 conf 文件丢过去,让它参考里面的分流规则,直接生成并部署 sing-box 配置。效率确实高了很多。

IPv6 踩坑:为什么要造一个 ULA 前缀

配完之后,v4 用起来没问题,但 v6 开始有点意思了。

先说下背景。我的代理节点是有 global v6 的,宿主机走代理 curl -6 ip.sb 也能正常返回 node 的 v6 地址。但是 LAN 里的客户端(手机、Mac)拿到的 v6 只有 link-local fe80::,基本等于没有可用的 v6 地址,自然也发不出 v6 流量。

原因是 iKuai 这边没有 v6 上行,也没有从上游拿到任何可以下发给 LAN 的 v6 前缀。那这种情况下,要怎么让客户端「用上 v6」?

答案是:自己在 LAN 里造一个 v6 前缀。具体做法是在 Debian 上用 radvd 广播一个 ULA(Unique Local Address) 前缀 fd00:dae::/64,让 LAN 客户端通过 SLAAC 自动配出一个 ULA 地址。

这里的关键是:ULA 在 v6 里有点像 v4 的 10.0.0.0/8 这种私有地址段,不能直接出公网。但我并不需要 ULA 真的能跨运营商路由出去。客户端发出的 v6 流量会被 sing-box 的 TUN 截获,最后通过 proxy node 的 global v6 出境。外面看到的源地址是 node 的 IP,不会看到 LAN 里的 ULA。对我这套环境来说,ULA 更像是一张 v6 世界里的「临时发车牌照」:先让客户端愿意生成 v6 流量,后面再交给 sing-box 送到真正有 v6 的出口。

除此之外 Debian 侧还要做几件事才能让这套跑起来:

  • net.ipv4.ip_forward=1net.ipv6.conf.all.forwarding=1net.ipv6.conf.ens18.forwarding=1(radvd 启动时会专门检查接口级别的 forwarding);
  • send_redirects=0:不然 kernel 会发 ICMP Redirect,告诉客户端「直接找 10.10.10.1 更快」,客户端就会绕过旁路由;
  • iptables FORWARD 放行 tun0 的入和出:Docker 会把 FORWARD 默认策略改成 DROP,导致 UDP / ICMP 在 ens18 → tun0 这条路径上被丢掉,TCP 反倒没事(因为走的是 REDIRECT 转本地,不走 FORWARD);
  • 给 ens18 挂上 fd00:dae::1/64 的静态地址:sing-box 的 auto_redirect 是从接口上读 v6 前缀加进 nftables 的 inet6_local_address_set 的,没有这个地址,LAN 前缀就不会进 set,v6 转发规则全部失效(参见 sing-box issue #3698)。

Happy Eyeballs 的奇怪现象

v6 通了之后,又遇到一个挺微妙的问题:打开国内一些 App(微信、抖音、小红书之类)的时候,经常要点好几次内容才加载出来。网页和国际 App 倒是完全没问题。

排查了一圈,原因是这样的:

TUN + radvd 让 LAN 客户端拿到了 v6 地址,所以客户端发起新连接时会做 Happy Eyeballs:同时试 v4 和 v6 两条路径,哪条先连上就用哪条。

国内大厂的 API 域名现在基本都有 AAAA 记录(腾讯、阿里、字节都在上 v6),但它们的 v6 CDN 通常只在国内 ISP 的 v6 链路上好用,也就是得从运营商的 v6 线路直连过去。

而我这边的出口情况是:

  • 代理走 v6:proxy node 在海外,有 global v6,能连上国内站点的 v6,但路径绕了一大圈,延迟很高;
  • 直连走 v6:Debian 的 direct outbound 走 ens18,ens18 只有 link-local v6,宿主机的 v6 主路由表里根本没有 default v6(default v6 被 sing-box TUN 接管了,放在 table 2022 给代理用),直连 v6 包发不出去,最后只能超时。

按照我的分流规则,国内 App 的 API 命中 geosite-cn → 走 direct → 客户端先试 v6 → 超时(默认 250ms 到 2s)→ Happy Eyeballs 回退 v4 → 成功。于是第一次连接会卡一下,再加上 App 自己的重试 / 缓存逻辑把这个体感放大了,看起来就像是「点一下没反应,再点一下才行」。

国际站不受影响,是因为它们命中 proxy 规则,代理节点有真 v6,直接就通了。

解法:direct outbound 强制 ipv4_only

解决也很小,只要给 sing-box 的 direct outbound 加一个 ipv4_only

  • 命中 direct 的流量只拿 A 记录,客户端只走 v4,不再先试一条注定会超时的 v6;
  • 代理出口(proxy / ai-vless)继续保留 v6 能力,node 有 v6,外部 v6 站照样快;
  • 国内 v6 原本就跑不通,这个限制等于把一个坏选项直接从菜单里删掉,不用再浪费 2 秒等它失败。

配完之后,国内 App 的点按体感马上就正常了。

小结

整套架构跑下来,主路由 iKuai 管稳定,旁路由 Debian 管分流,管理面板用 metacubexd,v4/v6 都能用,家里每台设备不用单独配置就能自动走分流。相比之前一台 BE7200 裸拨号的方案,可玩性和可控性确实高了不少。

唯一比较「耗心力」的是 v6 这一块,踩了好几个坑才理清:「LAN 要有 v6 地址,客户端才会发 v6」「ULA 不是为了出公网,而是为了让客户端愿意生成 v6 流量」「direct 出口没 v6,但客户端有 v6,就会触发 Happy Eyeballs 回退」。这些点想明白之后,以后再配类似环境应该会轻松很多。

接下来还有几件事想慢慢补上:先把分流规则再细一点,尤其是国内 App、AI 服务、流媒体这些容易走错出口的场景;再加上 DDNS,方便在外面稳定访问家里的入口;最后把 Tailscale 也配起来,之后人在外面排查网络或者访问家里服务会省事很多。

AP 也想换掉。现在 BE7200 在这套架构里只是当 AP 用,感觉有点大材小用。也许换成小一点的 Wi-Fi 7 AP 就够用了。

I Got My Lost Phone Back in Japan

2026-04-05 11:37:18

这一次我从新潟搭「海里」去酒田,抵达酒田之后,再搭特急列车回新潟,最终赶回東京。结果特急列车和回東京的新幹線换乘时间比较短,当时也急着回東京,备用机就忘在了前排座椅靠背后的置物袋里。等新幹線已经从新潟出发了,我都还没意识到,直到突然弹出一条 Find My 通知,我才想起来手机没拿。于是马上就在下一站長岡下车,再搭新幹線回新潟。因为这次旅行买的是五天的 JR EAST PASS,所以这样来来回回折返,全程都没有任何额外费用,全部包含在 PASS 里。和朋友商量后,我们决定先在長岡站的 Lost and Found Office 登记一下,好让新潟那边的工作人员留意。全程工作人员态度都特别好,也帮忙打电话联系,说手机应该没有丢,只是那趟车现在已经回车库了,所以今天可能拿不到。对方让我们先回東京,等后天再过来取,或者直接帮我寄到新宿。因为我是后天傍晚的航班离开日本,宅急便意味着我离开之前拿不到手机。后来我们还是决定坐新幹線回新潟,在路上的时候,大概还不到一个小时,長岡站的工作人员就打电话联系我们了,说手机已经找到了,让我后天下午两点准时去取就行。到了新潟站之后,我们又去了 Lost and Found Office。工作人员立刻意识到这就是刚刚打电话说过的那部手机,对应的登记单也已经提前打印好了。那张纸上有各种情况说明,以及车库那边拍摄的我手机正反面的照片。那时候我就很放心了。朋友帮我和他们沟通了一下,结论还是今天拿不到。我们说自己打车去车库,他们说没有人可以进车库,他们的工作人员也进不去,只能等后天车库那边把手机通过宅急便送到车站。他还说快递员一般 12 点左右就会开始送货,所以下午 2 点我再去取就可以了。即使车库离车站只有 2 英里左右,我也尊重他们的规定,毕竟是他们在免费帮我找回我自己粗心落下的物品。

今天早上我又搭新幹線从東京来新潟。一路上我时不时就在看 Find My,一直关注手机到哪里了。我发现今天早上的时候,定位就已经出现在邮局了,基本上就说明今天稳稳会送到。在我还在路上的时候,我又看到快递员已经到车站附近了。等我 11:29 AM 到达新潟的时候,手机定位已经在车站了。我走到 Lost and Found Office 的时候,设备甚至已经显示 With You 了。还好我的手机没有开蜂窝,耗电很少,所以这几天都还能通过附近的 iPhone 获取定位,类似 AirTag,这也许就是 Apple 很强大的功能之一。

在 Lost and Found Office 排了一会队之后,我签了一份领取文件,工作人员从密封袋里拿出手机,让我检查是否完好,然后让我当场解锁手机、出示证件,流程就完成了。总体体验还是相当丝滑。我人生中第一次把手机落下,没想到是在日本,但也很幸运是在日本。就像朋友跟我说的,你不用担心,一定不会丢。

Lost and Found Office receipt