mesh

package
v0.0.0-...-632f32b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 43 Imported by: 0

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

View Source
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

func ClaudeLoginAllowed(host string) bool

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

func ClearClaudeLoginContext(host string) (int, error)

ClearClaudeLoginContext 删除某云端的登录上下文(host 为空 → 删全部)。供 `ccfly claude logout` 用。 返回删掉的文件数。文件不存在不算错。

func ClearLoginStatus

func ClearLoginStatus() error

func Connect

func Connect(ctx context.Context, target, version string) error

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。优先级:

  1. `ccfly claude login` 落盘的「按账号 /128 路由」上下文(~/.ccfly/claude-login.json)—— 若它带了可用的按账号代理 URL,本机后续 claude 会话即从该账号的 /128 出网(见 claudeLoginProxyURL)。
  2. 否则回退到云端下发并持久化的设备级 overlay 代理策略(State.ProxyPort)。

二者都没有 → no-op(零行为变化)。用户已显式设 CCFLY_TMUX_PROXY(shell/plist)→ 一律尊重不覆盖。

func NudgeSNIPolicy

func NudgeSNIPolicy()

NudgeSNIPolicy 请求立刻拉一次 device/config 并收敛 SNI 段。切换 Claude 账号后调用: 云端已把出口重指到新账号,本机 :443 relay 要跟着换到新出口,否则最长要等一整个刷新周期。

func Pair

func Pair(ctx context.Context, target, version string) error

Pair 只做无码配对并把设备身份落盘,然后返回(不持有隧道)。供 `ccfly install <host>` 在安装服务前先交互式配对一次用——配对成功后安装的 launchd/systemd 服务跑 `connect <host>`,凭这份已保存身份重连,不会开机就重新配对。若本机对该 host 已有完整 身份则直接跳过配对(幂等)。

func PrepareContainerSNIGuard

func PrepareContainerSNIGuard() error

PrepareContainerSNIGuard 在 connect 的 control listener、配对和 mesh 网络请求之前调用。 未显式选择容器模式时为 no-op,保持桌面/full 部署原行为。

func RunSNIHelper

func RunSNIHelper() error

RunSNIHelper 仅 macOS 支持(其余平台单进程直绑 :443)。

func SNIStatusJSON

func SNIStatusJSON(fresh bool) any

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

func SetOverlayExpose(s string) error

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

func SetOverlayForward(s string) error

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

type MeshEndpoint struct {
	URL      string `json:"url"`
	DialAddr string `json:"dial_addr,omitempty"`
}

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 SNIExit

type SNIExit struct {
	Host string `json:"host"`
	Port int    `json:"port"`
}

SNIExit 是账号 SNI 出口端点(overlay 上的 byway-sni,host:port)。

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL