Documentation
¶
Overview ¶
Package mesh — overlay TCP port bridges.
The overlay IP lives only inside the userspace netstack, so ordinary processes (byway, sing-box) can neither listen on it nor dial it. Two bridges connect them to the overlay:
EXPOSE (exit side): listen on <overlayIP>:<port> and proxy to a local 127.0.0.1 service (e.g. byway). An allowlist of source overlay prefixes gates who may reach it — byway is unauthenticated, so the exit only opens its door to the center node.
FORWARD (center side): listen on 127.0.0.1:<port> and dial out to another node's overlay service (<overlayIP>:<port>) through the netstack. sing-box points an outbound at the loopback port; bytes ride the overlay to the chosen exit's byway.
Both are declared via `ccfly connect --overlay-expose/--overlay-forward` (env CCFLY_OVERLAY_EXPOSE / CCFLY_OVERLAY_FORWARD) and brought up/torn down with the WireGuard session.
Package mesh — kernel-interface (kernel TUN) datapath.
By default the device runs WireGuard over a userspace gVisor netstack, whose overlay IP is invisible to the kernel — so ordinary processes need the expose/forward bridges to use it. Kernel mode instead brings up a REAL kernel TUN interface (`ccfly0`) with the overlay IP assigned, visible to `ip addr` and the kernel routing table. Services (byway, sing-box) then bind/dial the overlay IPs directly — no bridges. Linux + root (CAP_NET_ADMIN) only; the transport (WG-over-WSS) and the hub's forwarding are unchanged.
Package mesh implements the device side of ccfly's overlay: `ccfly connect <host>/<code>` enrolls the device with a ccfly-cloud control plane and holds a WebSocket tunnel (`/mesh`) open.
Increment 1 (this file): X25519 key generation, the `/connect` enrollment handshake, local state persistence, and a self-healing /mesh tunnel that keeps the device marked online. Increment 2 will frame actual WireGuard packets over this tunnel (custom conn.Bind + netstack) so the cloud can reach the device's local ccfly control API over the overlay.
Package mesh — Increment 2: the device-side WireGuard datapath.
Once the /mesh WebSocket (Increment 1, mesh.go) is up, we bring up a userspace WireGuard device whose transport IS that single WebSocket. Every WG UDP datagram travels as exactly one WS BINARY frame containing only the raw bytes (no header) — the cloud identifies us by which device WS delivered the frame.
On top of the resulting netstack overlay we run a TCP listener on <OverlayIP>:7699 that proxies connections from the fixed cloud gateway 100.64.0.1 to the local `ccfly serve` (127.0.0.1:7699 by default). Other overlay peers are closed before any loopback dial. Source address alone is not identity on the cloud host itself — containers there SNAT to the same 100.64.0.1 — so a cloud-issued control token is checked on top of it; see controltoken.go.
FRAME CONTRACT: one WG packet == one WS BINARY message, raw datagram bytes. UAPI keys are HEX (base64-decode the stored keys to 32 raw bytes, then hex).
Index ¶
- Constants
- func AcquireSingleton() error
- func ClaudeLoginAllowed(host string) bool
- func CleanupResolver() error
- func ClearClaudeLoginContext(host string) (int, error)
- func ClearLoginStatus() error
- func Connect(ctx context.Context, target, version string) error
- func CurrentSNIAccount() string
- func EnsureTmuxProxyEnv()
- func NudgeSNIPolicy()
- func Pair(ctx context.Context, target, version string) error
- func PrepareContainerSNIGuard() error
- func RunSNIHelper() error
- func SNIStatusJSON(fresh bool) any
- func SaveClaudeLoginContext(c ClaudeLoginContext) error
- func SaveLoginStatus(s LoginStatus) error
- func SetControlProxyEnabled(on bool)
- func SetControlToken(tok string)
- func SetKernelMode(on bool)
- func SetLocalControlPort(p int)
- func SetOverlayExpose(s string) error
- func SetOverlayForward(s string) error
- func StopContainerSNIGuard()
- type ClaudeLoginContext
- type LoginStatus
- type LoginTarget
- type MeshEndpoint
- type SNIConfig
- type SNIExit
- type State
Constants ¶
const ( LoginPhaseInit = "init" LoginPhaseCheckChannel = "checking_channel" LoginPhaseWaitChannel = "waiting_channel" LoginPhaseStarting = "starting" LoginPhasePolling = "polling" LoginPhaseDecrypting = "decrypting" LoginPhaseWriting = "writing" LoginPhaseSucceeded = "succeeded" LoginPhaseFailed = "failed" )
Variables ¶
This section is empty.
Functions ¶
func AcquireSingleton ¶
func AcquireSingleton() error
AcquireSingleton takes the global connect lock, or returns an error telling the user another instance is already running. Call once at connect startup.
func ClaudeLoginAllowed ¶
ClaudeLoginAllowed 读某云端持久化的「内部功能准入」标志(can_use_claude),判断本机是否应 暴露 `ccfly claude login`。语义:仅当云端**明确**下发 false 时才拒绝;nil(老云端 / 尚未刷新 device/config)一律放行,绝不误伤未开闸的部署。真正鉴权仍在云端(login/start 返回 403), 此处只是本地「不暴露命令」的纵深防御 / 更友好的即时报错。
func CleanupResolver ¶
func CleanupResolver() error
CleanupResolver 兜底清掉本机的系统解析改动(Windows hosts 托管块 / macOS /etc/resolver 标记文件 / Linux resolv.conf 备份恢复),给 `ccfly uninstall` 收尾用:常驻服务是被硬杀的 (schtasks /End、launchctl),不会走 teardown —— Windows 上残留的 hosts 块会把 Anthropic 域钉死在 loopback(无人监听 :443),整机 Claude 全断。幂等,未写过时是 no-op。
func ClearClaudeLoginContext ¶
ClearClaudeLoginContext 删除某云端的登录上下文(host 为空 → 删全部)。供 `ccfly claude logout` 用。 返回删掉的文件数。文件不存在不算错。
func ClearLoginStatus ¶
func ClearLoginStatus() error
func Connect ¶
Connect 把设备接入 <target> 并持有 mesh 隧道,直到 ctx 取消。target 形态:
- "<host>/<code>"(及带 scheme 前缀)→ 走【既有 connect code 流程】(POST /connect)
- "<host>"(不含 "/")→ 走【无码配对流程】(/api/pair/start + 轮询); 但若本机对该 host 已有保存的设备身份(私钥+device_id+mesh_token),则直接用 旧身份重连,不再重新配对——这样 install 出来的 launchd/systemd 服务每次重启 都是同一台设备,而不是开机就刷一个新设备。
loopback host 默认 http,其余 https(可被显式 scheme 覆盖),与下方 scheme 逻辑一致。
func CurrentSNIAccount ¶
func CurrentSNIAccount() string
CurrentSNIAccount 返回当前 arm 生效的 SNI 段所绑定的账号(= 云端认定的本机出口归属)。 空 = 没有 SNI 出口(未绑定/非 SNI 账号/未接入)。凭据库拿它与「本机活凭据属于谁」比对错配。
func EnsureTmuxProxyEnv ¶
func EnsureTmuxProxyEnv()
EnsureTmuxProxyEnv 给 CLI(ccfly new / attach,独立进程、不入网)用:把会话出网代理 设进本进程环境(若用户未已设),这样 CLI 创建的 tmux 会话和服务进程一样默认带好代理 + 局域网 bypass。优先级:
- `ccfly claude login` 落盘的「按账号 /128 路由」上下文(~/.ccfly/claude-login.json)—— 若它带了可用的按账号代理 URL,本机后续 claude 会话即从该账号的 /128 出网(见 claudeLoginProxyURL)。
- 否则回退到云端下发并持久化的设备级 overlay 代理策略(State.ProxyPort)。
二者都没有 → no-op(零行为变化)。用户已显式设 CCFLY_TMUX_PROXY(shell/plist)→ 一律尊重不覆盖。
func NudgeSNIPolicy ¶
func NudgeSNIPolicy()
NudgeSNIPolicy 请求立刻拉一次 device/config 并收敛 SNI 段。切换 Claude 账号后调用: 云端已把出口重指到新账号,本机 :443 relay 要跟着换到新出口,否则最长要等一整个刷新周期。
func Pair ¶
Pair 只做无码配对并把设备身份落盘,然后返回(不持有隧道)。供 `ccfly install <host>` 在安装服务前先交互式配对一次用——配对成功后安装的 launchd/systemd 服务跑 `connect <host>`,凭这份已保存身份重连,不会开机就重新配对。若本机对该 host 已有完整 身份则直接跳过配对(幂等)。
func PrepareContainerSNIGuard ¶
func PrepareContainerSNIGuard() error
PrepareContainerSNIGuard 在 connect 的 control listener、配对和 mesh 网络请求之前调用。 未显式选择容器模式时为 no-op,保持桌面/full 部署原行为。
func SNIStatusJSON ¶
SNIStatusJSON 是 mesh 暴露给 control 包的注入点(control 不能 import mesh,见 cmd/ccfly 接线)。 fresh=true 同步跑一次新探测(供人工 GET /sni-status?probe=1 拿实时结果);否则用调度器缓存。
func SaveClaudeLoginContext ¶
func SaveClaudeLoginContext(c ClaudeLoginContext) error
SaveClaudeLoginContext 原子落盘某云端的登录上下文(0600)。host 为空则 no-op。
func SaveLoginStatus ¶
func SaveLoginStatus(s LoginStatus) error
func SetControlProxyEnabled ¶
func SetControlProxyEnabled(on bool)
SetControlProxyEnabled toggles the overlay control-API proxy on this node.
func SetControlToken ¶
func SetControlToken(tok string)
SetControlToken 装载/轮换云端下发的控制令牌。空串被忽略:云端没发不等于要拆闸, 已经装上的门不能被一个缺字段的响应卸掉。
func SetKernelMode ¶
func SetKernelMode(on bool)
SetKernelMode toggles kernel-interface mode. Must be set before connect.
func SetLocalControlPort ¶
func SetLocalControlPort(p int)
SetLocalControlPort overrides the local `ccfly serve` target port that the overlay TCP listener proxies to (and the overlay port it listens on).
func SetOverlayExpose ¶
SetOverlayExpose parses a comma-separated expose list, replacing any prior config. Each item is "overlayPort:localPort[@allowCIDR|allowCIDR|...]"; a bare allow IP means a host (/32) rule. Empty input clears the config.
func SetOverlayForward ¶
SetOverlayForward parses a comma-separated forward list, replacing any prior config. Each item is "localPort:overlayIP:overlayPort". Empty input clears it.
func StopContainerSNIGuard ¶
func StopContainerSNIGuard()
StopContainerSNIGuard 只在 connect 进程退出时调用。普通撤权不会停止 DNS。
Types ¶
type ClaudeLoginContext ¶
type ClaudeLoginContext struct {
Host string `json:"host"` // 登录所经的云端(与 conn-<host>.json 对应)
AccountEmail string `json:"account_email"` // 云端最终分配/登录的账号邮箱
EgressV6 string `json:"egress_v6"` // 该账号的稳定出口 /128
OutNodeIP string `json:"out_node_ip"` // 该 /128 所在出口节点的 overlay IP
ProxyURL string `json:"proxy_url,omitempty"` // 直接可用的按账号 /128 出网代理 URL(拿不到则空,见上)
ProxyCA string `json:"proxy_ca,omitempty"` // 出口若 MITM 需信任的 CA(登录代理 blind-tunnel,通常空)
UpdatedAt string `json:"updated_at,omitempty"`
}
ClaudeLoginContext 是 `ccfly claude login` 成功后落盘的「本机当前登录的 Claude 账号 + 其出口」。 用途:让本机后续 claude 会话从**该账号的 /128**(EgressV6,在出口节点 OutNodeIP 上)出网。
ProxyURL 是「直接可用的、把流量绑定到该账号 /128 的出网代理 URL」。它由 byway 的登录代理提供: 形如 http://<EgressV6 的 32 位 hex>:<login-secret>@<byway-login-endpoint>(见 ~/byway/login.go 的 Basic-auth 用户名=/128 hex 机制,与 ~/ccfly-cloud/internal/server/login.go 的 loginProxyURL)。
关键现状:**设备目前拿不到 byway 登录代理的 endpoint + secret** —— 云端只在 worker-facing 的 /api/worker/jobs/{id}/params(worker bearer 鉴权)里下发 proxy_url,设备侧的 login/poll 与 device/config(mesh_token 鉴权)都不带它,且 endpoint(host:port)与 secret 均不可推导。故除非 云端额外下发(建议:poll 返回里加 account_proxy_url,或 device/config 下发 login endpoint+secret), 否则 ProxyURL 留空。留空时 EnsureTmuxProxyEnv 会安全回退到设备级 overlay 代理(见 applyClaudeLoginProxy)。
func LoadClaudeLoginContexts ¶
func LoadClaudeLoginContexts() []ClaudeLoginContext
LoadClaudeLoginContexts 扫 ~/.ccfly/claude-login-*.json,返回所有已落盘的登录上下文。
type LoginStatus ¶
type LoginStatus struct {
Phase string `json:"phase"`
JobID string `json:"job_id,omitempty"`
Email string `json:"email,omitempty"`
EgressV6 string `json:"egress_v6,omitempty"`
Host string `json:"host"`
PID int `json:"pid"`
Error string `json:"error,omitempty"`
StartedAt int64 `json:"started_at"`
UpdatedAt int64 `json:"updated_at"`
CloudStatus string `json:"cloud_status,omitempty"`
// Stashed:本次交付只把凭据存进了本地凭据库(备用),没有接管当前账号 —— 本机已经在用别的号时
// 的默认行为(接管要关掉在跑的会话,那只能在设备跟前用 `ccfly claude use` 做)。推送方据此
// 如实告诉用户「已存为备用」,而不是让他以为设备已经换号了。
Stashed bool `json:"stashed,omitempty"`
// LastSuccessAt/LastSuccessEmail:最近一次**成功**交付的时刻与账号,失败不会擦掉它。
//
// 为什么需要:这个文件描述的是「最近一次登录任务」,而网页把它当作「这台设备的凭据状态」在读。
// 于是一次因为**云端库存为空**而失败的推送(设备手里的活凭据一个字节都没被碰),会把记录从
// succeeded 改写成 failed,页面上就成了「登录失败」—— 2026-08-03 真实误导过一次排查,让人
// 以为设备没凭据,差点去做多余的补救。保留这两个字段,失败态也说得清「上次成功是什么时候、哪个号」。
LastSuccessAt int64 `json:"last_success_at,omitempty"`
LastSuccessEmail string `json:"last_success_email,omitempty"`
}
func LoadLoginStatus ¶
func LoadLoginStatus() (*LoginStatus, error)
func (*LoginStatus) Terminal ¶
func (s *LoginStatus) Terminal() bool
type LoginTarget ¶
type LoginTarget struct {
Host string
Scheme string
DeviceID string
MeshToken string
PrivateKey string // 设备 WG 私钥(base64);开 sealed-box 用
PublicKey string
}
LoginTarget 是 `ccfly claude login` 需要的:某个已入网云端的控制面地址 + mesh_token(鉴权登录 API) + 设备 WG 密钥对(用私钥解开 worker 封装的 NaCl sealed-box 凭证)。
func LoginTargets ¶
func LoginTargets() []LoginTarget
LoginTargets 扫 ~/.ccfly/conn-*.json,返回所有已入网云端(有 mesh_token 的)。
type MeshEndpoint ¶
MeshEndpoint separates the HTTPS identity from the TCP destination. URL is used for the HTTP Host header and certificate verification; DialAddr, when set, is the host:port actually dialled. This is needed for an IP certificate whose publicly validated address differs from a private/IX transport address.
type SNIConfig ¶
type SNIConfig struct {
Enabled bool `json:"enabled"`
Account string `json:"account"`
Exit SNIExit `json:"exit"`
Intercept []string `json:"intercept"` // apex 域清单(含所有子域)
Upstream []string `json:"upstream"` // 拦截 DNS 的上游(阿里)
}
SNIConfig 是云端下发的 sni 段。
type State ¶
type State struct {
Host string `json:"host"`
Scheme string `json:"scheme"` // http | https (control plane)
DeviceID string `json:"device_id"`
Name string `json:"name"`
Owner string `json:"owner"`
PrivateKey string `json:"private_key"` // device WG private key (base64)
PublicKey string `json:"public_key"`
OverlayIP string `json:"overlay_ip"`
OverlayCIDR string `json:"overlay_cidr"`
CloudPublicKey string `json:"cloud_public_key"`
CloudOverlayIP string `json:"cloud_overlay_ip"`
MeshURL string `json:"mesh_url"`
MeshEndpoints []MeshEndpoint `json:"mesh_endpoints,omitempty"`
MeshToken string `json:"mesh_token"`
KeepaliveSec int `json:"keepalive_sec"`
// 云端下发的出网代理策略(可选):设了 ProxyPort 即「该云端在 overlay 上跑着代理」,设备据此
// 零配置自动起 127.0.0.1:<port> → cloud_overlay_ip:<port> 转发 + 给会话注入代理环境(见 applyMeshProxy)。
ProxyPort int `json:"proxy_port,omitempty"`
ProxyScheme string `json:"proxy_scheme,omitempty"`
// 出口若做 MITM,设备需信任其 CA。云端把所有出口的 CA 合成一个 PEM bundle 下发,设备落盘到
// ~/.ccfly/proxy-ca.pem 并给会话注入 NODE_EXTRA_CA_CERTS(见 applyProxyCA)。
ProxyCA string `json:"proxy_ca,omitempty"`
// 云端下发的「内部功能准入」标志(claude 账号 / 代理 / 组网)。三态指针:nil=未知(老云端/老
// State 没这字段)→ 一律按「允许」处理,绝不误伤未开闸的部署;显式 false 才在设备侧「不暴露」
// 相关命令(见 cmd/ccfly 的 claude 门控)。真正鉴权仍在云端(端点 403),这里只是本地 UX。
CanUseClaude *bool `json:"can_use_claude,omitempty"`
CanUseProxy *bool `json:"can_use_proxy,omitempty"`
CanUseMesh *bool `json:"can_use_mesh,omitempty"`
// 会话同步总开关(云端按用户下发)。显式 false ⇒ 本机**不再把任何会话内容发上云**
// (连摘要里的 cwd/title/preview 都不发,见 syncer.go 的 syncOnce);nil = 老云端没下发,
// 按开处理,行为与从前一致。**持久化**是刻意的:进程重启后、首次配置拉取成功之前,
// 继续按上次已知策略走,不会因为一次网络失败就把敏感账号的会话内容漏上去。
SessionSync *bool `json:"session_sync,omitempty"`
// 云端下发的 SNI 出口段(可选):设备装本地 DNS 拦截器 + :443 透传,把 AI 域流量经 overlay 送到
// 账号出口 byway-sni(真证书,无 HTTP 代理/无 MITM)。nil/无段 = 不装/卸载(见 sni.go applySNI)。
SNI *SNIConfig `json:"sni,omitempty"`
// 云端下发的 overlay 控制面准入令牌:网关反代设备时带 Authorization: Bearer,设备据此确认
// 「对面真是云」。持久化是有意的 —— 配置拉取失败或进程重启期间也要继续把门,不能退回到
// 「只认源 IP」(云主机上的同宿主容器 SNAT 后源 IP 也是 100.64.0.1)。见 controltoken.go。
ControlToken string `json:"control_token,omitempty"`
}
State is the persisted per-host connection state (~/.ccfly/conn-<host>.json).
Source Files
¶
- controltoken.go
- coredns.go
- dnspolicy.go
- egresscheck.go
- forward.go
- identity_reject.go
- kerneltun.go
- login_status.go
- machineid.go
- mesh.go
- mesh_endpoints.go
- singleton.go
- singleton_unix.go
- sni.go
- sni_container.go
- sni_hosts.go
- sni_resolv_linux.go
- sni_resolver_domains_other.go
- snihelper_other.go
- sniresolve.go
- sniresolve_default.go
- snistatus.go
- syncer.go
- wg.go