<?xml version="1.0" encoding="utf-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"><channel><title>愚蠢的地球人</title><link>https://blog.fairysoft.net/</link><description>Hello, Earth!</description><item><title>佳明表盘汉化实战：Segment34 中文版与农历显示</title><link>https://blog.fairysoft.net/post/74.html</link><description>&lt;p&gt;我的佳明手表什么都好，就是表盘上没农历。作为中国人，手表上看不到&quot;今天初几&quot;，总觉得少了点什么。佳明官方表盘和大部分第三方表盘都是纯英文的，别说农历了，连星期都是 Mon、Tue。我很喜欢的开源表盘 &lt;a href=&quot;https://github.com/ludw/Segment34mkII&quot;&gt;Segment34 MkII&lt;/a&gt; 也不例外——34段数码管的风格非常惊艳、高度可定制的字段布局，几乎满足了我对表盘的全部审美，唯独它是全英文的。&lt;/p&gt;&lt;p&gt;于是就有了这个 &lt;a href=&quot;https://apps.garmin.com/apps/cd2b47e9-46a1-4032-b6ff-1b130db10576&quot;&gt;Segment34 中文版&lt;/a&gt;：在原版的基础上做了完整汉化（设置菜单、天气、星期、摄氏度图标），并添加了农历和二十四节气显示。整个过程前后迭代了三个月、发布了二十多个版本，现在已经在佳明 Connect IQ 商店上架，完全免费。这篇文章记录一下整个汉化过程和农历模块的实现思路。&lt;/p&gt;&lt;p&gt;&lt;img class=&quot;image_resized&quot; style=&quot;width:420px;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/11/202611062134521.jpg&quot; alt=&quot;Segment34 中文版上手效果&quot;&gt;&lt;/p&gt;&lt;h3&gt;认识 Segment34 MkII&lt;/h3&gt;&lt;p&gt;先介绍一下原版。Segment34 MkII 是开发者 ludw 做的一款开源表盘（Monkey C 编写），最大的特色是七段数码管（LED 段码）拟物风格：数字不是普通的字体，而是模拟老式 LED 显示屏的段码渲染，还带四种&quot;笔刷&quot;质感（点阵、混合、线条等）。表盘支持四套内置配色主题外加自定义主题（用作者提供的在线主题设计器生成），几乎每个字段都可以自定义显示内容，还支持点击字段直接跳转到对应的 widget。&lt;/p&gt;&lt;p&gt;&lt;img class=&quot;image_resized&quot; style=&quot;width:720px;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/11/202611065621438.jpg&quot; alt=&quot;Segment34 MkII 的四套内置配色主题&quot;&gt;&lt;/p&gt;&lt;p&gt;原作者在 FAQ 里被问得最多的问题就是&quot;能不能翻译成某某语言&quot;，他的回复是&quot;工作量大，在我的待办清单上，但别 promises&quot;。确实，等是等不来的，好在他把源码开源了——自己动手，丰衣足食。&lt;/p&gt;&lt;h3&gt;汉化的第一层：字符串资源替换&lt;/h3&gt;&lt;p&gt;Connect IQ 的应用是一个叫 Monkey C 的语言写的，资源管理机制和 Android 很像：默认资源放在 &lt;code&gt;resources&lt;/code&gt; 目录，如果你针对特定语言建一个 &lt;code&gt;resources-zhs&lt;/code&gt;（简体中文）目录，那么当手表系统语言是简体中文时，同名资源会自动覆盖默认资源。设置菜单里所有选项、天气描述、星期名称，在代码里都只是一个 string id 引用，所以第一层汉化非常朴素：把默认的 strings.xml 在 &lt;code&gt;resources-zhs/strings/strings.xml&lt;/code&gt; 里翻译一遍。&lt;/p&gt;&lt;pre class=&quot;prettyprint&quot;&gt;&amp;lt;strings&amp;gt;
   &amp;lt;string id=&quot;WEATHER_0&quot;&amp;gt;晴朗&amp;lt;/string&amp;gt;
   &amp;lt;string id=&quot;WEATHER_1&quot;&amp;gt;局部多云&amp;lt;/string&amp;gt;
   &amp;lt;string id=&quot;WEATHER_2&quot;&amp;gt;大部分多云&amp;lt;/string&amp;gt;
   &amp;lt;string id=&quot;WEATHER_3&quot;&amp;gt;雨&amp;lt;/string&amp;gt;
   &amp;lt;string id=&quot;WEATHER_4&quot;&amp;gt;雪&amp;lt;/string&amp;gt;
   &amp;lt;!-- ...共 30 多种天气描述 --&amp;gt;
&amp;lt;/strings&amp;gt;&lt;/pre&gt;&lt;p&gt;这一步做完，手表系统语言设为简体中文的用户就能看到全中文的设置菜单了——包括表盘的全部三十多项设置：数据更新频率、顶部显示、各个字段显示什么、日期格式、农历选项等等。佳明手机 App 里的设置页面会直接用上这些翻译。&lt;/p&gt;&lt;p&gt;&lt;img class=&quot;image_resized&quot; style=&quot;width:420px;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/11/202611067845692.jpg&quot; alt=&quot;汉化后的设置菜单&quot;&gt;&lt;/p&gt;&lt;h3&gt;汉化的第二层：表盘上的字不是&quot;文字&quot;，是&quot;图&quot;&lt;/h3&gt;&lt;p&gt;如果只是设置菜单汉化，那这个项目半小时就能干完。真正的麻烦在于：表盘上显示的一切数字和文字，用的都不是系统字体，而是作者自制的位图字体（BMFont 格式的 .fnt + .png 图集）。段码风格的数字、英文星期、天气图标，全都是画在一张张图集里的字形贴图。这些字体里当然一个汉字都没有。&lt;/p&gt;&lt;p&gt;摆在面前的选择有两个：一是给点阵风格的字体做汉字字形，二是给&quot;线条&quot;风格的字体做汉字。点阵风格要保证和原版像素风一致，一个汉字至少 16×16 像素起步，而表盘用的字体有好几种尺寸，全部做下来工作量巨大；&quot;线条&quot;风格是把段码笔画用斜线画出来的，汉字用线条风格来画反而和数字部分很搭。所以我选择了后者：用位图字体工具往线条风格的字体图集里补汉字字形——天干地支、月名日名、节气名、星期名，一个个字符按原字体的基线、间距对齐画进去，然后手工微调了好几轮像素（git 历史里&quot;细微修整了一下24px字体&quot;提交了好几次）。&lt;/p&gt;&lt;p&gt;点阵风格的字体不做汉字，所以文档里专门写了一条 FAQ：请将设置中&quot;小点阵字体变体&quot;设为默认的&quot;线条&quot;，以确保能正常显示汉字。&lt;/p&gt;&lt;p&gt;字体补完并不意味着一劳永逸，后面还陆续踩了一串&quot;方框&quot;坑——凡是被显示出来的字符在字体图集里没有字形，就会渲染成方框：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;顶部和底部字段显示温度时，摄氏度 &lt;code&gt;℃&lt;/code&gt; 图标是方框——原版用一个特殊字形表示度数符号，我的字体里漏了它；&lt;/li&gt;&lt;li&gt;AOD（息屏常显）模式下时钟下方的星期是方框——AOD 用的是另一套小号字体，我给它补字形时忘了星期；&lt;/li&gt;&lt;li&gt;训练状态偶尔显示方框——Garmin 的训练状态文案里有个别生僻字，我又往字体里补了几个汉字。&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;这一轮下来我总结出一条经验：汉化一个表盘，&quot;翻译文本&quot;只占两成工作量，剩下八成都在和字体图集较劲——你得把&lt;strong&gt;所有可能被显示出来的字符&lt;/strong&gt;都提前塞进字体，一个都漏不得。&lt;/p&gt;&lt;h3&gt;重头戏：农历显示&lt;/h3&gt;&lt;p&gt;汉化只是让表盘&quot;说中文&quot;，农历才是这个项目的灵魂。表盘的显示行是可配置的，我把农历作为时钟上方第二行的默认内容，也可以设置到其他位置。&lt;/p&gt;&lt;p&gt;实现农历第一个绕不开的问题是：表盘应用的内存限制非常苛刻，老款设备只有 128KB 级别的内存给一块表盘用。常见的做法是引入一份完整的农历数据表（每一年用几个位掩码表示大小月和闰月）加上算法，但那样代码体积和运行时开销都偏大，v1.x 版本就是这么写的，在部分老设备上跑不动，后来我干脆重写了整个农历模块，改成了&lt;strong&gt;纯查表&lt;/strong&gt;方案。&lt;/p&gt;&lt;p&gt;思路很简单：农历算法再复杂，落到&quot;2026 到 2030 这五年&quot;这个有限范围内，所有信息都可以预计算成三张整数表：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;LUNAR_OFFSET_DAYS&lt;/code&gt;：60 个元素，每个农历月初一相对 2026-01-01 的天数偏移。有了它，任何公历日期算出相对天数后做一次线性查找，就能定位到它落在哪个农历月、是这个月的第几天；&lt;/li&gt;&lt;li&gt;&lt;code&gt;LEAP_MONTH_OFFSETS&lt;/code&gt;：这五年里哪几个月是闰月（2026—2030 年间有闰六月等两个闰月），顺便标记当前月是否就是闰月；&lt;/li&gt;&lt;li&gt;&lt;code&gt;SOLAR_TERMS_OFFSETS&lt;/code&gt;：五年里全部 120 个节气的相对天数，用来查&quot;今天是哪个节气&quot;或者&quot;距离下一个节气还有几天&quot;。&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;显示所需的文本也全部压成几个字符串常量：天干地支各一个字符串、&quot;正月二月……冬月腊月&quot;一个月名字符串、&quot;初一初二……廿九三十&quot;一个日名字符串、24 个节气一个字符串，需要哪个就按索引 &lt;code&gt;substring&lt;/code&gt; 切两个字符出来，连数组都省了。&lt;/p&gt;&lt;p&gt;&lt;img class=&quot;image_resized&quot; style=&quot;width:720px;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/11/202611069184237.png&quot; alt=&quot;农历转换的查表流程&quot;&gt;&lt;/p&gt;&lt;p&gt;核心的月定位逻辑大概长这样：&lt;/p&gt;&lt;pre class=&quot;prettyprint&quot;&gt;// 返回值: [农历年份, 农历月份, 农历日期, 是否闰月]
private function getLunarDate(year, month, day) as Array&amp;lt;Number&amp;gt; {
   var targetDays = getDayCount(year, month, day); // 相对 2026-01-01 的天数
   // 在月首偏移表里找到 targetDays 所属的农历月
   var baseMonthIdx = 0;
   for (var i = 0; i &amp;lt; lunarSize; i++) {
       if (LUNAR_OFFSET_DAYS[i] &amp;gt; targetDays) {
           baseMonthIdx = i - 1;
           break;
       }
   }
   // 闰月修正：当前月之前有几个闰月、当前月本身是否闰月
   var leapCount = 0;
   var isLeap = 0;
   // ...遍历 LEAP_MONTH_OFFSETS 统计...

   var realMonthOffset = baseMonthIdx - leapCount;
   var lunarYear = START_YEAR + Math.floor(realMonthOffset / 12);
   var lunarMonth = realMonthOffset % 12 + 1;
   var lunarDay = targetDays - LUNAR_OFFSET_DAYS[baseMonthIdx] + 1;
   return [lunarYear, lunarMonth, lunarDay, isLeap];
}&lt;/pre&gt;&lt;p&gt;整个农历模块 217 行 Monkey C，覆盖 2026—2030 五年，支持闰月、干支纪年、二十四节气，运行时只需要一次线性查找加几次减法，内存占用相比完整农历库小了一个量级。后续版本又做了两轮针对性优化（v2.4&quot;进一步优化农历模块以节省运行内存&quot;），把老设备也照顾到了。代价是五年之后表盘上的农历会&quot;过期&quot;，到时候发个新版本更新三张表就行——我给过期的情况留了个显眼的提示串，看到它就知道该升级了。&lt;/p&gt;&lt;p&gt;节气显示做了个小设计：如果今天是节气当天就直接显示节气名，否则显示&quot;距离下一个节气还有几天&quot;，比如&quot;+10清明&quot;。这样一年里节气只有 24 天，剩下三百多天这一栏也有信息量。&lt;/p&gt;&lt;p&gt;&lt;img class=&quot;image_resized&quot; style=&quot;width:420px;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/11/202611063458127.jpg&quot; alt=&quot;模拟器中的农历显示效果&quot;&gt;&lt;/p&gt;&lt;p&gt;最后呈现出来的表盘就是开头那张上手图的样子，从上到下依次是：日出日落和月相、汉化的天气状况和温度、农历行（丙午年 二月初八 +10清明）、七段管大时钟、中文星期和日期、底部自定义数据字段。中文星期和&quot;周一&quot;到&quot;周日&quot;也是字体图集里画出来的。&lt;/p&gt;&lt;h3&gt;设备兼容：另一个大坑&lt;/h3&gt;&lt;p&gt;表盘不同于手机 App，要面对佳明几十款屏幕形状、分辨率、内存各不相同的设备。这个项目的做法是在 manifest 里声明支持的设备列表，再用 Connector IQ 注解按分辨率分派排版资源（Round240/260/280/360/390/416/454/Square 等），编译时通过 &lt;code&gt;excludeAnnotations&lt;/code&gt; 只保留目标设备对应的变体。为此踩过的坑包括：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;有用户反馈 Forerunner 255S 无法安装：根因是 manifest 的设备列表只列了 255/255m（260×260 屏），没有 255S（218×218 屏），而 218 宽度下时钟字体的排版宽度是写死的，放不下，需要单独做一套字号。权衡之后我放弃了对它的支持，并向用户说明可以试试&quot;Segment34 中文精简版&quot;；&lt;/li&gt;&lt;li&gt;F6S、F6SP、245、945 等老设备内存太小，农历模块初版跑不动——这也是后来把农历重写成纯查表的直接原因，但最终这几款还是被移出了支持列表；&lt;/li&gt;&lt;li&gt;Fenix 9 系列发布后，官方 SDK 更新了设备定义，编译环境里要先补齐 7 个 fenix9 设备描述文件才能出包（本机 SDK 目录缺这些定义时，编译任何设备都会报错，这个坑坑了我一整天）。&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;上架商店与持续迭代&lt;/h3&gt;&lt;p&gt;2026 年 3 月 24 日第一次提交代码，3 月 26 日第一个完全汉化版，3 月 27 日农历上线，此后一边在佳明论坛和商店评论区收集反馈一边修修补补，一直迭代到现在的 v2.5.6：&lt;/p&gt;&lt;figure class=&quot;table&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;时间&lt;/th&gt;&lt;th&gt;版本&lt;/th&gt;&lt;th&gt;内容&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;2026-03-24&lt;/td&gt;&lt;td&gt;v1.x&lt;/td&gt;&lt;td&gt;初版：中文星期、重写农历算法以适应老设备、AMOLED 大字体&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2026-03-26&lt;/td&gt;&lt;td&gt;v2.0&lt;/td&gt;&lt;td&gt;完全汉化：天气、星期、摄氏度图标全部中文化&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2026-03-27&lt;/td&gt;&lt;td&gt;v2.1&lt;/td&gt;&lt;td&gt;时钟上方两行和下方都可以设置为显示农历&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2026-03-30&lt;/td&gt;&lt;td&gt;v2.3&lt;/td&gt;&lt;td&gt;补训练状态汉字，修复 AOD 星期方框&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2026-04-02&lt;/td&gt;&lt;td&gt;v2.4&lt;/td&gt;&lt;td&gt;进一步优化农历模块节省内存，修复电量条形图显示异常&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2026-09-08&lt;/td&gt;&lt;td&gt;v2.5.0&lt;/td&gt;&lt;td&gt;新增 Fenix 9 系列支持，重绘更精细的月相图标&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2026-09-15&lt;/td&gt;&lt;td&gt;v2.5.6&lt;/td&gt;&lt;td&gt;设置菜单新增月相阴影显示开关&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/figure&gt;&lt;p&gt;比较有意思的几个反馈驱动的小修复：有用户反映身体压力偶尔不显示，加了缓存；有用户反映湿度缓存过期后表盘崩溃，排查后发现是原版逻辑的空指针问题（顺手给原版 bug 也修了）；还有用户希望默认底部布局不用自己调，就把默认值改成了大多数人不看文档也能正常显示的 334。&lt;/p&gt;&lt;h3&gt;写在最后&lt;/h3&gt;&lt;p&gt;这个项目技术上并不复杂，甚至可以说&quot;土&quot;——位图字体、查表算法、字符串覆盖，没有一处用到了炫技的东西。但它是那种&quot;自己真的需要&quot;的项目：现在我抬腕就能看到农历和节气，这一行的信息密度可能是整个表盘最高的。&lt;/p&gt;&lt;p&gt;再次感谢原作者 ludw 的开源精神，这个中文版完全免费上架在佳明 Connect IQ 商店：&lt;a href=&quot;https://apps.garmin.com/apps/cd2b47e9-46a1-4032-b6ff-1b130db10576&quot;&gt;Segment34中文版&lt;/a&gt;，如果你的手表在支持列表里，欢迎安装试用。旧款设备（F6S、245、945 等）可以试试&quot;Segment34 中文精简版&quot;。&lt;/p&gt;</description><pubDate>Fri, 06 Nov 2026 10:14:32 +0800</pubDate></item><item><title>OpenWRT路由器替代IPTV机顶盒收看电视直播保姆级教程</title><link>https://blog.fairysoft.net/post/73.html</link><description>&lt;p&gt;我最近翻出了吃灰多年的IPTV机顶盒。在这个短视频时代，很少有人看传统的电视节目了，电视可以不看，但不能没有，所以我决定还是把它装上。但是一看到那个遥控器，我的强迫症就犯了：长得丑不说，看个电视还要用两个遥控器，实在是不方便。那么有没有办法直接在电视机上看IPTV直播呢？经过一天的折腾，我终于搞定了直接在电视机上播放IPTV电视节目，而且家庭局域网内所有的电脑、手机、平板也都能收看。整个过程记录如下，照着做就行。&lt;/p&gt;&lt;p&gt;&lt;img title=&quot;workflow.png&quot; style=&quot;width: 720px;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/11/202611043284185.png&quot; alt=&quot;从抓包到全屋可看的完整流程&quot; width=&quot;720&quot;/&gt;&lt;/p&gt;
&lt;h3&gt;先弄懂：IPTV 是怎么工作的&lt;/h3&gt;
&lt;p&gt;在进入正题之前，先了解一下IPTV是怎么工作的。运营商的IPTV和普通宽带上网是两条&quot;互不相干&quot;的通道，即使它们共用同一根入户光纤和同一个光猫。理解这一点，是后面所有操作的前提。&lt;/p&gt;
&lt;p&gt;我家的主路由是一台J4125处理器的软路由，直装的x86_64的OpenWRT系统，这个机器有4个网口，硬件名分别为eth0、eth1、eth2、eth3。eth0为WAN口，连接到光猫的Internet网口，通过PPPoE拨号接入互联网；eth2为LAN口，连接到主交换机；eth1和eth3暂时空置。光猫上有一个IPTV网口，专门承载IPTV业务，机顶盒插这个口，用内置的PPPoE账号拨号，接入运营商的IPTV网络。&lt;/p&gt;
&lt;p&gt;&lt;img title=&quot;topology.png&quot; style=&quot;width: 720px;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/11/202611068148690.png&quot; alt=&quot;改造前后的网络拓扑对比&quot; width=&quot;720&quot;/&gt;&lt;/p&gt;
&lt;p&gt;机顶盒起的作用有两个：一是拨号接入IPTV网络，二是将接收到的电视节目流媒体数据包解码成HDMI信号传输给电视机播放。现代的电视机都内置了智能系统，本身就具备网络连接和视频解码的功能。所以我的思路就是：用OpenWRT路由器代替机顶盒进行PPPoE拨号接入，再把IPTV组播视频流转换成单播流，在电视机上用Kodi这样的播放器直接播放。这样就能彻底抛弃&quot;机顶盒&quot;这个硬件角色。&lt;/p&gt;
&lt;p&gt;这里简单解释一下什么是&quot;组播&quot;、什么是&quot;单播&quot;。IPTV的&quot;直播&quot;本质是IP组播（Multicast）：一个组播组（比如 239.x.x.x:port）里，运营商机房不停往外推节目流，机顶盒想看哪个台，就发一条IGMP Join请求加入那个组，加入之后就能持续接收这个组的数据流。组播流量在局域网中会被泛洪到所有设备，WiFi下体验极差，经常卡顿或者干脆收不到。而单播（Unicast）则是点对点的HTTP流，任何设备都能稳定播放。所以中间需要一个&quot;组播转单播&quot;的转换器。&lt;/p&gt;
&lt;p&gt;&lt;img title=&quot;multicast.png&quot; style=&quot;width: 720px;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/11/202611011606267.png&quot; alt=&quot;rtp2httpd 组播转单播原理&quot; width=&quot;720&quot;/&gt;&lt;/p&gt;
&lt;p&gt;传统的方案是用udpxy来做这个转换。不过这次我没有用udpxy，而是选择了功能更强大的&lt;strong&gt;rtp2httpd&lt;/strong&gt;——它除了组播转单播，还内置了一个网页版播放器，浏览器打开就能直接看电视，而且播放列表格式跟udpxy完全兼容（都是 &lt;code&gt;/udp/组播地址:端口&lt;/code&gt; 的形式），之前为udpxy生成的m3u列表可以一字不改直接使用。区别放到第五节细说。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;以下教程基于两个前提&lt;/strong&gt;：光猫IPTV口无VLAN（直接透传）、机顶盒认证是PAP明文（密码抓包直接可见）。有的运营商会在光猫IPTV口给流量打上VLAN标签，桥接/拨号前要先剥离VLAN，具体可以自行了解OpenWRT中VLAN的设置方法；如果机顶盒认证不是明文，抓包数据中就提取不出密码，需要通过其他途径获取机顶盒的PPPoE账号密码。&lt;/p&gt;
&lt;h3&gt;一、准备抓包&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1.1 接线&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;机顶盒插eth2，光猫IPTV口插eth3。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.2 把 eth2 和 eth3 做成一个二层桥接 br-iptv&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;打开浏览器访问路由器的管理页面，登录后：&lt;/p&gt;
&lt;p&gt;菜单：网络 → 接口，点&quot;添加新接口&quot;按钮。&lt;/p&gt;
&lt;p&gt;名称填iptv，&quot;在以下设备上创建接口&quot;下面勾选eth2和eth3，协议选&quot;不配置协议&quot;。&lt;/p&gt;
&lt;p&gt;点&quot;保存并应用&quot;。会自动创建br-iptv桥，并让eth2、eth3都归它管。&lt;/p&gt;
&lt;p&gt;注意，&quot;不配置协议&quot;是关键：别给它配IP，也别把它加进LAN/WAN区域。这个桥的唯一作用就是让光猫和机顶盒的流量透明穿过，供你抓包，设了IP反而会干扰机顶盒的PPPoE。&lt;/p&gt;
&lt;h3&gt;二、抓包&lt;/h3&gt;
&lt;p&gt;这一步必须用命令行（LuCI没有抓包界面），SSH登录路由器，执行下面这条指令：&lt;/p&gt;
&lt;pre class=&quot;prettyprint&quot;&gt;tcpdump -i br-iptv -nn -s 0 -w /tmp/iptv.pcap&lt;/pre&gt;
&lt;p&gt;给机顶盒通电开机，开机之后机顶盒会自动进行PPPoE拨号，然后在机顶盒上依次切几个台（每个台停几秒），触发IGMP加入和组播流。切完台，回终端按Ctrl+C停止抓包。&lt;/p&gt;
&lt;p&gt;注意播放节目的时间不要太长，因为抓包会把视频流也抓下来，时间长了抓包文件会很大。&lt;/p&gt;
&lt;h3&gt;三、读出密码和组播地址&lt;/h3&gt;
&lt;p&gt;在电脑上用Wireshark打开抓包文件iptv.pcap。它自带抓包分析，能看到明文密码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.1 获取 PAP 密码&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在Wireshark顶部过滤框输入 &lt;code&gt;ppp.password&lt;/code&gt;，下方列表会直接列出携带密码的PAP报文。点开一个，在协议树里展开 PPP → PAP → Password，明文密码就在那里；用户名同理（&lt;code&gt;ppp.peer_id&lt;/code&gt; 或 &lt;code&gt;ppp.user_id&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.2 获取组播频道地址&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;笨办法是用命令行工具tshark把整份pcap里的组播地址、端口以及频道名称提取出来。不过一百多个频道手工整理实在太麻烦，现在AI这么强大，完全可以让AI帮我们写一个Python脚本来完成这个任务，顺便生成最终的m3u文件供播放器使用。&lt;/p&gt;
&lt;p&gt;最终生成的m3u文件长这个样子：&lt;/p&gt;
&lt;pre class=&quot;prettyprint&quot;&gt;#EXTM3U
#EXTINF:-1,CCTV-1
http://192.168.1.1:4022/udp/239.0.1.2:1234
#EXTINF:-1,CCTV-2
http://192.168.1.1:4022/udp/239.0.1.3:1234
#EXTINF:-1,CCTV-3
http://192.168.1.1:4022/udp/239.0.1.4:1234&lt;/pre&gt;
&lt;p&gt;这里的地址格式就是udpxy风格：&lt;code&gt;http://路由器IP:4022/udp/组播地址:端口&lt;/code&gt;。记住这个格式，后面第五节配置rtp2httpd的时候会用到——端口保持4022，这份列表到最后一行都不用改。&lt;/p&gt;
&lt;h3&gt;四、软路由代拨号&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;4.1 建立 PPPoE 接口&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;进入路由器管理后台，把抓包用的桥停掉，再把eth3单独建一个PPPoE接口：&lt;/p&gt;
&lt;p&gt;网络 → 接口，找到iptv (br-iptv)，直接点&quot;删除&quot;删掉整个桥。&lt;/p&gt;
&lt;p&gt;点&quot;添加新接口&quot;：名称IPTV_WAN，设备选eth3，协议选PPPoE，填上抓包得到的账号和密码。&lt;/p&gt;
&lt;p&gt;展开&quot;高级设置&quot;：把&quot;默认网关/使用默认路由&quot;取消勾选（不让IPTV接口抢你的上网默认路由，这一步很重要，否则你的上网流量会跑到IPTV专线里去）。&lt;/p&gt;
&lt;p&gt;防火墙设置里面把IPTV_WAN加入wan区域，保存并应用。&lt;/p&gt;
&lt;p&gt;然后进入网络 → 防火墙 → 通信转发，将LAN到IPTV_WAN的转发设置为允许。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4.2 验证拨号成功&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;回到 网络 → 接口，找到IPTV_WAN，点&quot;连接&quot;。不出意外的话，几秒钟之后接口状态会变成&quot;已连接&quot;，并且拿到了10开头的IPv4地址，就说明认证通过、已连接上IPTV网络。&lt;/p&gt;
&lt;p&gt;这时候就可以拔掉机顶盒，让它正式下岗了（建议先留着别收起来，出问题好回退）。&lt;/p&gt;
&lt;h3&gt;五、配置 rtp2httpd&lt;/h3&gt;
&lt;p&gt;拨号已经通了，但组播不会自己&quot;流&quot;到局域网里来，需要一个组播转单播的网关。传统教程一般到这一步是装udpxy，我最后没有选它，而是装了&lt;strong&gt;rtp2httpd&lt;/strong&gt;，理由有三：&lt;/p&gt;
&lt;p&gt;一是&lt;strong&gt;内置网页播放器&lt;/strong&gt;。udpxy只负责转流，想看电视还得在各设备上装播放器；rtp2httpd自带一个响应式网页播放器，手机、平板、电脑浏览器打开 &lt;code&gt;http://192.168.1.1:4022/player&lt;/code&gt; 就能直接看，电视盒子连软件都不用装。家里长辈想看电视，发个网址过去就行，这是udpxy给不了的体验。&lt;/p&gt;
&lt;p&gt;二是&lt;strong&gt;播放列表格式与udpxy完全兼容&lt;/strong&gt;。它保留了 &lt;code&gt;/udp/组播地址:端口&lt;/code&gt; 的地址风格，我们第三步生成的m3u列表一行都不用改。如果以后想从udpxy换过来，也是无缝迁移。&lt;/p&gt;
&lt;p&gt;三是&lt;strong&gt;功能更丰富&lt;/strong&gt;。它还支持RTSP转HTTP、EPG节目单代理、快速换台（FCC）等进阶功能，状态页也能直观看到每个客户端在看什么台、占了多少带宽，比udpxy那个简陋的文本状态页好用得多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5.1 设置 rtp2httpd&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OpenWRT软件包里直接搜索rtp2httpd安装（iStore软件源里也有），装好后进入 服务 → rtp2httpd，填这几个关键项：&lt;/p&gt;
&lt;p&gt;启用：勾选。&lt;/p&gt;
&lt;p&gt;端口：4022（和m3u列表里的端口保持一致）。&lt;/p&gt;
&lt;p&gt;上游接口：填 pppoe-IPTV_WAN（告诉它组播流从这个IPTV专线接口进来，这是最关键的一项，填错了收不到组播）。&lt;/p&gt;
&lt;p&gt;保存并应用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5.2 验证&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;电脑浏览器访问这个地址：&lt;code&gt;http://192.168.1.1:4022/status&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果能看到状态页（服务运行时间、活跃连接、带宽等统计信息），说明rtp2httpd工作正常。&lt;/p&gt;
&lt;p&gt;这时候电脑上用PotPlayer或者其他支持IPTV的播放器打开之前生成的m3u文件，应该就能正常播放电视节目了。也可以直接在浏览器打开 &lt;code&gt;http://192.168.1.1:4022/player&lt;/code&gt;，用内置的网页播放器选台观看，手机平板同样适用。&lt;/p&gt;
&lt;h3&gt;六、Kodi 配置播放&lt;/h3&gt;
&lt;p&gt;Android TV上的Kodi用PVR IPTV Simple Client插件读取m3u，然后在&quot;电视 → 频道&quot;里看直播：&lt;/p&gt;
&lt;p&gt;Kodi 设置 → 插件 → 我的插件 → 全部 → PVR客户端 → IPTV Simple Client → 安装。&lt;/p&gt;
&lt;p&gt;进插件设置 → M3U播放列表路径，设置好m3u文件的路径（把m3u放到U盘或NAS上，也可以直接填 &lt;code&gt;http://192.168.1.1:4022/playlist.m3u&lt;/code&gt; 这样的在线地址），点确定保存设置后重启Kodi。&lt;/p&gt;
&lt;p&gt;返回Kodi主页面，进入 电视 → 频道，应该能看到m3u里的全部频道，点开就能播放了，换台速度和原生机顶盒几乎没有区别。&lt;/p&gt;
&lt;h3&gt;写在最后&lt;/h3&gt;
&lt;p&gt;到这里，改造就全部完成了：一台软路由同时接管了互联网拨号和IPTV专线拨号，机顶盒彻底退休，全屋的电视、手机、平板、电脑打开浏览器或者播放器就能看直播。整套方案没花一分钱，只是把本来就有的设备用到了极致。如果你家也是光猫桥接加软路由的组合，不妨试试。&lt;/p&gt;</description><pubDate>Fri, 06 Nov 2026 04:02:57 +0800</pubDate></item><item><title>警惕GPS欺骗导致FM位置偏移的风险</title><link>https://blog.fairysoft.net/post/72.html</link><description>&lt;p&gt;先来看一个民航西北地区管理局通告的案例：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;关于进近期间GPS位置与飞机实际位置不一致风险的警示（NWCAAC-SA-2026-03)&lt;/p&gt;&lt;p&gt;2026年8月24日，A319飞机执行西安-榆林航班，使用34号跑道RNP进近。在进近过程中，飞机触发 NAV FM/GPS位置不一致警戒，23秒后警戒自行消失，机组检查飞机状态正常，导航精度高，决策继续实施 RNP 进近。飞机在距离YLX台约12NM处建立“最后进近”模式并开始自行下降，此时较实际的最后下降点提前3.4 海里，飞机沿错误的剖面下降，通过实际最后下降点时低于正常程序高度1244英尺，继续下降过程中穿越最低超障高度。经塔台提醒，机组发现高度偏低后复飞，改用34号VOR/DME进近程序，安全着陆。经初步调查，译码显示在飞机触发 NAV FM/GPS 位置不一致警戒之前，飞机 UTC时间跳回18秒，飞机实际位置与GPS位置开始产生偏差，机组未能察觉，最终导致飞机沿错误的剖面下降。该事件机载UTC时间跳变、FM/GPS 位置与飞机实际位置不一致现象的发生，不排除 GNSS欺骗式干扰、机载设备故障或客舱无线电设备干扰等因素导致。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;这个通告中有很多细节没说清楚，看完之后我产生了两个疑问：&lt;/p&gt;&lt;p&gt;1、如果是飞机FM位置漂移了，怎么会刚好纵向偏了3.4海里，横侧方向没偏？因为如果横向也偏了的话，管制雷达应该早就发现他位置不对了。&lt;/p&gt;&lt;p&gt;2、从系统设计原理上讲，GPS欺骗只能欺骗GPS，理论上不应该会欺骗到飞机的FM位置。如果飞机的FM都偏了3.4海里，为什么飞机显示的导航精度还是“高”，飞机的机载性能监控告警系统(OPMA)为什么没有告警？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;GPS从开放民用频率的第一天开始，工程师就明白这东西是很容易被欺骗的，民用频率完全没有加密，NASA网站上就有公开的GPS星历可以下载，github上有免费开源的GPS模拟程序，下载星历文件导入到程序中再输入一个需要模拟的经纬度位置坐标立刻就能生成一个模拟的GPS报文数据包，使用一个无线电发射机调谐到1575Mhz的频率把这个数据包发射出去就能欺骗GPS接收机了。&lt;/p&gt;&lt;p&gt;导航系统是飞机最重要的系统之一，空客的设计师在设计飞机导航系统时一定是考虑过如何防范GPS欺骗式干扰的。基本原理就是利用惯导短时高精度的特性对接收到的GPS定位信号进行校验，不合理的定位直接会被剔除，只有合理的GPS位置才会被IRS采纳并生成GPIRS并传递给FMGC使用。那为什么A320飞机还是会因为GPS欺骗而出现位置漂移呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;带着这个疑惑，我查了很多资料，我在空客的Safetyfirst网站上找到了一篇文章&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/9/2026092558338469.png&quot; width=&quot;600&quot; height=&quot;472&quot; title=&quot;65616ef95e921ea2bfab95a88e56c217.png&quot; alt=&quot;65616ef95e921ea2bfab95a88e56c217.png&quot; border=&quot;0&quot; hspace=&quot;0&quot; vspace=&quot;0&quot; style=&quot;max-width:100%; height:auto; border:0; margin:0; padding:0;&quot;/&gt;&lt;/p&gt;&lt;p&gt;这篇文章中，空客表示A320系列飞机在受到GPS欺骗式干扰时，FMS或者ADIRU确实有可能无法识别出错误的GPS信号，从而导致FM位置发生偏移。A350由于使用了更强健的位置混合算法，迄今为止还没有发生过类似情况。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;后来我又在一个飞行技术交流群里面看到了另外一起FM位置漂移事件，这两个事件非常相似：都是纵向位置偏了几海里，横侧没有偏移，飞机OPMA系统未发出告警，显示的导航精度为“高”。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;这就很奇怪了，这两次GPS欺骗是如何突破惯导的校验的？经过一番研究，我想到了一个可能的原因。惯导计算飞机位置是使用经典的牛顿运动定理，先将加速度对时间积分得到速度，然后用速度对时间积分得到位移，初始位置加位移就是现在的位置。由于飞机在直线平飞时始终是向前运动，速度的横向分量为零，因此如果一个虚假的GPS位置出现在飞机的左右两侧，惯导会立刻判定该位置不合理。也就是说惯导在面对一个跳变的GPS位置时，横侧方向的容忍度非常低，而纵向的容忍度则要宽松很多。如果欺骗者先通过接收飞机的ADSB信号或者使用雷达获取了飞机的位置和飞行方向，然后利用这些动态信息伪造一整套完美的、位于飞机前后方向的虚假位置信号发射给飞机，飞机就极有可能采纳这个虚假的GPS信号，从而导致飞机的FM位置向前后漂移。欺骗者伪造的信号不会一下子把飞机的GPS位置拉偏几百海里，这样会被IRS的融合算法瞬间剔除。它会以微小的速度平滑地向前后方向伪造GPS位置。惯导内部的卡尔曼滤波器在收到这种“连续、平滑、信号质量极高”的假GPS信号时，丝毫不会察觉到异常，会继续融合出GPIRS并传输给FMGC。随着时间的推移，FMGC会逐步将FM的位置修正到假的GPIRS位置上。飞机在脱离干扰区之后，接收到了正确的GPS信号，正确的GPS位置反而看起来好像是发生了跳变而暂时甚至永久被拒绝，导致FMGC无法及时将FM位置更新到正确的位置，飞机会继续保持在错误的航径上飞行。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;再来解释一下为什么OPMA没有发出告警的问题，机载性能监控告警系统（OPMA）在GPS主用模式下依赖于MMR内部的RAIM（接收机自主完整性监控）算法，使用卫星在天空中的几何分布（DOP 因子）以及信号的质量计算出GPS的品质因数（FOM），FMGC使用FOM计算出预测位置误差（EPU）并呈现在MCDU的PROG页面。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/9/2026092558370373.png&quot; width=&quot;600&quot; height=&quot;269&quot; title=&quot;截屏2026-09-25 15.49.30.png&quot; alt=&quot;截屏2026-09-25 15.49.30.png&quot; border=&quot;0&quot; hspace=&quot;0&quot; vspace=&quot;0&quot; style=&quot;max-width:100%; height:auto; border:0; margin:0; padding:0;&quot;/&gt;&lt;/p&gt;&lt;p&gt;如果欺骗信号非常完美（同时伪造了多颗卫星的伪距，且各卫星间的相对时延符合逻辑），MMR就会认为信号“极其完美”，从而给出极低的EPU。而OPMA的告警逻辑是：如果EPU≤RNP（当前所需导航精度），页面显示导航精度“HIGH”；一旦EPU＞RNP，导航精度降级为“LOW”，并立即触发ECAM警告：NAV ACCUR DOWNGRAD。由于欺骗信号逻辑自洽，EPU算法被成功欺骗，无法识别到位置错误，导致OPMA无法触发告警。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;解决了这两个疑问，我再来分析一下通告中的其他几个细节：&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;1、“经初步调查，译码显示在飞机触发 NAV FM/GPS 位置不一致警戒之前，飞机 UTC时间跳回18秒，飞机实际位置与GPS位置开始产生偏差。”&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;“飞机 UTC时间跳回18秒”这一个细节正是GPS信号受到欺骗式干扰的证据。由于GPS报文可以使用任意时间来伪造，所以大多数GPS欺骗式干扰中都会出现UTC时间跳变的现象。一般来说伪造者在生成GPS报文时为了省事会直接使用他电脑当前的系统时间。但是GPS卫星原子钟的时间跟地球UTC时间有偏差，这个差值在GPS系统中被称为“闰秒”，接收机通过解算出的GPS时间减去闰秒来计算UTC时间，目前闰秒刚好是18秒。如果伪造者没有修正这个闰秒偏移，那么用他伪造的GPS信号解算出来的时间本身就是UTC时间，接收机用这个时间减去18秒算出来的UTC时间自然就比真实的UTC时间慢了18秒。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;2、“飞机触发 NAV FM/GPS位置不一致警戒，23秒后警戒自行消失。”&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;飞机一开始接收到伪造的GPS位置时，如果伪造的GPS经纬度跟FM位置偏差量超过了0.5分（大约0.5海里），就会触发“NAV FM/GPS 位置不一致”的警戒。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/9/2026092576315801.png&quot; width=&quot;600&quot; height=&quot;255&quot; title=&quot;FM_GPS位置不一致.png&quot; alt=&quot;FM_GPS位置不一致.png&quot; border=&quot;0&quot; hspace=&quot;0&quot; vspace=&quot;0&quot; style=&quot;white-space: normal; max-width: 100%; height: auto; border: 0px; margin: 0px; padding: 0px;&quot;/&gt;&lt;/p&gt;&lt;p&gt;但是随后FM位置会向错误的GPS位置靠近，当FM位置靠近到跟GPS经纬度偏差小于0.5分时警戒就会自行消失。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/9/2026092576267777.png&quot; width=&quot;600&quot; height=&quot;308&quot; title=&quot;FM位置.png&quot; alt=&quot;FM位置.png&quot; border=&quot;0&quot; hspace=&quot;0&quot; vspace=&quot;0&quot; style=&quot;max-width:100%; height:auto; border:0; margin:0; padding:0;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;至此为止，通告中所披露的所有细节都有了合理的解释，所有的证据链形成了闭环。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;如果飞机的FM位置向前漂移了几海里会出现什么情况？&lt;/p&gt;&lt;p&gt;首先，FMGC并不会发现自己被欺骗了，它只会认为是惯导位置漂移了，所以它计算出的预估位置误差（EPE）还是零点零几海里，但是如果你查看惯导位置页面，你会发现三部惯导都显示漂移了几海里；&lt;/p&gt;&lt;p&gt;其次，只要EPE小于RNP，OPMA也就不会发出告警；&lt;/p&gt;&lt;p&gt;最后，由于横侧没有偏移，空管的雷达显示屏上你的飞机仍然在正确的航路上飞行，管制员也无法发现你的纵向位置发生了漂移。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;这次榆林的事件就是如此，直到飞机接近FDP点时，飞机提前截获了下降航径，这时候管制员才发现飞机低于了正常的进近程序高度。好在有雷达兜底，管制员及时提醒，才避免了一次可能的可控撞地事故。如果是在没有雷达覆盖的山区复杂地形的机场进行RNP-AR飞行呢？没有雷达监控兜底，一旦飞机位置出现偏差且OPMA失去告警能力，后果可能是非常严重的。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;【2026年9月27日更新】&lt;/p&gt;&lt;p&gt;之前的分析中有一点小错误，我之前以为从西安飞榆林应该是一路向北直接飞长五边，所以之前我认为在遭遇GPS欺骗时飞机FM位置是向前产生了偏移。今天我在飞常准APP里面看到了这个航班的ADSB轨迹图之后才发现，这个航班可能是因为绕飞雷雨飞到了榆林机场的西侧，所以飞机先向东南方向飞向机场南边，然后再左转加入五边。从这个图上可以看到飞机第一次进近时，转向五边的时机明显晚了很多，转过来之后并没有沿着跑道延长线飞行而是偏在跑道延长线的东侧；复飞之后第二次进近使用VOR程序，ADSB航迹完全落在跑道延长线上。根据两次进近的GPS航迹综合推断，GPS欺骗导致FM位置发生偏移应该是发生在第一次进近转弯之前的直线飞行阶段，FM的位置不是向前而是向后偏移了几海里。由于FM偏移方向是向西北方向，所以飞机左转飞向五边之后飞机的横向和纵向都产生了偏移，纵向偏移大概是3海里，横向偏移从图上看大概是1海里。飞机在转弯之前应该已经脱离了干扰区，后面的ADSB轨迹应该是飞机的真实飞行轨迹。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.fairysoft.net/zb_users/upload/2026/9/2026092780641921.jpg&quot; width=&quot;600&quot; height=&quot;769&quot; title=&quot;飞常准航迹.jpg&quot; alt=&quot;飞常准航迹.jpg&quot; border=&quot;0&quot; hspace=&quot;0&quot; vspace=&quot;0&quot; style=&quot;max-width:100%; height:auto; border:0; margin:0; padding:0;&quot;/&gt;&lt;/p&gt;&lt;p&gt;飞机本应该沿着A→B→C→D的轨迹飞行，但实际上飞机却沿着A→B→E→F→G的错误轨迹飞行，本应该在D点进行最后下降，但却在G点就开始下降了。所以我推测飞机可能是在A和B之间某个位置遭遇了GPS欺骗，导致FM位置向西北方向偏移了几海里，当飞机到达B点时，本应该向左转弯飞向C点，但是由于FM位置向西北方向偏移了，飞机认为自己位置还没有到达转弯点，于是继续往前飞，到达E点时才开始左转。飞机在E点之前应该已经脱离了干扰区，接收到了真实的GPS信号，由于真实GPS位置跟FM位置偏差太大超过了阈值，FMGC反而有可能把正确的GPS位置认为是突变，从而不更新FM位置，FM继续使用错误的位置，所以飞机一直偏在跑道延长线的东侧，飞机飞到G点时以为自己是在D点，D点大概就是RNP程序的FDP点，于是飞机在G点开始下降。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;分析完这个图，最后我又想到了一个新的疑点，根据ADSB轨迹，既然后来GPS信号已经恢复，FM位置跟GPS位置偏差超过了3海里，为什么这时候没有再次触发“FM/GPS 位置不一致”的警戒？我的猜测是系统在那个时候GPS主用状态已经丢失了，而“FM/GPS 位置不一致”警戒触发的前提是“GPS主用功能有效”，由于整个通告中都没有提到GPS主用功能是否丢失，所以仅仅只是猜测，无从考证。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;</description><pubDate>Sat, 26 Sep 2026 00:10:39 +0800</pubDate></item><item><title>Windows Server服务器上原生部署Headplane</title><link>https://blog.fairysoft.net/post/71.html</link><description>&lt;p&gt;Headscale 官方原生并没有提供内置的后台管理界面。官方只专注于开发高性能的命令行工具和控制层服务。 不过，得益于 Headscale 提供了完善的 REST API，开源社区开发了多款优秀的第三方网页管理后台。&lt;/p&gt;&lt;p&gt;Headplane 是目前社区中最受推崇、维护也最活跃的 Web UI 之一。创建用户、审批客户端、创建预登录密钥、审批路由等操作都可以在 Headplane 的图形化的管理界面（Web UI）中完成。&lt;/p&gt;&lt;p&gt;同样的，Headplane 原生也是针对 Linux 环境设计的，不过它是用 Node.js 编写的，理论上也可以在 Windows 上编译运行。以下是我经过踩坑总结出来的编译教程：&lt;/p&gt;&lt;p&gt;先安装 Node.js 的 Windows 版本，然后下载 Headplane 的源代码。&lt;/p&gt;&lt;p&gt;Headplane 官方使用 pnpm 管理依赖。运行以下命令安装：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false;&quot;&gt;npm&amp;nbsp;install&amp;nbsp;-g&amp;nbsp;pnpm&lt;/pre&gt;&lt;p&gt;在 C:\headplane 目录下，运行 pnpm 自动下载并安装所有前端与后端依赖组件：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false;&quot;&gt;pnpm&amp;nbsp;install&lt;/pre&gt;&lt;p&gt;install 过程中会有一些警告，基本上都是一些可选的依赖组件安装失败了，不影响正常使用，可以忽略。&lt;/p&gt;&lt;p&gt;安装完成后，运行以下命令编译前端和后端代码：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false;&quot;&gt;pnpm&amp;nbsp;build&lt;/pre&gt;&lt;p&gt;编译完成后在 C:\headplane\build 目录下会生成前端和后端的编译结果。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;先在 headscale 里面创建一个 API_KEY 供 Headplane 使用：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false;&quot;&gt;.\headscale.exe&amp;nbsp;apikeys&amp;nbsp;create&lt;/pre&gt;&lt;p&gt;然后在 C:\headplane 根目录下，新建一个名为 config.yaml 的文件，写入以下基础配置内容：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false;&quot;&gt;server:
&amp;nbsp;&amp;nbsp;host:&amp;nbsp;&amp;quot;127.0.0.1&amp;quot;
&amp;nbsp;&amp;nbsp;port:&amp;nbsp;3003
&amp;nbsp;&amp;nbsp;base_url:&amp;nbsp;&amp;quot;https://headscale.myhost.com&amp;quot;
&amp;nbsp;&amp;nbsp;cookie_secret:&amp;nbsp;&amp;quot;abcdefghijklmnopqrstuvwxyz123456&amp;quot;
&amp;nbsp;&amp;nbsp;cookie_secure:&amp;nbsp;true
&amp;nbsp;&amp;nbsp;cookie_max_age:&amp;nbsp;86400
&amp;nbsp;&amp;nbsp;cookie_domain:&amp;nbsp;&amp;quot;headscale.myhost.com&amp;quot;
&amp;nbsp;&amp;nbsp;data_path:&amp;nbsp;&amp;quot;./data&amp;quot;
headscale:
&amp;nbsp;&amp;nbsp;url:&amp;nbsp;&amp;quot;https://myhost.com:8080&amp;quot;
&amp;nbsp;&amp;nbsp;public_url:&amp;nbsp;&amp;quot;https://myhost.com:8080&amp;quot;
&amp;nbsp;&amp;nbsp;config_path:&amp;nbsp;&amp;quot;C:/Headscale/config.yaml&amp;quot;
&amp;nbsp;&amp;nbsp;api_key:&amp;nbsp;&amp;quot;hskey-api-xxxxxxxx&amp;quot;
&amp;nbsp;&amp;nbsp;config_strict:&amp;nbsp;false
integration:
&amp;nbsp;&amp;nbsp;agent:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;enabled:&amp;nbsp;false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pre_authkey:&amp;nbsp;&amp;quot;&amp;quot;
&amp;nbsp;&amp;nbsp;docker:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;enabled:&amp;nbsp;false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;container_label:&amp;nbsp;&amp;quot;me.tale.headplane.target=headscale&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;socket:&amp;nbsp;&amp;quot;unix:///var/run/docker.sock&amp;quot;
&amp;nbsp;&amp;nbsp;kubernetes:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;enabled:&amp;nbsp;false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;validate_manifest:&amp;nbsp;true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pod_name:&amp;nbsp;&amp;quot;headscale&amp;quot;
&amp;nbsp;&amp;nbsp;proc:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;enabled:&amp;nbsp;false&lt;/pre&gt;&lt;p&gt;输入以下命令运行服务：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false;&quot;&gt;$env:HEADPLANE_CONFIG_PATH=&amp;quot;$PWD\config.yaml&amp;quot;
node&amp;nbsp;build/server/index.js&lt;/pre&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;如果在运行之后报错，找到错误提示，按照提示修改代码，直到能够成功运行。&lt;/p&gt;&lt;p&gt;我在运行过程中遇到了两个错误，分别是：&lt;/p&gt;&lt;p&gt;1. 在 hp-agent.ts 文件中，使用了 getegid 和 geteuid 这两个函数来获取当前进程的用户和组 ID，这两个函数在 Windows 上是不存在的，所以需要修改代码来兼容 Windows。&lt;/p&gt;&lt;p&gt;2. 在 index.ts 文件中，使用了 drizzle 函数来创建数据库连接，传入的参数是一个文件路径，但是在 Windows 上需要将文件路径转换为 URL 格式才能正常使用，所以需要修改代码来兼容 Windows。&lt;/p&gt;&lt;p&gt;我是先修改了两处源代码：&lt;/p&gt;&lt;pre class=&quot;brush:js;toolbar:false;&quot;&gt;//&amp;nbsp;编辑文件&amp;nbsp;/app/server/hp-agent.ts
///将原来的代码：
import&amp;nbsp;{&amp;nbsp;getegid,&amp;nbsp;geteuid&amp;nbsp;}&amp;nbsp;from&amp;nbsp;&amp;#39;node:process&amp;#39;;
//&amp;nbsp;替换为：
import&amp;nbsp;process&amp;nbsp;from&amp;nbsp;&amp;#39;node:process&amp;#39;;
const&amp;nbsp;getegid&amp;nbsp;=&amp;nbsp;process.getegid&amp;nbsp;?&amp;nbsp;process.getegid&amp;nbsp;:&amp;nbsp;()&amp;nbsp;=&amp;gt;&amp;nbsp;0;
const&amp;nbsp;geteuid&amp;nbsp;=&amp;nbsp;process.geteuid&amp;nbsp;?&amp;nbsp;process.geteuid&amp;nbsp;:&amp;nbsp;()&amp;nbsp;=&amp;gt;&amp;nbsp;0;

//&amp;nbsp;编辑文件&amp;nbsp;/app/server/index.ts
//&amp;nbsp;在文件顶部引入&amp;nbsp;pathToFileURL：
import&amp;nbsp;{&amp;nbsp;pathToFileURL&amp;nbsp;}&amp;nbsp;from&amp;nbsp;&amp;#39;node:url&amp;#39;;
//&amp;nbsp;找到&amp;nbsp;createDbClient&amp;nbsp;函数，将原来的：
const&amp;nbsp;db&amp;nbsp;=&amp;nbsp;drizzle(`file://${realPath}`);
//&amp;nbsp;替换为：
const&amp;nbsp;dbUrl&amp;nbsp;=&amp;nbsp;pathToFileURL(realPath).toString();
const&amp;nbsp;db&amp;nbsp;=&amp;nbsp;drizzle(dbUrl);&lt;/pre&gt;&lt;p&gt;修改完成后重新编译，再运行就没有报错了。&lt;/p&gt;&lt;p&gt;浏览器访问 http://localhost:3003/admin 进入 Headplane 的登录界面，输入之前创建的 API_KEY 点登录就可以进入到管理界面了。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;如果需要远程访问，可以在IIS上设置反向代理，将外部访问的请求转发到 http://localhost:3003 上，这样就可以通过 https://headscale.yourhost.com 来访问 Headplane 的管理界面了。&lt;/p&gt;&lt;p&gt;IIS反向代理规则：&lt;/p&gt;&lt;pre class=&quot;brush:xml;toolbar:false;&quot;&gt;&amp;lt;?xml&amp;nbsp;version=&amp;quot;1.0&amp;quot;&amp;nbsp;encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;system.webServer&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;rewrite&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;rules&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;rule&amp;nbsp;name=&amp;quot;Headplane&amp;quot;&amp;nbsp;stopProcessing=&amp;quot;true&amp;quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;match&amp;nbsp;url=&amp;quot;^(.*)$&amp;quot;&amp;nbsp;/&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;action&amp;nbsp;type=&amp;quot;Rewrite&amp;quot;&amp;nbsp;url=&amp;quot;http://localhost:3003/{R:1}&amp;quot;&amp;nbsp;/&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;serverVariables&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;set&amp;nbsp;name=&amp;quot;HTTP_X_FORWARDED_PROTO&amp;quot;&amp;nbsp;value=&amp;quot;https&amp;quot;&amp;nbsp;/&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/serverVariables&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/rule&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/rules&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/rewrite&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/system.webServer&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/pre&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;</description><pubDate>Sun, 24 May 2026 05:14:36 +0800</pubDate></item><item><title>Windows Server服务器上原生部署私有Tailscale服务端</title><link>https://blog.fairysoft.net/post/70.html</link><description>&lt;p&gt;Tailscale 是一款基于 WireGuard 协议的虚拟组网工具，它能将不同网络环境下的设备连接至同一个加密的虚拟局域网，无需公网 IP，也免去了复杂的端口映射和防火墙配置。&amp;nbsp;&lt;/p&gt;&lt;p&gt;WireGuard 要求连接的其中一端必须有公网 IP 或者在路由器上做端口映射。若两台设备都在严格的 NAT后方，通常无法直接建立连接。由于是点对点的连接，所以它的设置比较复杂，需要在每一台设备上单独手动配置密钥、IP 地址和路由等信息。一旦设备过多，管理会非常繁琐。&lt;/p&gt;&lt;p&gt;Tailscale 由于引入了中心协调服务器，所有设备连接到同一个虚拟网络中，由中心服务器给每个设备分配固定 IP 地址，通过图形化界面进行统一管理，它还能自动识别网络环境并实现 NAT 穿透。如果直连失败，它会通过加密中转服务器（DERP）转发流量，保证连接成功率。&amp;nbsp;&lt;/p&gt;&lt;p&gt;Tailscale 除了可以使用官方的服务器之外，还允许用户自建私有服务器。Headscale 是一个开源、自托管的 Tailscale 服务器。在 Windows Server 上搭建 Headscale 最便捷且推荐的方法是利用 WSL (Windows Subsystem for Linux) 安装 Linux 版本的 Headscale，或直接使用 Docker 部署，因为 Headscale 原生主要是针对 Linux 环境设计的。但是我的云服务器配置太低，我不想安装 Docker 服务。&lt;/p&gt;&lt;p&gt;打开Headscale 的 Github 主页看了一下，这个项目是用 Go 语言编写的，Go 语言是跨平台的，所以理论上应该可以直接在 Windows 上编译运行。&lt;/p&gt;&lt;p&gt;于是我就安装了 Go 语言的 Windows 版本，下载 Headscale 的源代码，然后输入以下指令来进行编译：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false&quot;&gt;$env:GOOS=&amp;quot;windows&amp;quot;
$env:GOARCH=&amp;quot;amd64&amp;quot;
$env:GOPROXY&amp;nbsp;=&amp;nbsp;&amp;quot;https://goproxy.cn,direct&amp;quot;
go&amp;nbsp;build&amp;nbsp;-o&amp;nbsp;headscale.exe&amp;nbsp;./cmd/headscale&lt;/pre&gt;&lt;p&gt;编译完成后在当前目录下生成了 headscale.exe 文件，说明编译已经成功了。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;接着按照官网的范例创建一个配置文件 config.yaml：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false&quot;&gt;#&amp;nbsp;基础访问配置
server_url:&amp;nbsp;https://myhost:8080
listen_addr:&amp;nbsp;0.0.0.0:8080
metrics_listen_addr:&amp;nbsp;127.0.0.1:9090
grpc_listen_addr:&amp;nbsp;0.0.0.0:50443
grpc_allow_insecure:&amp;nbsp;false
#&amp;nbsp;密钥与协议
noise:
&amp;nbsp;&amp;nbsp;private_key_path:&amp;nbsp;./data/noise_private.key
#&amp;nbsp;IP&amp;nbsp;分配
prefixes:
&amp;nbsp;&amp;nbsp;v4:&amp;nbsp;100.100.64.0/24
&amp;nbsp;&amp;nbsp;v6:&amp;nbsp;fd7a:115c:a1e0::/48
&amp;nbsp;&amp;nbsp;allocation:&amp;nbsp;sequential
#&amp;nbsp;DERP&amp;nbsp;核心配置
derp:
&amp;nbsp;&amp;nbsp;server:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;enabled:&amp;nbsp;true&amp;nbsp;#&amp;nbsp;开启内置&amp;nbsp;DERP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;region_id:&amp;nbsp;999
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;region_code:&amp;nbsp;&amp;quot;headscale&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;region_name:&amp;nbsp;&amp;quot;Headscale&amp;nbsp;Embedded&amp;nbsp;DERP&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;verify_clients:&amp;nbsp;true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stun_listen_addr:&amp;nbsp;&amp;quot;0.0.0.0:3478&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ipv4:&amp;nbsp;129.28.179.56
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private_key_path:&amp;nbsp;./data/derp_server_private.key
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;automatically_add_embedded_derp_region:&amp;nbsp;true
&amp;nbsp;&amp;nbsp;#&amp;nbsp;清空官方&amp;nbsp;URL，不使用外部路径
&amp;nbsp;&amp;nbsp;urls:&amp;nbsp;[]
&amp;nbsp;&amp;nbsp;paths:&amp;nbsp;[]
&amp;nbsp;&amp;nbsp;auto_update_enabled:&amp;nbsp;true
&amp;nbsp;&amp;nbsp;update_frequency:&amp;nbsp;3h
disable_check_updates:&amp;nbsp;false
#&amp;nbsp;其他设置
node:
&amp;nbsp;&amp;nbsp;expiry:&amp;nbsp;0
&amp;nbsp;&amp;nbsp;ephemeral:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;inactivity_timeout:&amp;nbsp;30m
&amp;nbsp;&amp;nbsp;routes:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ha:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;probe_interval:&amp;nbsp;10s
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;probe_timeout:&amp;nbsp;5s
#&amp;nbsp;数据库配置
database:
&amp;nbsp;&amp;nbsp;type:&amp;nbsp;sqlite
&amp;nbsp;&amp;nbsp;sqlite:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;path:&amp;nbsp;./data/db.sqlite
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;write_ahead_log:&amp;nbsp;true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;wal_autocheckpoint:&amp;nbsp;1000
#&amp;nbsp;证书配置
tls_cert_path:&amp;nbsp;./data/fairysoft.net-chain.pem
tls_key_path:&amp;nbsp;./data/fairysoft.net-key.pem
log:
&amp;nbsp;&amp;nbsp;#&amp;nbsp;Valid&amp;nbsp;log&amp;nbsp;levels:&amp;nbsp;panic,&amp;nbsp;fatal,&amp;nbsp;error,&amp;nbsp;warn,&amp;nbsp;info,&amp;nbsp;debug,&amp;nbsp;trace
&amp;nbsp;&amp;nbsp;level:&amp;nbsp;info
&amp;nbsp;&amp;nbsp;#&amp;nbsp;Output&amp;nbsp;formatting&amp;nbsp;for&amp;nbsp;logs:&amp;nbsp;text&amp;nbsp;or&amp;nbsp;json
&amp;nbsp;&amp;nbsp;format:&amp;nbsp;text
policy:
&amp;nbsp;&amp;nbsp;#&amp;nbsp;The&amp;nbsp;mode&amp;nbsp;can&amp;nbsp;be&amp;nbsp;&amp;quot;file&amp;quot;&amp;nbsp;or&amp;nbsp;&amp;quot;database&amp;quot;&amp;nbsp;that&amp;nbsp;defines
&amp;nbsp;&amp;nbsp;#&amp;nbsp;where&amp;nbsp;the&amp;nbsp;policies&amp;nbsp;are&amp;nbsp;stored&amp;nbsp;and&amp;nbsp;read&amp;nbsp;from.
&amp;nbsp;&amp;nbsp;mode:&amp;nbsp;file
&amp;nbsp;&amp;nbsp;#&amp;nbsp;If&amp;nbsp;the&amp;nbsp;mode&amp;nbsp;is&amp;nbsp;set&amp;nbsp;to&amp;nbsp;&amp;quot;file&amp;quot;,&amp;nbsp;the&amp;nbsp;path&amp;nbsp;to&amp;nbsp;a&amp;nbsp;HuJSON&amp;nbsp;file&amp;nbsp;containing&amp;nbsp;policies.
&amp;nbsp;&amp;nbsp;path:&amp;nbsp;&amp;quot;&amp;quot;
#&amp;nbsp;DNS&amp;nbsp;配置
dns:
&amp;nbsp;&amp;nbsp;magic_dns:&amp;nbsp;true
&amp;nbsp;&amp;nbsp;base_domain:&amp;nbsp;home
&amp;nbsp;&amp;nbsp;override_local_dns:&amp;nbsp;true
&amp;nbsp;&amp;nbsp;nameservers:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;global:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;223.5.5.5
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;114.114.114.114&lt;/pre&gt;&lt;p&gt;&lt;br/&gt;&lt;span style=&quot;text-wrap-mode: nowrap;&quot;&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;测试一下看能不能运行&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false&quot;&gt;.\headscale.exe&amp;nbsp;serve&lt;/pre&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;测试通过之后就可以使用 nssm.exe 将 Headscale 注册为服务，这样就不需要每次都手动运行了。&lt;/p&gt;&lt;p&gt;先下载 nssm.exe，然后输入以下指令注册服务：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false&quot;&gt;.\nssm.exe&amp;nbsp;install&amp;nbsp;headscale&lt;/pre&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;在弹出来的界面中做以下设置：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false&quot;&gt;Path:&amp;nbsp;C:\headscale\headscale.exe
Startup&amp;nbsp;directory:&amp;nbsp;C:\headscale
Arguments:&amp;nbsp;serve&amp;nbsp;-c&amp;nbsp;C:\headscale\config\config.yam&lt;/pre&gt;&lt;p&gt;设置好之后点击安装，安装完成后就可以在服务管理器里面看到 headscale 服务了，右键点击启动即可。&lt;/p&gt;&lt;p&gt;到现在为止，Headscale 服务器的编译安装工作就全部完成了。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;然后我们进行配置。首先，添加一个用户，后面的操作会用到，用户名随便取：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false&quot;&gt;.\headscale.exe&amp;nbsp;users&amp;nbsp;create&amp;nbsp;Admin&lt;/pre&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;部署好了服务器，接下来就是将客户端加入到这个私有的虚拟局域网，第一种方法是先在客户端上发起入网认证申请，我第一个测试的设备是威联通：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false&quot;&gt;/share/CACHEDEV1_DATA/.qpkg/Tailscale/tailscale&amp;nbsp;up&amp;nbsp;--login-server&amp;nbsp;https://myhost:8080&amp;nbsp;--reset&amp;nbsp;--force-reauth&lt;/pre&gt;&lt;p&gt;输入这条指令按回车之后终端会输出一个 URL，将这个 URL 复制粘贴到浏览器打开就会向服务器提交入网申请，服务器会在浏览器上返回一行包含了临时密钥的审批指令，按照提示将这条指令在服务器上执行就可以通过审批：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false&quot;&gt;.\headscale.exe&amp;nbsp;auth&amp;nbsp;register&amp;nbsp;--auth-id&amp;nbsp;hskey-authreq-xxxxxx&amp;nbsp;--user&amp;nbsp;Admin&lt;/pre&gt;&lt;p&gt;审批通过之后，这台设备就成功加入了这个虚拟局域网，有图形界面的客户端这时就可以看到自己的登录状态以及这个局域网内其他所有设备了。&lt;/p&gt;&lt;p&gt;这种登录方式主要适用于 Linux 这种使用命令行的客户端。对于有操作界面的 Tailscale 客户端就不用输入指令这么麻烦，直接在菜单中直接输入自定义的登录网址之后会自动弹出系统浏览器并打开入网审批的 URL。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;对于 Windows、安卓、苹果等有操作界面的客户端，更方便的入网申请方法是先在服务器上创建一个预认证密钥(authkey)，然后在客户端上输入服务器的登录网址和这个密钥来加入网络。在服务器上创建预认证密钥的指令如下：&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false&quot;&gt;.\headscale.exe&amp;nbsp;auth&amp;nbsp;create-api-key&amp;nbsp;--user&amp;nbsp;Admin&lt;/pre&gt;&lt;p&gt;这种入网认证方法同样适用于纯命令行的 Linux 客户端：&lt;/p&gt;&lt;pre class=&quot;prism-highlight prism-language-bash&quot;&gt;tailscale&amp;nbsp;up&amp;nbsp;--login-server=https://myhost:8080&amp;nbsp;--authkey&amp;nbsp;hskey-auth-xxxxxxxx&lt;/pre&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;如果需要通过威联通访问整个家庭局域网，可以在威联通上声明子网路由(--advertise-routes)，如果需要将威联通作为出口进行互联网访问，可以声明出口节点(--advertise-exit-node)：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false&quot;&gt;/share/CACHEDEV1_DATA/.qpkg/Tailscale/tailscale&amp;nbsp;up&amp;nbsp;\
&amp;nbsp;&amp;nbsp;--login-server&amp;nbsp;https://myhost:8080&amp;nbsp;\
&amp;nbsp;&amp;nbsp;--advertise-routes=192.168.1.0/24&amp;nbsp;\
&amp;nbsp;&amp;nbsp;--advertise-exit-node&amp;nbsp;\
&amp;nbsp;&amp;nbsp;--reset&lt;/pre&gt;&lt;p&gt;客户端申明了路由之后并不会自动生效，而是需要在 Headscale 服务端进行审批，因为出于安全考虑，服务端默认不信任客户端声明的路由，需要手动进行确认。&lt;/p&gt;&lt;pre class=&quot;brush:ps;toolbar:false&quot;&gt;#查看威联通节点的&amp;nbsp;ID
.\headscale.exe&amp;nbsp;nodes&amp;nbsp;list
#查看节点&amp;nbsp;2&amp;nbsp;的所有路由请求
.\headscale.exe&amp;nbsp;nodes&amp;nbsp;list-routes&amp;nbsp;-i&amp;nbsp;2
#同意审批
.\headscale.exe&amp;nbsp;nodes&amp;nbsp;approve-routes&amp;nbsp;-i&amp;nbsp;2&amp;nbsp;-r&amp;nbsp;&amp;quot;192.168.1.0/24,0.0.0.0/0,::/0&amp;quot;&lt;/pre&gt;&lt;p&gt;服务器端完成确认之后，其他客户端就能通过威联通访问整个家庭局域网了。其他设备上还可以选择是否通过威联通进行互联网访问。如果想要让家庭局域网中的其他没有安装 Tailscale 的设备也能访问整个虚拟局域网，可以在出口路由器上添加一条静态路由，把 100.100.64.0/24 这个网段路由到威联通的局域网 IP 地址上。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;其实以上这些创建用户、审批客户端、创建预登录密钥、审批路由等操作都可以在图形化的管理界面（Web UI）中完成。下一篇文章我会介绍如何部署 Headscale 的 Web UI 管理界面。&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;</description><pubDate>Sun, 24 May 2026 04:12:26 +0800</pubDate></item><item><title>威联通的防火墙把自己干死了，多亏了Tailscale</title><link>https://blog.fairysoft.net/post/69.html</link><description>&lt;p&gt;
    今天在家里电脑上打开威联通的 QTS 系统，结果发现页面打不开。
&lt;/p&gt;
&lt;p&gt;
    我还以为是 QTS 系统崩溃了，于是我想通过 SSH 连接到威联通上，结果发现 SSH 也连不上。
&lt;/p&gt;
&lt;p&gt;
    然后我输入 ping 指令测试，居然连 ping 都不通！
&lt;/p&gt;
&lt;p&gt;
    打开机柜检查发现网口的指示灯正常闪亮，硬件连接是好的。
&lt;/p&gt;
&lt;p&gt;
    我想起来之前部署过 Tailscale 的服务，于是在手机上打开 Tailscale 看了一下，结果惊奇的发现威联通竟然在线。
&lt;/p&gt;
&lt;p&gt;
    于是我试着用 Tailscale 的虚拟IP地址连接，居然成功连上了。
&lt;/p&gt;
&lt;p&gt;
    这就有点诡异了，物理网卡无法连接但是虚拟网卡却能连接，这太不正常。
&lt;/p&gt;
&lt;p&gt;
    仔细分析了一下，怀疑是防火墙的问题。
&lt;/p&gt;
&lt;p&gt;
    于是开始排查，先试着在 QTS 里面关闭防火墙，结果 QuFirewall 的窗口一直转圈，根本无法加载。
&lt;/p&gt;
&lt;p&gt;
    登录 SSH，输入 /etc/init.d/QuFirewall.sh stop，执行成功。
&lt;/p&gt;
&lt;p&gt;
    再测试 QTS，现在能通过局域网 IP 打开了，再输入 /etc/init.d/QuFirewall.sh start
&lt;/p&gt;
&lt;p&gt;
    出现一堆错误，内容基本看不懂，大概意思就是启动失败。测试局域网访问，失败。
&lt;/p&gt;
&lt;p&gt;
    现在终于真相大白了，看来确实是防火墙的问题。
&lt;/p&gt;
&lt;p&gt;
    分析原因，我昨天访问 QTS 的时候系统提示启用安全中心，于是我就点了启用。进入安全中心之后，显示防火墙没有开，于是我就点了一下防火墙的启动按钮。
&lt;/p&gt;
&lt;p&gt;
    可能是因为我很长时间没有开启 QuFirewall，期间又升级过 QTS 系统或者 QuFirewall 应用程序。之前的防火墙配置文件可能不兼容了，导致防火墙无法正常启动。而当时我访问 NAS 的设备是通过 Tailscale 接入的，所以当时并没有发现异常。
&lt;/p&gt;
&lt;p&gt;
    防火墙正常启动时，第一步就是先封锁住所有的网络，然后再按照配置文件里面的规则逐条放行，结果现在防火墙在完成第一步之后就卡死了，所以整个网络就永久被封住了。
&lt;/p&gt;
&lt;p&gt;
    那为什么 Tailscale 能突破这个封锁呢，我想可能是因为防火墙封锁的是入站连接，而 Tailscale 是一个从内部发起的出站连接，所以能够突破防火墙的封锁。
&lt;/p&gt;
&lt;p&gt;
    于是卸载掉 QuFirewall，重新安装了一下，安装完成后重新设置规则，一切正常。
&lt;/p&gt;
&lt;p&gt;
    这次多亏了 Tailscale，留下了自救的通道。看来这东西不仅可以用来做内网穿透，关键时刻还能救砖。
&lt;/p&gt;
&lt;p&gt;
    下次我来讲一讲如何在 Windows Server 服务器上部署 Tailscale 的私有服务端。
&lt;/p&gt;
&lt;p&gt;
    &lt;br/&gt;
&lt;/p&gt;</description><pubDate>Sun, 24 May 2026 00:54:12 +0800</pubDate></item><item><title>在不同Linux系统下用脚本实现SSL证书的自动更新</title><link>https://blog.fairysoft.net/post/68.html</link><description>&lt;p&gt;从去年开始，各大ssl证书服务商统一把免费证书的有效期从 1 年缩短到了 90 天，以前一年更新一次，手动操作还可以忍，现在每年要操作四到五次，就有点麻烦了。于是我在 Windows Server 云服务器上部署了 Win-acme，解决了证书更新的问题。但是我还有几台其他的 Linux 设备，也需要同步更新，于是我想到了通过计划任务定时执行脚本来实现证书自动更新。&lt;/p&gt;&lt;p&gt;首先，把 Win-acme 自动下载的证书挂到 ftp 下，直接用 IIS 内置的的 ftp 服务就行，注意开启 SSL。&lt;/p&gt;&lt;p&gt;原理很简单，写一个 sh 脚本检查证书的有效期，如果临近过期，则连接到远程 ftp 服务器，下载最新的证书并替换原来快过期的证书，然后重启 Web 服务器。最后把这个脚本添加到系统的计划任务中，每天定时执行就行了。&lt;/p&gt;&lt;p&gt;这一切在 Ubuntu 和 Debian 系统下都非常容易实现，直接贴代码，/etc/ssl/update_ssl.sh：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false;&quot;&gt;#!/usr/bin/bash
#&amp;nbsp;自动获取当前脚本所在的绝对路径
SCRIPT_DIR=$(cd&amp;nbsp;&amp;quot;$(dirname&amp;nbsp;&amp;quot;$0&amp;quot;)&amp;quot;;&amp;nbsp;pwd)

#&amp;nbsp;如果不是在终端运行（说明是&amp;nbsp;crontab&amp;nbsp;触发），则启用静默和错误日志模式
if&amp;nbsp;[&amp;nbsp;!&amp;nbsp;-t&amp;nbsp;1&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exec&amp;nbsp;2&amp;gt;&amp;gt;&amp;nbsp;&amp;quot;${SCRIPT_DIR}/update_ssl_err.log&amp;quot;&amp;nbsp;&amp;gt;/dev/null
fi

#&amp;nbsp;====================&amp;nbsp;配置区域&amp;nbsp;====================
FTP_HOST=&amp;quot;ftp.your_host&amp;quot;
FTP_USER=&amp;quot;ftp_user&amp;quot;
FTP_PASS=&amp;quot;password&amp;quot;
FTP_DIR=&amp;quot;/ssl&amp;quot;
FTP_URL=&amp;quot;ftp://${FTP_HOST}${FTP_DIR}&amp;quot;
CURL_OPTS=&amp;quot;--ssl-reqd&amp;nbsp;-k&amp;nbsp;-sS&amp;nbsp;-u&amp;nbsp;${FTP_USER}:${FTP_PASS}&amp;quot;

REMOTE_CERT_FILE=&amp;quot;fairysoft.net-chain.pem&amp;quot;&amp;nbsp;
REMOTE_KEY_FILE=&amp;quot;fairysoft.net-key.pem&amp;quot;&amp;nbsp;&amp;nbsp;

LOCAL_CERT_FILE=&amp;quot;/etc/ssl/fairysoft.net-chain.pem&amp;quot;
LOCAL_KEY_FILE=&amp;quot;/etc/ssl/fairysoft.net-key.pem&amp;quot;

NGINX_RELOAD_CMD=&amp;quot;systemctl&amp;nbsp;reload&amp;nbsp;nginx&amp;quot;
EXPIRE_DAYS=28
#&amp;nbsp;=================================================

#&amp;nbsp;检查本地证书文件是否存在
if&amp;nbsp;[&amp;nbsp;!&amp;nbsp;-f&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;本地证书文件不存在，将直接下载新证书。&amp;quot;
else
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;获取当前证书的过期时间并计算剩余天数
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;EXPIRE_DATE=$(openssl&amp;nbsp;x509&amp;nbsp;-in&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;-enddate&amp;nbsp;-noout&amp;nbsp;|&amp;nbsp;cut&amp;nbsp;-d=&amp;nbsp;-f2)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;EXPIRE_SECS=$(date&amp;nbsp;-d&amp;nbsp;&amp;quot;$EXPIRE_DATE&amp;quot;&amp;nbsp;+%s)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CURRENT_SECS=$(date&amp;nbsp;+%s)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;DAYS_LEFT=$((&amp;nbsp;($EXPIRE_SECS&amp;nbsp;-&amp;nbsp;$CURRENT_SECS)&amp;nbsp;/&amp;nbsp;86400&amp;nbsp;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;EXPIRE_DATE=$(date&amp;nbsp;-d&amp;nbsp;&amp;quot;$EXPIRE_DATE&amp;quot;&amp;nbsp;&amp;#39;+%Y-%m-%d&amp;#39;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;判断是否需要更新
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if&amp;nbsp;[&amp;nbsp;&amp;quot;$DAYS_LEFT&amp;quot;&amp;nbsp;-ge&amp;nbsp;&amp;quot;$EXPIRE_DAYS&amp;quot;&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;当前证书有效期($EXPIRE_DATE)，将在&amp;nbsp;$DAYS_LEFT&amp;nbsp;天后过期&amp;nbsp;，无需更新。&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit&amp;nbsp;0
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;当前证书有效期($EXPIRE_DATE)，将在&amp;nbsp;$DAYS_LEFT&amp;nbsp;天后过期&amp;nbsp;，需要更新。&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fi
fi

echo&amp;nbsp;&amp;quot;开始从远程&amp;nbsp;FTP&amp;nbsp;服务器下载新证书...&amp;quot;

#&amp;nbsp;通过&amp;nbsp;curl&amp;nbsp;从&amp;nbsp;FTP&amp;nbsp;服务器下载最新证书
echo&amp;nbsp;&amp;quot;正在下载证书...&amp;quot;
curl&amp;nbsp;$CURL_OPTS&amp;nbsp;&amp;quot;${FTP_URL}/${REMOTE_CERT_FILE}&amp;quot;&amp;nbsp;-o&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.new&amp;quot;

echo&amp;nbsp;&amp;quot;正在下载私钥...&amp;quot;
curl&amp;nbsp;$CURL_OPTS&amp;nbsp;&amp;quot;${FTP_URL}/${REMOTE_KEY_FILE}&amp;quot;&amp;nbsp;-o&amp;nbsp;&amp;quot;${LOCAL_KEY_FILE}.new&amp;quot;

#&amp;nbsp;检查下载是否成功
if&amp;nbsp;[&amp;nbsp;-s&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.new&amp;quot;&amp;nbsp;]&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;[&amp;nbsp;-s&amp;nbsp;&amp;quot;${LOCAL_KEY_FILE}.new&amp;quot;&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;下载成功！正在替换旧证书...&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;备份旧证书
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[&amp;nbsp;-f&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;]&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;mv&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.bak&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[&amp;nbsp;-f&amp;nbsp;&amp;quot;$LOCAL_KEY_FILE&amp;quot;&amp;nbsp;]&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;mv&amp;nbsp;&amp;quot;$LOCAL_KEY_FILE&amp;quot;&amp;nbsp;&amp;quot;${LOCAL_KEY_FILE}.bak&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;移动新证书到正式位置
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mv&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.new&amp;quot;&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mv&amp;nbsp;&amp;quot;${LOCAL_KEY_FILE}.new&amp;quot;&amp;nbsp;&amp;quot;$LOCAL_KEY_FILE&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;设置正确的权限
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chmod&amp;nbsp;644&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chmod&amp;nbsp;600&amp;nbsp;&amp;quot;$LOCAL_KEY_FILE&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;重载&amp;nbsp;Nginx&amp;nbsp;使新证书生效
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;正在重载&amp;nbsp;Nginx&amp;nbsp;服务...&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if&amp;nbsp;eval&amp;nbsp;$NGINX_RELOAD_CMD;&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;SSL&amp;nbsp;证书更新并重载&amp;nbsp;Nginx&amp;nbsp;成功！&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;Nginx&amp;nbsp;重载失败，请检查配置文件！&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fi
else
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;错误：从&amp;nbsp;FTP&amp;nbsp;下载证书失败或文件为空，更新终止。&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;rm&amp;nbsp;-f&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.new&amp;quot;&amp;nbsp;&amp;quot;${LOCAL_KEY_FILE}.new&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit&amp;nbsp;1
fi

echo&amp;nbsp;&amp;quot;SSL&amp;nbsp;证书自动更新成功并已生效！&amp;quot;&lt;/pre&gt;&lt;p&gt;然后用 crontab -e 编辑计划任务，添加：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false;&quot;&gt;0&amp;nbsp;2&amp;nbsp;*&amp;nbsp;*&amp;nbsp;*&amp;nbsp;/usr/bin/bash&amp;nbsp;/etc/ssl/update-ssl.sh&lt;/pre&gt;&lt;p&gt;完成！就这么简单&lt;/p&gt;&lt;p&gt;在搞定了 Debian 服务器之后，我又开始研究威联通的 NAS，威联通的 QNAP 系统是深度定制的 Linux 系统，跟常见的 Ubuntu/Debian 系统区别很大。最大的区别，也是我踩过的最大的坑，就是证书的格式。&lt;/p&gt;&lt;p&gt;QNAP 虽然使用的是 Apache 作为 Web 服务器，但是证书管理却使用了 stunnel，配置文件如下：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false;&quot;&gt;SSLCertificateFile&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;quot;/etc/stunnel/stunnel.pem&amp;quot;&amp;nbsp;&amp;nbsp;#&amp;nbsp;这里面同时包含私钥和网站证书
SSLCertificateChainFile&amp;nbsp;&amp;quot;/etc/stunnel/uca.pem&amp;quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;中继证书&lt;/pre&gt;&lt;p&gt;从配置文件可以看出来，威联通使用的证书格式跟 Nginx 和 Apache 都不一样，私钥和网站证书合并成一个证书链，而中继证书却单独放。&lt;/p&gt;&lt;p&gt;经过几天时间的折腾，最后终于搞定了，/mnt/HDA_ROOT/.config/stunnel/update_ssl.sh：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false;&quot;&gt;#!/bin/bash
#&amp;nbsp;自动获取当前脚本所在的绝对路径
SCRIPT_DIR=$(cd&amp;nbsp;&amp;quot;$(dirname&amp;nbsp;&amp;quot;$0&amp;quot;)&amp;quot;;&amp;nbsp;pwd)

#&amp;nbsp;如果不是在终端运行（说明是&amp;nbsp;crontab&amp;nbsp;触发），则启用静默和错误日志模式
if&amp;nbsp;[&amp;nbsp;!&amp;nbsp;-t&amp;nbsp;1&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exec&amp;nbsp;2&amp;gt;&amp;gt;&amp;nbsp;&amp;quot;${SCRIPT_DIR}/update_ssl_err.log&amp;quot;&amp;nbsp;&amp;gt;/dev/null
fi

#&amp;nbsp;====================&amp;nbsp;配置区域&amp;nbsp;====================
FTP_HOST=&amp;quot;ftp.your_host&amp;quot;
FTP_USER=&amp;quot;ftp_user&amp;quot;
FTP_PASS=&amp;quot;password&amp;quot;
FTP_DIR=&amp;quot;/ssl&amp;quot;
FTP_URL=&amp;quot;ftp://${FTP_HOST}${FTP_DIR}&amp;quot;
CURL_OPTS=&amp;quot;--ssl-reqd&amp;nbsp;-k&amp;nbsp;-sS&amp;nbsp;-u&amp;nbsp;${FTP_USER}:${FTP_PASS}&amp;quot;

REMOTE_CRT_FILE=&amp;quot;fairysoft.net-crt.pem&amp;quot;
REMOTE_KEY_FILE=&amp;quot;fairysoft.net-key.pem&amp;quot;
REMOTE_CHAIN_FILE=&amp;quot;fairysoft.net-chain-only.pem&amp;quot;

TMP_DIR=&amp;quot;/tmp/ssl_download&amp;quot;
STUNNEL_DIR=&amp;quot;/mnt/HDA_ROOT/.config/stunnel&amp;quot;
LOCAL_CERT_FILE=&amp;quot;${STUNNEL_DIR}/stunnel.pem&amp;quot;
LOCAL_UCA_FILE=&amp;quot;${STUNNEL_DIR}/uca.pem&amp;quot;
EXPIRE_DAYS=28
#&amp;nbsp;=================================================

#&amp;nbsp;检查本地证书文件是否存在
if&amp;nbsp;[&amp;nbsp;!&amp;nbsp;-f&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;本地证书文件不存在，将直接下载新证书。&amp;quot;
else
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;获取当前证书的过期时间并计算剩余天数
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;EXPIRE_DATE=$(openssl&amp;nbsp;x509&amp;nbsp;-in&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;-enddate&amp;nbsp;-noout&amp;nbsp;|&amp;nbsp;cut&amp;nbsp;-d=&amp;nbsp;-f2)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;EXPIRE_SECS=$(date&amp;nbsp;-d&amp;nbsp;&amp;quot;$EXPIRE_DATE&amp;quot;&amp;nbsp;+%s)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CURRENT_SECS=$(date&amp;nbsp;+%s)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;DAYS_LEFT=$((&amp;nbsp;($EXPIRE_SECS&amp;nbsp;-&amp;nbsp;$CURRENT_SECS)&amp;nbsp;/&amp;nbsp;86400&amp;nbsp;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;EXPIRE_DATE=$(date&amp;nbsp;-d&amp;nbsp;&amp;quot;$EXPIRE_DATE&amp;quot;&amp;nbsp;&amp;#39;+%Y-%m-%d&amp;#39;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;判断是否需要更新
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if&amp;nbsp;[&amp;nbsp;&amp;quot;$DAYS_LEFT&amp;quot;&amp;nbsp;-ge&amp;nbsp;&amp;quot;$EXPIRE_DAYS&amp;quot;&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;当前证书有效期($EXPIRE_DATE)，将在&amp;nbsp;$DAYS_LEFT&amp;nbsp;天后过期&amp;nbsp;，无需更新。&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit&amp;nbsp;0
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;当前证书有效期($EXPIRE_DATE)，将在&amp;nbsp;$DAYS_LEFT&amp;nbsp;天后过期&amp;nbsp;，需要更新。&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fi
fi

echo&amp;nbsp;&amp;quot;开始从远程&amp;nbsp;FTP&amp;nbsp;服务器下载新证书...&amp;quot;

#&amp;nbsp;创建并清理临时下载目录
mkdir&amp;nbsp;-p&amp;nbsp;&amp;quot;$TMP_DIR&amp;quot;

#&amp;nbsp;通过&amp;nbsp;curl&amp;nbsp;从&amp;nbsp;FTP&amp;nbsp;服务器下载最新证书
echo&amp;nbsp;&amp;quot;正在下载&amp;nbsp;crt&amp;nbsp;文件...&amp;quot;
curl&amp;nbsp;$CURL_OPTS&amp;nbsp;&amp;quot;${FTP_URL}/${REMOTE_CRT_FILE}&amp;quot;&amp;nbsp;-o&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_CRT_FILE}&amp;quot;

echo&amp;nbsp;&amp;quot;正在下载&amp;nbsp;key&amp;nbsp;文件...&amp;quot;
curl&amp;nbsp;$CURL_OPTS&amp;nbsp;&amp;quot;${FTP_URL}/${REMOTE_KEY_FILE}&amp;quot;&amp;nbsp;-o&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_KEY_FILE}&amp;quot;

echo&amp;nbsp;&amp;quot;正在下载&amp;nbsp;chain&amp;nbsp;文件...&amp;quot;
curl&amp;nbsp;$CURL_OPTS&amp;nbsp;&amp;quot;${FTP_URL}/${REMOTE_CHAIN_FILE}&amp;quot;&amp;nbsp;-o&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_CHAIN_FILE}&amp;quot;

#&amp;nbsp;校验下载的文件大小，防止下载了空文件导致服务瘫痪
if&amp;nbsp;[&amp;nbsp;!&amp;nbsp;-s&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_CRT_FILE}&amp;quot;&amp;nbsp;]&amp;nbsp;||&amp;nbsp;[&amp;nbsp;!&amp;nbsp;-s&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_KEY_FILE}&amp;quot;&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;错误：从&amp;nbsp;FTP&amp;nbsp;下载证书失败或文件为空！更新终止。&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;rm&amp;nbsp;-rf&amp;nbsp;&amp;quot;$TMP_DIR&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit&amp;nbsp;1
fi

echo&amp;nbsp;&amp;quot;下载完成！开始更新旧证书...&amp;quot;
#&amp;nbsp;备份旧证书
[&amp;nbsp;-f&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;]&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;mv&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.bak&amp;quot;
[&amp;nbsp;-f&amp;nbsp;&amp;quot;$LOCAL_UCA_FILE&amp;quot;&amp;nbsp;]&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;mv&amp;nbsp;&amp;quot;$LOCAL_UCA_FILE&amp;quot;&amp;nbsp;&amp;quot;${LOCAL_UCA_FILE}.bak&amp;quot;

#&amp;nbsp;合并私钥与域名证书到&amp;nbsp;stunnel.pem，私钥在前，证书在后&amp;nbsp;
cat&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_KEY_FILE}&amp;quot;&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_CRT_FILE}&amp;quot;&amp;nbsp;&amp;gt;&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}&amp;quot;

#&amp;nbsp;写入中间证书链
if&amp;nbsp;[&amp;nbsp;-s&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_CHAIN_FILE}&amp;quot;&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cat&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_CHAIN_FILE}&amp;quot;&amp;nbsp;&amp;gt;&amp;nbsp;&amp;quot;${LOCAL_UCA_FILE}&amp;quot;
fi

#&amp;nbsp;覆盖两个&amp;nbsp;backup&amp;nbsp;文件
cat&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_CRT_FILE}&amp;quot;&amp;nbsp;&amp;gt;&amp;nbsp;&amp;quot;${STUNNEL_DIR}/backup.cert&amp;quot;
cat&amp;nbsp;&amp;quot;${TMP_DIR}/${REMOTE_KEY_FILE}&amp;quot;&amp;nbsp;&amp;gt;&amp;nbsp;&amp;quot;${STUNNEL_DIR}/backup.key&amp;quot;

chmod&amp;nbsp;600&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}&amp;quot;&amp;nbsp;&amp;quot;${LOCAL_UCA_FILE}&amp;quot;

#&amp;nbsp;清理临时文件
rm&amp;nbsp;-rf&amp;nbsp;&amp;quot;$TMP_DIR&amp;quot;

echo&amp;nbsp;&amp;quot;正在重启系统服务使证书生效...&amp;quot;

#&amp;nbsp;重启核心&amp;nbsp;stunnle、web&amp;nbsp;与反向代理服务
/etc/init.d/stunnel.sh&amp;nbsp;stop
sleep&amp;nbsp;3
/etc/init.d/Qthttpd.sh&amp;nbsp;restart&amp;nbsp;2&amp;gt;&amp;amp;1&amp;nbsp;|&amp;nbsp;grep&amp;nbsp;-v&amp;nbsp;&amp;quot;php_ext.ini&amp;quot;
sleep&amp;nbsp;3
/etc/init.d/thttpd.sh&amp;nbsp;restart
sleep&amp;nbsp;3
/etc/init.d/stunnel.sh&amp;nbsp;start
sleep&amp;nbsp;3
if&amp;nbsp;[&amp;nbsp;-f&amp;nbsp;&amp;quot;/etc/init.d/reverse_proxy.sh&amp;quot;&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;/etc/init.d/reverse_proxy.sh&amp;nbsp;reload&amp;nbsp;&amp;nbsp;2&amp;gt;&amp;amp;1&amp;nbsp;|&amp;nbsp;grep&amp;nbsp;-v&amp;nbsp;&amp;quot;AH00558&amp;quot;
fi

echo&amp;nbsp;&amp;quot;SSL&amp;nbsp;证书自动更新成功并已生效！&amp;quot;&lt;/pre&gt;&lt;p&gt;脚本跑通之后就是添加计划任务，威联通系统添加计划任务的方法跟其他 Linux 系统不一样，网上有很多教程，很容易就能搜到。需要先编辑 /etc/config/crontab，可以使用 vi 或者 nano，然后再执行指令：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false;&quot;&gt;crontab&amp;nbsp;/etc/config/crontab&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;/etc/init.d/crond.sh&amp;nbsp;restart&lt;/pre&gt;&lt;p&gt;在搞定了威联通之后，我又把目光转到了我家里的一台 Home Assistant 服务器，HAOS 系统更加特殊，虽然底层是 Linux，但是默认并没有开启 SSH，需要先下载一个 SSH工具。&lt;/p&gt;&lt;p&gt;在 HA 插件商店里，搜索并安装 Advanced SSH &amp;amp; Web Terminal，开启&amp;nbsp;SSH，编写脚本测试，运行正常，/ssl/update_ssl.sh：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false;&quot;&gt;#!/bin/bash
#&amp;nbsp;自动获取当前脚本所在的绝对路径
SCRIPT_DIR=$(cd&amp;nbsp;&amp;quot;$(dirname&amp;nbsp;&amp;quot;$0&amp;quot;)&amp;quot;;&amp;nbsp;pwd)

#&amp;nbsp;如果不是在终端运行（说明是&amp;nbsp;crontab&amp;nbsp;触发），则启用静默和错误日志模式
if&amp;nbsp;[&amp;nbsp;!&amp;nbsp;-t&amp;nbsp;1&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exec&amp;nbsp;2&amp;gt;&amp;gt;&amp;nbsp;&amp;quot;${SCRIPT_DIR}/update_ssl_err.log&amp;quot;&amp;nbsp;&amp;gt;/dev/null
fi
#&amp;nbsp;====================&amp;nbsp;HAOS&amp;nbsp;专用配置区域&amp;nbsp;====================
FTP_HOST=&amp;quot;ftp.your_host&amp;quot;
FTP_USER=&amp;quot;ftp_user&amp;quot;
FTP_PASS=&amp;quot;password&amp;quot;
FTP_DIR=&amp;quot;/ssl&amp;quot;
FTP_URL=&amp;quot;ftp://${FTP_HOST}${FTP_DIR}&amp;quot;
CURL_OPTS=&amp;quot;--ssl-reqd&amp;nbsp;-k&amp;nbsp;-sS&amp;nbsp;-u&amp;nbsp;${FTP_USER}:${FTP_PASS}&amp;quot;

REMOTE_CERT_FILE=&amp;quot;fairysoft.net-chain.pem&amp;quot;&amp;nbsp;
REMOTE_KEY_FILE=&amp;quot;fairysoft.net-key.pem&amp;quot;&amp;nbsp;&amp;nbsp;

LOCAL_CERT_FILE=&amp;quot;/ssl/fairysoft.net-chain.pem&amp;quot;
LOCAL_KEY_FILE=&amp;quot;/ssl/fairysoft.net-key.pem&amp;quot;

NGINX_RELOAD_CMD=&amp;quot;ha&amp;nbsp;apps&amp;nbsp;restart&amp;nbsp;core_nginx_proxy&amp;quot;
EXPIRE_DAYS=28
#&amp;nbsp;=========================================================


#&amp;nbsp;检查本地证书文件是否存在
if&amp;nbsp;[&amp;nbsp;!&amp;nbsp;-f&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;];&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;本地证书文件不存在，将直接下载新证书。&amp;quot;
else
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;获取&amp;nbsp;OpenSSL&amp;nbsp;的原始英文时间
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;RAW_DATE=$(openssl&amp;nbsp;x509&amp;nbsp;-in&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;-enddate&amp;nbsp;-noout&amp;nbsp;|&amp;nbsp;cut&amp;nbsp;-d=&amp;nbsp;-f2)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;示例格式:&amp;nbsp;May&amp;nbsp;30&amp;nbsp;23:59:59&amp;nbsp;2026&amp;nbsp;GMT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;针对&amp;nbsp;HAOS&amp;nbsp;(Alpine)&amp;nbsp;环境，用&amp;nbsp;awk&amp;nbsp;将英文月份转换为纯数字格式&amp;nbsp;(YYYY-MM-DD&amp;nbsp;HH:MM:SS)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CLEAN_DATE=$(echo&amp;nbsp;&amp;quot;$RAW_DATE&amp;quot;&amp;nbsp;|&amp;nbsp;awk&amp;nbsp;&amp;#39;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;BEGIN&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;split(&amp;quot;Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec&amp;quot;,&amp;nbsp;months,&amp;nbsp;&amp;quot;|&amp;quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for&amp;nbsp;(i=1;&amp;nbsp;i&amp;lt;=12;&amp;nbsp;i++)&amp;nbsp;m_num[months[i]]&amp;nbsp;=&amp;nbsp;sprintf(&amp;quot;%02d&amp;quot;,&amp;nbsp;i);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print&amp;nbsp;$4&amp;nbsp;&amp;quot;-&amp;quot;&amp;nbsp;m_num[$1]&amp;nbsp;&amp;quot;-&amp;quot;&amp;nbsp;sprintf(&amp;quot;%02d&amp;quot;,&amp;nbsp;$2)&amp;nbsp;&amp;quot;&amp;nbsp;&amp;quot;&amp;nbsp;$3
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&amp;#39;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;EXPIRE_DATE=$(date&amp;nbsp;-d&amp;nbsp;&amp;quot;$CLEAN_DATE&amp;quot;&amp;nbsp;&amp;#39;+%Y-%m-%d&amp;#39;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;计算具体的剩余天数&amp;nbsp;$DAYS_LEFT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;EXPIRE_SECS=$(date&amp;nbsp;-d&amp;nbsp;&amp;quot;$CLEAN_DATE&amp;quot;&amp;nbsp;+%s)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CURRENT_SECS=$(date&amp;nbsp;+%s)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;DAYS_LEFT=$((&amp;nbsp;($EXPIRE_SECS&amp;nbsp;-&amp;nbsp;$CURRENT_SECS)&amp;nbsp;/&amp;nbsp;86400&amp;nbsp;))

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;检查证书是否在指定的&amp;nbsp;$EXPIRE_DAYS&amp;nbsp;天数内过期
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CHECK_SECS=$((&amp;nbsp;EXPIRE_DAYS&amp;nbsp;*&amp;nbsp;86400&amp;nbsp;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if&amp;nbsp;openssl&amp;nbsp;x509&amp;nbsp;-in&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;-checkend&amp;nbsp;$CHECK_SECS&amp;nbsp;&amp;gt;&amp;nbsp;/dev/null;&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;当前证书有效期($EXPIRE_DATE)，将在&amp;nbsp;$DAYS_LEFT&amp;nbsp;天后过期&amp;nbsp;，无需更新。&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit&amp;nbsp;0
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;当前证书有效期($EXPIRE_DATE)，将在&amp;nbsp;$DAYS_LEFT&amp;nbsp;天后过期&amp;nbsp;，需要更新。&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fi
fi

echo&amp;nbsp;&amp;quot;开始从远程&amp;nbsp;FTP&amp;nbsp;服务器下载新证书...&amp;quot;

#&amp;nbsp;通过&amp;nbsp;curl&amp;nbsp;从&amp;nbsp;FTP&amp;nbsp;服务器下载最新证书
echo&amp;nbsp;&amp;quot;正在下载证书...&amp;quot;
curl&amp;nbsp;$CURL_OPTS&amp;nbsp;&amp;quot;${FTP_URL}/${REMOTE_CERT_FILE}&amp;quot;&amp;nbsp;-o&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.new&amp;quot;

echo&amp;nbsp;&amp;quot;正在下载私钥...&amp;quot;
curl&amp;nbsp;$CURL_OPTS&amp;nbsp;&amp;quot;${FTP_URL}/${REMOTE_KEY_FILE}&amp;quot;&amp;nbsp;-o&amp;nbsp;&amp;quot;${LOCAL_KEY_FILE}.new&amp;quot;

#&amp;nbsp;检查下载是否成功
if&amp;nbsp;[&amp;nbsp;-s&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.new&amp;quot;&amp;nbsp;]&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;[&amp;nbsp;-s&amp;nbsp;&amp;quot;${LOCAL_KEY_FILE}.new&amp;quot;&amp;nbsp;]&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;!&amp;nbsp;grep&amp;nbsp;-q&amp;nbsp;-E&amp;nbsp;&amp;quot;html|Error&amp;quot;&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.new&amp;quot;;&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;下载成功！正在替换旧证书...&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;备份旧证书
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[&amp;nbsp;-f&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;]&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;mv&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.bak&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[&amp;nbsp;-f&amp;nbsp;&amp;quot;$LOCAL_KEY_FILE&amp;quot;&amp;nbsp;]&amp;nbsp;&amp;amp;&amp;amp;&amp;nbsp;mv&amp;nbsp;&amp;quot;$LOCAL_KEY_FILE&amp;quot;&amp;nbsp;&amp;quot;${LOCAL_KEY_FILE}.bak&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;移动新证书到正式位置
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mv&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.new&amp;quot;&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mv&amp;nbsp;&amp;quot;${LOCAL_KEY_FILE}.new&amp;quot;&amp;nbsp;&amp;quot;$LOCAL_KEY_FILE&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;设置正确的权限
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chmod&amp;nbsp;644&amp;nbsp;&amp;quot;$LOCAL_CERT_FILE&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chmod&amp;nbsp;600&amp;nbsp;&amp;quot;$LOCAL_KEY_FILE&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;重载&amp;nbsp;Nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;正在重载&amp;nbsp;Nginx&amp;nbsp;服务...&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if&amp;nbsp;eval&amp;nbsp;$NGINX_RELOAD_CMD;&amp;nbsp;then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;SSL&amp;nbsp;证书更新并重启&amp;nbsp;Nginx&amp;nbsp;成功！&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;Nginx&amp;nbsp;重启失败，请检查命令行！&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fi
else
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo&amp;nbsp;&amp;quot;错误：从&amp;nbsp;FTP&amp;nbsp;下载证书失败，更新终止。&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;rm&amp;nbsp;-f&amp;nbsp;&amp;quot;${LOCAL_CERT_FILE}.new&amp;quot;&amp;nbsp;&amp;quot;${LOCAL_KEY_FILE}.new&amp;quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit&amp;nbsp;1
fi

echo&amp;nbsp;&amp;quot;SSL&amp;nbsp;证书自动更新成功并已生效！&amp;quot;&lt;/pre&gt;&lt;p&gt;脚本跑通之后，下一步就是添加计划任务了，但这时候问题来了，计划任务却怎么都跑不起来。&lt;/p&gt;&lt;p&gt;问 AI，告诉我说 HAOS 不支持计划任务，让我使用系统自带的自动化功能配合 Shell Command 来实现，在 AI 的引导下，一顿操作猛如虎，折腾了一上午，脚本硬是没有执行起来。&lt;/p&gt;&lt;p&gt;只好自己想办法排查问题，经过一番排查，发现 Shell Command 根本没有 /ssl 目录的写入权限，然后我改目录，证书倒是能下载了，最后却又没有 ha apps restart core_nginx_proxy&amp;nbsp;的执行权限。&lt;/p&gt;&lt;p&gt;被一堆权限问题搞得焦头烂额之后，我只好换了一家 AI，请出 Gemini 大哥出场，他让我直接在&amp;nbsp;Advanced SSH &amp;amp; Web Terminal 的配置页面里编辑YAML：&lt;/p&gt;&lt;pre class=&quot;brush:bash;toolbar:false;&quot;&gt;init_commands:
&amp;nbsp;&amp;nbsp;-&amp;nbsp;echo&amp;nbsp;&amp;quot;00&amp;nbsp;03&amp;nbsp;*&amp;nbsp;*&amp;nbsp;*&amp;nbsp;/bin/bash&amp;nbsp;/ssl/update_ssl.sh&amp;quot;&amp;nbsp;&amp;gt;&amp;nbsp;/etc/crontabs/root
&amp;nbsp;&amp;nbsp;-&amp;nbsp;echo&amp;nbsp;&amp;quot;&amp;quot;&amp;nbsp;&amp;gt;&amp;gt;&amp;nbsp;/etc/crontabs/root
&amp;nbsp;&amp;nbsp;-&amp;nbsp;chmod&amp;nbsp;600&amp;nbsp;/etc/crontabs/root
&amp;nbsp;&amp;nbsp;-&amp;nbsp;crond&lt;/pre&gt;&lt;p&gt;改完之后点保存，系统会自动重启 Advanced SSH &amp;amp; Web Terminal，测试正常，还得是Gemini。&lt;/p&gt;</description><pubDate>Thu, 21 May 2026 20:24:15 +0800</pubDate></item><item><title>腾讯云 SSL 证书文件格式踩坑笔记</title><link>https://blog.fairysoft.net/post/67.html</link><description>&lt;p&gt;最近在给网站部署 SSL 证书，由于我的设备跨越了 Windows Server、Debian、QNAP 等多个平台，所以证书格式也要用到很多种，以下是我经过几天时间踩坑之后总结出来的干货。&lt;/p&gt;

&lt;p&gt;先说证书，IIS 的就不说了，只说说 Nginx 和 Apache 证书格式的区别，以及威联通 QNAP 系统独特的证书格式。&lt;/p&gt;

&lt;p&gt;腾讯云的 Nginx 证书包解压之后有四个文件，文件名分别为：&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;
fairysoft.net_bundle.crt 网站证书跟中继证书合并的证书链
fairysoft.net_bundle.crt 这个文件跟上面那个文件内容完全一样，只是扩展名不同
fairysoft.net.csr        申请证书时生成的请求文件，部署时用不到，可删
fairysoft.net.key        私钥文件&lt;/pre&gt;

&lt;p&gt;腾讯云的 Apache 证书包解压之后也有四个文件，文件名分别是：&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;
fairsoft.net.crt 网站证书
fairsoft.net.key 私钥
fairsoft.net.csr 申请证书时生成的请求文件，部署时用不到，可删
root_bundle.crt  中继证书&lt;/pre&gt;

&lt;p&gt;Nginx 的配置文件如下，需要指定两个文件，一个是网站证书跟中继证书合并成的证书链，另外一个是单独的私钥：&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;
ssl_certificate     /etc/ssl/fairysoft.net_bundle.pem;
ssl_certificate_key /etc/ssl/fairysoft.net.key;&lt;/pre&gt;

&lt;p&gt;旧版 Apache（2.4.8 之前）配置文件中需要指定三个文件：&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;
SSLCertificateFile      /etc/ssl/fairsoft.net.crt
SSLCertificateChainFile /etc/ssl/root_bundle.crt
SSLCertificateKeyFile   /etc/ssl/fairsoft.net.key&lt;/pre&gt;

&lt;p&gt;新版 Apache（2.4.8 及以后）配置文件中废弃了 SSLCertificateChainFile，强制要求把域名证书跟中继证书顺序拼接成一个证书链，私钥依旧单独文件，格式跟 Nginx 一致了。&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;
SSLCertificateFile    /etc/ssl/fairysoft.net_bundle.pem
SSLCertificateKeyFile /etc/ssl/fairsoft.net.key&lt;/pre&gt;

&lt;p&gt;&lt;br /&gt;
总结一下，腾讯云 Apache 证书包是拆分文件：域名证书.crt + 中继.crt + 私钥.key，旧版专用，新版不能直接用。新版 Apache 2.4.8+ 如果要用旧版的 Apache 证书包，必须手动将：域名证书.crt + 中继.crt &amp;rarr; 合并成证书链文件，和 Nginx 格式一致，其实直接下载 Nginx 的证书包就可以直接用了，不需要手动合并。&lt;/p&gt;

&lt;p&gt;威联通 QNAP 系统则有些特殊，QNAP 虽然使用的是 Apache 作为 Web 服务器，但是证书管理却使用了&amp;nbsp;stunnel，配置文件如下：&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;
SSLCertificateFile      &amp;quot;/etc/stunnel/stunnel.pem&amp;quot;  # 这里面同时包含私钥和网站证书
SSLCertificateChainFile &amp;quot;/etc/stunnel/uca.pem&amp;quot;      # 中继证书&lt;/pre&gt;

&lt;p&gt;从配置文件可以看出来，威联通使用的证书格式跟 Nginx 和 Apache 都不一样，私钥和网站证书合并成一个证书链，而中继证书却单独放。&lt;/p&gt;

&lt;p&gt;最近我还在 Windows Server 服务器上部署了 Win‑acme ，下载的证书文件格式如下：&lt;/p&gt;

&lt;pre class=&quot;prettyprint&quot;&gt;
fairsoft.net‑key.pem        私钥文件
fairsoft.net‑crt.pem        网站证书
fairsoft.net‑chain‑only.pem 仅中继证书
fairsoft.net‑chain.pem      网站证书和中级证书合并的证书链
&lt;/pre&gt;

&lt;p&gt;对比一下就会发现 Win‑acme 下载的证书跟 Nginx 格式的证书包完全互通，可直接通用于 Nginx 和新版 Apache。&lt;/p&gt;

&lt;p&gt;下一期我来讲一下如何在各种 Linux 系统上部署脚本，自动更新快过期的证书。&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
</description><pubDate>Thu, 21 May 2026 05:54:49 +0800</pubDate></item><item><title>当飞机遇到GPS干扰</title><link>https://blog.fairysoft.net/post/66.html</link><description>&lt;p&gt;两年前我就想写一篇关于飞机GPS干扰的文章，一直没找到机会写，直到昨天我自己遭遇了一次严重的GPS干扰事件。&lt;/p&gt;

&lt;p&gt;昨天飞晚班回重庆，着陆前10分钟，在我们飞过沙坪坝上空时，飞机弹出一个&amp;ldquo;ADS-B故障&amp;rdquo;的信息，我们明白这是遇到了GPS干扰。但这时候我们还以为这只是一次普通的干扰，对这种由于GPS干扰导致的ADS-B故障我们最近几年经常遇到，早就习以为常了，一般这种干扰只会持续几秒钟就会消失，因为通常干扰范围是局部的，以我们飞机的速度很快就能脱离这个干扰区域，而且就算是持续受到干扰，对我们飞机影响也不大，因为我们飞机有三套高精度的惯性导航系统，它们可以完全不依赖外部的GPS信号来工作。&lt;/p&gt;

&lt;p&gt;可是随后我在无线电频率里听到一架在我前面的山东航空的飞机报告管制员说他们遭遇GPS干扰中止进近拉升了，这时候我才意识到这次的干扰可能跟之前不一样，于是我们开始评估风险，我们重点检查了飞机的导航系统，确认飞机的位置没有出现偏移，但我们还是通告了管制员要求帮我们通过地面雷达监控我们飞机的位置和高度，因为空管的一次雷达是使用无线电波反射原理来主动测量飞机的位置，而不依赖飞机的位置报告系统。&lt;/p&gt;

&lt;p&gt;没过多久，飞机又弹出一个&amp;ldquo;FMS/GPS位置不一致&amp;rdquo;的信息，这个信息意味着GPS受到的不是普通的干扰&amp;mdash;&amp;mdash;普通的干扰属于&amp;ldquo;压制式信号干扰&amp;rdquo;，只是用一个杂乱无章的无线电噪声淹没掉正常的GPS卫星信号，使得GPS接收机无法从这一堆噪声中提取出有用的信息，也就无法计算出经纬度位置数据。这种情况下，GPS只是暂时罢工但并不会添乱，失去了GPS定位，飞机的位置精度会略微下降一点点，但还是能满足除了RNP进近之外的大多数飞行程序的要求。但是这个&amp;ldquo;FMS/GPS位置不一致&amp;rdquo;的告警信息意味着GPS接收机接收到了一组精心伪造的无线电信号并解码计算出了一个错误的经纬度位置信息，这种升级版的干扰被称为&amp;ldquo;GPS欺骗&amp;rdquo;，这种情况下，GPS不再是罢工这么简单了，它会直接给你添乱。虽然GPS欺骗很难对飞机的导航系统产生严重的影响，因为飞机的FMGS计算机会识别出明显不合理的GPS位置信息并将其舍弃掉，但是错误的位置信息会对飞机的增强型近地警告系统（EGPWS）造成极大的影响，因为EGPWS系统的位置信号是直接来源于GPS接收机而不是FMGS计算机，EGPWS系统在发出告警之前并不会对GPS接收机提供的位置信息做有效性校验。&lt;/p&gt;

&lt;p&gt;果然这个信息弹出来没多久，驾驶舱里PULL UP警告就开始响了起来，当时我的内心也是慌得一批，倒不是担心会撞山&amp;mdash;&amp;mdash;因为我很清楚我飞机当时的位置根本没有任何撞山的可能。我在重庆飞了近20年，这条航路我太熟悉了：当时我们正在左转五边准备建立盲降，我左前方是鹅公岩长江大桥，江对岸是南岸区，虽然正前方是南山，但是南山海拔最高的地方也只有600米，而我们飞机当时的高度在1500米以上。&lt;/p&gt;

&lt;p&gt;那我慌啥呢？非业内人士可能不知道&amp;ldquo;PULL UP&amp;rdquo;警告有多严重，可以这么说，这个警告是飞机上最严重的警告，没有之一！99%的飞行员一辈子都不会在真飞机上听到这个警告，如果你触发了这个警告，在现在的处罚制度下，基本上就可以宣告你的职业生涯终结了。不过好在这是个假警告，所以我们三个人的饭碗暂时是保住了，算是虚惊一场。随后的重点是如何决策，当时我们有两个选择，一是直接中止进近拉升，二是忽略警告继续进近。&lt;/p&gt;

&lt;p&gt;原则上讲，PULL UP警告是最高优先级、立即动作级警告，根本没时间给你去思考判断和决策，你要做的只能是立即执行地形避让的记忆项目：迅速前推油门杆到底同时断开自动驾驶并向后拉杆到底。这个原则曾经是不容置疑的，关键时刻能保命。但问题是在GPS干扰越来越频发的当今，这种地形避让操作带来的衍生风险也不容忽视：首先，迅速带杆到底的操作对飞机的飞控系统是一个很大的挑战，虽说空客的正常法则会保护飞机不进入失速，但是飞行数据上必然会出现很多超限，每一个超限都够飞行员喝一壶了，如果造成旅客受伤你更是吃不了兜着走了；其次，警告持续响你就得持续爬升，势必会导致穿越多个高度层，在繁忙的机场很容易跟空中其他飞机造成飞行冲突，任何一个冲突都可能会造成事故征候级别的后果，何况受到干扰的飞机不止你一架，大家全都满油门大角度爬升，估计空管都要疯掉了。所以空客公司在最近的一个&amp;ldquo;关于应对虚假TAWS告警&amp;rdquo;的技术通告里面很委婉的提示飞行员，首先，如果在安全高度之上&amp;ldquo;不应当机械的执行TAWS记忆项目&amp;rdquo;；其次，在明确GPS受到干扰并执行完地形意识简令之后可以考虑关闭飞机的TERR告警功能。不过，可能是为了免责，空客在通告中还是强调触发TAWS告警之后处置原则依然是立即执行地形避让的记忆项目。&lt;/p&gt;

&lt;p&gt;在犹豫了0.01秒之后我选择了违反原则，忽略警告并抑制TERR告警功能，继续进近。虽然从结果来看我的选择是对的，如果我选择拉升避让，可能后果会非常严重。但是直到现在我一直在反复思考这个问题：飞行员一直以来被要求原则上应当无条件服从机器的告警指示，是因为我们一直以来都认为机器比人更可靠，人会犯错，机器不会，但是当这个机器明确被证实存在某个Bug，在某些情况下会给出错误的指示，当这种情况发生时，我们是否还应该遵循之前的原则。&lt;/p&gt;

&lt;p&gt;这个问题就不多写了，大家有兴趣可以讨论一下。最后留点科普时间给大家粗略的讲解一下飞机FMGS计算位置的方法。&lt;/p&gt;

&lt;p&gt;首先讲一下惯性导航系统（INS），惯导是一种完全自主的导航设备，它不依赖任何外部信号，仅通过飞机自身，就能实时计算出自身的位置、速度、姿态信息。惯导的核心部件有两个，一个是激光陀螺：负责测量飞机的俯仰、倾斜、转弯角度等，跟位置计算没啥关系，这里就不细说了。另一个核心部件是加速度计：负责测量飞机在空间坐标系中xyz三个方向的加速度。根据牛顿运动定律，加速度对时间积分得到速度，速度对时间积分得到位移，初始位置加位移就是飞机的当前位置。惯导的短时精度非常高，但惯导有一个无法避免的缺陷&amp;mdash;&amp;mdash;累计误差。因为积分运算会不断累积测量过程中的微小偏差，随着飞行时间延长、飞行距离增加，这些误差会一点点累积，导致位置精度逐渐下降。飞行时间越久，惯导算出的位置与真实位置偏差就越大，单纯依靠惯导，无法满足长途飞行的高精度定位需求。&lt;/p&gt;

&lt;p&gt;再说一下GPS定位，GPS是我们最熟悉的定位方式，飞机通过接收卫星发射的信号，实时测算与多颗卫星的距离，通过计算得出自身的位置。它的优势十分突出：定位精度高，且误差不会随飞行时间累积，无论飞多久，只要能稳定接收卫星信号，就能保持较高的位置准确性。但GPS的短板也很明显：信号极易受干扰。就像此前提到的GPS干扰问题，一旦GPS失效，飞机就会失去这一精准定位来源。&lt;/p&gt;

&lt;p&gt;飞机上的FMGS计算机融合惯导与GPS各自的优点，将二者数据融合，取长补短，打造出稳定又精准的定位最优解。工作原理简单来说就是利用惯导短时精度高的优点来对接收到的GPS位置进行校验，采纳可信GPS位置，舍弃不可信GPS位置，不断修正惯导的累积位置偏差。&lt;/p&gt;

&lt;p&gt;所以大家大可不必担心GPS干扰甚至GPS欺骗会对飞机FMGS的定位导航造成严重的影响，就算是受到严重的GPS干扰，飞机位置也不会突然发生剧烈的变化，最多不过是随着时间的累积，慢慢的发生微小漂移。再说了，失去GPS辅助以后FMGS还会降级到无线电位置更新，利用地面导航台的方位距离信息来计算自身的位置，并修正惯导的漂移。就算是无线电位置更新也不可用，惯导仅依靠自身的精度也能维持一段时间的导航精度。这就是为什么没有GPS的飞机如果执行RNAV10的跨洋航线会限制纯惯导的飞行时间不能大于6小时，因为据说惯导漂移的适航认证要求每小时漂移距离不得大于2海里，纯惯导飞行6小时基本能满足RNAV10的要求。实际飞行时我观察过惯导的漂移量，一般每小时只有零点几海里的误差，远低于适航认证的上限。RNP AR(0.3)进离港的GPS空洞期最大允许5分钟也跟这个有关，其逻辑就是在GPS信号丢失之后，惯导仅靠自身的精度能保证在5分钟之内位置偏差不会超过0.3海里。&lt;/p&gt;

&lt;p&gt;最后再说一下受到GPS干扰之后，如果两部GPS都失效的情况下到底能不能使用RNAV进离港程序的问题。我查阅了RNAV1的设计规范，发现设计规范并不强制机载设备必须具备GNSS，可使用 GNSS、DME/DME 或DME/DME/IRU中的任意一种，只有RNP进近和RNP AR进近才强制必须具备GNSS，普通RNP进近要求1部GPS，RNP AR要求两部GPS。但是实际运行中我发现国内几乎所有机场的RNAV进离港程序都要求GNSS，所以在这些机场运行时，一旦GPS失效，就只能申请飞传统进离港程序。不过也有例外，比如成都双流机场的进离港程序就没有要求必须要有GNSS，理论上，双GPS失效的情况下只要DME/DME/IRU可用，在成都双流机场一样能飞RNAV进离港程序，只不过最后进近只能接盲降或者VOR程序，不能飞RNP进近。&lt;/p&gt;
</description><pubDate>Thu, 09 Apr 2026 06:23:17 +0800</pubDate></item><item><title>飞行员专用座舱高度监控报警器</title><link>https://blog.fairysoft.net/post/65.html</link><description>&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/20241208110662916291.jpg&quot; style=&quot;height:270px; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;空客A319飞机在飞高原的时候，座舱压力控制计算机有一个BUG，偶尔小概率会触发一个假警告，空客公司认为只是个假警告而已，不理他就行了，所以一直不去修复。但CAAC太卷，领导说了哪怕是假警告也坚决不能响，所以制定了一套补充程序要求机组在座舱高度达到8500ft时执行，以避免这个假警告。但是机组有时候事太多，经常忘了去做这个程序，如果运气不好碰巧这个警告响了的话，回来就会被收拾。所以我做了这个物理外挂，在需要执行程序的时候来提醒机组。使用ESP32处理器，MS5611高精度气压传感器，1.3寸低功耗OLED显示屏，三个物理按键，内置500mAh电池。&lt;/p&gt;

&lt;p&gt;以下是产品简要说明：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;简介：&lt;/strong&gt;&lt;/p&gt;

&lt;p style=&quot;margin-left:40px&quot;&gt;一个带有高度报警和升降率报警功能的气压高度表。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;基本操作：&lt;/strong&gt;&lt;/p&gt;

&lt;p style=&quot;margin-left:21pt&quot;&gt;1、&amp;nbsp;在主界面下，左键开关显示屏，关屏状态下高度提醒功能仍然在后台工作。&lt;/p&gt;

&lt;p style=&quot;margin-left:21pt&quot;&gt;2、&amp;nbsp;主界面下短按中键进入设置模式，设置气压基准和高度提醒值。&lt;/p&gt;

&lt;p style=&quot;margin-left:21pt&quot;&gt;3、&amp;nbsp;主界面下长按中键设置升降率报警值。&lt;/p&gt;

&lt;p style=&quot;margin-left:21pt&quot;&gt;4、&amp;nbsp;主界面下短按右键切换公英制。&lt;/p&gt;

&lt;p style=&quot;margin-left:21pt&quot;&gt;5、&amp;nbsp;主界面下长按右键将气压基准恢复为1013hPa，高度提醒值恢复为8300ft。&lt;/p&gt;

&lt;p style=&quot;margin-left:21pt&quot;&gt;6、&amp;nbsp;设置模式下，左右键操作数值的减和加。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;四种提醒：&lt;/strong&gt;&lt;/p&gt;

&lt;p style=&quot;margin-left:21pt&quot;&gt;1、&amp;nbsp;高度超过8300ft，提醒CPC转换，持续滴滴约20秒，按任意键停止。&lt;/p&gt;

&lt;p style=&quot;margin-left:21pt&quot;&gt;2、&amp;nbsp;巡航时首次升降率超过500ft/m，滴滴两声，提醒主控CPC已进入下降模式。&lt;/p&gt;

&lt;p style=&quot;margin-left:21pt&quot;&gt;3、&amp;nbsp;座舱高度超过13000ft，滴滴5声，提醒检查高高度电门是否接通。&lt;/p&gt;

&lt;p style=&quot;margin-left:21pt&quot;&gt;4、&amp;nbsp;升降率超过设定值提醒，只要升降率超过设定值就一直滴滴，无法通过按键停止。本提醒只在亮屏模式下才工作，因为需要高采样频率的支持。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;自动熄屏：&lt;/strong&gt;&lt;/p&gt;

&lt;p style=&quot;margin-left:40px&quot;&gt;正常开机后默认5分钟无按键操作自动熄屏进入待机模式。&lt;/p&gt;

&lt;p style=&quot;margin-left:40px&quot;&gt;按住右键再打开电源开关则不启用自动熄屏功能。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;气压传感器校准：&lt;/strong&gt;&lt;/p&gt;

&lt;p style=&quot;margin-left:40px&quot;&gt;按住左键再打开电源开关进入气压传感器校准页面，左右键调整校准值，中键保存。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;记录一下开发过程：&lt;/p&gt;

&lt;p&gt;突然有一天有了这个想法，于是画了这张手稿&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/2024120810420196196.jpg&quot; style=&quot;height:640px; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;说干就干，用ESP32开发板搭建试验平台&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/20241208104379977997.jpg&quot; style=&quot;height:270px; opacity:0.9; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;学习单片机编程，第一步当然是写&amp;ldquo;Hello World！&amp;rdquo;&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/20241208104772177217.jpg&quot; style=&quot;height:203px; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;带上飞机调试，精度非常满意&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/2024120810490568568.jpg&quot; style=&quot;height:480px; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;开始学习画电路板&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/20241208105093029302.jpg&quot; style=&quot;height:411px; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;学习手工焊接CPU&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/20241208105163256325.jpg&quot; style=&quot;height:270px; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;第一台试验样机组装成功&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/20241208105280448044.jpg&quot; style=&quot;height:480px; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;为了提高续航，研究ESP32的睡眠模式，经过一番努力终于把待机电流控制到了2毫安以下，算下来待机时间可以达到200小时以上。&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/20241208105392809280.jpg&quot; style=&quot;height:480px; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;最终实现了量产。&lt;/p&gt;

&lt;p&gt;经济版用的3D打印塑料外壳，定价319元。&lt;/p&gt;

&lt;p&gt;豪华版使用铝合金CNC外壳，定价499元。&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/20241208105688668866.jpg&quot; style=&quot;height:270px; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/20241208105762646264.jpg&quot; style=&quot;height:480px; width:360px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;有需要的可以联系我购买&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;https://blog.fairysoft.net/zb_users/upload/2024/12/20241208105981938193.jpg&quot; style=&quot;height:480px; width:360px&quot; /&gt;&lt;/p&gt;
</description><pubDate>Sun, 08 Dec 2024 18:37:56 +0800</pubDate></item></channel></rss>