Documentation
¶
Overview ¶
Package mobilebridge bridges fairpeer's desktop Controller to linkpeer mobile clients over an end-to-end-encrypted WebRTC P2P link.
Status (2026-09): the desktop-side bridge is implemented and wired into the main app — Ed25519 identity + pairing, signaling client (primary long-poll and cloud-relay second path), AES-GCM framing with rekey and tab-ring resync, per-command permission routing, UPnP/TURN hole punching, and an audit log; cmd/linkpeer-signal is the standalone signaling server. The Flutter mobile app lives in a separate repository and is still in development. See docs/LINKPEER_FAIRPEER_SPEC.md and LINKPEER.md.
Index ¶
- Constants
- Variables
- func BuildClientHello(longPriv ed25519.PrivateKey, ephPub, nc []byte, cid, sid string, ts int64) proto.ClientHello
- func BuildServerHello(longPriv ed25519.PrivateKey, ephPub, ns []byte, cid, sid string, ts int64) proto.ServerHello
- func ClientEphPub(ch proto.ClientHello) ([]byte, error)
- func ClientNonce(ch proto.ClientHello) ([]byte, error)
- func ConstantTimeEqual(a, b []byte) bool
- func DeriveSessionKeys(shared, nc, ns []byte) (c2s, s2c []byte)
- func DevID(pub []byte) string
- func ECDHShared(ephPriv *ecdh.PrivateKey, peerEphPub []byte) ([]byte, error)
- func Fingerprint(pub []byte) string
- func FinishedMessage(role string, transcript []byte) proto.Finished
- func GenerateEphemeral() (*ecdh.PrivateKey, error)
- func GenerateLongTerm() (ed25519.PublicKey, ed25519.PrivateKey, error)
- func NewAEAD(key []byte) (cipher.AEAD, error)
- func OpenFrame(aead cipher.AEAD, frame []byte) (seq uint64, plaintext []byte, err error)
- func ParseTurnCred(paste string) (user, pass, host string, port int, ok bool)
- func ProbeUPnP(localPort int) (externalIP string, externalPort int)
- func Random(n int) ([]byte, error)
- func SealFrame(aead cipher.AEAD, seq uint64, nonce, plaintext []byte) []byte
- func ServerEphPub(sh proto.ServerHello) ([]byte, error)
- func ServerNonce(sh proto.ServerHello) ([]byte, error)
- func UPnPCandidate(localPort int) string
- func VerifyClientHello(pub ed25519.PublicKey, ch proto.ClientHello) error
- func VerifyFinished(f proto.Finished, role string, transcript []byte) error
- func VerifyServerHello(pub ed25519.PublicKey, sh proto.ServerHello) error
- type Audit
- func (a *Audit) Cmd(dev, cmd, tab string, ok bool)
- func (a *Audit) ConnClose(dev string)
- func (a *Audit) ConnOpen(dev, iceMode string)
- func (a *Audit) Denied(dev, cmd, reason string)
- func (a *Audit) Error(evt, dev string, err error)
- func (a *Audit) Info(evt string, args ...any)
- func (a *Audit) PairConfirmed(devC, devS string)
- func (a *Audit) PairStart(devS string)
- func (a *Audit) Unpaired(dev string)
- type Bridge
- func (b *Bridge) CloudConnected() bool
- func (b *Bridge) CloudRelayURL() string
- func (b *Bridge) ConfirmPairing(pairID string) error
- func (b *Bridge) ForwardEvent(tabID string, wireEventJSON []byte)
- func (b *Bridge) KnockEnabled() bool
- func (b *Bridge) KnockServer() string
- func (b *Bridge) ListPairNics() []NicInfo
- func (b *Bridge) NotifySessionListChanged()
- func (b *Bridge) OnSignalMsg(msg SignalMsg)
- func (b *Bridge) PendingPairings() []PendingPair
- func (b *Bridge) RejectPairing(pairID string)
- func (b *Bridge) SetCloudRelay(url string)
- func (b *Bridge) SetKnock(enabled bool, server string)
- func (b *Bridge) SetOnReady(fn func(*Conn))
- func (b *Bridge) SetPairAddress(ip string)
- func (b *Bridge) SetResolveTab(fn func(string) string)
- func (b *Bridge) SignalConnected() bool
- func (b *Bridge) SignalURL() string
- func (b *Bridge) Start(ctx context.Context) error
- func (b *Bridge) StartPairing() (code, qrURL string, err error)
- func (b *Bridge) Unpair(devC string)
- type CommandExecutor
- type CommandRouter
- func (r *CommandRouter) Perms() PerConnPermissions
- func (r *CommandRouter) Route(plaintext []byte) error
- func (r *CommandRouter) SetListModelsHook(fn func([]ModelInfo))
- func (r *CommandRouter) SetListSessionsHook(fn func([]SessionInfo))
- func (r *CommandRouter) SetListTemplatesHook(fn func([]TemplateInfo))
- func (r *CommandRouter) SetLoadSessionHook(fn func(tab string, history []map[string]any))
- func (r *CommandRouter) SetNewTabHook(fn func(string))
- func (r *CommandRouter) SetOnErrorHook(fn func(code, msg string))
- func (r *CommandRouter) SetResyncHook(fn func(tabID string, sinceSeq uint64))
- func (r *CommandRouter) SetSubscribeHook(fn func(tab string))
- type CompletedHandshake
- type Config
- type Conn
- func (c *Conn) AddICECandidate(cand string) error
- func (c *Conn) AttachPC(pc *webrtc.PeerConnection)
- func (c *Conn) DevC() string
- func (c *Conn) HandleOffer(offerSDP string) (string, error)
- func (c *Conn) IsEncrypted() bool
- func (c *Conn) SendEvent(plaintext []byte) error
- func (c *Conn) SetOnClose(fn func(*Conn))
- func (c *Conn) SetOnReady(fn func(*Conn))
- type KeyStore
- type MemoryKeyStore
- type ModelInfo
- type NicInfo
- type Pairing
- func (p *Pairing) Confirm(pairID string) error
- func (p *Pairing) IsRevoked(devC string) bool
- func (p *Pairing) OnExchange(msg SignalMsg)
- func (p *Pairing) OnExchangeFrom(msg SignalMsg, kURL string)
- func (p *Pairing) PeerPub(devC string) (ed25519.PublicKey, bool)
- func (p *Pairing) Pending() []PendingPair
- func (p *Pairing) Reject(pairID string)
- func (p *Pairing) SetAutoConfirm(v bool)
- func (p *Pairing) SetCloudRelay(url, turnParam string)
- func (p *Pairing) SetOnExchange(fn func(pairID, devC, fpC string))
- func (p *Pairing) SetPairAddress(ip string)
- func (p *Pairing) StartPairing() (code, qrURL string, err error)
- func (p *Pairing) Unpair(devC string)
- type PendingPair
- type PerConnPermissions
- type SessionInfo
- type SignalClient
- type SignalHandler
- type SignalMsg
- type TemplateInfo
Constants ¶
const ( FrameVer = 1 // RekeyThreshold is the seq at which a direction must rotate keys. // 2^32 frames at ~1KB each ≈ 4TB — far beyond any real connection, but // we enforce the invariant rather than relying on that. RekeyThreshold = 1 << 32 )
Frame layout on the DataChannel (PROTOCOL §6):
[1B ver=1][8B seq LE][12B nonce][N B ciphertext][16B GCM tag]
AAD = ver(1) || seq(8): the header is itself integrity-protected, so an attacker can't tamper with seq or version without failing authentication.
Each direction uses its own key (c2s / s2c) and its own seq counter starting at 0. Receivers reject any seq ≤ their recvMaxSeq to defeat replay.
The 2^32-rekey invariant (FAIRPEER_SPEC §11.5): when a direction's seq approaches 2^32, the connection MUST rekey (new X25519, new keys). This keeps AES-GCM nonce-collision probability negligible for the key's lifetime.
Variables ¶
var ( ErrShortFrame = errors.New("frame too short") ErrBadVersion = errors.New("unsupported frame version") )
var ( ErrBadSig = errors.New("handshake signature invalid") ErrBadEphemeral = errors.New("handshake ephemeral key invalid") ErrFinishedMismatch = errors.New("handshake finished transcript mismatch") )
var ErrForbidden = errors.New("forbidden")
ErrForbidden means this C lacks permission for the requested command. The Conn translates this into a `{"type":"error","code":"forbidden"}` frame.
var ErrNoPending = errors.New("no pending pair")
ErrNoPending means no C is waiting at the given pairId when Confirm is called.
var ErrNotEncrypted = errors.New("connection not encrypted yet")
ErrNotEncrypted is returned by SendEvent before the handshake completes.
var ErrNotFound = errors.New("key not found")
ErrNotFound is returned by KeyStore.Get for a missing key.
var ErrNotStarted = errors.New("bridge not started")
ErrNotStarted is returned when Bridge operations are attempted before Start.
var ErrStaleTS = errors.New("stale_ts")
ErrStaleTS 表示握手 ts 超出新鲜度窗口(P2-1 防握手重放)。
var ErrVersionMismatch = errors.New("version_mismatch")
VerifyClientHello checks the signature under the given long-term public key and that the ephemeral/nonce are well-formed. Does NOT check pairing status — that's the caller's job (so it can refuse silently on revoked devices). ErrVersionMismatch 表示 ClientHello 声明的协议版本不符(T10 降级防护)。
Functions ¶
func BuildClientHello ¶
func BuildClientHello(longPriv ed25519.PrivateKey, ephPub, nc []byte, cid, sid string, ts int64) proto.ClientHello
BuildClientHello constructs a signed ClientHello from C's long-term key.
func BuildServerHello ¶
func BuildServerHello(longPriv ed25519.PrivateKey, ephPub, ns []byte, cid, sid string, ts int64) proto.ServerHello
BuildServerHello constructs the signed S→C reply.
func ClientEphPub ¶
func ClientEphPub(ch proto.ClientHello) ([]byte, error)
ClientEphPub / ServerEphPub decode the ephemeral public key from a hello.
func ClientNonce ¶
func ClientNonce(ch proto.ClientHello) ([]byte, error)
func ConstantTimeEqual ¶
ConstantTimeEqual guards signature/fingerprint comparisons against timing oracles. Use this anywhere secret-derived bytes are compared.
func DeriveSessionKeys ¶
DeriveSessionKeys runs HKDF-SHA256 over the X25519 shared secret to produce two direction-split AES-256 keys: c2s (C→S) and s2c (S→C). Splitting direction prevents reflection attacks (an attacker can't bounce S's own frames back at it). Salt = nc‖ns binds both nonces into the key.
func DevID ¶
DevID = base32(SHA256(pub)[:10]). The self-consistency invariant K uses for stateless WS auth: anyone can recompute it from pub, so K never stores it.
func ECDHShared ¶
func ECDHShared(ephPriv *ecdh.PrivateKey, peerEphPub []byte) ([]byte, error)
ECDHShared computes the X25519 shared secret from our ephemeral private key and the peer's ephemeral public key bytes.
func Fingerprint ¶
Fingerprint = base32(SHA256(pub)[:8]). Human-comparable; verified out-of-band (C compares it against the QR code locally to defeat MITM at pairing time).
func FinishedMessage ¶
FinishedMessage builds the Finished plaintext (to be AEAD-sealed by caller).
func GenerateEphemeral ¶
func GenerateEphemeral() (*ecdh.PrivateKey, error)
GenerateEphemeral returns a fresh X25519 keypair for one handshake.
func GenerateLongTerm ¶
func GenerateLongTerm() (ed25519.PublicKey, ed25519.PrivateKey, error)
GenerateLongTerm creates a fresh Ed25519 keypair for device identity.
func OpenFrame ¶
OpenFrame authenticates+decrypts a frame. It returns the seq (so the caller can enforce anti-replay against its recvMaxSeq) and the plaintext. Authentication failure (tag mismatch / truncation / version mismatch) → error.
func ParseTurnCred ¶
ParseTurnCred 从 turn-cred.sh 输出(或任意包含凭据串的粘贴文本)解析 `user:pass@host[:port]`(UX_ONBOARDING W3)。返回归一化的 TURN 配置; 解析不到返回 ok=false。
func ProbeUPnP ¶
ProbeUPnP discovers the local gateway via SSDP and attempts AddPortMapping to get a server-reflexive candidate without STUN (FAIRPEER_SPEC §11.6). 3s timeout, silent on failure (graceful degradation to STUN).
func SealFrame ¶
SealFrame encrypts plaintext into a complete frame. The caller supplies the 12-byte nonce (random per frame) and tracks seq (monotonic per direction).
func ServerEphPub ¶
func ServerEphPub(sh proto.ServerHello) ([]byte, error)
func ServerNonce ¶
func ServerNonce(sh proto.ServerHello) ([]byte, error)
func UPnPCandidate ¶
UPnPCandidate generates a srflx ICE candidate from a UPnP port mapping.
func VerifyClientHello ¶
func VerifyClientHello(pub ed25519.PublicKey, ch proto.ClientHello) error
func VerifyFinished ¶
VerifyFinished checks the decrypted Finished plaintext against the transcript.
func VerifyServerHello ¶
func VerifyServerHello(pub ed25519.PublicKey, sh proto.ServerHello) error
Types ¶
type Audit ¶
type Audit struct {
// contains filtered or unexported fields
}
Audit logs ONLY metadata about mobile-side activity: devIds (truncated), command types, connection lifecycle, errors. It NEVER logs command inputs, conversation text, file contents, or any business data — that would defeat the privacy promise. Retention is local (FAIRPEER_SPEC §11.2④).
func (*Audit) Info ¶
Info 是通用日志(联调诊断用:信令消息、WebRTC 状态等)。生产日志优先用上面 的具体方法(语义清晰);联调临时诊断用 Info 灵活传 key/value。
func (*Audit) PairConfirmed ¶
type Bridge ¶
type Bridge struct {
// contains filtered or unexported fields
}
Bridge is the desktop-side mobilebridge entry object: it owns S's identity, the Pairing state machine, the SignalClient long-link to K, and the live Conn map. It implements SignalHandler (dispatching K messages) and exposes ForwardEvent for the tabEventSink injection point.
Lifecycle: NewBridge → Start (connects to K) → runs until ctx done. The desktop layer (desktop/app.go) constructs it once at startup.
func NewBridge ¶
func NewBridge(cfg Config, sPriv ed25519.PrivateKey, sPub ed25519.PublicKey, store KeyStore, exec CommandExecutor, audit *Audit) *Bridge
NewBridge wires everything but does NOT connect yet (Start does that).
func (*Bridge) CloudConnected ¶
CloudConnected 报告云跳板长连是否在线(状态面板)。
func (*Bridge) CloudRelayURL ¶
CloudRelayURL 返回当前云跳板配置(状态面板显示)。
func (*Bridge) ConfirmPairing ¶
ConfirmPairing accepts a C. Wails UI calls this after the user clicks confirm.
func (*Bridge) ForwardEvent ¶
ForwardEvent is the injection point called by desktop/tabs.go's tabEventSink.Emit: it broadcasts the already-serialized wireEvent JSON to every Conn currently subscribed to tabId. Non-blocking — a slow/full Conn just drops that frame rather than stalling the Controller.
func (*Bridge) KnockEnabled ¶
KnockEnabled / KnockServer expose the UDP-knock config (settings panel).
func (*Bridge) KnockServer ¶
func (*Bridge) ListPairNics ¶
ListPairNics 枚举「配对网卡」候选(含默认标记),设置面板下拉框用。
func (*Bridge) NotifySessionListChanged ¶
func (b *Bridge) NotifySessionListChanged()
NotifySessionListChanged 广播 session_list_changed 给所有在线加密 Conn(方案B)。 fairpeer 端 tab 增删改时调,让 linkpeer 自动刷新会话列表,无需用户手动下拉。
func (*Bridge) OnSignalMsg ¶
OnSignalMsg 是主链路的入口(兼容旧调用方/测试)。带来源的分发在 onSignalMsg —— 云跳板链路也走那里。
func (*Bridge) PendingPairings ¶
func (b *Bridge) PendingPairings() []PendingPair
PendingPairings lists C's awaiting desktop confirm.
func (*Bridge) RejectPairing ¶
RejectPairing declines a C.
func (*Bridge) SetCloudRelay ¶
SetCloudRelay 热切换云跳板长连(设置面板「公网跳板」开关)。url 为空 = 关闭并断开云链路,回到纯局域网/单 K 行为。turnParam 为进二维码 turn= 字段的凭据串("user:pass@host:port",空 = 不带 TURN)。
func (*Bridge) SetOnReady ¶
SetOnReady 注册全局 onReady hook:每个 Conn 握手完成时回调(debug-server 用它 在握手后发测试 wireEvent,模拟 fairpeer 下行)。
func (*Bridge) SetPairAddress ¶
SetPairAddress 钉死二维码 relay 使用的网卡 IP("" = 自动)。
func (*Bridge) SetResolveTab ¶
SetResolveTab injects the tab-alias resolver: linkpeer sends "default"/"" but fairpeer tabs are UUIDs. The desktop layer maps the alias to the active tab.
func (*Bridge) SignalConnected ¶
SignalConnected reports whether the long-link to K is up. Surfaces K reachability in the settings panel so a stale/failed link is visible.
func (*Bridge) Start ¶
Start connects the SignalClients to their Ks and keeps them connected. Blocks until ctx is done. Call in a goroutine.
func (*Bridge) StartPairing ¶
StartPairing kicks off a new pairing session (Wails UI calls this).
type CommandExecutor ¶
type CommandExecutor interface {
Submit(tab, input, cmdID string) error
Cancel(tab string) error
Steer(tab, text string) error
Pause(tab string) error
Resume(tab string) error
Approve(tab, approvalID string, allow, session, persist bool) error
Answer(tab, askID string, answers []string) error
SetPlan(tab string, on bool) error
SetModel(tab, model string) error
ListSessions() ([]SessionInfo, error)
ListModels() ([]ModelInfo, error)
ListTemplates() ([]TemplateInfo, error)
NewTab(workspaceRoot, profile string) (string, error)
RenameSession(tab, title string) error
DeleteSession(tab string) error
OfficeRun(tab, template string, args map[string]string) error
FileStart(tab, name string, size int64) error
FileChunk(tab string, seq int, data string) error
FileEnd(tab, name string) error
LoadSession(tab string) ([]map[string]any, error)
}
CommandExecutor runs commands against fairpeer's Controller. The desktop integration layer (desktop/app.go) implements it; tests mock it. This is the seam that keeps mobilebridge free of fairpeer-internal dependencies.
type CommandRouter ¶
type CommandRouter struct {
// contains filtered or unexported fields
}
CommandRouter validates per-connection permissions and dispatches to the executor. One router per Conn (each Conn carries its own PerConnPermissions).
func NewCommandRouter ¶
func NewCommandRouter(devC string, exec CommandExecutor, perm PerConnPermissions, audit *Audit) *CommandRouter
func (*CommandRouter) Perms ¶
func (r *CommandRouter) Perms() PerConnPermissions
Perms 返回该连接的权限快照(UX_ONBOARDING W11:握手后经 permissions wireEvent 下发给 C 端展示——权限执法在 S 端,C 端只读显示真实生效值)。
func (*CommandRouter) Route ¶
func (r *CommandRouter) Route(plaintext []byte) error
Route parses a decrypted command, enforces permissions, dispatches.
func (*CommandRouter) SetListModelsHook ¶
func (r *CommandRouter) SetListModelsHook(fn func([]ModelInfo))
SetListModelsHook lets the Bridge reply to list_models by sending the model list back over the encrypted Conn.
func (*CommandRouter) SetListSessionsHook ¶
func (r *CommandRouter) SetListSessionsHook(fn func([]SessionInfo))
SetListSessionsHook lets the Bridge reply to list_sessions by sending the session list back over the encrypted Conn.
func (*CommandRouter) SetListTemplatesHook ¶
func (r *CommandRouter) SetListTemplatesHook(fn func([]TemplateInfo))
SetListTemplatesHook lets the Bridge reply to list_templates (UX M5 W14).
func (*CommandRouter) SetLoadSessionHook ¶
func (r *CommandRouter) SetLoadSessionHook(fn func(tab string, history []map[string]any))
SetLoadSessionHook lets the Bridge reply to load_session with the tab's conversation history (history_messages wireEvent).
func (*CommandRouter) SetNewTabHook ¶
func (r *CommandRouter) SetNewTabHook(fn func(string))
SetNewTabHook lets the Bridge reply to new_tab with the new tab ID so C can switch_tab + subscribe to it.
func (*CommandRouter) SetOnErrorHook ¶
func (r *CommandRouter) SetOnErrorHook(fn func(code, msg string))
SetOnErrorHook lets the Bridge send error events back to C (S1: tab_not_found 等).
func (*CommandRouter) SetResyncHook ¶
func (r *CommandRouter) SetResyncHook(fn func(tabID string, sinceSeq uint64))
SetResyncHook lets the Bridge reply to resync with delta/full events (§11.2).
func (*CommandRouter) SetSubscribeHook ¶
func (r *CommandRouter) SetSubscribeHook(fn func(tab string))
SetSubscribeHook lets the Bridge learn tab-subscription changes so it can route wireEvents to the right Conn (FAIRPEER_SPEC §11.1).
type CompletedHandshake ¶
type CompletedHandshake struct {
C2S, S2C []byte // 32B each
Transcript []byte // SHA256(ClientHelloJSON || ServerHelloJSON)
}
CompletedHandshake bundles the outputs a Conn needs after a successful handshake: the two direction keys (to wrap AEADs) and the transcript hash (to verify Finished).
func CompleteHandshake ¶
func CompleteHandshake(ephPriv *ecdh.PrivateKey, peerEphPub, nc, ns []byte, chJSON, shJSON []byte) (*CompletedHandshake, error)
CompleteHandshake runs ECDH + HKDF from the two helos and our ephemeral private key, returning the session keys + transcript hash. Both sides call this with the same (ch, sh) pair and their own ephPriv.
type Config ¶
type Config struct {
Enabled bool
SignalURL string // e.g. "wss://signal.example.com"
STUNServers []string // e.g. ["stun:signal.example.com:3478"]
CloudSignalURL string // 公网跳板 K(跨网候选信令);空 = 关(纯局域网/单 K)
TURNEnabled bool // opt-in relay (default off, pure-P2P)
TURNServers []string
TURNUser string // coturn REST 凭据(use-auth-secret 模式)
TURNPass string
UPnP bool // probe router for port mapping
ReadOnlyDefault bool // new peers default to read-only
RequireApproval bool // mobile submits require desktop approval
AllowFileDrop bool // allow phone→desktop file delivery
AllowHighRisk bool // allow triggering shell/exec via mobile
MaxConnections int
LogLevel string
AutoConfirm bool // 联调:收到 exchange 立即自动确认(不等用户点允许)
// UDPKnock 单包敲门(M3 NAT 穿透辅助,默认关):ICE 建连前 S 从 ICE
// 同一 UDP socket 向 C 的 srflx 公网映射发敲门包,提前打开 S 侧 NAT,
// 让 C 的 connectivity check 能进来。对 cone NAT 有效;双对称 NAT 无解
// (PROTOCOL §7)。KnockServer 是敲门依赖的远程 STUN 服务器——两端
// 靠它学到各自公网映射(srflx 候选),没有它敲门无目标可敲。
UDPKnock bool
KnockServer string // e.g. "stun:stun.example.com:3478";空 = 不追加
}
Config is the mobilebridge section of fairpeer.toml. The desktop integration layer (desktop/app.go) fills this from fairpeer's config load and passes it to Bridge — mobilebridge itself never reads TOML, keeping it independent of fairpeer's config internals. See FAIRPEER_SPEC §5.
func ApplyKnockDefault ¶
ApplyKnockDefault 补齐 knock_server 智能默认(UX_ONBOARDING W4):开了 单包敲门但没填 STUN 地址时,取云 K 域名拼 coturn(云跳板同机部署)。 没配云 K 则保持空(调用方 UI 提示需手填)。返回最终生效值。
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig matches FAIRPEER_SPEC §5 defaults. The SignalURL placeholder is overridden by the QR code's relay field at pairing time per device.
func (Config) DefaultPermissions ¶
func (c Config) DefaultPermissions() PerConnPermissions
DefaultPermissions snapshots the config-level defaults for a new peer.
type Conn ¶
type Conn struct {
// contains filtered or unexported fields
}
Conn is one live P2P link to a linkpeer device. It owns a pion PeerConnection + the DataChannel C created, runs the handshake state machine on the channel, then shuttles AEAD frames (PROTOCOL §5/§6).
Refusal is silent: an unpaired or revoked C's hello gets a dc.Close() with NO ServerHello — the device learns nothing about whether its target exists (enumeration protection, PROTOCOL §5.4, §11.2).
func NewConn ¶
func NewConn(sPriv ed25519.PrivateKey, sPub ed25519.PublicKey, pairing *Pairing, router *CommandRouter, audit *Audit) (*Conn, error)
NewConn creates a Conn with a fresh X25519 ephemeral for forward secrecy. The Bridge attaches the PeerConnection + lifecycle hooks after.
func (*Conn) AddICECandidate ¶
AddICECandidate feeds a remote ICE candidate from C (arrived via K).
func (*Conn) AttachPC ¶
func (c *Conn) AttachPC(pc *webrtc.PeerConnection)
AttachPC wires PeerConnection callbacks. The Bridge calls this right after creating the PC for an incoming offer. S 接受 C 创建的 in-band DataChannel。
func (*Conn) HandleOffer ¶
HandleOffer sets C's offer as remote desc and returns S's answer SDP.
func (*Conn) IsEncrypted ¶
func (*Conn) SendEvent ¶
SendEvent encrypts a wireEvent JSON (or any plaintext) and writes it to the DataChannel. Called by Bridge.ForwardEvent when tabEventSink fires.
func (*Conn) SetOnClose ¶
func (*Conn) SetOnReady ¶
type KeyStore ¶
type KeyStore interface {
Get(key string) ([]byte, error) // ErrNotFound if missing
Set(key string, val []byte) error
Delete(key string) error
}
KeyStore persists long-term secrets: this device's Ed25519 private key and each paired peer's public key. Production wires this to fairpeer's secret.Store (DPAPI-encrypted at rest, FAIRPEER_SPEC §6); tests use MemoryKeyStore. The abstraction keeps mobilebridge testable without fairpeer internals.
type MemoryKeyStore ¶
type MemoryKeyStore struct {
// contains filtered or unexported fields
}
MemoryKeyStore is an in-process KeyStore for tests and ephemeral runs.
func NewMemoryKeyStore ¶
func NewMemoryKeyStore() *MemoryKeyStore
func (*MemoryKeyStore) Delete ¶
func (s *MemoryKeyStore) Delete(key string) error
type NicInfo ¶
type NicInfo struct {
IP string `json:"ip"`
Name string `json:"name"` // 系统接口名(如 "以太网" / "WLAN")
Label string `json:"label"` // 友好标签:有线 / Wi-Fi / 局域网
IsDefault bool `json:"isDefault"` // 是否为默认(见 defaultLanIPInfo 评判标准)
Reason string `json:"reason"` // 默认判定理由(仅默认项有值,UI 展示用)
}
NicInfo 是设置面板「配对网卡」下拉框的一条候选。
func ListPairNics ¶
func ListPairNics() []NicInfo
ListPairNics 枚举可进二维码的真实网卡候选。 评判标准(谁当默认)见 defaultLanIPInfo:默认路由出口(metric 最低、 私网、非 TUN)优先;其余真实网卡作多候选补充,.1 网关位垫底。 默认项带判定理由(Reason),UI 直接展示"为什么是它"。
type Pairing ¶
type Pairing struct {
// contains filtered or unexported fields
}
Pairing drives the desktop side of pair/confirm/unpair against K and the local KeyStore. The Wails UI calls StartPairing/Confirm/Unpair; the SignalClient feeds OnExchange when K pushes a pair_exchange notice.
func NewPairing ¶
func (*Pairing) Confirm ¶
Confirm: the desktop user accepted. POST /pair/confirm to K, then persist C's public key. After this, C can handshake (Pairing.PeerPub returns its key).
func (*Pairing) OnExchange ¶
OnExchange handles a pair_exchange notice pushed from K (delivered by the SignalClient). Records C as pending and fires the UI hook.
func (*Pairing) OnExchangeFrom ¶
OnExchangeFrom 带 exchange 发生源 K 的通知入口(Bridge 双链路分别传入)。 Confirm 必须回源 POST——pair 记录(含 PubC)存在 exchange 发生的那台 K 上。
func (*Pairing) Pending ¶
func (p *Pairing) Pending() []PendingPair
Pending lists C's awaiting confirm (for the Wails UI).
func (*Pairing) SetAutoConfirm ¶
SetAutoConfirm enables automatic confirmation: OnExchange immediately POSTs /pair/confirm + persists C's key, so C's ClientHello (which follows fast) passes PeerPub. Desktop user still sees the pending→confirmed UI.
func (*Pairing) SetCloudRelay ¶
SetCloudRelay 配置云跳板 K + 二维码 TURN 凭据参数(Bridge.SetCloudRelay 热切换时同步到这里)。url 空 = 关闭。
func (*Pairing) SetOnExchange ¶
SetOnExchange installs the UI callback fired when a C scans + exchanges.
func (*Pairing) SetPairAddress ¶
SetPairAddress 钉死配对二维码使用的网卡 IP("" 恢复自动)。设置面板调。
func (*Pairing) StartPairing ¶
StartPairing generates a code, registers with K(s), returns the QR payload (linkpeer://pair?...). The QR carries the fingerprint out-of-band so C can defeat a MITM'd K at the exchange step.
云跳板开启时双 K 注册:S 自己生成 pairId,把同一 pairId+code 注册到主 K 与云 K —— QR 里的单个 pid 经任一 K 都能 exchange。云 K 注册失败(VPS 挂了/断网)不阻塞局域网配对:候选里去掉云地址即可。
type PendingPair ¶
type PendingPair struct {
PairID string
DevC string
PubC ed25519.PublicKey
FpC string
CreatedAt time.Time
}
PendingPair is a C that has exchanged and is awaiting the desktop user's confirm.
type PerConnPermissions ¶
type PerConnPermissions struct {
ReadOnly bool
RequireApproval bool
AllowFileDrop bool
AllowHighRisk bool
}
PerConnPermissions is what one connected C may do, derived from Config defaults and per-device overrides (set via the Wails MobileBridgeSetReadOnly binding). command_router enforces these on every inbound command.
type SessionInfo ¶
SessionInfo is one row of the session list sent to C.
type SignalClient ¶
type SignalClient struct {
// contains filtered or unexported fields
}
SignalClient is S's persistent OUTBOUND WSS link to K. Outbound pierces NAT (S needs no public IP). Reconnects with exponential backoff + ±100% jitter to avoid the thundering-herd when K restarts under 5000 peers (PROTOCOL §4.5, §7.2, ENGINEERING §10.7). Authenticates statelessly per §4.1 (URL-safe b64 in the query string).
func NewSignalClient ¶
func NewSignalClient(signalURL string, pub ed25519.PublicKey, priv ed25519.PrivateKey, h SignalHandler, audit *Audit) *SignalClient
func (*SignalClient) Close ¶
func (c *SignalClient) Close() error
Close stops Run and drops the current connection.
func (*SignalClient) Connected ¶
func (c *SignalClient) Connected() bool
Connected reports whether the WS link to K is currently up. Used by the settings panel so the user can see "已连接信令" vs "连接中…/失败".
func (*SignalClient) Run ¶
func (c *SignalClient) Run(ctx context.Context) error
Run connects and stays connected until ctx is done or Close is called.
func (*SignalClient) Send ¶
func (c *SignalClient) Send(msg SignalMsg) error
Send posts a message to K (answer/ice generated by the peer). Error if disconnected.
type SignalHandler ¶
type SignalHandler interface {
OnSignalMsg(msg SignalMsg)
}
SignalHandler receives inbound messages from K. The Bridge implements this and dispatches offer/ice to peer, pair_exchange to Pairing.
type SignalMsg ¶
type SignalMsg struct {
Type string `json:"type"`
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
Ts int64 `json:"ts,omitempty"`
Sig string `json:"sig,omitempty"`
SDP string `json:"sdp,omitempty"`
SDPType string `json:"sdpType,omitempty"` // "offer" | "answer"
Cand string `json:"cand,omitempty"`
ConnID string `json:"connId,omitempty"`
// pair_exchange notice fields:
PairID string `json:"pairId,omitempty"`
PubC string `json:"pubC,omitempty"`
FpC string `json:"fpC,omitempty"`
DevC string `json:"devC,omitempty"`
}
SignalMsg is the envelope forwarded through K. Pair-exchange notices reuse this shape (PROTOCOL §3.1: K pushes pubC to S over this same WS).
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package proto holds the wire-format message types shared between fairpeer desktop (S), linkpeer-signal cloud (K), and linkpeer mobile (C).
|
Package proto holds the wire-format message types shared between fairpeer desktop (S), linkpeer-signal cloud (K), and linkpeer mobile (C). |