mobilebridge

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 36 Imported by: 0

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

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

View Source
var (
	ErrShortFrame = errors.New("frame too short")
	ErrBadVersion = errors.New("unsupported frame version")
)
View Source
var (
	ErrBadSig           = errors.New("handshake signature invalid")
	ErrBadEphemeral     = errors.New("handshake ephemeral key invalid")
	ErrFinishedMismatch = errors.New("handshake finished transcript mismatch")
)
View Source
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.

View Source
var ErrNoPending = errors.New("no pending pair")

ErrNoPending means no C is waiting at the given pairId when Confirm is called.

View Source
var ErrNotEncrypted = errors.New("connection not encrypted yet")

ErrNotEncrypted is returned by SendEvent before the handshake completes.

View Source
var ErrNotFound = errors.New("key not found")

ErrNotFound is returned by KeyStore.Get for a missing key.

View Source
var ErrNotStarted = errors.New("bridge not started")

ErrNotStarted is returned when Bridge operations are attempted before Start.

View Source
var ErrStaleTS = errors.New("stale_ts")

ErrStaleTS 表示握手 ts 超出新鲜度窗口(P2-1 防握手重放)。

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

func ConstantTimeEqual(a, b []byte) bool

ConstantTimeEqual guards signature/fingerprint comparisons against timing oracles. Use this anywhere secret-derived bytes are compared.

func DeriveSessionKeys

func DeriveSessionKeys(shared, nc, ns []byte) (c2s, s2c []byte)

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

func DevID(pub []byte) string

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

func Fingerprint(pub []byte) string

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

func FinishedMessage(role string, transcript []byte) proto.Finished

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 NewAEAD

func NewAEAD(key []byte) (cipher.AEAD, error)

NewAEAD wraps AES-256-GCM under a 32-byte session key.

func OpenFrame

func OpenFrame(aead cipher.AEAD, frame []byte) (seq uint64, plaintext []byte, err error)

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

func ParseTurnCred(paste string) (user, pass, host string, port int, ok bool)

ParseTurnCred 从 turn-cred.sh 输出(或任意包含凭据串的粘贴文本)解析 `user:pass@host[:port]`(UX_ONBOARDING W3)。返回归一化的 TURN 配置; 解析不到返回 ok=false。

func ProbeUPnP

func ProbeUPnP(localPort int) (externalIP string, externalPort int)

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 Random

func Random(n int) ([]byte, error)

Random fills n bytes from crypto/rand.

func SealFrame

func SealFrame(aead cipher.AEAD, seq uint64, nonce, plaintext []byte) []byte

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

func UPnPCandidate(localPort int) string

UPnPCandidate generates a srflx ICE candidate from a UPnP port mapping.

func VerifyClientHello

func VerifyClientHello(pub ed25519.PublicKey, ch proto.ClientHello) error

func VerifyFinished

func VerifyFinished(f proto.Finished, role string, transcript []byte) error

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 NewAudit

func NewAudit(level string) *Audit

func (*Audit) Cmd

func (a *Audit) Cmd(dev, cmd, tab string, ok bool)

func (*Audit) ConnClose

func (a *Audit) ConnClose(dev string)

func (*Audit) ConnOpen

func (a *Audit) ConnOpen(dev, iceMode string)

func (*Audit) Denied

func (a *Audit) Denied(dev, cmd, reason string)

func (*Audit) Error

func (a *Audit) Error(evt, dev string, err error)

func (*Audit) Info

func (a *Audit) Info(evt string, args ...any)

Info 是通用日志(联调诊断用:信令消息、WebRTC 状态等)。生产日志优先用上面 的具体方法(语义清晰);联调临时诊断用 Info 灵活传 key/value。

func (*Audit) PairConfirmed

func (a *Audit) PairConfirmed(devC, devS string)

func (*Audit) PairStart

func (a *Audit) PairStart(devS string)

func (*Audit) Unpaired

func (a *Audit) Unpaired(dev string)

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

func (b *Bridge) CloudConnected() bool

CloudConnected 报告云跳板长连是否在线(状态面板)。

func (*Bridge) CloudRelayURL

func (b *Bridge) CloudRelayURL() string

CloudRelayURL 返回当前云跳板配置(状态面板显示)。

func (*Bridge) ConfirmPairing

func (b *Bridge) ConfirmPairing(pairID string) error

ConfirmPairing accepts a C. Wails UI calls this after the user clicks confirm.

func (*Bridge) ForwardEvent

func (b *Bridge) ForwardEvent(tabID string, wireEventJSON []byte)

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

func (b *Bridge) KnockEnabled() bool

KnockEnabled / KnockServer expose the UDP-knock config (settings panel).

func (*Bridge) KnockServer

func (b *Bridge) KnockServer() string

func (*Bridge) ListPairNics

func (b *Bridge) ListPairNics() []NicInfo

ListPairNics 枚举「配对网卡」候选(含默认标记),设置面板下拉框用。

func (*Bridge) NotifySessionListChanged

func (b *Bridge) NotifySessionListChanged()

NotifySessionListChanged 广播 session_list_changed 给所有在线加密 Conn(方案B)。 fairpeer 端 tab 增删改时调,让 linkpeer 自动刷新会话列表,无需用户手动下拉。

func (*Bridge) OnSignalMsg

func (b *Bridge) OnSignalMsg(msg SignalMsg)

OnSignalMsg 是主链路的入口(兼容旧调用方/测试)。带来源的分发在 onSignalMsg —— 云跳板链路也走那里。

func (*Bridge) PendingPairings

func (b *Bridge) PendingPairings() []PendingPair

PendingPairings lists C's awaiting desktop confirm.

func (*Bridge) RejectPairing

func (b *Bridge) RejectPairing(pairID string)

RejectPairing declines a C.

func (*Bridge) SetCloudRelay

func (b *Bridge) SetCloudRelay(url string)

SetCloudRelay 热切换云跳板长连(设置面板「公网跳板」开关)。url 为空 = 关闭并断开云链路,回到纯局域网/单 K 行为。turnParam 为进二维码 turn= 字段的凭据串("user:pass@host:port",空 = 不带 TURN)。

func (*Bridge) SetKnock

func (b *Bridge) SetKnock(enabled bool, server string)

SetKnock 运行时更新单包敲门开关/服务器(设置面板;影响之后新建的连接)。

func (*Bridge) SetOnReady

func (b *Bridge) SetOnReady(fn func(*Conn))

SetOnReady 注册全局 onReady hook:每个 Conn 握手完成时回调(debug-server 用它 在握手后发测试 wireEvent,模拟 fairpeer 下行)。

func (*Bridge) SetPairAddress

func (b *Bridge) SetPairAddress(ip string)

SetPairAddress 钉死二维码 relay 使用的网卡 IP("" = 自动)。

func (*Bridge) SetResolveTab

func (b *Bridge) SetResolveTab(fn func(string) string)

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

func (b *Bridge) SignalConnected() bool

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

func (b *Bridge) SignalURL() string

SignalURL returns the configured K base URL (for display in the panel).

func (*Bridge) Start

func (b *Bridge) Start(ctx context.Context) error

Start connects the SignalClients to their Ks and keeps them connected. Blocks until ctx is done. Call in a goroutine.

func (*Bridge) StartPairing

func (b *Bridge) StartPairing() (code, qrURL string, err error)

StartPairing kicks off a new pairing session (Wails UI calls this).

func (*Bridge) Unpair

func (b *Bridge) Unpair(devC string)

Unpair removes + revokes a previously-paired C.

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

func ApplyKnockDefault(cfg Config) Config

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

func (c *Conn) AddICECandidate(cand string) error

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

func (c *Conn) DevC() string

func (*Conn) HandleOffer

func (c *Conn) HandleOffer(offerSDP string) (string, error)

HandleOffer sets C's offer as remote desc and returns S's answer SDP.

func (*Conn) IsEncrypted

func (c *Conn) IsEncrypted() bool

func (*Conn) SendEvent

func (c *Conn) SendEvent(plaintext []byte) error

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 (c *Conn) SetOnClose(fn func(*Conn))

func (*Conn) SetOnReady

func (c *Conn) SetOnReady(fn func(*Conn))

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

func (*MemoryKeyStore) Get

func (s *MemoryKeyStore) Get(key string) ([]byte, error)

func (*MemoryKeyStore) Set

func (s *MemoryKeyStore) Set(key string, val []byte) error

type ModelInfo

type ModelInfo struct {
	ID    string `json:"id"`
	Label string `json:"label"`
}

ModelInfo is one row of the model list sent to C (list_models reply).

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 NewPairing(signalURL string, pub ed25519.PublicKey, store KeyStore, audit *Audit) *Pairing

func (*Pairing) Confirm

func (p *Pairing) Confirm(pairID string) error

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

func (p *Pairing) IsRevoked(devC string) bool

IsRevoked checks the local revocation list.

func (*Pairing) OnExchange

func (p *Pairing) OnExchange(msg SignalMsg)

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

func (p *Pairing) OnExchangeFrom(msg SignalMsg, kURL string)

OnExchangeFrom 带 exchange 发生源 K 的通知入口(Bridge 双链路分别传入)。 Confirm 必须回源 POST——pair 记录(含 PubC)存在 exchange 发生的那台 K 上。

func (*Pairing) PeerPub

func (p *Pairing) PeerPub(devC string) (ed25519.PublicKey, bool)

PeerPub returns C's stored public key if paired, else (nil,false).

func (*Pairing) Pending

func (p *Pairing) Pending() []PendingPair

Pending lists C's awaiting confirm (for the Wails UI).

func (*Pairing) Reject

func (p *Pairing) Reject(pairID string)

Reject drops a pending pair without persisting (desktop user declined).

func (*Pairing) SetAutoConfirm

func (p *Pairing) SetAutoConfirm(v bool)

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

func (p *Pairing) SetCloudRelay(url, turnParam string)

SetCloudRelay 配置云跳板 K + 二维码 TURN 凭据参数(Bridge.SetCloudRelay 热切换时同步到这里)。url 空 = 关闭。

func (*Pairing) SetOnExchange

func (p *Pairing) SetOnExchange(fn func(pairID, devC, fpC string))

SetOnExchange installs the UI callback fired when a C scans + exchanges.

func (*Pairing) SetPairAddress

func (p *Pairing) SetPairAddress(ip string)

SetPairAddress 钉死配对二维码使用的网卡 IP("" 恢复自动)。设置面板调。

func (*Pairing) StartPairing

func (p *Pairing) StartPairing() (code, qrURL string, err error)

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 挂了/断网)不阻塞局域网配对:候选里去掉云地址即可。

func (*Pairing) Unpair

func (p *Pairing) Unpair(devC string)

Unpair deletes C's key and appends to the local revocation list. A revoked device's next handshake gets refused at hello_c verification (PROTOCOL §5.4).

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

type SessionInfo struct {
	Path  string `json:"path"`
	Title string `json:"title"`
}

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

type TemplateInfo

type TemplateInfo struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Category string `json:"category"`
	Desc     string `json:"desc"`
}

TemplateInfo is one row of the office template list sent to C (list_templates reply; source = scheduler.BuiltinTemplates).

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

Jump to

Keyboard shortcuts

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