# fatedier/frp — codebase unpack

> Canonical page: https://codevetter.com/unpack/frp

This is a deterministic Repo Unpack scan of fatedier/frp — a structural map, not a review, security audit, or maintainer-endorsed description.

- Scanned commit: d20a232996007dfe6ab425abc0a39a3ae9a0889b (2026-09-10)
- Last upstream commit: test: strengthen health check coverage (#5528)
- Generated by codevetter 1.12.0 on 2026-09-22
- 516 files scanned · 2.1 MB

## Languages

- Go: 351 files
- Vue: 47 files
- TypeScript: 42 files
- Markdown: 14 files
- YAML: 11 files
- JSON: 10 files
- CSS: 4 files
- Sass: 4 files

## Repository structure

- pkg/ — 189 files
- web/ — 128 files
- test/ — 70 files
- server/ — 44 files
- client/ — 34 files
- cmd/ — 10 files
- .github/ — 9 files
- conf/ — 6 files

Probable source entrypoint: `cmd/frpc` (inferred from the tree, not upstream docs)

## Package

- go.mod: github.com/fatedier/frp — 0 declared dependencies

## Structural health signals

Average score 7.5/10 across 96 files; 12 hotspots. These are deterministic review leads, not proof of a bug.

## Recent history

- d20a23299600 (2026-09-10) test: strengthen health check coverage (#5528)
- b0d840858fed (2026-09-10) ci: upgrade CircleCI config to 2.1 (#5530)
- c5508627c4c4 (2026-09-10) docs: add rapidproxy as gold sponsor (#5529)
- 832df8dff66d (2026-08-29) fix(health): reset failed count after successful check (#5502)
- ebb4773c2cd4 (2026-08-29) docs: update release notes (#5521)

## Overview

frp is a fast reverse proxy written in Go that exposes services behind NATs/firewalls via a public server (frps) and an in-NAT client (frpc). It supports TCP, UDP, HTTP, HTTPS, TCPMUX, and secret/P2P modes (stcp, sudp, xtcp) over TCP, KCP, QUIC, WebSocket, or TLS control transports, with token/OIDC auth, HTTP-based server plugins, embedded Vue dashboards, and optional dynamic proxy persistence. The codebase is split into cmd/ entrypoints, server/ and client/ runtimes, and a large pkg/ shared library (config v1, msg protocol, wire v2, auth, nathole, plugins, vnet).

## System map

The repo builds two binaries from a single Go module: frps (server, cmd/frps) and frpc (client, cmd/frpc), both driven by cobra. server/service.go constructs a Service that multiplexes control-plane listeners (TCP mux, KCP, QUIC, WebSocket, raw TLS, internal SSH pipe) on shared ports and owns the ControlManager, proxy.Manager, plugin.Manager, ports.Manager and vhost routers. client/service.go constructs a Service that dials frps through a pluggable Connector and keeps a Control session alive with backoff retries. Shared protocol and config code lives under pkg/ (msg, proto/wire, config/v1, auth, transport, util). State is entirely in-memory except an optional client-side JSON store for dynamic proxies. Two Vue 3 apps under web/ provide dashboards and are embedded via the assets package, or excluded with the 'noweb' build tag.
- frps entrypoint is cmd/frps/root.go: cobra rootCmd.RunE loads config via config.LoadServerConfig, validates with validation.NewConfigValidator, then calls runServer -> server.NewService -> svr.Run. — [cmd/frps/root.go#L53-127](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frps/root.go#L53-L127) [server/service.go#L139](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L139)
- frpc entrypoint is cmd/frpc/sub/root.go: runClient -> config.LoadClientConfigResult -> source.NewAggregator -> client.NewService -> svr.Run; a --config_dir flag runs one Service per file via runMultipleClients. — [cmd/frpc/sub/root.go#L59-134](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/root.go#L59-L134) [client/service.go#L163](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L163)
- server.Service multiplexes the bind port with golib mux.Mux: default listener for TCP control, a prefix-matched websocket listener ('GET /~frp'), and a TLS listener that sniffs first byte 0x16 / FRPTLSHeadByte. — [server/service.go#L245-253](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L245-L253) [server/service.go#L290-294](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L290-L294) [server/service.go#L352-355](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L352-L355)
- Optional listeners are created for KCP (netpkg.ListenKcp on KCPBindPort), QUIC (quic.ListenAddr with ALPN 'frp'), SSH tunnel gateway (ssh.NewGateway), and a tcpmux HTTPConnect multiplexer on TCPMuxHTTPConnectPort. — [server/service.go#L256-287](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L256-L287) [server/service.go#L191-204](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L191-L204) [pkg/ssh/gateway.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/ssh/gateway.go)
- The wire protocol has two versions: acceptConnection calls wire.CheckMagic to detect v2, then negotiates crypto/UDP-codec via ClientHello/ServerHello frames before reading the first msg. — [server/service.go#L555-593](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L555-L593) [server/service.go#L623-680](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L623-L680) [pkg/proto/wire](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/proto/wire)
- The client dials frps through a Connector interface (client/connector.go) supporting tcp, kcp, quic (one quic.Conn, streams converted via QuicStreamToNetConn), websocket/wss, and tls; ServiceOptions.ConnectorCreator allows swapping in a virtual pipe connector. — [client/connector.go#L33-60](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/connector.go#L33-L60) [client/connector.go#L104-137](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/connector.go#L104-L137) [client/connector.go#L216-226](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/connector.go#L216-L226) [client/service.go#L83-90](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L83-L90)
- Server state is in-memory: registry.ClientRegistry tracks logical clients by user.clientID, proxy.Manager tracks live proxies, ports.Manager allocates TCP/UDP remote ports honoring AllowPorts, and vhost.Routers route HTTP(S) vhosts. — [server/service.go#L167-180](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L167-L180) [server/registry](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/registry) [server/ports/ports.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/ports/ports.go) [pkg/util/vhost](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/util/vhost)
- The only persistent state is the optional client-side StoreSource: a JSON file (store.path) holding dynamic proxies/visitors, merged with file config by source.Aggregator. — [pkg/config/source/store.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/source/store.go) [pkg/config/source/aggregator.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/source/aggregator.go) [cmd/frpc/sub/root.go#L143-163](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/root.go#L143-L163)
- Dashboards are Vue 3 apps web/frps and web/frpc (npm workspaces under web/), embedded into Go via the assets package; Makefile adds a 'noweb' build tag when web/*/dist is absent. — [web/package.json](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/web/package.json) [assets/assets.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/assets/assets.go) [Makefile#L4](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L4) [web/frps/package.json](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/web/frps/package.json) [web/frpc/package.json](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/web/frpc/package.json)
- Build produces CGO_ENABLED=0 binaries with build tags 'frps'/'frpc' plus optional 'noweb' (Makefile frps/frpc targets); go.mod pins go 1.25.0 with quic-go, kcp-go, yamux, gorilla/mux, go-oidc, prometheus, wireguard as key deps. — [Makefile#L40-44](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L40-L44) [go.mod#L3-39](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/go.mod#L3-L39)
- server/http and client/http packages implement admin API controllers behind gorilla/mux routers registered by registerRouteHandlers on each Service's webServer (httppkg.Server). — [server/api_router.go#L27-71](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/api_router.go#L27-L71) [client/api_router.go#L26-64](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/api_router.go#L26-L64) [server/http](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/http) [client/http](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/http)
- Both services disable quic-go receive-buffer warnings and ECN via env vars in package init, and set crypto.DefaultSalt = 'frp' for golib crypto. — [server/service.go#L70-78](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L70-L78) [client/service.go#L47-55](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L47-L55)

## Feature catalog

frp exposes eight proxy types (tcp, udp, http, https, tcpmux, stcp, sudp, xtcp) plus three visitor types (stcp, sudp, xtcp) for private/P2P access. Features include token and OIDC auth, per-proxy encryption/compression/bandwidth limits, load-balancer groups, health checks, PROXY protocol and host-header rewriting, HTTP server plugins, client plugins (static_file, unix_domain_socket, http(s) converters, socks5, tls2raw, virtual_net), NAT hole punching via STUN, an SSH tunnel gateway, a WireGuard-based VirtualNet, Prometheus metrics, dashboards for both daemons, and a rich frpc CLI with per-type subcommands, verify, nathole discover and admin commands.
- Eight proxy types are defined by proxyConfigTypeMap in pkg/config/v1/proxy.go (TCP, UDP, HTTP, HTTPS, TCPMUX, STCP, XTCP, SUDP); NewProxyConfigurerByType instantiates typed configurers via reflection. — [pkg/config/v1/proxy.go#L242-261](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/v1/proxy.go#L242-L261)
- frpc auto-generates a cobra subcommand per proxy type (tcp, udp, tcpmux, http, https, stcp, sudp, xtcp) plus visitor subcommands for stcp/sudp/xtcp, wiring flags via config.RegisterClientCommonConfigFlags/RegisterProxyFlags. — [cmd/frpc/sub/proxy.go#L45-87](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/proxy.go#L45-L87)
- Auth supports token and OIDC methods: NewAuthSetter picks NewTokenAuth or Oidc setters/verifiers, and TokenSource can dynamically resolve tokens (file/exec) via Resolve. — [pkg/auth/auth.go#L64-81](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/auth/auth.go#L64-L81) [pkg/auth/auth.go#L118-127](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/auth/auth.go#L118-L127) [pkg/auth/oidc.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/auth/oidc.go) [pkg/auth/token.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/auth/token.go)
- Client-side plugins registered via init() include static_file, unix_domain_socket, http2http, http2https, https2http, https2https, socks5, tls2raw, virtual_net, and http_proxy; each registers a CreatorFn into pkg/plugin/client's creators map. — [pkg/plugin/client/plugin.go#L33-49](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/client/plugin.go#L33-L49) [pkg/plugin/client/static_file.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/client/static_file.go) [pkg/plugin/client/unix_domain_socket.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/client/unix_domain_socket.go) [pkg/plugin/client/socks5.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/client/socks5.go)
- Server manage plugins are HTTP webhooks configured via HTTPPlugins; plugin.Manager supports six ops (Login, NewProxy, CloseProxy, Ping, NewWorkConn, NewUserConn) that can reject or mutate request content. — [server/service.go#L207-211](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L207-L211) [pkg/plugin/server/manager.go#L27-44](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/server/manager.go#L27-L44) [pkg/plugin/server/manager.go#L108-170](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/server/manager.go#L108-L170) [pkg/plugin/server/http.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/server/http.go)
- xtcp P2P mode uses NAT hole punching: client/visitor exchange NatHoleVisitor/NatHoleClient messages through frps' nathole.Controller, with STUN discovery defaulting to stun.easyvoip.com:3478 (natHoleStunServer). — [pkg/nathole/controller.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/nathole/controller.go) [pkg/nathole/nathole.go#L118-120](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/nathole/nathole.go#L118-L120) [pkg/config/v1/client.go#L50-89](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/v1/client.go#L50-L89) [server/control.go#L730-732](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L730-L732)
- stcp/sudp 'secret' proxies require a shared secretKey; visitors bind a local port and connect through frps visitor-manager admission (admitVisitorByRunID / visitor.Manager). — [server/control.go#L289-303](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L289-L303) [server/visitor](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/visitor) [client/visitor](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/visitor)
- Load balancing groups exist for TCP (TCPGroupCtl), HTTP (HTTPGroupCtl), HTTPS and TCPMux, letting multiple proxies share a group key and remote port. — [server/service.go#L214-220](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L214-L220) [server/service.go#L348](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L348) [server/proxy/tcp.go#L51-65](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/proxy/tcp.go#L51-L65) [server/group](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/group)
- SSH tunnel gateway (SSHTunnelGateway.BindPort) accepts native ssh connections and forwards them into frps via an InternalListener handled like regular frp connections. — [server/service.go#L101](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L101) [server/service.go#L280-287](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L280-L287) [server/service.go#L381](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L381) [pkg/ssh/gateway.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/ssh/gateway.go)
- VirtualNet creates a TUN device (water + wireguard-go) so visitors get an IP on a virtual network; enabled via featureGates.VirtualNet and virtualNet.address, and cannot be enabled by hot reload alone. — [client/service.go#L220-222](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L220-L222) [client/service.go#L515-520](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L515-L520) [pkg/vnet/controller.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/vnet/controller.go) [pkg/vnet/tun_linux.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/vnet/tun_linux.go)
- frps dashboard API exposes /healthz, /api/serverinfo, /api/proxy/{type}[/{name}], /api/traffic/{name}, /api/clients, DELETE /api/proxies, plus a parallel /api/v2 surface (users, system/info, system/prune, clients, proxies) and /metrics when EnablePrometheus. — [server/api_router.go#L27-60](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/api_router.go#L27-L60) [server/http](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/http)
- frpc admin API exposes /api/reload, /api/stop, /api/status, GET/PUT /api/config, per-proxy/visitor config reads, and CRUD under /api/store/proxies|visitors when the store source is enabled. — [client/api_router.go#L36-55](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/api_router.go#L36-L55)
- frpc CLI extras: 'verify' subcommand validates auth against a server, 'nathole discover' probes NAT behavior, and admin subcommands call the admin API. — [cmd/frpc/sub/verify.go#L27-30](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/verify.go#L27-L30) [cmd/frpc/sub/nathole.go#L34-47](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/nathole.go#L34-L47) [cmd/frpc/sub/admin.go#L48-54](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/admin.go#L48-L54)
- Per-proxy transport features include useEncryption/useCompression (golib io wrappers), bandwidthLimit with client/server mode (rate.Limiter), and proxyProtocolVersion headers built by BuildProxyProtocolHeaderStruct. — [client/proxy/proxy.go#L66-70](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/proxy.go#L66-L70) [client/proxy/proxy.go#L134-154](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/proxy.go#L134-L154) [client/proxy/proxy.go#L210-215](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/proxy.go#L210-L215)
- Health checking of local services lives in client/health, and event hooks in client/event let proxies react to lifecycle events. — [client/health/health.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/health/health.go) [client/event/event.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/event/event.go)

## Data flow

Config enters as TOML/YAML/JSON (or deprecated INI), is template-rendered with environment variables, decoded into pkg/config/v1 types, completed, and validated before use. Control-plane data flows as typed msg structs (Login, NewProxy, NewWorkConn, Ping, NatHole*) over a single muxed connection, versioned by the v1/v2 wire protocol. User traffic flows: public listener on frps -> proxy -> pooled work connection -> frpc -> optional plugin -> local service, joined byte streams via libio.Join. Admin data flows through gorilla/mux routers into adminapi controllers, and reloads re-merge config via the source.Aggregator.
- Config loading: LoadFileContentWithTemplate renders Go templates exposing .Envs plus parseNumberRange helpers, then LoadConfigure tries TOML->JSON marshal, JSON detection via yaml.IsJSONBuffer, or YAML (with dot-field stripping), honoring strict mode. — [pkg/config/load.go#L84-128](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L84-L128) [pkg/config/load.go#L176-217](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L176-L217)
- Legacy INI configs are detected by a 'common' section and converted through pkg/config/legacy converters (Convert_ServerCommonConf_To_v1, Convert_ClientCommonConf_To_v1) before joining the v1 path. — [pkg/config/load.go#L65-82](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L65-L82) [pkg/config/load.go#L295-324](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L295-L324) [pkg/config/legacy](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/legacy)
- Client proxy/visitor configs come from inline [[proxies]]/[[visitors]] plus includeConfigFiles globs (LoadAdditionalClientConfigs), dedup-checked by validateNoDuplicateNames, filtered by common.Start and enabled flags in FilterClientConfigurers. — [pkg/config/load.go#L379-402](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L379-L402) [pkg/config/load.go#L427-447](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L427-L447) [pkg/config/load.go#L465-500](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L465-L500) [pkg/config/load.go#L502-538](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L502-L538)
- Login handshake: client sends msg.Login over the control conn; server runs pluginManager.Login hooks, wraps the conn in an encrypted ReadWriter (AEAD for v2, crypto RW for v1), then RegisterControl admits and activates the Control, replying LoginResp{RunID}. — [server/service.go#L454-511](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L454-L511) [server/service.go#L607-621](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L607-L621) [server/control.go#L109-220](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L109-L220)
- Proxy registration: client pm.UpdateAll sends msg.NewProxy; server handleNewProxy runs NewProxy plugin hooks, builds a configurer via config.NewProxyConfigurerFromMsg, creates a Proxy through proxyFactoryRegistry, runs it (acquiring ports/vhost routes) and answers NewProxyResp. — [server/control.go#L736-755](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L736-L755) [pkg/config/load.go#L278-293](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L278-L293) [server/proxy/proxy.go#L43-47](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/proxy/proxy.go#L43-L47) [server/proxy/tcp.go#L49-89](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/proxy/tcp.go#L49-L89)
- Work connections are pooled: server keeps workConnCh buffered at poolCount+10, sends msg.ReqWorkConn on demand; client handleReqWorkConn dials a fresh conn, writes authenticated NewWorkConn, waits for StartWorkConn, then hands the conn to the owning proxy. — [server/control.go#L595-637](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L595-L637) [client/control.go#L124-162](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/control.go#L124-L162) [server/control.go#L49](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L49)
- TCP user traffic path on frpc: BaseProxy.InWorkConn -> HandleTCPWorkConnection -> wrapWorkConn (limiter -> encryption -> compression) -> optional proxyPlugin.Handle or dial localIP:localPort -> libio.Join(localConn, remote). — [client/proxy/proxy.go#L160-167](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/proxy.go#L160-L167) [client/proxy/proxy.go#L170-258](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/proxy.go#L170-L258)
- HTTP(S) traffic is routed by hostname through vhost.Routers into the matching proxy; frps can share the bind port with vhost ports by muxing HTTP/HTTPS listeners off mux.Mux when BindPort == VhostHTTP(S)Port. — [server/service.go#L296-349](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L296-L349) [server/service.go#L229-236](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L229-L236) [pkg/util/vhost](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/util/vhost)
- UDP traffic uses msg.UDPPacket frames; the negotiated codec (udpPacketCodec) is fixed per session via the v2 ServerHello and passed into proxy/visitor managers. — [server/service.go#L678](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L678) [client/control.go#L50-52](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/control.go#L50-L52) [pkg/msg/msg.go#L186](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/msg/msg.go#L186)
- Hot reload path: /api/reload -> apiController.Reload -> Service.UpdateConfigSource -> configSource.ReplaceAll -> reloadConfigFromSourcesLocked -> aggregator.Load -> Filter/Complete -> UpdateAllConfigurer -> ctl.pm/vm.UpdateAll. — [client/api_router.go#L36](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/api_router.go#L36) [client/service.go#L385-412](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L385-L412) [client/service.go#L496-527](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L496-L527) [client/control.go#L294-298](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/control.go#L294-L298)
- Metrics flow: server/metrics.Server records NewClient/NewProxy/CloseProxy events during Control lifecycle; pkg/metrics EnableMem/EnablePrometheus are activated when webServer.Port > 0 and exposed at /metrics. — [server/control.go#L685](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L685) [server/control.go#L717](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L717) [server/service.go#L156-159](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L156-L159) [server/api_router.go#L35-37](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/api_router.go#L35-L37) [pkg/metrics/metrics.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/metrics/metrics.go)
- NatHole orchestration: NatHoleVisitor/NatHoleClient/NatHoleReport messages are dispatched as async handlers on the server Control and routed to nathole.Controller, which analyzes NAT behavior and replies NatHoleResp to visitors. — [server/control.go#L730-732](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L730-L732) [pkg/nathole/controller.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/nathole/controller.go) [pkg/msg/msg.go#L192-244](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/msg/msg.go#L192-L244)

## Behavior traces

Four flows were traced end-to-end. (1) frps startup: cobra -> LoadServerConfig -> validate -> NewService wires all listeners/managers -> Run spawns per-listener goroutines. (2) frpc startup+login: cobra -> LoadClientConfigResult -> aggregator merge -> NewService -> Run -> loopLoginUntilSuccess -> controlSessionDialer.Dial -> NewControl -> ctl.Run. (3) TCP proxy request path: TCPProxy.Run listens -> user conn -> GetWorkConn -> ReqWorkConn -> client NewWorkConn -> StartWorkConn -> InWorkConn -> join to local service. (4) Admin reload: GET /api/reload -> Reload -> UpdateConfigSource -> reloadConfigFromSourcesLocked -> UpdateAllConfigurer.
- frps startup order: rootCmd.RunE -> config.LoadServerConfig (legacy INI detection -> template render -> decode -> Complete) -> security.NewUnsafeFeatures + ValidateServerConfig -> runServer -> log.InitLogger -> server.NewService -> svr.Run. — [cmd/frps/root.go#L56-127](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frps/root.go#L56-L127) [pkg/config/load.go#L295-324](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L295-L324)
- server.NewService order: build TLS config -> optional webServer (+metrics) -> BuildServerAuth -> construct managers (ctlManager, pxyManager, pluginManager, ResourceController with port managers) -> register HTTP plugins -> group controllers -> bind muxed TCP listener -> optional KCP/QUIC/SSH/vhost listeners -> NatHoleController. — [server/service.go#L139-363](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L139-L363)
- svr.Run spawns goroutines: webServer.Run, HandleListener(sshTunnelListener internal=true), HandleListener(kcp), HandleQUICListener, HandleListener(websocket), HandleListener(tls), NatHoleController.CleanWorker, sshTunnelGateway.Run, then blocks in HandleListener(main listener). — [server/service.go#L366-407](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L366-L407)
- Inbound conn handling: HandleListener -> handleConnection -> acceptConnection (CheckMagic v1/v2, ClientHello negotiation, read first msg) -> switch on msg.Login/msg.NewWorkConn/msg.NewVisitorConn -> RegisterControl / RegisterWorkConn / RegisterVisitorConn. — [server/service.go#L443-593](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L443-L593)
- Control lifecycle is generation-guarded: ControlManager.Add installs ctl as pending generation keyed by runID (replacing the old control and inheriting its runMu), Activate registers user.clientID in ClientRegistry (conflict -> error), completeLogin writes LoginResp then startLocked flips state to running and launches ctl.worker. — [server/control.go#L109-171](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L109-L171) [server/control.go#L175-220](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L175-L220) [server/control.go#L226-248](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L226-L248) [server/control.go#L520-527](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L520-L527)
- frpc startup order: runClient -> LoadClientConfigResult -> NewConfigSource.ReplaceAll -> optional NewStoreSource -> NewAggregator.Load -> Filter/Complete configurers -> ValidateAllClientConfig -> startServiceWithAggregator -> client.NewService -> svr.Run. — [cmd/frpc/sub/root.go#L122-212](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/root.go#L122-L212)
- client.Service.Run order: optional DNSServer override -> vnetController.Init/Run -> admin webServer -> loopLoginUntilSuccess (first login failure can exit when loginFailExit) -> keepControllerWorking reconnection loop with FastBackoff (3 fast retries/min then exponential to 20s). — [client/service.go#L226-306](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L226-L306)
- Client login + proxy bring-up: loopLoginUntilSuccess -> controlSessionDialer.Dial(runID) -> NewControl -> ctl.SetInWorkConnCallback -> ctl.Run -> pm.UpdateAll(proxyCfgs) sends NewProxy per proxy and vm.UpdateAll(visitorCfgs); worker starts heartbeatWorker and msgDispatcher. — [client/service.go#L308-357](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L308-L357) [client/control.go#L110-118](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/control.go#L110-L118) [client/control.go#L280-292](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/control.go#L280-L292)
- Work conn establishment on demand: server GetWorkConn drains pool else sends ReqWorkConn and waits up to UserConnTimeout; client handleReqWorkConn -> connectServer -> authenticated NewWorkConn -> server RegisterWorkConn pushes into workConnCh -> proxy.Start writes StartWorkConn back to client. — [server/control.go#L595-637](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L595-L637) [client/control.go#L124-162](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/control.go#L124-L162) [server/proxy/proxy.go#L57-62](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/proxy/proxy.go#L57-L62)
- TCP data path completion: client receives StartWorkConn -> pm.HandleWorkConn(proxyName) -> Proxy.InWorkConn -> HandleTCPWorkConnection wraps conn (limit/encrypt/compress) -> writes PROXY protocol header if configured -> dials localIP:localPort -> libio.Join for bidirectional copy. — [client/control.go#L158-162](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/control.go#L158-L162) [client/proxy/proxy.go#L160-258](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/proxy.go#L160-L258)
- Heartbeat: client heartbeatWorker sends authenticated msg.Ping every HeartbeatInterval with backoff and closes session when lastPong exceeds HeartbeatTimeout; server heartbeatWorker closes the Control when lastPing exceeds Transport.HeartbeatTimeout. — [client/control.go#L240-278](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/control.go#L240-L278) [server/control.go#L639-652](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L639-L652)
- Admin reload trace: GET /api/reload -> apiController.Reload -> Service.UpdateConfigSource (reloadMu serialized) -> cfgSource.ReplaceAll -> reloadConfigFromSourcesLocked -> aggregator.Load -> VirtualNet requirement check -> UpdateAllConfigurer -> ctl.vm.UpdateAll + ctl.pm.UpdateAll. — [client/api_router.go#L36](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/api_router.go#L36) [client/service.go#L385-412](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L385-L412) [client/service.go#L496-527](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L496-L527)

## Testing signals

Unit tests use Go's testing with testify and rodaine/table, spread through pkg/, client/, server/ and cmd/ (run via `make test`/`gotest` with the noweb tag). End-to-end tests use Ginkgo/Gomega under test/e2e with a framework that spawns real frps/frpc binaries, organized into v1/basic, v1/features, plugin, legacy and mock server helpers; `make e2e` runs hack/run-e2e.sh with 16-way parallelism. Cross-version compatibility suites run released binaries via hack/run-e2e-compatibility.sh and download scripts. The web workspace uses Vitest + @vue/test-utils; linting is via golangci-lint (.golangci.yml) plus gofmt/gofumpt/gci/vet make targets and GitHub Actions workflows.
- make gotest runs `go test -tags noweb -v --cover` over ./assets/..., ./cmd/..., ./client/..., ./server/..., ./pkg/...; make alltest adds vet and e2e. — [Makefile#L46-56](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L46-L56) [Makefile#L84](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L84)
- E2E harness: test/e2e/e2e.go RunE2ETests registers Gomega fail handler, enables EmitSpecProgress and RandomizeAllSpecs; hack/run-e2e.sh installs ginkgo v2.23.4 and runs against bin/frpc and bin/frps (overridable via FRPC_PATH/FRPS_PATH). — [test/e2e/e2e.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/test/e2e/e2e.go) [hack/run-e2e.sh](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/hack/run-e2e.sh) [Makefile#L55-59](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L55-L59)
- E2E framework provides process management, port allocation, mock servers and request helpers under test/e2e/framework and test/e2e/pkg (cert, plugin, port, process, relay, request, rpc, ssh). — [test/e2e/framework/framework.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/test/e2e/framework/framework.go) [test/e2e/framework/process.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/test/e2e/framework/process.go) [test/e2e/mock](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/test/e2e/mock)
- E2E coverage areas include basic client/server behavior, config, HTTP, tcpmux, xtcp, OIDC, token_source, wire protocol, plus features: bandwidth_limit, chaos, control_replacement, group, heartbeat, monitor, real_ip, ssh_tunnel, store. — [test/e2e/v1/basic](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/test/e2e/v1/basic) [test/e2e/v1/features](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/test/e2e/v1/features)
- Compatibility testing: make e2e-compatibility runs hack/run-e2e-compatibility.sh against FRP_COMPAT_BASELINE_COUNT released versions (floor 0.61.0); e2e-compatibility-last-frpc/last-frps download the last release into ./lastversion and run the suite with one side pinned old. — [Makefile#L5-6](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L5-L6) [Makefile#L61-82](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L61-L82) [test/e2e/compatibility](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/test/e2e/compatibility)
- Unit tests exist for config decode/load (pkg/config/v1 *_test.go, pkg/config/load_test.go), msg codecs (pkg/msg msg_test.go, wire_v2_test.go, udp_binary_test.go + benchmarks), auth OIDC (pkg/auth/oidc_test.go), source aggregator/store, client service/config_manager/control_session, server control/service/proxy, nathole analysis/discovery, vnet controller, health. — [pkg/config/load_test.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load_test.go) [pkg/msg/wire_v2_test.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/msg/wire_v2_test.go) [pkg/auth/oidc_test.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/auth/oidc_test.go) [client/service_test.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service_test.go) [server/control_test.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control_test.go) [pkg/nathole/analysis_test.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/nathole/analysis_test.go)
- cmd/frpc/sub has verify_test.go covering the verify subcommand; most other cmd code (proxy/admin/nathole subcommands) has no direct unit tests. — [cmd/frpc/sub/verify_test.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/verify_test.go)
- Web frontend testing: web/package.json defines test:unit via vitest run (vitest 4.1.10, jsdom, @vue/test-utils) across workspaces shared/frpc/frps; Makefile web-ci runs npm ci, lint:check, test:unit and builds. — [web/package.json](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/web/package.json) [Makefile#L19-20](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L19-L20)
- Lint/format toolchain: golangci-lint run configured by .golangci.yml; make fmt (go fmt), fmt-more (gofumpt), gci (import grouping with frp prefix), vet; CI workflows include golangci-lint.yml and goreleaser.yml. — [.golangci.yml](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/.golangci.yml) [Makefile#L28-38](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L28-L38) [.github/workflows/golangci-lint.yml](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/.github/workflows/golangci-lint.yml) [.github/workflows/goreleaser.yml](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/.github/workflows/goreleaser.yml)
- runMultipleClients (--config_dir) is explicitly documented as 'only designed for testing. It's not guaranteed to be stable' yet is reachable via a public flag. — [cmd/frpc/sub/root.go#L70-75](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/root.go#L70-L75)
- Ginkgo parallel runs default to CONCURRENCY=16 in run-e2e.sh and framework.RunID isolates per-node resources. — [hack/run-e2e.sh](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/hack/run-e2e.sh) [test/e2e/e2e.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/test/e2e/e2e.go)

## Risk map

The highest-risk areas are command-execution token sources (gated behind the TokenSourceExec unsafe feature), filesystem-serving and socket-exposing client plugins (static_file, unix_domain_socket which can expose docker.sock), the VirtualNet TUN setup that shells out to ifconfig/route on macOS, and admin web servers that depend on auth middleware correctness. Concurrency risk concentrates in server/control.go's ControlManager, which enforces a documented lock order (runMu -> cm.mu -> lifecycleMu -> registry) and generation handoff barriers; regressions there cause deadlocks or leaked controls. The deprecated INI path (pkg/config/legacy) and the testing-only --config_dir mode are legacy surface. Plugin content mutation uses a deliberate panic on wrong types.
- ExecSource.Resolve executes arbitrary commands (exec.CommandContext with env passthrough) to produce auth tokens; it is gated by the TokenSourceExec entry in both ClientUnsafeFeatures and ServerUnsafeFeatures requiring --allow-unsafe. — [pkg/config/v1/value_source.go#L136-160](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/v1/value_source.go#L136-L160) [pkg/policy/security/unsafe.go#L1-30](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/policy/security/unsafe.go#L1-L30)
- VirtualNet setup on Darwin shells out to ifconfig and route to configure the TUN device; tun_linux.go uses netlink. Errors here are runtime-privilege dependent. — [pkg/vnet/tun_darwin.go#L51-80](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/vnet/tun_darwin.go#L51-L80) [pkg/vnet/tun_linux.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/vnet/tun_linux.go)
- unix_domain_socket and static_file client plugins expose local filesystem paths/sockets remotely; README documents exposing /var/run/docker.sock as a TCP proxy, an intentionally dangerous capability. — [pkg/plugin/client/unix_domain_socket.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/client/unix_domain_socket.go) [pkg/plugin/client/static_file.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/client/static_file.go) [README.md#L334-385](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/README.md#L334-L385)
- Admin APIs (client /api/config PUT, /api/stop, /api/store/*; server DELETE /api/proxies, /api/v2/system/prune) are powerful mutation endpoints guarded only by helper.AuthMiddleware on the dashboard port — misconfiguration of webServer auth exposes full control. (inferred) — [client/api_router.go#L33-55](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/api_router.go#L33-L55) [server/api_router.go#L29-60](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/api_router.go#L29-L60)
- ControlManager implements a strict multi-lock protocol (never wait on runMu holding cm.mu; order runMu -> cm.mu -> ctl.lifecycleMu -> registry locks) with generation IDs and handoff barriers; the complexity concentrates deadlock/race risk in control.go. — [server/control.go#L51-105](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L51-L105) [server/control.go#L222-248](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L222-L248)
- GetWorkConn recovers from panics and the worker teardown closes pooled conns, but a full workConnCh silently discards registered work conns ('work connection pool is full, discarding'). — [server/control.go#L595-637](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L595-L637) [server/control.go#L329-337](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L329-L337)
- Plugin content mutation in handleMutableContent does retContent.(*T) with a comment 'Buggy Plugin implementations still panic here, by design' — a malicious/broken server plugin path can panic the handler goroutine. — [pkg/plugin/server/manager.go#L99-103](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/server/manager.go#L99-L103)
- Server plugins make outbound HTTP calls to user-configured addresses during login/proxy/workconn events; a plugin failure rejects the operation (errors fail closed for mutable ops like Login/NewProxy). — [pkg/plugin/server/manager.go#L90-98](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/server/manager.go#L90-L98) [pkg/plugin/server/http.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/server/http.go) [server/service.go#L461-476](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L461-L476)
- Legacy INI support is deprecated and routed through pkg/config/legacy converters; conf/legacy holds reference INI files. New features are TOML/YAML/JSON-only, so the legacy path is dead-end code that still runs when a 'common' section is detected. — [pkg/config/load.go#L65-82](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L65-L82) [pkg/config/legacy](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/legacy) [conf/legacy](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/conf/legacy) [cmd/frps/root.go#L73-76](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frps/root.go#L73-L76)
- xtcp relies on public STUN (default stun.easyvoip.com:3478) and NAT-behavior heuristics in nathole; P2P may silently fail on incompatible NATs — README itself recommends stcp fallback. — [pkg/config/v1/client.go#L89](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/v1/client.go#L89) [pkg/nathole/analysis.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/nathole/analysis.go) [README.md#L455-459](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/README.md#L455-L459)
- SSH tunnel gateway accepts native SSH connections on a dedicated port and pipes them into the control plane (InternalListener); it expands the server's attack surface to the SSH protocol. (inferred) — [server/service.go#L280-287](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L280-L287) [pkg/ssh/gateway.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/ssh/gateway.go)
- Duplicate proxy/visitor names across merged config sources are rejected at load by validateNoDuplicateNames; before this check a duplicate would have been silently dropped — indicates historically fragile merge semantics. (inferred) — [pkg/config/load.go#L424-447](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L424-L447)
- frpc tlsListener sniffs first bytes to distinguish frp TLS from HTTPS; misclassification risk exists when vhostHTTPS shares the bind port (comment notes 0x16 ambiguity). (inferred) — [server/service.go#L352-355](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L352-L355)

## Extension points

frp is built around registries. New proxy types plug in through three coordinated tables: proxyConfigTypeMap in pkg/config/v1, server-side RegisterProxyFactory (init() per type in server/proxy), and client-side RegisterProxyFactory (init() in client/proxy). Client connection-handling plugins register via pkg/plugin/client's Register/Create; server hooks come from external HTTP plugin processes managed by pkg/plugin/server. frpc CLI subcommands are generated automatically from the proxy type list. Config extensibility goes through source.Source implementations aggregated by source.Aggregator, and embedding is supported via pkg/virtual.Client plus ServiceOptions hooks (ConnectorCreator, HandleWorkConnCb). Feature gates (pkg/policy/featuregate) guard experimental features.
- server/proxy uses a reflect.Type-keyed factory registry: each type file (tcp.go, udp.go, http.go, https.go, tcpmux.go, stcp.go, sudp.go, xtcp.go) calls RegisterProxyFactory in init(); adding a server proxy type means adding a config type + factory init. — [server/proxy/proxy.go#L43-47](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/proxy/proxy.go#L43-L47) [server/proxy/tcp.go#L26-28](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/proxy/tcp.go#L26-L28)
- client/proxy mirrors the pattern: RegisterProxyFactory keyed by configurer reflect.Type, invoked from NewProxy which first builds a BaseProxy with limiter/encryption key/msgTransporter. — [client/proxy/proxy.go#L42-89](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/proxy.go#L42-L89) [client/proxy/general_tcp.go#L23](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/general_tcp.go#L23) [client/proxy/xtcp.go#L36](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/xtcp.go#L36)
- Client plugins register a CreatorFn by name via plugin.Register (panics on duplicate) and are instantiated lazily in BaseProxy.Run when baseCfg.Plugin.Type is set; the Plugin interface is Handle(ctx, *ConnectionInfo) + Close. — [pkg/plugin/client/plugin.go#L30-55](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/client/plugin.go#L30-L55) [client/proxy/proxy.go#L109-121](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/proxy.go#L109-L121)
- Server plugin ops are an open enum: Manager.Register bins each Plugin by IsSupport(op) into per-op slices for Login, NewProxy, CloseProxy, Ping, NewWorkConn, NewUserConn; new ops require extending the manager dispatch. — [pkg/plugin/server/manager.go#L27-44](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/server/manager.go#L27-L44) [pkg/plugin/server/manager.go#L108-127](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/server/manager.go#L108-L127)
- frpc single-proxy subcommands are generated in a loop over proxyTypes, so a new type automatically gains a CLI command and (for stcp/sudp/xtcp) a nested visitor command. — [cmd/frpc/sub/proxy.go#L45-87](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/proxy.go#L45-L87)
- Config sources are pluggable: source.Aggregator merges a ConfigSource (in-memory/file) with an optional StoreSource; Service consumes aggregator.Load() at boot and on reload, so new persistence backends slot in as Source implementations. — [pkg/config/source/aggregator.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/source/aggregator.go) [pkg/config/source/source.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/source/source.go) [cmd/frpc/sub/root.go#L137-168](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/root.go#L137-L168)
- Embedding hooks: client.ServiceOptions exposes ConnectorCreator (custom transports/virtual clients) and HandleWorkConnCb (intercept work conns); pkg/virtual.Client demonstrates running frpc against an in-process InternalListener. — [client/service.go#L83-96](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L83-L96) [pkg/virtual/client.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/virtual/client.go)
- pkg/sdk/client provides a programmatic client API for running proxies from Go code rather than config files. — [pkg/sdk/client](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/sdk/client)
- Admin routes are registered centrally in each Service's registerRouteHandlers, so new endpoints slot into the gorilla/mux subRouter (with auth middleware already applied); server has a separate encoded-path subrouter for /api/v2 names. — [server/api_router.go#L27-71](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/api_router.go#L27-L71) [client/api_router.go#L26-64](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/api_router.go#L26-L64)
- Feature gates: ClientCommonConfig.FeatureGates map plus pkg/policy/featuregate control rollout of features like VirtualNet; reload path validates gate-dependent config against runtime capabilities. — [pkg/config/v1/client.go#L68-70](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/v1/client.go#L68-L70) [pkg/policy/featuregate/feature_gate.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/policy/featuregate/feature_gate.go) [client/service.go#L514-520](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L514-L520)
- vhost.Routers + vhost.NewHTTPReverseProxy/NewHTTPSMuxer are the extension seam for HTTP(S) routing behavior; group controllers (TCP/HTTP/HTTPS/TCPMux) encapsulate shared-port load balancing. — [server/service.go#L296-349](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L296-L349) [pkg/util/vhost](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/util/vhost) [server/group](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/group)
- Message/protocol extension: msg.Dispatcher.RegisterHandler maps message structs to handlers on both sides; adding a control message means defining it in pkg/msg and registering handlers in client/control.go and server/control.go. — [client/control.go#L232-237](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/control.go#L232-L237) [server/control.go#L727-734](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L727-L734) [pkg/msg/msg.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/msg/msg.go)

## Contributor handoff

Start with AGENTS.md for the command surface: `make build|frps|frpc|test|e2e|fmt|fmt-more|gci|vet`, `golangcici-lint run`, `make web`. Read cmd/frps/root.go + cmd/frpc/sub/root.go, then server/service.go and client/service.go for wiring, and server/control.go for the lifecycle/locking protocol before touching session code. Config changes must go through pkg/config/v1 types + validation and be tested for TOML/YAML/JSON and strict mode. Protocol changes must update pkg/msg on both sides and consider the v2 wire negotiation in pkg/proto/wire. E2E tests need built binaries and Ginkgo; unit tests run with the noweb tag when dashboards aren't built.
- AGENTS.md documents the canonical commands: make build/frps/frpc/test/e2e/e2e-trace/alltest/fmt/fmt-more/gci/vet, make web for dashboards, and points to doc/agents/ runbooks (release.md). — [AGENTS.md](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/AGENTS.md)
- First files to read: cmd/frps/root.go and cmd/frpc/sub/root.go (entrypoints), server/service.go and client/service.go (composition roots), server/control.go (session lifecycle/locking), pkg/config/v1 (config schema), pkg/msg/msg.go (protocol). — [cmd/frps/root.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frps/root.go) [cmd/frpc/sub/root.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/root.go) [server/service.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go) [client/service.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go) [server/control.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go) [pkg/msg/msg.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/msg/msg.go)
- Build tags matter: 'frps'/'frpc' select per-binary code and 'noweb' is auto-added when web/*/dist is missing — run go test/vet with -tags noweb or build dashboards first. — [Makefile#L4](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L4) [Makefile#L40-53](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L40-L53)
- When editing server/control.go, respect the documented global lock order (runMu, cm.mu, ctl.lifecycleMu, then registry locks) and the pending/activated/running state machine — generation handoff uses barriers between replaced controls. — [server/control.go#L51-105](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L51-L105) [server/control.go#L378-435](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L378-L435)
- Config edits require updating v1 types, Complete() defaults, validation.Validate* and ideally e2e coverage; strict_config=true by default rejects unknown fields, so new fields break older parsers cross-version. — [cmd/frps/root.go#L46](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frps/root.go#L46) [pkg/config/load.go#L176-217](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/load.go#L176-L217) [pkg/config/v1/validation](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/v1/validation)
- Client config now flows through source.Aggregator (ConfigSource + StoreSource); bypassing it breaks /api/reload and store CRUD. Keep Service.reloadMu semantics when touching UpdateConfigSource. — [cmd/frpc/sub/root.go#L137-183](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/root.go#L137-L183) [client/service.go#L385-412](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L385-L412) [pkg/config/source/aggregator.go](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/source/aggregator.go)
- Run `go test -tags noweb ./pkg/... ./client/... ./server/...` for unit tests; `make e2e` requires bin/frps+bin/frpc built first and ginkgo installed (script auto-installs). — [Makefile#L46-56](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L46-L56) [hack/run-e2e.sh](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/hack/run-e2e.sh)
- Don't add proxy types piecemeal: a type needs a configurer in proxyConfigTypeMap, marshal/unmarshal to msg, server factory init, client factory init, validation, and usually e2e — the CLI subcommand is then auto-generated. — [pkg/config/v1/proxy.go#L242-261](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/config/v1/proxy.go#L242-L261) [server/proxy/tcp.go#L26-28](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/proxy/tcp.go#L26-L28) [client/proxy/general_tcp.go#L23](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/proxy/general_tcp.go#L23) [cmd/frpc/sub/proxy.go#L45-87](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/cmd/frpc/sub/proxy.go#L45-L87)
- Auth token bytes double as the work-conn encryption key (ClientAuth/ServerAuth key = []byte(resolved.Token)) — changing token handling affects transport encryption. — [pkg/auth/auth.go#L58-61](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/auth/auth.go#L58-L61) [pkg/auth/auth.go#L112-115](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/auth/auth.go#L112-L115) [server/service.go#L468](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/service.go#L468)
- Version pinning/compat is enforced by tests: FRP_COMPAT_FLOOR_VERSION=0.61.0 and baseline-count e2e runs mean wire/protocol changes must remain compatible across ~8 released versions. — [Makefile#L5-6](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L5-L6) [Makefile#L61-68](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/Makefile#L61-L68)
- Logging uses pkg/util/log and xlog (context-scoped, prefixed by runID); follow existing xl.Debugf/Tracef patterns rather than fmt.Println in service code (entrypoints print user-facing errors via fmt). — [client/service.go#L330-332](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/client/service.go#L330-L332) [server/control.go#L331-334](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/control.go#L331-L334) [pkg/util/log](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/util/log)
- Panic-on-duplicate registries (plugin.Register, proxy factories) run in init() — import cycles or double registration crash at startup, so new plugins/types must use unique names/types. — [pkg/plugin/client/plugin.go#L33-39](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/pkg/plugin/client/plugin.go#L33-L39) [server/proxy/proxy.go#L43-47](https://github.com/fatedier/frp/blob/d20a232996007dfe6ab425abc0a39a3ae9a0889b/server/proxy/proxy.go#L43-L47)

Source repository: https://github.com/fatedier/frp
Full evidence corpus: https://github.com/Codevetter/codevetter/tree/main/benchmarks/repo-unpacks/frp.json

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
