# frpclient **Repository Path**: vipkwds/frpclient ## Basic Information - **Project Name**: frpclient - **Description**: No description available - **Primary Language**: Go - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-21 - **Last Updated**: 2026-10-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # frp —— 可复用的内嵌 frp 客户端库 把 **frp v0.52.3 客户端以库的形式**内嵌到任意 Go 服务:进程内 goroutine 运行 frpc—— **无独立进程、无 ini 落盘、无 admin 监听、无新增本地端口**(frp 是纯出站协议)。 定位:把一个本地 HTTP 服务穿透到公网(`http` + `subdomain` 单 proxy)。 换 frps、多 proxy、tcp/udp 类型通过 `Options` 逃生舱或自行扩展 `renderINI` 实现。 模块导入路径:`gitee.com/vipkwds/frpclient`(`go get gitee.com/vipkwds/frpclient`)。 **go 1.21+ 即可引入**——依赖 deliberately 锁在 frp v0.52.3 原生解析版本, 不追新;调用方需要更高版本的传递依赖(安全补丁等)用自己的 go.mod 抬版即可 (MVS 取高者,无需 replace)。 > 本包是从 goprint 的 frp 能力**独立抽出的复用版**,不依赖 goprint 的任何内部包 > (无 config / logger / instance 耦合),任何 Go 系统可直接引入。 ## 快速开始 ```bash cd frp go test ./... # 全部单测(含不可达 frps 的生命周期用例) ``` ```go import "gitee.com/vipkwds/frpclient" ctx, cancel := context.WithCancel(context.Background()) defer cancel() c, err := frpclient.Start(ctx, frpclient.Options{ LocalPort: 8080, // 被穿透的本地端口(必填,1-65535) DataDir: "./data", // subdomain 持久化目录;空 = 每次随机、不落盘 Log: os.Stdout, // [frp] 事件行(connected/error)进自己的日志;nil = 丢弃 OnState: func(s frpclient.Status) { log.Printf("frp:%s %s(%s)", s.State, s.Domain, s.LastEvent) }, }) if err != nil { log.Fatal(err) // 只可能是「组装期」错误:端口非法 / subdomain 非法 / ini 解析失败 } defer c.Close() if !c.WaitConnected(ctx, 10*time.Second) { log.Fatal("隧道未连通:", c.Status().LastEvent) // 连不上 frps 不阻塞 Start,走这里兜底 } fmt.Println("公网入口:http://" + c.Domain()) ``` 连不上 frps **不会**让 `Start` 失败:ini 里 `login_fail_exit=false`,客户端持续重试, 状态机走 `error`。感知方式二选一:`OnState` 回调(实时)或轮询 `c.Status().State`。 ## API | 成员 | 说明 | | --- | --- | | `Start(ctx, Options) (*Client, error)` | 组装并启动;ctx 取消即停 | | `(*Client) Close()` | 优雅停 frpc + 还原 stdout 接管;幂等、nil 安全 | | `(*Client) Status() Status` | 线程安全状态快照(nil Client 回 `disabled`) | | `(*Client) Domain() / SubDomain()` | 完整穿透域名 / 生效 subdomain | | `(*Client) WaitConnected(ctx, timeout) bool` | 阻塞等隧道连通 | | `Options.LocalPort` | 被穿透端口(唯一必填) | | `Options.ServerAddr/Port/Token/User/MetaToken` | frps 连接参数,空 = 内置默认 | | `Options.ProxyURL` | 拨 frps 的 HTTP 代理;空 = frpc 原生(回落读 `http_proxy` 环境变量,ini 层无法禁用) | | `Options.SubDomain / DataDir` | 显式 subdomain / 持久化目录 | | `Options.Log / OnState` | 事件行落盘 / 状态迁移回调(锁外调用) | | `Options.ExtraCommonIni / ExtraProxyIni` | 追加到 ini 的逃生舱行 | | `Options.NoConsoleTee` | true = 不回 tee 原 stdout | | `Status` / `State` | 快照结构 + 五态(`disabled/starting/connected/error/stopped`) | | `ResolveSubDomain / SanitizeSubDomain / LoadPersistedSubDomain` | subdomain 三态治理(只读版不会生成) | | `DomainPreview / DisabledStatus` | 「未启用也亮网址」的只读预览口径 | | `ClassifyLogLine` | frp 日志行 → 状态事件分类器(导出便于测试/复用) | | `HostOf` | 剥离端口(域名拼接用) | | `Default*` 常量 | 内置 frps 默认值(`dev.88i8.vip:60634` 那一套) | `Status` 的 JSON 字段名与 goprint `GET /admin/frp/status` 一致——任何系统的 管理面前端可以直接复用同一套渲染。 ## 设计要点(每条都是踩过的坑) 1. **版本 pin 在 v0.52.3**。这是最后一个支持 ini 配置的 frp 版本;v0.53+ 只认 toml,`legacy.FromIni` 系列 API 已移除。升级 frp 大版本前先读 frp 的 changelog。 2. **ini 关键项**(见 `ini.go` 模板注释): - `login_fail_exit=false`——否则登录失败时 frp 内部直接退出,goroutine 静默死亡; - `log_to=console` + `log_level=info`——`"login to server success"` 是 **Info 级**, warn 下状态机收不到 connected;info 噪声在接管侧逐行过滤,只放行事件行进调用方日志; - 无 `admin_port` 段——本地零监听,任务管理器 / 端口探测不可见; - `use_encryption/use_compression=true`——公网隧道加密是底线。 3. **接管顺序纪律**:`takeoverStdout()` 必须先于 `initFrpLog()`。beego console adapter 在 `SetLogger` 那一刻就捕获了当时的 `os.Stdout`——顺序反了管道对 frp 日志无效,状态机永远停在 `starting`。单测 `TestStartLifecycleUnreachableFRPS` 焊死这条(用必然拒绝的 `127.0.0.1:1` 跑完整生命周期)。 4. **subdomain 三态治理**:显式 > `DataDir` 持久化 > crypto/rand 12 位随机+持久化。 frps 上 subdomain 是全局唯一资源,撞名即「先赢者通吃」;不取机器指纹(克隆机重复)、 不取实例名/主机名(分布式同名相撞)。`DataDir` 为空 = 不持久化,每次随机。 随机值用 rejection sampling 严格等概(见 `randSubDomain` 注释)。 5. **输入底线校验**:`LocalIP` 只接受回环(穿透只暴露本机服务,不借道局域网); 渲染后的 ini 挡 `admin_port`——逃生舱也不许破坏「本地零监听」承诺。 6. **stdout 接管是进程级副作用**(beego 无日志注入点,这是 v0.52.3 的限制): 同一进程同时只应运行一个 `Client`;`Close` 会还原 `os.Stdout` 并 tee 回原句柄。 ## 发布 / 引入 模块已发布在 Gitee:`gitee.com/vipkwds/frpclient`(MIT,见 LICENSE)。 ```bash go get gitee.com/vipkwds/frpclient ``` 调用方 go.mod 需要 `go 1.21`+(本库 go 指令 deliberately 保守; frp v0.52.3 自身只要求 go 1.20)。换自有仓库:`go mod edit -module=<新路径>` 后推送,调用方 `go get <新路径>`,无需 replace。 ## 已知边界(诚实披露) - **出站连接条目在宿主机 `netstat` 可见**(frp 是出站协议,不可免);进程、文件、 本地端口不可见。 - frpc 原生行为:连 frps 时回落读 `http_proxy` 环境变量(本机测试机带代理时, 连 `127.0.0.1` 都会被拽去代理报 502——frpc 一向如此)。ini 层无法显式禁用, 需要指定代理用 `Options.ProxyURL`,强制直连就清掉进程的 `http_proxy`。 - 内置默认凭据指向 `dev.88i8.vip:60634`(内部生态 frps),仅供同生态使用; 换 frps 用 `Options` 覆盖 `ServerAddr/ServerPort/Token/User/MetaToken`。 - 单 http proxy 定位:需要 tcp/udp/多 proxy 时,以 `renderINI` 为模板扩展, 状态机 / 接管 / subdomain 治理均可原样复用。 ## 目录 ``` frp/ doc.go 包文档 + 设计纪律 + 已知边界 options.go Options 与默认值回填 client.go Client 生命周期:Start/Close/Status/WaitConnected service.go frp client.Service 组装(内存 ini → service) ini.go ini 模板 / 渲染 / stdout 接管 / frp 日志初始化 status.go 五态状态机 + ClassifyLogLine subdomain.go subdomain 三态解析与持久化 domain.go HostOf / DomainPreview / DisabledStatus *_test.go 单测(含 example) ```