碎碎念&需求&总体方案
尽管如此,中心辐射型还是有一些根本性的尴尬之处:它们不允许各个节点相互通信。如果您像我一样老,您还记得计算机可以直接交换文件而无需往返云端的情况。不管你信不信,这就是互联网过去的运作方式!可悲的是,开发人员已经停止构建点对点应用程序,因为现代互联网的架构几乎是偶然地完全演变为这种中心辐射式设计,通常以主要云提供商为中心收取租金。
关注NAS也有一段时间了,作为科班出身的对设备本身也比较了解。越发接近的时候就有疑问,专门的NAS和我使用自己的电脑到底有啥区别?
主要能分为软件和硬件两个方面考虑吧。
在硬件上,普通家用NAS价格比普通PC还是要低的,特别是其中一大块价格要为溢出的硬盘空间付费,一般来说NAS空间比普通PC要多不少。普通NAS计算资源应该是远不如普通PC的。本质上讲,NAS的硬件情况无非就是一个储存(硬盘)服务器。虽然不少NAS会加上一些备份策略提高储存服务的可靠性,但这种其实本来自己也可以做,多买点盘(花钱就行)。
那按照我的理解,主要目的就在于软件层面的支持咯(一直有听说买群晖就是为它的软件生态付费的说法)。那值不值?得从用户需求入手,仔细考虑需求大概分为这几个方面吧:
- 影音媒体库。相当于一个私有化的腾讯视频。这种视频平台作为成熟的产品理论上体验是最好的(只要给够钱),那么私有化只需要靠近它的体验,具体有什么呢:
- 随时随地打开即看,一般WIFI条件下没有延迟感知,直接就能开始看。
- 云同步,有许多设备同时观看,不同地方无缝衔接。比如在外面用手机看,回到家用回大屏看。
- 多账户、共享。家里多人都可以享用。
- 其他媒体库的一些使用需求。比如快速定位想看的,要有好看的海报墙(一眼看到帅哥才衡量要不要看这部剧)、一些剧的相关信息比如评分;进度条预览功能(快进时大概知道应该拖到哪里)等等。
- 备份-共享中心。相当于私有化的OneDrive、阿里/百度网盘。这里大概可以又分类抽离一下。首先讨论面向顶层应用和具体场景的,我个人觉得在日常生活里备份和共享的需求就是:照片和微信聊天记录。只说照片。也是目前很多云服务商最重要的卖点吧。包括各家手机厂商,在自家相册应用都会推云同步、云备份;阿里/百度网盘也能做、也想给你做这种事;还有其他专门做相册类应用的,著名的就是Google Photo这种。照片这种属于一定程度私密的敏感数据。特别讨论对于这个场景的需求,包括:
- 自动备份、同步终端设备的照片到服务器;
- 终端可以方便查阅服务器的储存;
- 图片管理,比如创建图集
- 支持多用户、共享、分享;
- 对于照片应用,支持一些更高级的功能:比如时间线、人物识别自动归类、AI打标签图片搜索、回忆(类似QQ的那年今日)等。主要能够帮助人更好的、更容易地管理、阅览自己的图库,毕竟积攒下来几千的照片放着一坨可能就是赛博积灰的下场。
- 对于上一点分岔出来的需求,主要就是偏下层一点的场景,比如:
- 放放不同设备不同用户的文件,需要时快速找到、使用、下载,就是普通的网盘服务;
- 文件自动备份同步,相当于OneDrive、坚果云这些备份网盘服务;
- 盘挂载,面向最底层的文件使用,不同电脑间无缝使用相同的空间和文件,像是使用本地文件资源管理器一样。
- 公网服务。随处访问。
工具本身并不重要,也没有意义,存在只是为了解决用户需求。那按照我整理的上述需求(可能有所遗漏),那只要去满足这些需求即可。而只要购买NAS就能完美满足我们的需求吗,对于这点暂时不得而知。
那我目前就按照手头的资源、现在的方案去做一个自己折腾版的NAS。这里我对NAS的定义就是一种家庭服务,能够支撑和满足我们的一些电子需求,包括但不仅限于上面所提的需求。
目前我的方案概述:
- 个人PC作为服务器
- 软件层使用EMBY作为媒体库支持+AList作为网盘支持+Immich作为图库支持。
- 中间网络层通过tailscale/zerotier的P2P打洞进行跨域访问,基本上除了移动(傻逼移动)都能打洞成功。但痛点在于必须完成前置的打洞步骤,虽然是一个设备完成过一次就一劳永逸了,但share给别人、作为一个team的配置还是有一点麻烦。
硬件方案
暂时用手头正在使用的笔记本。后续将其淘汰,专职用于NAS的硬件。
储存上目前:普通PC(3T固态),加上一个2T的移动硬盘。
储存策略:PC固态存还没看的资源,看完后的资源归档到移动硬盘,相当于备份。
扩展思路:购置一到两块更大的机械硬盘(8T左右),扩展储存空间,同时考虑认真的备份方案。比如增加一块8T储存硬盘和一块8T备份硬盘。后者周期性地手动插入,备份储存硬盘的内容(基于FrreFileSync等?)
软件方案
影音媒体库
基于PT站/普通资源站+Emby,参考相应的帖子。
照片库
目前用的是Google Photo,Google One100G空间就算是美区结算也就十来块钱,主要还是敏感数据的担心。
考虑改为本地部署的方案。
更新:Google Photo真恶心,甚至连方便清空上面照片的功能都没有(给你制造离开他平台的阻力),必须要清除空间,否则不订阅Google One后连GMail都用不了。
更新:自部署方案选择:
- https://tonfotos.com/articles/self-hosted-photo-gallery/
- https://meichthys.github.io/foss_photo_libraries/
先使用Immich:
- github(两年开发、3w+stars):
https://github.com/immich-app/immich
- 官方文档:https://immich.app/docs/overview/quick-start
- 官方频道是discord(因为正在快速开发,因此咨询官方人员挺重要的,回复很快):https://discord.com/channels/979116623879368755/994044917355663450
- windows安装流程参考:https://www.reddit.com/r/immich/comments/1b5u6p2/how_to_install_in_windows/。
概述来说主要是要通过Docker Compose使用,windows上可以通过Docker Desktop来进行。
使用起来也很简单,基本等同GooglePhoto(Logo也差不多)。但是通过ML(机器学习)进行人脸识别还是挺慢的(Google Photo也很慢),要等很久才行。
然后有和GooglePhoto一样的痛点,在移动端上传和备份照片时不能自动上传到对应文件夹名的相册中,默认情况下查看一张相册中的所有照片(数百或数千张)非常不方便。只能手动将照片一张一张地添加到不同的相册中。(一年前的issue了,可能会更新这个功能吧:https://github.com/immich-app/immich/discussions/1678)
个人解法:
- 先使用微力同步上传到电脑。
- 然后使用immich-cli命令上传,根据官方文档(here),这是支持自动命名文件夹作为相册的。
- 没有npm的话安装,考虑使用VMR一键安装。
- npm i -g @immich/cli
- 到Immich里获取API
- 登录:immich login http://localhost:2283/api key
- 然后就可以上传相应的图片(主要是需要文件夹信息创建相应的相册)。
可以用:immich upload –dry-run –album –recursive ./ 尝试一下发生什么. 图片在手机备份过了,检查HASH码,所以这里不会重复上传,只提取相册进行更新,不错。
1 2 3 4 5 6
Crawling for assets... Checking files | ████████████████████████████████████████ | 100% | ETA: 0s | 1884/1884 assets Found 0 new files and 1884 duplicates All assets were already uploaded, nothing to do. Would have created 12 new albums Would have updated 1884 assets
- 没问题直接运行:immich upload –album –recursive ./
- 但是出现问题:Failed to add assets to album。然后
docker logs -n 100 immich_server查看日志会发现ERROR [QueryFailedError: duplicate key value violates unique constraint “PK_c67bc36fa845fb7b18e0e398180”。意思就是有重复的照片了就不行。Github有人提过这个issue了:https://github.com/immich-app/immich/issues/9115。看啥时候解决吧。 一种手动解决思路:根据上面issue下某个人的方案
fdupes -rn --delete .在windows上实现类似的效果,即查找重复的文件,进行删除。参考脚本:1 2 3 4 5
# 这是先输出查看哪些重复的 $folderPath = "C:\path\to\your\folder"; $hashes = Get-ChildItem -Path $folderPath -Recurse -File | Get-FileHash; $duplicates = $hashes | Group-Object -Property Hash | Where-Object { $_.Count -gt 1 }; $duplicates | ForEach-Object { Write-Output "Duplicate files found:"; $_.Group | ForEach-Object { Write-Output $_.Path }; Write-Output "---" } # 然后删除 $folderPath = "C:\path\to\your\folder"; $hashes = Get-ChildItem -Path $folderPath -Recurse -File | Get-FileHash; $duplicates = $hashes | Group-Object -Property Hash | Where-Object { $_.Count -gt 1 }; $duplicates | ForEach-Object { $_.Group | Select-Object -Skip 1 | Remove-Item -Force }
之后就可以正常导入相册了。
- 总体评价来说也是一种思路吧,就是有一份冗余数据存着。但也符合官方说的3-2-1备份策略。就是手机备份照片、然后同步到电脑另一个备份,之后通过这个使用CLI提取相册信息进行更新。麻烦的是每次都需要这样做(而不是在手机上使用自带图库管理后自动同步),也可以考虑完全用它的app来管理,那么就不需要多次管理了。
由于用到Docker和虚拟化环境,还是挺耗资源的,官方建议4GB内存以上,个人使用上这个服务单独就占用了4GB+的内存,开销挺大的,根据个人情况考虑取舍。
网盘(普通文件)
支持普通放放文件、多端储存下载、分享等。
选择参考:https://zhuanlan.zhihu.com/p/44103820
主要的选择标准:开源、好看、跨平台支持。
个人比较喜欢的:
- AList
- Cloudreve
- figegator
目前我自己选择了AList,能够实现的功能:
- 使用本地储存映射为网盘(意味着无需导入,想存到网盘的东西,直接就映射到网盘里了),跟本地使用一样管理文件一样,无感地初始化网盘;
- 普通网盘的功能,上传下载分享;
- 盘符挂载,挂载到文件系统里,当做普通文件使用。(windows挂载webdav参考:https://echo.xuchaoji.com/index.php/archives/400/)
网盘储存:
- 软件包
- 归档学习、工作记录。
将AList注册为开机自动服务,使用nssm。nssm install AList.
备份网盘
使用坚果云。
备份
考虑应该备份的清单:
- 照片。参考照片库构建的方案。
- 微信聊天记录。自带备份+Memotrace
- 各个阶段的学习工作记录归档:本科、研究生等。
- 资源:影音资源等。
网络方案
目前综合来看,对于自己(小家庭内部)使用而言,P2P(或者说VPN)方案是最优秀的,兼具了安全性和便捷性,一次配置一劳永逸,需要时启动软件(VPN)一键使用。进一步便捷化地,当NAS固定(一般情况都是固定的),其P2P分配的虚拟IP可以通过域名映射固定下来,就更加方便好记。就算不固定,也可以配置DDNS来进一步解决。
为什么打开VPN这步是必要的呢,这是重要的安全性保护。相比于直接暴露在公网使用服务,VPN加持下使用无疑是安全的多,也带来省心的好处,不需要太考虑安全保护和被攻击的后果。
那对于真的需要公网服务的场景呢。对于所述需求(媒体、网盘等),确实存在,比如对于网盘而言能够分享链接让任何人快速下载是一个重要的功能。对我们来说,确实不可能让每个人都先加入P2P再下载链接,或者我们使用一台临时的新设备访问资源也一样。确实存在一定的公网服务需求。
但是具体分析其实需求很小(或者很难解决)。如果还是对于个人使用的场景,无论什么设备都可以快速安装一个VPN软件,也不麻烦,或者使用常用设备(手机)来获取资源再转发到新设备。
对于任何人的场景,这个需求本来就很难办。如果兼具便捷性与安全性,没有什么风险和难度,那目前所有网盘厂商岂不是直接完蛋。毕竟对于任意Web设备,文件都存在自己Peer端,下载方和上传方都有资源,同时又都有上传和下载带宽,那为啥需要这个第三方。说着说着这其实不就是p2p下载(磁链、种子)吗。那直接用现成的P2P方案不就行了。比如qbittorrent这些软件制作种子发出去以供下载(ps种子制作需要tracker服务器,可以使用一些公用的比如:http://bt1.archive.org:6969/announce),或者类似的send anywhere这种在想服务(实测更慢,无法完全利用上行带宽)。总结来说,这种需求的方案已经被广泛地讨论和研究过,没有那么完美的,选择一个适合自己的就好。
实在想暴露服务,可以用frps等转发和内网穿透技术暴露内网服务,谨慎使用和挑选暴露的服务。
公网服务
Tailscale/zerotier P2P打洞。
关于Tailscale(它真的非常好用!):
- How it works: https://tailscale.com/blog/how-tailscale-works. 概述来说,
- 数据平面(P2P节点交互数据)使用WireGuard构建轻量级加密隧道,而且所有节点之前会组成Mesh组网,可任意联通。
- 这是通过控制平面(一个轻量的中心的协调服务器)实现的,作为公钥投递箱,负责给所有节点交换信息。同时控制域、ACL访问策略等严格控制哪些节点可以交互。这个服务器只能知道哪些节点可以互联,但无法窃听它们之间的通信,因为私钥始终保存在本地,只有持有私钥的节点自己才能解密发给自己的流量。
- 这就是所谓的”零信任”原则。控制平面是中心化的,由Tailscale运营,让网络管理变得很简单。数据平面则是去中心化的,由所有节点组成Mesh网络,让通信更高效安全。
- 在身份认证方面,基于原本的身份认证体系,比如单点登录SSO,使用常见的身份提供商有Google Workspace、Office 365等。
- Claude解释
好的,我会针对每一点进行详细的解释和举例,力求让你全面了解Tailscale的工作原理。
- 数据平面:使用WireGuard创建节点之间的加密隧道
数据平面指的是实际承载网络流量的基础设施。Tailscale使用一种叫WireGuard的技术在节点之间建立加密隧道。
WireGuard是一个非常高效和安全的VPN协议。传统的VPN使用IPSec、OpenVPN等协议,配置复杂,性能也不够好。而WireGuard使用最新的加密算法,代码量只有它们的1%,因此非常轻量级,基本不消耗CPU。
同时WireGuard隧道连接速度快,延迟低,非常适合在数据中心、办公室、云端等分支机构之间组网。
举个例子,如果公司在北京、上海各有一个机房,员工使用传统VPN时,北京的员工要访问上海的服务器,流量可能需要绕道总部的VPN集中器。而Tailscale可以在北京和上海两地的服务器之间直接建立WireGuard隧道,不需要绕行,延迟大大降低。这就是所谓的多Hub架构。
如果再进一步,公司不仅在两地有机房,每个员工的笔记本电脑和手机也都安装了Tailscale,那他们之间也可以点对点直接加密通信,无需经过中心节点。这样的组网模式叫Mesh网络,可扩展性很强。
- 控制平面:密钥交换和协调
光有高速的数据平面还不够,节点之间如何验证身份、交换密钥才能建立WireGuard隧道呢?这就是控制平面的作用。
Tailscale的控制平面由一个中心的协调服务器承担。每个节点首先生成自己的公私钥对,然后将公钥和当前的IP地址发给协调服务器。
当一个新节点加入网络时,它也把自己的公钥发给协调服务器,然后就可以收到同一个域(如公司域名)下其他节点的公钥和地址。
这个协调服务器有点像密钥中转站,各节点不直接交换密钥,而是通过它完成。协调服务器知道哪些节点可以互联,但无法窃听它们之间的通信,因为私钥始终保存在本地,只有持有私钥的节点自己才能解密发给自己的流量。这就是所谓的”零信任”原则。
控制平面是中心化的,由Tailscale运营,让网络管理变得很简单。数据平面则是去中心化的,由所有节点组成Mesh网络,让通信更高效安全。
- 登录认证和双因素认证
讲完了密钥交换,你可能会问,协调服务器如何知道某个节点是可信的呢?Tailscale巧妙地利用了企业现有的身份认证体系。
企业一般都会为员工配置单点登录(SSO),常见的身份提供商有Google Workspace、Office 365等。员工日常访问企业内部系统时,都是用同一个账号密码登录的。
Tailscale集成了这些身份提供商的OAuth、OIDC等认证协议。员工在自己的设备上登录Tailscale时,会跳转到企业的身份提供商,使用平时的企业账号密码登录,再跳回来。
这个过程有点像你用微信扫码登录第三方网站,Tailscale只负责提供VPN服务,不保存你的密码,安全又方便。企业也不需要为Tailscale单独创建一套账号体系。
如果企业还启用了双因素认证,比如手机上收验证码,Tailscale也会要求你完成这一步,保证账号不会被盗用。
一旦通过认证,你的设备就可以收发密钥,连接其他可信的设备了。协调服务器establishes trust,但不会保存你的任何隐私信息。
- NAT穿透:使用STUN/ICE等技术打通双NAT环境下的节点连接
光能登录还不行,如果两台设备都在NAT(比如家用路由器)后面,就没有公网IP,无法直接连通,怎么办呢?
Tailscale使用了ICE协议,全称是”Interactive Connectivity Establishment”。这个协议会先用STUN技术探测两个节点的网络拓扑,看看它们是否在NAT后面,UDP端口是否被屏蔽。
如果条件允许,ICE就会指导两个节点直接建立UDP连接,打通 WireGuard隧道。这个过程有点像给对方发送”我在XXX.XXX.XXX.XXX这个IP的YYYY端口,你在AAA.AAA.AAA.AAA的BBBB端口,我们从此通信吧”的明信片。
如果条件不允许,Tailscale还有一个备用方案,请继续阅读。
- 加密TCP中继(DERP):在UDP被完全屏蔽的网络下,通过全球中继服务器转发加密流量
在一些严格的网络环境下,可能UDP流量被全面屏蔽了,即使用了STUN/ICE也无法穿透。这时Tailscale祭出了最后一招:DERP。
DERP是Designated Encrypted Relay for Packets的缩写,指加密的包中继服务。如果两个节点怎么也无法直连,它们就把流量通过最近的DERP服务器中转。
Tailscale在全球部署了多个DERP节点,这样无论你在哪里,都能找到一个延迟较低的中继。DERP只传输经过WireGuard加密的流量,无法窥探你的通信内容,保证了安全性。
有意思的是,DERP借鉴了HTTPS的思路,先在TCP上建立TLS加密连接,然后在TLS上承载WireGuard的UDP包。这样即使TCP效率低一些,安全依然有保障。
- ACL和安全策略:不依赖中心防火墙,而是每个节点基于中心下发的策略做访问控制
传统的企业网络喜欢在出口处部署一个防火墙,限制哪些IP可以访问哪些端口。但Tailscale的Mesh网络是扁平化的,没有一个出口,怎么实现安全隔离呢?
Tailscale把防火墙策略从硬件设备改成了软件。管理员在协调服务器上配置好哪些用户和设备可以访问
好的,让我通过一个例子来详细讲解WireGuard的原理。
假设你在家里用笔记本电脑远程办公,需要登录公司内网的服务器。你的笔记本和公司服务器都安装了WireGuard,我们称它们为节点(Peer)。
- 密钥生成
首先,每个节点都要生成自己的一对公钥和私钥。私钥要严格保密,公钥可以告诉其他节点。
你的笔记本生成了一个密钥对,假设公钥是 TvX4gcsDHMb1ax4mJHBSpSRb6BPjQq3eSAcSYm7gcNY= 公司服务器生成的公钥是 EO4fmhSA2mIa7t4ImQOVuQBF5sZs5BK01iDs4rIelwE=
- 交换公钥
你需要把笔记本的公钥告诉服务器,服务器也要把公钥给你。通常这一步通过Tailscale的控制平面自动完成。
- 配置对端地址
光有对方的公钥还不够,还需要知道对方的IP地址。这一般分配一个虚拟内网段,比如你的笔记本是 100.64.0.1,服务器是100.64.0.2。
- 允许对端
你们互相把对方的公钥和IP输入到WireGuard的配置文件里,并允许连接请求。
笔记本上的配置:
1 2 3 4 5 6 7
[Interface] PrivateKey = <笔记本的私钥> ListenPort = 12345 [Peer] PublicKey = EO4fmhSA2mIa7t4ImQOVuQBF5sZs5BK01iDs4rIelwE= AllowedIPs = 100.64.0.2/32
服务器上的配置:
1 2 3 4 5 6 7
[Interface] PrivateKey = <服务器的私钥> ListenPort = 12345 [Peer] PublicKey = TvX4gcsDHMb1ax4mJHBSpSRb6BPjQq3eSAcSYm7gcNY= AllowedIPs = 100.64.0.1/32
- 建立隧道
然后你在笔记本上 ping 100.64.0.2,流量就会通过WireGuard隧道发送:
- 笔记本用服务器的公钥,加密数据包并放入UDP包
- 笔记本从之前得到的对端地址表里查到服务器的实际IP地址
- 笔记本把UDP包通过互联网发给服务器
- 服务器收到UDP包,用自己的私钥解密,取出数据包
- 服务器看到数据包的目的IP是自己的内网IP 100.64.0.2
- 服务器处理该数据包,并原路返回经过加密的响应包
这个过程有几个关键点:
- 传输的数据包全程都是加密的,即使有人拦截UDP包也无法知道里面的内容
- 两个节点之间不需要交换私钥,确保安全
- 数据包里的IP是虚拟内网IP,和物理网卡的实际IP是分离的
通过WireGuard,两个节点之间仿佛有一条单独的加密隧道,可以直接用内网IP通信。而这条隧道实际是建立在UDP包之上的,非常轻量级。
这就是WireGuard的基本工作原理。当然在实际使用中,Tailscale还提供了更多的功能,比如密钥轮换、连接认证、网络穿透、访问控制等,让WireGuard更容易使用和管理。
- 私人使用:3个用户、100台设备,基本满足需求了。
普通个人使用非常傻瓜便捷,多个设备下载app装上,用同一个身份认证账号(比如Google账号)登录即可,完全无感。不过多用户使用要注意一下,首先在原本那个账户处管理页面User中邀请新用户,用新用户登录然后接受这个邀请后。注销登录,然后重新在app里登录账户,在身份认证之后返回tailscale会询问你此时选择哪个tailnet加入,这个时候选择原本那个账户的tailnet加入,如下图。因为tailscale对于每个新注册的账户,都会默认建立分配一个tailnet,每个设备用对应账户登录后,判断这个账户在哪些net里,选择一个net加入后就进入这个域里,这个域内的设备才能相互交流。这里好像有一个bug,就是安卓app选择账户登录后,如果是在app内直接选择Google账户登录,并没有跳转到选择相应的net,而是直接进入这个账户对应的默认net,这时可以考虑选择其他登录方式,然后跳转到浏览器处进行登录,这时就没有问题了。
HTTPS保护(可选)
上面最主要的问题是,P2P打洞也是裸ip访问,没有HTTPS保护,在公网裸奔的流量(如果用Tailscale这种类似VPN的工具那还是安全的,经过了加密)。
这就需要反向代理工具以及一个域名。域名也更方便记忆,P2P分配的虚拟IP也是固定的。
反向代理使用Caddy,把内部服务器不同端口的服务,映射到不同域名,不同域名绑定相应的SSL证书(caddy自动申请管理)。
域名使用个人域名的三级域名即可。
Caddy反向代理非常简单:
1
2
3
4
5
example.com {
reverse_proxy 127.0.0.1:5244
}
将 `example.com` 替换为你自己解析后的域名。
问题:
在我的使用场景里,服务部署在内网中,然后不同地方的用户通过tailscale等软件进行P2P连接,然后使用P2P分配的虚拟ip地址获取到服务器的服务。这个时候,我想使用Caddy进行反向代理,通过域名访问相应的服务,同时希望加上HTTPS,但是这里是无法自动申请到证书的。
解决:
Caddy支持使用 DNS 验证方式获取证书:
- 使用支持 DNS 验证的证书颁发机构(如 Let’s Encrypt、ZeroSSL 等)。
- 在您的域名注册商或 DNS 服务提供商处为您的域名添加所需的 DNS TXT 记录。
- 使用 Caddy 的 DNS 插件自动获取和更新证书。
此时的CaddyFile:
1
2
3
4
5
6
7
8
9
example.com {
tls {
dns tencentcloud {
secret_id YOUR_SECRET_ID
secret_key YOUR_SECRET_KEY
}
}
reverse_proxy http://localhost:8080
}
我是使用Windows,在Caddy官方下载页面下载的caddy,这里要使用dns.providers.tencentcloud这个插件,所以在下载页要勾选这个模块:
之后去腾讯云访问管理那添加一个API key。
然后就可以自动申请到证书,就可以使用HTTPS服务了。
注册为开机自启服务:
打开命令提示符为管理员模式,导航到NSSM的解压目录(包含
nssm.exe的目录),然后运行:1
nssm install Caddy- 在NSSM的界面上,设置:
- Path: 浏览到你的Caddy可执行文件的位置。
- Startup directory: 设置为Caddy可执行文件所在的目录。
- Arguments: 输入
run。
- 点击“Install service”。
不同应用可能要求反向代理的一些相关配置,比如AList要求添加site_url,看各自服务的说明文档。
局域网域名映射DDNS(可选)
在这个场景的基础上。服务部署在内网中。当设备和服务器在同一个局域网内时,就无需使用P2P连接,而是直接使用服务器在内网中的ip访问服务即可。
但是这时,希望使用一个域名映射到服务器在局域网内的ip,而这个ip随着DHCP服务可能发生变化,考虑使用DDNS服务,将一个域名动态映射到服务器在局域网内的静态ip。然后局域网内的设备都使用这个固定的域名访问服务。
这里找了一下,我使用的域名提供商(腾讯云)并没有提供DDNS客户端(群晖的都是有),因此参考了一下云API用法和一些github的脚本,自己写了一个DDNS脚本,思路很简单:首先是定期执行,windows平台没有crontab,就用了python的apscheduler;定期执行的内容就是判断自己当前的ip,对我来说要的是局域网内的ip(供局域网其他设备连接),就查找物理网卡(以太网或者无线网卡)的ip;拿到ip后,使用腾讯云API修改DNS。
脚本参考如下:参考来源这里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
import json import sys import psutil import subprocess from tencentcloud.common import credential from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException from tencentcloud.dnspod.v20210323 import dnspod_client, models #################### 修改以下位置即可 ########################## SecretKey = { "SecretId": '', # SecretId "secretKey": '' # secretKey } params = { "Domain": "olimi.icu", # 域名,例如:example.com "Subdomains": ['i1','v1', 'p1'], # 子域名列表,例如:['www', 'blog', 'api'] "SubdomainsV6": ['i6','v6', 'p6'] # IPv6子域名列表,例如:['ipv6', 'v6', 'api6'] } ############################################################### def ping(ip): try: output = subprocess.check_output(["ping", "-n", "1", "-w", "500", ip], stderr=subprocess.STDOUT, universal_newlines=True) return True except subprocess.CalledProcessError: return False def get_address(family): try: # 获取所有网络接口 ifaces = psutil.net_if_addrs() # 优先查找以太网适配器 for iface_name in ifaces: if 'Ethernet adapter' in iface_name or '以太网' in iface_name: # 获取网卡信息 iface_info = ifaces[iface_name] # 遍历网卡信息,找到有效的IP地址 for addr in iface_info: if addr.family == family: if family == 2 and addr.address != '0.0.0.0' and ping(addr.address): # AF_INET表示IPv4 return addr.address elif family == 23 and 'Temporary' not in addr.address and not addr.address.startswith('fe80'): # AF_INET6表示IPv6 return addr.address # 如果没有找到以太网适配器,再查找WLAN适配器 for iface_name in ifaces: if 'WLAN' in iface_name or 'Wireless' in iface_name: # 获取网卡信息 iface_info = ifaces[iface_name] # 遍历网卡信息,找到有效的IP地址 for addr in iface_info: if addr.family == family: if family == 2 and addr.address != '0.0.0.0' and ping(addr.address): # AF_INET表示IPv4 return addr.address elif family == 23 and 'Temporary' not in addr.address and not addr.address.startswith('fe80'): # AF_INET6表示IPv6 return addr.address print(f"无法获取到有效的{'IPv4' if family == 2 else 'IPv6'}地址") return None except Exception as e: print(f"获取{'IPv4' if family == 2 else 'IPv6'}地址时出错: {str(e)}") return None def update_dns_record(subdomain, ip): try: # 实例化一个请求对象,每个接口都会对应一个request对象 req = models.DescribeRecordListRequest() req_params = { "Domain": params["Domain"], "Subdomain": subdomain } req.from_json_string(json.dumps(req_params)) # 返回的resp是一个DescribeRecordListResponse的实例,与请求对象对应 resp = client.DescribeRecordList(req) # 获取记录ID和LINE参数 recordid = resp.RecordList[0].RecordId recordline = resp.RecordList[0].Line # 参数赋值 update_params = { "Domain": params["Domain"], "SubDomain": subdomain, "RecordId": recordid, "RecordLine": recordline, "Value": ip, } # 实例化一个请求对象,每个接口都会对应一个request对象 req = models.ModifyDynamicDNSRequest() req.from_json_string(json.dumps(update_params)) # 返回的resp是一个ModifyDynamicDNSResponse的实例,与请求对象对应 resp = client.ModifyDynamicDNS(req) # 输出json格式的字符串回包 print(resp.to_json_string()) except TencentCloudSDKException as err: print(err) def update_ddns(): try: # 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 cred = credential.Credential(SecretKey["SecretId"], SecretKey["secretKey"]) # 实例化一个http选项,可选的,没有特殊需求可以跳过 httpProfile = HttpProfile() httpProfile.endpoint = "dnspod.tencentcloudapi.com" # 实例化一个client选项,可选的,没有特殊需求可以跳过 clientProfile = ClientProfile() clientProfile.httpProfile = httpProfile # 实例化要请求产品的client对象,clientProfile是可选的 global client client = dnspod_client.DnspodClient(cred, "", clientProfile) # 获取本机IPv4地址 ipv4 = get_address(2) # AF_INET表示IPv4 if ipv4: print(f"IPv4地址: {ipv4}") # 更新IPv4的DNS解析记录 for subdomain in params["Subdomains"]: update_dns_record(subdomain, ipv4) # 获取本机IPv6地址 ipv6 = get_address(23) # AF_INET6表示IPv6 if ipv6: print(f"IPv6地址: {ipv6}") # 更新IPv6的DNS解析记录 for subdomain in params["SubdomainsV6"]: update_dns_record(subdomain, ipv6) except Exception as e: print(f"更新DNS解析记录时出错: {str(e)}") if __name__ == '__main__': update_ddns()requirement.txt
1 2 3 4 5 6 7 8 9 10 11 12
APScheduler==3.10.4 certifi==2024.2.2 charset-normalizer==3.3.2 idna==3.7 psutil==5.9.8 pytz==2024.1 requests==2.31.0 six==1.16.0 tencentcloud-sdk-python==3.0.1143 tzdata==2024.1 tzlocal==5.2 urllib3==2.2.1
然后Caddy添加一个HTTP监听的反向代理,局域网内就不弄HTTPS了。参考:
1
2
3
4
5
6
7
http://p1.olimi.icu {
reverse_proxy http://localhost:5244
}
http://v1.olimi.icu {
reverse_proxy http://localhost:8096
}
这里有碰到一个坑是,一开始忘记加上HTTP了,取消自动HTTPS的方法(来源):
Any of the following will prevent automatic HTTPS from being activated, either in whole or in part:
- Explicitly disabling it via JSON or via Caddyfile
- Not providing any hostnames or IP addresses in the config
- Listening exclusively on the HTTP port
- Prefixing the site address with
http://in the Caddyfile- Manually loading certificates (unless
ignore_loaded_certificatesis set)
但是我加上后p1.olimi.icu这个域名可以了,v1.olimi.icu这个域名一直不行,还是一直转去https,最后发现是由于缓存问题(我倒)。
当前可用网络搜索(可选)
按照上面所做的不同网络环境的域名映射(公网HTTPS域名、局域网域名),那就更进一步地进行便捷化:到底当前应该选用哪个域名(即哪个网络是可用的)。比如对于我的网盘服务,内网需要访问v1.olimi.icu,公网使用P2P时访问v2.olimi.icu,对我来说哪个域名都无所谓,只要当前网络可达,那自动帮我选择一个就最好了。
根据这个需求,实现思路也比较简单,只要能够在当前客户端上,能够知道所需服务的域名列表(这里就是v1.olimi.icu、v2.olimi.icu)哪个可用,就跳转至哪个即可。
为此,我需要一个服务,实现让客户端感知服务端的哪个网络(域名、IP)可用。这里使用一个非常简单的静态前端脚本实现。思路很简单,就是客户端在浏览器执行这个脚本,尝试访问一下对应的域名,如果可通,则跳转过去。
代码参考(index.html):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
<!DOCTYPE html> <html> <head> <title>Dynamic Domain Redirection</title> <script> async function testConnectivity(domain) { try { const timeout = 1000; // 设置超时时间,单位为毫秒 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const response = await fetch(`https://${domain}`, { signal: controller.signal }); clearTimeout(timeoutId); return response.ok; } catch (error) { return false; } } async function redirectToDomain() { const domainString = '__NEXT_PUBLIC_DOMAIN_LIST__'; if (!domainString) { alert('没有配置可用的域名!'); return; } const domains = domainString.split(','); for (const domain of domains) { const isReachable = await testConnectivity(domain); if (isReachable) { window.location.href = `https://${domain}`; return; } } alert('无法连接到任何可用的域名!如果不是在局域网环境中,请考虑打开P2P(TailScale)软件。'); } window.onload = function() { redirectToDomain(); }; </script> </head> <body> <h1>Redirecting...</h1> <p>You will be redirected to the appropriate domain based on your network environment.</p> </body> </html>
这个脚本就需要放到一定可达的网络,即公网上,供客户端任何时候都能够访问。我选择Github+Vercel部署(就是一个静态网页)。另外,由于有多个不同的服务,所以这个脚本需要把访问测试的域名列表提取成环境变量, 这样一套代码可以在Vercel部署多次,然后替换掉环境变量即可重复使用。为此,加上必要的JS和node的需求提取和设置环境变量:
代码参考
build.js
1 2 3 4 5 6 7 8 9
const fs = require('fs'); const path = require('path'); const htmlFilePath = path.join(__dirname, 'index.html'); let htmlContent = fs.readFileSync(htmlFilePath, 'utf8'); htmlContent = htmlContent.replace('__NEXT_PUBLIC_DOMAIN_LIST__', process.env.NEXT_PUBLIC_DOMAIN_LIST); fs.writeFileSync(htmlFilePath, htmlContent);
package.json
1 2 3 4 5
{ "scripts": { "build": "node build.js" } }
简单说一下部署:
- 在Github创建一个仓库、public的就行。上传这三个文件(git clone、git add、git commit、git push)。
- 在vercel中Add project,选择github对应的仓库。在build那里override npm的设置,对着提示写
- 设置环境变量NEXT_PUBLIC_DOMAIN_LIST为对应的域名列表,比如v1.olimi.icu,v2.olimi.icu.
- 部署。可以设置一个单独的域名跳转到这个vercel部署网站,比如v.olimi.icu.
- 可以参考我的代码:https://github.com/Olimiya/NASPublicDomainHelp
问题:混合请求与CORS问题。在浏览器里由一个域名跳转到另一个域名是有很多限制的。首先两者必须是同一种协议,比如都是HTTPS,这里vercel部署的默认都是https,因此就要保证我们跳转过去的域名都是https,这个交给caddy去办吧。另外CORS问题也很讨厌,由一个域名跳转到另一个域名,浏览器默认添加上cors请求头,这时就要求服务器响应头有Access-Control-Allow-Origin,且和源域名匹配,才允许跳转。也交给caddy去办。
CaddyFile参考(测试了几种写法,好像都可以):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
(tencentcloud) { tls { dns tencentcloud { secret_id xxx secret_key xxx } } } v2.olimi.icu, v1.olimi.icu { import tencentcloud @cors { header Sec-Fetch-Mode cors } header @cors Access-Control-Allow-Origin https://v.olimi.icu reverse_proxy http://localhost:8096 { header_down -Access-Control-Allow-Origin } } p2.olimi.icu, p1.olimi.icu { import tencentcloud reverse_proxy http://localhost:5244 } i2.olimi.icu, i1.olimi.icu { import tencentcloud header { Access-Control-Allow-Origin * Access-Control-Allow-Methods GET, POST, OPTIONS Access-Control-Allow-Headers Origin, X-Requested-With, Content-Type, Accept } reverse_proxy http://localhost:2283 }
但折腾到最后还是有很多缺陷,上述服务还是针对于浏览器的访问到,v.olimi.icu的统一域名也不能等同实际上服务的域名v1.olimi.icu,很多使用是在特定的客户端,比如emby app和immich app上使用,这个使用v.olimi.icu这个域名是不行的。有缺陷。
公网服务器跳板(可选)
内网穿透参考:https://blog.zilch40.wang/post/way-to-access-a-local-server-from-internet/
考虑一个分享给陌生人的场景,或者在无法P2P的场景使用(比如在一台临时的新设备上),通过一台在公网的服务器(轻量云服务器)作为跳板,转发内部服务。此时方案的瓶颈在于公网服务器的带宽(带宽是很贵的,小服务器1-2Mbps小水管)
转发方案考虑:隧道转发、frps等。
更新:IPV6配置
碎碎念:最近租房办理家庭带宽,可以弄自己的ipv6了。于是折腾。
大概步骤包括(完整过程指导参考https://ipw.cn/doc/ipv6/user/enable_ipv6.html):
- 要到光猫后台管理员密码
- 登录光猫后台设置
INTERNET_R_VID这个连接为桥接(Bridge),勾选连接模式IPV4/V6,应用。 - 到路由器后台选择PPPoe上网模式,输入拨号上网账号密码(这里如果不知道的打电话给运营商找人工要)。
V6特殊:这里我V6上网,需要在IPV6上网时选择PPPoeV6上网,不知道是不是运营商或者路由器的特点。注意这里NAT6不要开。
验证。
验证方法有很多,首先在自己PC上找网络信息,终端输入ipconfig。
这里IPV6有多个地址,前面两个感觉都能用,不知道有什么区别。然后Temporary IPv6 Address和IPV6 Address区别是,后面的是永久地址,用于需要稳定连接的场景,如服务器通信;前者是临时地址,用于浏览网页等外部通信,防止追踪。
这里可以到一些ipv6测试网站,比如下面的,可以显示出地址,看到网站跟踪的就是临时IPV6地址。所以我们自己使用的ipv6用上面的两个地址之一就行。
- DDNS补充。这里虽然说是永久地址,但只是对我们局域网内而言,不会重新计算地址。然而WLAN公网地址可能是会变更的,这个和运营商有关。固定公网IP也不一定能申请到,申请不到也没关系。自己做一层DDNS映射即可。完整脚本更新在上述DDNS章节。
反向代理。注意上面CaddyFile配置不能直接暴露域名,而是需要设置一个端口,比如。
1 2 3
p6.olimi.icu:8002 { reverse_proxy localhost:xxxx }
这里是大坑。前面使用反向代理都是在私域流量实现的,比如P2P,虽然看起来我们直接访问了一个域名,但其实中间经过了Wireguard协议层。而现在我们直接使用公网流量访问,反向代理如果只设置为域名,外部访问直接使用域名访问,则默认使用443(HTTPS)端口访问,而家庭带宽运营商默认会屏蔽掉80和443端口。
注意:
- 光猫连接模式,有些教程显示为: 选择
3_INTERNET_R_VID_41。但我这里是2_INTERNET_R_VID_41。前面数字不重要,后面的对就行。 - 光猫设置为桥接模式后,通过路由器连接的电脑就访问不到光猫的后台了。这时候如果出错了有两种恢复手段:一是将电脑通过有线直接连接到光猫(光猫如果有WIFI功能直接连接WIFI也可);二是将路由器设置为DHCP上网,同时手动修改电脑IP为光猫LAN的网店,比如光猫LAN是192.168.1.1,那电脑设置为192.168.1.10,就能够上网。
- DDNS时,域名设置选择类型为AAAA。



