首页
关于
友情链接
推荐
悠笙の喵罐头
Search
1
给Android 4.9内核添加KernelSU支持
325 阅读
2
Docker下中心化部署EasyTier
188 阅读
3
记一次为Android 4.9内核的ROM启用erofs支持
146 阅读
4
在TrueNAS上使用Docker安装1Panel
97 阅读
5
为黑群晖迁移RR引导盘
91 阅读
Android
运维
NAS
开发
登录
Search
标签搜索
Linux
Android
AOSP
C&C++
Docker
TrueNAS
Windows
caf/clo
Kernel
MSVC
编程
EasyTier
Web
群晖
Alist
OneDrive
1Panel
STL
神楽悠笙
累计撰写
11
篇文章
累计收到
1
条评论
首页
栏目
Android
运维
NAS
开发
页面
关于
友情链接
推荐
悠笙の喵罐头
搜索到
11
篇与
的结果
2025-04-19
跨平台服务编写日记 Ep.1 统一的日志管理
前阵子心血来潮,想为在使用的一个跨平台的控制台服务类应用程序编写一个自己的管理程序,加一些功能。因而设计了一套简单的服务运行流程。{alert type="warning"}本系列文章中的观点和方案为我根据自己已有的知识储备结合DeepSeek的帮助所归纳设计,并未经过严格测试,并不保证在生产环境中使用的可行性和稳定性{/alert}大致思路大致分为个线程,分别用作:日志记录目标应用实例管理,可能不止一个线程监听IPC消息处理IPC收到的消息(主进程)本文着重讨论的是日志记录部分。编写思路为什么要给日志记录单开一个线程,个人考虑是因为本身就是多线程的架构,需要编写一个统一的日志记录模块。如果每个线程单独打印,则很有可能出现两个线程同时写入文件或者同时输出到控制台,造成日志混乱。 因此,日志记录大致思路就是:定义一个队列,存储日志内容和等级创建一个线程,不断地从线程中取出元素,根据设定的日志等级决定是否打印到控制台或者输出到文件外部push日志内容到队列中一些细节上的内容保证可移植性,尽量使用STL库编写,如使用std::thread而不是pthread保证线程安全,需要使用互斥锁之类的保护相应变量让日志队列为空时线程等待,想到编写一个类似于Java下BlockingQueue的阻塞队列指定一个日志等级,超过这个等级的日志才会被保存或者打印通过va_list实现不定参数,使日志记录有sprintf的的使用体验开始编写有了上述思路,整体编写就很简单了。BlockingQueue偷懒了,这部分直接让DeepSeek写的为了实现一个多线程安全的阻塞队列,当队列为空时调用front()会阻塞直到其他线程添加元素,我们可以结合互斥锁(std::mutex)和条件变量(std::condition_variable)来同步线程操作。代码实现互斥锁(std::mutex)所有对队列的操作(push、front、pop、empty)都需要先获取锁,确保同一时间只有一个线程能修改队列,避免数据竞争。条件变量(std::condition_variable)当调用front()时,如果队列为空,线程会通过cv_.wait()释放锁并阻塞,直到其他线程调用push()添加元素后,通过cv_.notify_one()唤醒一个等待线程。cv_.wait()需配合std::unique_lock,并在等待时自动释放锁,避免死锁。使用谓词检查([this] { return !queue_.empty(); })防止虚假唤醒。元素获取与移除front()返回队首元素的拷贝(而非引用),确保调用者获得数据时队列的锁已释放,避免悬空引用。pop()需显式调用以移除元素,确保队列状态可控。#include <queue> // 队列 #include <mutex> // 互斥锁 #include <condition_variable> // 条件变量 template<typename T> class BlockingQueue { public: // 向队列中添加元素 void push(const T& item) { std::lock_guard<std::mutex> lock(mtx_); queue_.push(item); cv_.notify_one(); // 通知一个等待的线程 } // 获取队首元素(阻塞直到队列非空) T front() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this] { return !queue_.empty(); }); // 阻塞直到队列非空 return queue_.front(); } // 获取队首元素并移除 T take() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this] { return !queue_.empty(); }); T item = std::move(queue_.front()); // 移动语义避免拷贝 queue_.pop(); return item; } // 移除队首元素(需外部调用,非阻塞) void pop() { std::lock_guard<std::mutex> lock(mtx_); if (!queue_.empty()) { queue_.pop(); } } // 检查队列是否为空 bool empty() const { std::lock_guard<std::mutex> lock(mtx_); return queue_.empty(); } private: mutable std::mutex mtx_; // 互斥锁 std::condition_variable cv_; // 条件变量 std::queue<T> queue_; // 内部队列 };Log类Log.h#pragma once #include <iostream> #include <fstream> #include <cstring> #include <thread> #include <chrono> #include <mutex> #include <cstdio> #include <cstdarg> #include <atomic> #include "BlockingQueue.h" enum LogLevel { LEVEL_VERBOSE,LEVEL_INFO,LEVEL_WARN,LEVEL_ERROR,LEVEL_FATAL,LEVEL_OFF }; struct LogMsg { short m_LogLevel; std::string m_strTimestamp; std::string m_strLogMsg; }; class Log { private: std::ofstream m_ofLogFile; // 日志文件输出流 std::mutex m_lockFile; // 文件操作互斥锁 std::thread m_threadMain; // 后台日志处理线程 BlockingQueue<LogMsg> m_msgQueue; // 线程安全阻塞队列 short m_levelLog, m_levelPrint; // 文件和控制台日志级别阈值 std::atomic<bool> m_exit_requested{ false }; // 线程退出标志 std::string getTime(); // 获取当前时间戳 std::string level2str(short level, bool character_only); // 级别转字符串 void logThread(); // 后台线程函数 public: Log(short default_loglevel = LEVEL_WARN, short default_printlevel = LEVEL_INFO); ~Log(); void push(short level, const char* msg, ...); // 添加日志(支持格式化) void set_level(short loglevel, short printlevel); // 设置日志级别 bool open(std::string filename); // 打开日志文件 bool close(); // 关闭日志文件 }; Log.cpp#include "Log.h" std::string Log::getTime() { using sc = std::chrono::system_clock; std::time_t t = sc::to_time_t(sc::now()); char buf[20]; #ifdef _WIN32 std::tm timeinfo; localtime_s(&timeinfo,&t); sprintf_s(buf, "%04d.%02d.%02d-%02d:%02d:%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec ); #else strftime(buf, 20, "%Y.%m.%d-%H:%M:%S", localtime(&t)); #endif return buf; } std::string Log::level2str(short level, bool character_only) { switch (level) { case LEVEL_VERBOSE: return character_only ? "V" : "Verbose"; case LEVEL_WARN: return character_only ? "W" : "Warning"; case LEVEL_ERROR: return character_only ? "E" : "Error"; case LEVEL_FATAL: return character_only ? "F" : "Fatal"; } return character_only ? "I" : "Info"; } void Log::logThread() { while (true) { LogMsg front = m_msgQueue.take(); // 阻塞直到有消息 // 处理文件写入 if (front.m_LogLevel >= m_levelLog) { std::lock_guard<std::mutex> lock(m_lockFile); // RAII 管理锁 if (m_ofLogFile) { m_ofLogFile << front.m_strTimestamp << ' ' << level2str(front.m_LogLevel, true) << ": " << front.m_strLogMsg << std::endl; } } // 处理控制台打印 if (front.m_LogLevel >= m_levelPrint) { printf("%s %s: %s\n", front.m_strTimestamp.c_str(), level2str(front.m_LogLevel, true).c_str(), front.m_strLogMsg.c_str()); } // 检查退出条件:队列为空且标志为真 if (m_exit_requested.load() && m_msgQueue.empty()) break; } return; } Log::Log(short default_loglevel, short default_printlevel) { set_level(default_loglevel, default_printlevel); m_threadMain = std::thread(&Log::logThread, this); } Log::~Log() { m_exit_requested.store(true); m_msgQueue.push({ LEVEL_INFO, getTime(), "Exit." }); // 唤醒可能阻塞的线程 if (m_threadMain.joinable()) m_threadMain.join(); close(); // 确保文件关闭 } void Log::push(short level, const char* msg, ...) { va_list args; va_start(args, msg); const int len = vsnprintf(nullptr, 0, msg, args); va_end(args); if (len < 0) return; std::vector<char> buf(len + 1); va_start(args, msg); vsnprintf(buf.data(), buf.size(), msg, args); va_end(args); m_msgQueue.push({level,getTime(),buf.data()}); } void Log::set_level(short loglevel, short printlevel) { m_levelLog = loglevel; m_levelPrint = printlevel; } bool Log::open(std::string filename) { m_lockFile.lock(); m_ofLogFile.open(filename.c_str(), std::ios::out); m_lockFile.unlock(); return (bool)m_ofLogFile; } bool Log::close() { m_lockFile.lock(); m_ofLogFile.close(); m_lockFile.unlock(); return false; }说明类/结构说明LogLevel 枚举定义日志级别:VERBOSE, INFO, WARN, ERROR, FATAL, OFF。OFF不应被用于记录的日志等级,仅用于当需要关闭日志记录时将阈值设置为该项以实现所有日志都不记录LogMsg 结构体封装日志消息:m_LogLevel:日志级别。m_strTimestamp:时间戳字符串。m_strLogMsg:日志内容。成员变量说明变量说明m_ofLogFile文件输出流,用于写入日志文件。m_lockFile互斥锁,保护文件操作。m_threadMain后台线程,处理日志消息的消费。m_msgQueue阻塞队列,存储待处理的日志消息。m_levelLog写入文件的最低日志级别(高于此级别的消息会被记录)。m_levelPrint打印到控制台的最低日志级别。m_exit_requested原子标志,控制日志线程退出。函数说明函数说明getTime获取当前时间戳字符串(跨平台实现)。level2str将日志级别转换为字符串(如 LEVEL_INFO → "I" 或 "Info")。logThread后台线程函数:消费队列消息,写入文件或打印。构造函数初始化日志级别,启动后台线程。析构函数设置退出标志,等待线程结束,确保处理剩余消息。push格式化日志消息(支持可变参数)并推入队列。set_level动态设置日志级别和打印级别。open/close打开/关闭日志文件。完整代码及测试样例下载:demo.zip
2025年04月19日
22 阅读
0 评论
0 点赞
2025-04-15
Docker下中心化部署EasyTier
EasyTier本身是个去中心化的p2p工具,任意节点都可作为转发服务器使用。但是每个节点的配置文件都得手动编写,从tailscale迁移过来时觉得有点不习惯。再加上摸索阶段经常需要修改配置文件的内容,故打算还是中心化部署EasyTier的Dashboard,统一管理设备。 项目仓库:{cloud title="easytier/easytier" type="github" url="https://github.com/easytier/easytier" password=""/}官方文档并未提供单独部署config-server的方式,但是实际上也不难,服务端已经包含在下载下来的二进制文件中了。本文着重论述通过Docker Compose安装,需要二进制模式安装的可参考下方参考文章。分析dashboard的部署主要分为两个部分,一个是后端RESTful API,一个是前端web控制台。其中API的服务端即Release里的easytier-web,直接运行即可运行服务,而web控制台是静态HTML,官方提供了一个网站但是自建api用不了,源码也开源在GitHub仓库下的easytier-web目录下。开整部署APIDocker部署,没什么好说的。 需要开放两个端口:11211/tcp: API接口,HTTP22020/udp: 用于客户端(easytier-core)和服务器通信目录映射需要映射容器内/app文件夹,用作存储持久化数据。 Compose文件如下:services: easytier: restart: always hostname: easytier volumes: - /opt/easytier/api:/app ports: - "127.0.0.1:11211:11211" - "22020:22020/udp" environment: - TZ=Asia/Shanghai image: easytier/easytier:latest entrypoint: easytier-web此处镜像和官方文档的Docker部署客户端用的是同一个,默认入口点是easytier-core,因此运行webapi需要指定entrypoint为easytier-web。 因为API接口需要HTTPS,故此处没有直接将11211端口暴露公网,而是暴露到127.0.0.1再使用反向代理实现HTTPS。设置反向代理我使用的是1Panel,因此直接在面板中创建一个网站,反向代理到设置的API端口即可。部署Web控制台如果直接使用官方的控制台然后填入自建API地址的话实测会有CORS跨域问题,解决后发现注册会过不了验证码,不清楚为什么。查阅issues说用自己的域名部署就不会出这个问题。正常部署流程拉取代码仓库,配置nodejs环境,执行下面两个指令编译出html文件:pnpm -r install pnpm -r build再将编译出的文件上传到服务器上。不正常部署流程因为懒得配置nodejs环境了我选择直接偷官方的(官方对不起!(红豆泥私密马赛qwq)) 在1Panel面板中为网站添加反向代理:将官方的web控制台反向代理到API站点下的/web/路径。注册控制台账号完成部署后打开https://你的域名/web/,将Api Host改为https://你的域名,注意填Api Host的时候URL末尾不要带“/”,否则会出莫名其妙的问题。点击下方Register注册一个账号再使用这个账号登录即可进入控制台。客户端配置将启动参数全部删除,仅保留--config-server udp://你的ip:22020/你的用户名即可。运行easytier-core,再回到控制台即可看到设备。点击右边的设置按钮,点击Create为其创建网络,接下来的步骤就和本地GUI模式操作一样了,这里就不赘述了。保存后在network栏选择新建的网络即可加入。因为Docker重启容器内数据会丢失,Docker下部署客户端时需要映射一个文件到容器内/usr/local/bin/et_machine_id用作保存machine id,否则每次重启后都需要重新为其配置网络。同时,给容器设置hostname可以作为设备在web控制台显示的名称。这里贴一下我的compose:services: easytier: command: '--config-server udp://<ip>:22020/KaguraiYoRoy' environment: - TZ=Asia/Shanghai hostname: truenas image: easytier/easytier:latest labels: com.centurylinklabs.watchtower.enable: 'true' mem_limit: 0m network_mode: host privileged: True restart: always volumes: - >- /mnt/systemdata/DockerData/easytier/app/et_machine_id:/usr/local/bin/et_machine_id watchtower: command: '--interval 3600 --cleanup --label-enable' environment: - TZ=Asia/Shanghai - WATCHTOWER_NO_STARTUP_MESSAGE image: containrrr/watchtower restart: always volumes: - /var/run/docker.sock:/var/run/docker.sock参考文章:https://blog.mitsea.com/1a57bda595c580088006c17d6ba2a744/https://github.com/EasyTier/EasyTier/issues/722https://github.com/EasyTier/EasyTier/issues/577
2025年04月15日
188 阅读
0 评论
1 点赞
2025-03-20
C++使用WINAPI查询DNS解析记录
这篇文章是古早时期的博客上迁移过来的(什么赛博秽土转生),如有错误欢迎指正背景为API配置了多条访问线路,以应对部分地区无法访问导致服务不可用的情况。开始是想到在网站里新建一个文件保存节点信息,发现行不通,联不通的地区根本无法获知其他线路;于是想到用DNS解析记录,用一个TXT解析记录来保存相应的节点数据,以备查询实战查询资料一番Bing下来,发现大部分现有的文章都是使用socket来直接发送查询数据包,获取到的也都是A和CNAME类型的记录,方案不可行。最后,把目光锁定到了MSDN上的DnsQuery函数上,同时找到一篇样例:使用DnsQuery解析主机名函数分析从官方文档上得知,函数所需头文件为windns.h,需要包含引入库文件Ws2_32.lib和Dnsapi.lib,函数的参数如下:DNS_STATUS DnsQuery_A( [in] PCSTR pszName, [in] WORD wType, [in] DWORD Options, [in, out, optional] PVOID pExtra, [out, optional] PDNS_RECORD *ppQueryResults, [out, optional] PVOID *pReserved );参数解释:pszName是要查询的主机名;wType是查询类型,如A记录,CNAME,TXT等,具体的MSDN官方给出了文档:DNS ConstantsOptions字面意思上理解是查询方式,我直接使用DNS_QUERY_STANDARD,具体也可以查询文档:DNS ConstantspExtra和pReserved直接给NULL就行ppQueryResults传入查询结果的指针,类型是PDNS_RECORD,这个需要传入变量值的引用。返回值,DNS_STATUS类型,如果查询失败会返回Winerror.h中的相应错误码使用明白了使用方法,那就简单了:我的TXT记录域名为test.iyoroy.cn,查询部分代码如下:PDNS_RECORD pDnsRecord; DNS_STATUS QueryRet = DnsQuery(L"test.iyoroy.cn", DNS_TYPE_TEXT, DNS_QUERY_STANDARD, NULL, &pDnsRecord, NULL); if (QueryRet) { MessageBox(GhWnd, L"DNS查询失败!\r\n将使用默认节点", L"Warning", MB_ICONERROR | MB_OK); } std::wstring strQueryRes = *pDnsRecord->Data.TXT.pStringArray;//这个就是查询结果 成功实现功能后记TXT记录会自动过滤掉空格、换行,因此我选择记录base64编码,使用时再解码并使用stringstream拆分空格。Base64解码我借鉴了这篇文章:C++进行base64编码和解码,以下是我用宽字节重写的解码函数{collapse}{collapse-item label="展开代码"}//Function:Base64解密 static const std::wstring base64_chars = L"ABCDEFGHIJKLMNOPQRSTUVWXYZ" L"abcdefghijklmnopqrstuvwxyz" L"0123456789+/"; static inline bool is_base64(wchar_t c) { return (isalnum(c) || (c == '+') || (c == '/')); } std::wstring base64_decode(std::wstring const& encoded_string) { int in_len = encoded_string.size(); int i = 0; int j = 0; int in_ = 0; wchar_t char_array_4[4], char_array_3[3]; std::wstring ret; while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { char_array_4[i++] = encoded_string[in_]; in_++; if (i == 4) { for (i = 0; i < 4; i++) char_array_4[i] = base64_chars.find(char_array_4[i]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (i = 0; (i < 3); i++) ret += char_array_3[i]; i = 0; } } if (i) { for (j = i; j < 4; j++) char_array_4[j] = 0; for (j = 0; j < 4; j++) char_array_4[j] = base64_chars.find(char_array_4[j]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; } return ret; }拆分:wstringstream wDnsRec(base64_decode(*pDnsRecord->Data.TXT.pStringArray)); wstring wNodes[16]; unsigned int nNodes = 0; while (wDnsRec >> wNodes[nNodes]) nNodes++;{/collapse-item}{/collapse}之后再选择可用节点就行参考文章:https://learn.microsoft.com/zh-cn/previous-versions/troubleshoot/windows/win32/use-dnsquery-resolve-host-nameshttps://learn.microsoft.com/en-us/windows/win32/dns/dns-constantshttps://blog.csdn.net/sky04/article/details/6881649
2025年03月20日
19 阅读
0 评论
1 点赞
2025-03-13
通过Alist使TrueNAS同步到OneDrive
背景手上有个E5订阅,本来用的方案是Docker运行driveone/onedrive:edge的方式来实现同步,但是这个方案一个是这种方式没有GUI/WebUI,一个是每次同步的时候都会占用掉CPU 25%-50%的性能。考虑到TrueNAS自带的同步方案可以向WebDAV同步,因此想到能用Alist来挂载OneDrive并转换成WebDAV供TrueNAS挂载。折腾过程安装Alist为Alist创建持久化存储文件夹,并根据Alist官方文档编写Docker Compose:services: alist: environment: - PUID=3000 - PGID=950 - UMASK=022 image: xhofe/alist:latest ports: - '8088:5244' restart: always volumes: - /mnt/systemdata/DockerData/alist/etc:/opt/alist/data - /mnt/data/Storage:/mnt/data此处我将Alist端口开放在8088,其中映射/mnt/data/Storage是为了让Alist可以管理本地的存储;映射/mnt/systemdata/DockerData/alist/etc作为存储Alist数据的文件夹。 关于如何配置Alist上的OneDrive本文不做讨论,请查询Alist官方文档。此处我将我的OneDrive挂载在/OneDrive。 完成后进入Alist后台-用户,编辑你的用户或者创建一个新用户,勾选Webdav 读取、Webdav 管理以使得该用户可以使用WebDAV。 配置TrueNAS同步进入TrueNAS后台-Credentials-Backup Credentials,添加一个Cloud Credential,参数如下:Provider: WebDAVName: 自定义URL: Alist地址+/dav,例如我这里填写http://127.0.0.1:8088/davWebDAV Service: OTHERUsername和Password: Alist账号密码Verify Credential确认没问题之后保存。 接着进入TrueNAS后台-Data Protection,添加一个Cloud Sync Task,Provider下的Credentials选择刚刚创建的Alist的WebDAV,点击下一步。此处的参数有很多种,详解如下:Direction: 分为PULL和PUSH,分别对应云端同步到本地和本地同步到云端Transfer Mode:COPY: 复制文件,若源文件夹中先前有的文件后来删除了云端的不会被删除MOVE: Copy后删除源文件夹相关文件SYNC: 保持源文件夹和目标文件夹同步,源文件夹删除的文件也会在目标里删除Directory/Files: 即本地需要同步的文件或文件夹Folder: 即云端需要同步的文件夹Description: 注释Schedule: Cron定时,可以使用他预设的时段或者自己编写比如我这里选择的是PUSH,SYNC,从/mnt/data/Storage同步到/OneDrive/TrueNAS,每天0:00执行。 编辑完成后保存,即可在你设置的时段自动将本地文件上传到OneDrive。旧方案的项目地址:https://github.com/abraunegg/onedrive参考文章:https://alist.nn.ci/zh/guide/install/docker.htmlhttps://alist.nn.ci/zh/guide/drivers/onedrive.html
2025年03月13日
77 阅读
0 评论
0 点赞
2025-03-09
使用家宽无公网IP+CDNfly实现建站
前言众所周知家宽建站一般需要有公网IP,但实际上没有公网IP时的某些情况通过特殊手段也可以实现在运营商的出口IP上公开一个端口用于外部对内连接的http端口,即NAT穿透(和FRP、ngrok、花生壳这样的内网穿透不一样,这种穿透不需要服务端)前置条件下载NatTypeTester,检测本机的NAT类型。关于NAT类型是什么请自行查阅资料。 请确保你的网络环境满足如下条件:RFC3489下的NAT类型需为Full ConeRFC5780下的TCP的映射行为需要是EndpointIndependent如果出现UDPBlocked请更换服务器提升NAT类型的办法减少路由层数,光猫拨号的尽量直接将设备接在光猫下,多层路由的尽量将设备直接接在最顶上那层路由下开启路由器的UPnP设置DMZ主机为服务器IP开始使用了Lucky作为NAT穿透的工具,通过Docker Compose部署在TrueNAS上,TrueNAS本地的9080开放了HTTP端口。 部署Web服务器不在本文论述的范围内。安装Lucky按照官网介绍,编写DockerCompose:services: lucky: image: gdy666/lucky network_mode: host restart: always volumes: - /mnt/systemdata/DockerData/lucky/luckyconf:/goodluck请修改持久化存储路径为你自己的路径。 其中network_mode需要为host,当然不是host也可以部署,但是不在本文讨论范围内。 安装完成后打开http://[YourIP]:16601,默认账号666默认密码666。登录后按照要求修改安全设置。配置穿透打开Lucky页面左侧的STUN内网穿透,并创建穿透规则。 穿透本地端口填写一任意端口(注意不要和本机的服务冲突),目标地址和端口填写内网下的Web服务所在IP和端口。 完成上述配置后以下几种穿透方式请任选一种{tabs}{tabs-pane label="NAT-PMP方式"}开启路由器UPnP功能,打开STUN穿透的NAT-PMP开关,NAT-PMP网关地址填入路由器地址即可。 {/tabs-pane}{tabs-pane label="DMZ主机方式"}在路由器DMZ主机设置将Lucky所在服务器IP设置为DMZ主机,然后关闭STUN穿透的UPnP、NAT-PMP选项。{/tabs-pane}{tabs-pane label="非Docker安装+UPnP"}开启路由器UPnP功能,打开STUN穿透的UPnP开关,UPnP网关IP填写路由器IP,UPnP客户端本地IP填写Lucky所在主机IP,其他留空。 {/tabs-pane}{tabs-pane label="Docker安装+UPnP"}需要先在非Docker的环境下运行一次Lucky以获取UPnP接口地址。比如我在电脑(Windows)上安装Lucky并创建一条测试隧道,填写内容和上文非Docker安装相同,目标地址和目标端口可随意填写。开启穿透后在日志里能看到类似如下内容:UPNP===>Control URL: http://192.168.3.1:5351/ctl/IPCon(这里的URL是我的小米路由器AX3600的UPnP管理地址,不同路由器不一样) 复制这里的Control URL,再在Docker内的Lucky中将Control URL填写到UPnP控制接口地址一项中并开启穿透即可。 {/tabs-pane}{/tabs}如果不出问题应该能看到运营商的公网出口所分配的地址和端口,若能够从外面访问或者itdog测速能绿就代表穿透成功了。配置CDNfly的WebHook因为这种方式获得的公网IP和端口都是动态的,因此需要类似于DDNS的服务来固定访问方式,可以使用CloudFlare Workers进行重定向,也可以使用CDN动态修改回源IP和端口。我使用的是后面那种方式。 我的CDN使用的是cdnfly管理系统,查阅cdnfly的官方文档可以找到API请求方式。在编辑STUN内网穿透规则时下面打开Webhook并勾选仅在地址和上一次不同时触发Webhook,接口地址填入cdn网站管理API地址:https://[your-cdn-domain]/v1/sites,请求方法PUT;请求头标填入api-key和api-secret(可以在CDNfly后台看到):api-key: Your API Key api-secret: Your API Secret请求体用json格式按照cdnfly官方文档的请求方式构建payload(请将id字段改成你的网站id,若存在多个网站则按照如下格式多复制几个并修改对应id即可):[ { "id": 114, "backend_http_port": #{port}, "backend": [ { "addr": "#{ip}", "weight": 1, "state": "up" } ] }, { "id": 514, "backend_http_port": #{port}, "backend": [ { "addr": "#{ip}", "weight": 1, "state": "up" } ] } ]其中,#{ip}和#{port}是Lucky提供的参数,标识获取到的公网IP和端口。 保存,不出意外的话会自动将CDN对应网站的回源地址和端口改成运营商公网IP和端口。安全问题我本人建议使用HTTPS穿透,毕竟HTTP是明文传输,安全性还是不能保证。如果使用HTTPS,需要将上文CDN API调用的Payload中backend_http_port改成backend_https_port并在CDN处改为使用HTTPS回源。参考文章:https://doc.cdnfly.cn/wangzhanguanli-v1-sites.htmlhttps://lucky666.cn/docs/introhttps://www.bilibili.com/opus/971100369193009187https://github.com/gdy666/lucky
2025年03月09日
72 阅读
0 评论
1 点赞
1
2
3