Compare commits

...

10 Commits

319 changed files with 21623 additions and 1900 deletions
+7
View File
@@ -8,6 +8,7 @@ _obj
_test
*.exe
*.msi
*.test
*.prof
@@ -18,7 +19,13 @@ release/
test/bin/
vendor/
lastversion/
.cache/
dist/
# Generated at runtime
admin.db
/frps
/frpc
.idea/
.vscode/
.autogen_ssh_key
+57 -28
View File
@@ -1,39 +1,68 @@
# AGENTS.md
## Development Commands
## Project
### Build
- `make build` - Build both frps and frpc binaries
- `make frps` - Build server binary only
- `make frpc` - Build client binary only
- `make all` - Build everything with formatting
`github.com/fatedier/frp` — fast reverse proxy. Go 1.25. Two binaries:
- **frps** (server): `cmd/frps/main.go``server/`
- **frpc** (client): `cmd/frpc/main.go``client/`
### Testing
- `make test` - Run unit tests
- `make e2e` - Run end-to-end tests
- `make e2e-trace` - Run e2e tests with trace logging
- `make alltest` - Run all tests including vet, unit tests, and e2e
## Build & Dev Commands
### Code Quality
- `make fmt` - Run go fmt
- `make fmt-more` - Run gofumpt for more strict formatting
- `make gci` - Run gci import organizer
- `make vet` - Run go vet
- `golangci-lint run` - Run comprehensive linting (configured in .golangci.yml)
| Command | What |
|---------|------|
| `make build` | Build both binaries to `bin/` |
| `make frps` / `make frpc` | Single binary |
| `make all` | `fmt → web → build` |
| `make web` | Build web dashboards (`web/frps/`, `web/frpc/`) |
| `make clean` | Remove `bin/`, `lastversion/`, `.cache/`, `.compat/` |
### Assets
- `make web` - Build web dashboards (frps and frpc)
Build tags: `frps`, `frpc`, and `noweb` (auto-added when web assets missing).
CGO disabled, `-trimpath`, stripped symbols.
### Cleanup
- `make clean` - Remove built binaries and temporary files
## Test
## Testing
| Command | What |
|---------|------|
| `make test` / `make gotest` | Unit tests with `--cover` per package |
| `make e2e` | E2E via ginkgo (parallel, 16 nodes) |
| `make e2e-trace` | Same with `LOG_LEVEL=trace` |
| `make alltest` | `vet → gotest → e2e` (CI pipeline) |
- E2E tests using Ginkgo/Gomega framework
- Mock servers in `/test/e2e/mock/`
- Run: `make e2e` or `make alltest`
- E2E uses Ginkgo/Gomega. Requires `ginkgo` binary (auto-installed by `hack/run-e2e.sh`).
- Binary paths: `FRPC_PATH` / `FRPS_PATH` env vars (default `bin/frpc`, `bin/frps`).
- E2E mock servers in `test/e2e/mock/`.
- Compatibility e2e: `make e2e-compatibility`, `make e2e-compatibility-smoke`, `make e2e-compatibility-floor`. Controls: `FRP_COMPAT_BASELINE_COUNT` (default 8), `FRP_COMPAT_FLOOR_VERSION` (default 0.61.0).
- CI: CircleCI runs `make alltest` after building web assets.
## Agent Runbooks
## Code Quality
Operational procedures for agents are in `doc/agents/`:
- `doc/agents/release.md` - Release process
| Command | What |
|---------|------|
| `make fmt` | `go fmt ./...` |
| `make fmt-more` | `gofumpt` |
| `make gci` | Import organizer: standard → default → `github.com/fatedier/frp/` |
| `make vet` | `go vet` with `noweb` build tag |
| `golangci-lint run` | Full lint (v2 config, `.golangci.yml`) |
## Architecture
- **Config**: TOML, YAML, or JSON. INI format is **deprecated** (v0.52.0+). Default: `./frpc.ini`.
- **DB-first config**: frps stores config in SQLite by default. Config file is optional (`-c` flag bypasses DB).
- **First-run flow**: No config file + no DB config → auto-saves defaults (bind 0.0.0.0:7000, web 0.0.0.0:7500) and redirects admin UI to setup wizard.
- **Flags**: `--config` / `-c`, `--strict_config` (default true), `--allow-unsafe`.
- **Feature gates**: Enabled via `featureGates = { VirtualNet = true }`.
- **Store**: `store.path` enables dynamic proxy CRUD at runtime (persisted to disk, takes precedence over static config on name conflict).
- **Config aggregation**: Static file + includes + store. `start` acts as global allowlist (not recommended for new configs; prefer per-proxy `enabled`).
- **frpc subcommands**: `frpc reload`, `frpc verify`, `frpc status`, `frpc tcp` (direct CLI proxy creation).
- **Multi-client mode**: `--config_dir` runs one frpc per config file (testing only, not stable).
- **Admin DB**: SQLite via `ent` ORM at `pkg/db/`. Schema: User, FrpcClient, Proxy, ServerConfig. Auto-creates `admin.db` and seeds default admin user (`admin`/`admin123`).
- **Admin UI**: htmx-based dashboard at `/admin/` on frps web server port. Login required, session cookie auth. Pages: Dashboard, Clients, Proxies, Settings.
- **Server Config via UI**: All frps settings editable from Settings page. First-run redirects to setup wizard. Save triggers restart prompt.
## Release
See `doc/agents/release.md` for the full process. Key steps:
- Bump version in `pkg/util/version/version.go`
- `make e2e` + compatibility tests
- Merge `dev``master` with merge commit (not squash)
- Tag `vX.Y.Z` and trigger `goreleaser` workflow manually via `gh`
+39 -25
View File
@@ -1,23 +1,26 @@
export PATH := $(PATH):`go env GOPATH`/bin
export GO111MODULE=on
LDFLAGS := -s -w
NOWEB_TAG = $(shell [ ! -d web/frps/dist ] || [ ! -d web/frpc/dist ] && echo ',noweb')
NOWEB_TAG = $(shell [ ! -d web/kanhole/dist ] || [ ! -d web/kanholec/dist ] && echo ',noweb')
KANHOLE_VERSION ?= 0.1.0
FRP_COMPAT_BASELINE_COUNT ?= 8
FRP_COMPAT_FLOOR_VERSION ?= 0.61.0
.PHONY: web frps-web frpc-web frps frpc
.PHONY: web kanhole-web kanholec-web kanhole kanholec kanholec-gui e2e-compatibility-smoke e2e-compatibility e2e-compatibility-floor
all: env fmt web build
build: frps frpc
build: kanhole kanholec
env:
@go version
web: frps-web frpc-web
web: kanhole-web kanholec-web
frps-web:
kanhole-web:
$(MAKE) -C web/frps build
frpc-web:
kanholec-web:
$(MAKE) -C web/frpc build
fmt:
@@ -27,16 +30,30 @@ fmt-more:
gofumpt -l -w .
gci:
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
gci write -s standard -s default -s "prefix(kanhole/)" ./
vet:
go vet -tags "$(NOWEB_TAG)" ./...
frps:
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frps$(NOWEB_TAG)" -o bin/frps ./cmd/frps
kanhole:
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "kanhole$(NOWEB_TAG)" -o bin/kanhole ./cmd/kanhole
frpc:
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frpc$(NOWEB_TAG)" -o bin/frpc ./cmd/frpc
kanholec:
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "kanholec$(NOWEB_TAG)" -o bin/kanholec ./cmd/kanholec
kanholec-windows:
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -tags "kanholec$(NOWEB_TAG)" -o bin/kanholec-windows-amd64.exe ./cmd/kanholec
kanholec-windows-arm64:
env CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -trimpath -ldflags "$(LDFLAGS)" -tags "kanholec$(NOWEB_TAG)" -o bin/kanholec-windows-arm64.exe ./cmd/kanholec
kanholec-windows-msi: kanholec-windows
wixl -o bin/kanholec-$(KANHOLE_VERSION).msi packaging/windows/kanholec.wxs 2>/dev/null \
|| { echo "wixl failed. Try: apt install msitools"; \
echo "Or on Windows: candle kanholec.wxs -o kanholec.wixobj && light kanholec.wixobj -o kanholec.msi"; \
exit 1; }
.PHONY: kanholec-windows kanholec-windows-arm64 kanholec-windows-msi
test: gotest
@@ -53,23 +70,20 @@ e2e:
e2e-trace:
DEBUG=true LOG_LEVEL=trace ./hack/run-e2e.sh
e2e-compatibility-last-frpc:
if [ ! -d "./lastversion" ]; then \
TARGET_DIRNAME=lastversion ./hack/download.sh; \
fi
FRPC_PATH="`pwd`/lastversion/frpc" ./hack/run-e2e.sh
rm -r ./lastversion
e2e-compatibility-smoke: build
FRP_COMPAT_BASELINE_COUNT=1 ./hack/run-e2e-compatibility.sh
e2e-compatibility-last-frps:
if [ ! -d "./lastversion" ]; then \
TARGET_DIRNAME=lastversion ./hack/download.sh; \
fi
FRPS_PATH="`pwd`/lastversion/frps" ./hack/run-e2e.sh
rm -r ./lastversion
e2e-compatibility: build
FRP_COMPAT_BASELINE_COUNT="$(FRP_COMPAT_BASELINE_COUNT)" ./hack/run-e2e-compatibility.sh
e2e-compatibility-floor: build
FRP_COMPAT_BASELINE_VERSIONS="$(FRP_COMPAT_FLOOR_VERSION)" ./hack/run-e2e-compatibility.sh
alltest: vet gotest e2e
clean:
rm -f ./bin/frpc
rm -f ./bin/frps
rm -f ./bin/kanhole
rm -f ./bin/kanholec
rm -rf ./lastversion
rm -rf ./.cache
rm -rf ./.compat
+8 -18
View File
@@ -13,6 +13,14 @@ frp is an open source project with its ongoing development made possible entirel
<h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start-->
<p align="center">
<a href="https://jb.gg/frp" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
<br>
<b>The complete IDE crafted for professional Go developers</b>
</a>
</p>
<p align="center">
<a href="https://github.com/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
@@ -32,24 +40,6 @@ If you're looking for a meeting recording API, consider checking out [Recall.ai]
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
</div>
<p align="center">
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
<br>
<b>Requestly - Free & Open-Source alternative to Postman</b>
<br>
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
</a>
</p>
<p align="center">
<a href="https://jb.gg/frp" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
<br>
<b>The complete IDE crafted for professional Go developers</b>
</a>
</p>
<!--gold sponsors end-->
## What is frp?
+8 -18
View File
@@ -15,6 +15,14 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
<h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start-->
<p align="center">
<a href="https://jb.gg/frp" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
<br>
<b>The complete IDE crafted for professional Go developers</b>
</a>
</p>
<p align="center">
<a href="https://github.com/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
@@ -34,24 +42,6 @@ If you're looking for a meeting recording API, consider checking out [Recall.ai]
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
</div>
<p align="center">
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
<br>
<b>Requestly - Free & Open-Source alternative to Postman</b>
<br>
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
</a>
</p>
<p align="center">
<a href="https://jb.gg/frp" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
<br>
<b>The complete IDE crafted for professional Go developers</b>
</a>
</p>
<!--gold sponsors end-->
## 为什么使用 frp
+8 -16
View File
@@ -1,18 +1,10 @@
## Compatibility Policy
Starting with v0.69.0, each minor release is supported until there are nine newer minor releases. For example, v0.69.0 will be supported until v0.78.0 is released. Within this window, frpc v0.69.0 is guaranteed to work with any frps from v0.61.0 to v0.77.0, and vice versa. Patch releases within the same minor are always compatible. Versions outside the support window may continue to work on a best-effort basis, but compatibility is no longer guaranteed.
For mixed-version deployments, upgrade frps first, then upgrade frpc. This keeps the server side ready for newer client-side protocol behavior before clients start using it.
## Notes
This release introduces wire protocol v2 as a transition path for future frpc/frps protocol changes. The existing wire protocol is difficult to extend without compatibility risk, and upcoming changes, including replacing deprecated stream encryption methods, require a versioned protocol.
**The default value of `transport.wireProtocol` remains `v1` in this release.** Users can keep the default for now. To test v2 early, upgrade both frpc and frps to versions that support it, then set `transport.wireProtocol = "v2"` in frpc. A v2-enabled frpc cannot connect to an older frps.
v1 will be deprecated when v2 becomes the default in a future release. It will continue to be supported until v0.78.0 is released, and may be removed in v0.78.0 or later.
## Features
* Added `transport.wireProtocol` for frpc to select the internal message protocol used between frpc and frps. Supported values are `v1` and `v2`.
* Added client protocol visibility in the frps dashboard and `/api/clients` API. Online clients now report their negotiated protocol as `v1` or `v2`.
* When `transport.wireProtocol = "v2"` is enabled, ordinary UDP proxy work connection payloads now use wire protocol v2 message framing. This keeps UDP message payloads aligned with the negotiated frpc/frps wire protocol.
## Compatibility Notes
* The default/empty `transport.wireProtocol` and `transport.wireProtocol = "v1"` continue to use the legacy message codec for ordinary UDP proxy payloads.
* Raw stream proxy paths such as TCP, HTTP, and STCP remain unframed and are not affected by the UDP payload framing change.
* SUDP and XTCP keep their existing legacy behavior in this release and will be considered separately in a future phase.
* `transport.wireProtocol = "v2"` requires both frpc and frps to use versions that support the same wire v2 semantics. Mixing a newer peer that sends v2-framed ordinary UDP payloads with an older v2-capable peer that still expects the legacy UDP payload codec can break ordinary UDP proxy traffic.
+4 -4
View File
@@ -17,10 +17,10 @@ package client
import (
"net/http"
adminapi "github.com/fatedier/frp/client/http"
"github.com/fatedier/frp/client/proxy"
httppkg "github.com/fatedier/frp/pkg/util/http"
netpkg "github.com/fatedier/frp/pkg/util/net"
adminapi "kanhole/client/http"
"kanhole/client/proxy"
httppkg "kanhole/pkg/util/http"
netpkg "kanhole/pkg/util/net"
)
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
+7 -7
View File
@@ -6,13 +6,13 @@ import (
"os"
"time"
"github.com/fatedier/frp/client/configmgmt"
"github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/source"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/util/log"
"kanhole/client/configmgmt"
"kanhole/client/proxy"
"kanhole/pkg/config"
"kanhole/pkg/config/source"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/config/v1/validation"
"kanhole/pkg/util/log"
)
type serviceConfigManager struct {
+3 -3
View File
@@ -5,9 +5,9 @@ import (
"path/filepath"
"testing"
"github.com/fatedier/frp/client/configmgmt"
"github.com/fatedier/frp/pkg/config/source"
v1 "github.com/fatedier/frp/pkg/config/v1"
"kanhole/client/configmgmt"
"kanhole/pkg/config/source"
v1 "kanhole/pkg/config/v1"
)
func newTestRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"errors"
"time"
"github.com/fatedier/frp/client/proxy"
v1 "github.com/fatedier/frp/pkg/config/v1"
"kanhole/client/proxy"
v1 "kanhole/pkg/config/v1"
)
var (
+6 -6
View File
@@ -28,12 +28,12 @@ import (
quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/wire"
"github.com/fatedier/frp/pkg/transport"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/proto/wire"
"kanhole/pkg/transport"
netpkg "kanhole/pkg/util/net"
"kanhole/pkg/util/xlog"
)
// Connector is an interface for establishing connections to the server.
+10 -10
View File
@@ -20,16 +20,16 @@ import (
"sync/atomic"
"time"
"github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/client/visitor"
"github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/naming"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
"kanhole/client/proxy"
"kanhole/client/visitor"
"kanhole/pkg/auth"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/naming"
"kanhole/pkg/transport"
"kanhole/pkg/util/wait"
"kanhole/pkg/util/xlog"
"kanhole/pkg/vnet"
)
type SessionContext struct {
+62 -14
View File
@@ -26,13 +26,13 @@ import (
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/wire"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/vnet"
"kanhole/pkg/auth"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/proto/wire"
netpkg "kanhole/pkg/util/net"
"kanhole/pkg/util/version"
"kanhole/pkg/vnet"
)
type controlSessionDialer struct {
@@ -74,17 +74,18 @@ func (d *controlSessionDialer) Dial(previousRunID string) (*SessionContext, erro
return nil, err
}
loginRespMsg, err := d.exchangeLogin(conn, loginMsg)
loginResult, err := d.exchangeLogin(conn, loginMsg)
if err != nil {
return nil, err
}
loginRespMsg := loginResult.resp
if loginRespMsg.Error != "" {
return nil, errors.New(loginRespMsg.Error)
}
var controlRW io.ReadWriter = conn
if d.clientSpec == nil || d.clientSpec.Type != "ssh-tunnel" {
controlRW, err = netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey())
controlRW, err = d.newControlReadWriter(conn, loginResult.crypto)
if err != nil {
return nil, fmt.Errorf("create control crypto read writer: %w", err)
}
@@ -125,9 +126,16 @@ func (d *controlSessionDialer) buildLoginMsg(previousRunID string) (*msg.Login,
return loginMsg, nil
}
func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*msg.LoginResp, error) {
type loginExchangeResult struct {
resp *msg.LoginResp
crypto *wire.CryptoContext
}
func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*loginExchangeResult, error) {
rw := msg.NewV1ReadWriter(conn)
var wireConn *wire.Conn
var clientHello wire.ClientHello
var clientHelloPayload []byte
if d.common.Transport.WireProtocol == wire.ProtocolV2 {
if err := wire.WriteMagic(conn); err != nil {
@@ -136,14 +144,23 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login)
wireConn = wire.NewConn(conn)
rw = msg.NewV2ReadWriterWithConn(wireConn)
hello := wire.DefaultClientHello(wire.BootstrapInfo{
var err error
clientHello, err = wire.NewClientHello(wire.BootstrapInfo{
Transport: d.common.Transport.Protocol,
TLS: lo.FromPtr(d.common.Transport.TLS.Enable) || d.common.Transport.Protocol == "wss" || d.common.Transport.Protocol == "quic",
TCPMux: lo.FromPtr(d.common.Transport.TCPMux),
})
if err := wireConn.WriteJSONFrame(wire.FrameTypeClientHello, hello); err != nil {
if err != nil {
return nil, err
}
clientHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeClientHello, clientHello)
if err != nil {
return nil, err
}
if err := wireConn.WriteFrame(clientHelloFrame); err != nil {
return nil, err
}
clientHelloPayload = clientHelloFrame.Payload
}
if err := rw.WriteMsg(loginMsg); err != nil {
return nil, err
@@ -154,19 +171,50 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login)
_ = conn.SetReadDeadline(time.Time{})
}()
var cryptoContext *wire.CryptoContext
if wireConn != nil {
serverHelloFrame, err := wireConn.ReadFrame()
if err != nil {
return nil, err
}
if serverHelloFrame.Type != wire.FrameTypeServerHello {
return nil, fmt.Errorf("unexpected frame type %d, want %d", serverHelloFrame.Type, wire.FrameTypeServerHello)
}
var serverHello wire.ServerHello
if err := wireConn.ReadJSONFrame(wire.FrameTypeServerHello, &serverHello); err != nil {
if err := wireConn.UnmarshalFrame(serverHelloFrame, &serverHello); err != nil {
return nil, err
}
if serverHello.Error != "" {
return nil, errors.New(serverHello.Error)
}
cryptoContext, err = wire.NewClientCryptoContext(clientHelloPayload, serverHelloFrame.Payload)
if err != nil {
return nil, err
}
}
var loginRespMsg msg.LoginResp
if err := rw.ReadMsgInto(&loginRespMsg); err != nil {
return nil, err
}
return &loginRespMsg, nil
return &loginExchangeResult{
resp: &loginRespMsg,
crypto: cryptoContext,
}, nil
}
func (d *controlSessionDialer) newControlReadWriter(conn net.Conn, cryptoContext *wire.CryptoContext) (io.ReadWriter, error) {
if d.common.Transport.WireProtocol == wire.ProtocolV2 {
if cryptoContext == nil {
return nil, errors.New("missing v2 crypto negotiation")
}
return netpkg.NewAEADCryptoReadWriter(
conn,
d.auth.EncryptionKey(),
netpkg.AEADCryptoRoleClient,
cryptoContext.Algorithm,
cryptoContext.TranscriptHash,
)
}
return netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey())
}
+59 -7
View File
@@ -25,10 +25,11 @@ import (
"github.com/stretchr/testify/require"
"github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/wire"
"kanhole/pkg/auth"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/proto/wire"
netpkg "kanhole/pkg/util/net"
)
type testConnector struct {
@@ -140,8 +141,17 @@ func TestControlSessionDialerDialV2(t *testing.T) {
}
wireConn := wire.NewConn(serverRaw)
clientHelloFrame, err := wireConn.ReadFrame()
if err != nil {
serverErrCh <- err
return
}
if clientHelloFrame.Type != wire.FrameTypeClientHello {
serverErrCh <- fmt.Errorf("unexpected frame type %d, want %d", clientHelloFrame.Type, wire.FrameTypeClientHello)
return
}
var hello wire.ClientHello
if err := wireConn.ReadJSONFrame(wire.FrameTypeClientHello, &hello); err != nil {
if err := wireConn.UnmarshalFrame(clientHelloFrame, &hello); err != nil {
serverErrCh <- err
return
}
@@ -160,11 +170,52 @@ func TestControlSessionDialerDialV2(t *testing.T) {
serverErrCh <- fmt.Errorf("unexpected user: %s", loginMsg.User)
return
}
if err := wireConn.WriteJSONFrame(wire.FrameTypeServerHello, wire.DefaultServerHello()); err != nil {
serverHello, err := wire.NewServerHello(hello)
if err != nil {
serverErrCh <- err
return
}
serverErrCh <- rw.WriteMsg(&msg.LoginResp{RunID: "run-v2"})
serverHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeServerHello, serverHello)
if err != nil {
serverErrCh <- err
return
}
cryptoContext := wire.NewCryptoContext(
serverHello.Selected.Crypto.Algorithm,
clientHelloFrame.Payload,
serverHelloFrame.Payload,
)
if err := wireConn.WriteFrame(serverHelloFrame); err != nil {
serverErrCh <- err
return
}
if err := rw.WriteMsg(&msg.LoginResp{RunID: "run-v2"}); err != nil {
serverErrCh <- err
return
}
controlRW, err := netpkg.NewAEADCryptoReadWriter(
serverRaw,
[]byte("token"),
netpkg.AEADCryptoRoleServer,
cryptoContext.Algorithm,
cryptoContext.TranscriptHash,
)
if err != nil {
serverErrCh <- err
return
}
controlMsgRW := msg.NewReadWriter(controlRW, wire.ProtocolV2)
var ping msg.Ping
if err := controlMsgRW.ReadMsgInto(&ping); err != nil {
serverErrCh <- err
return
}
if ping.PrivilegeKey != "v2-ping" || ping.Timestamp != 12345 {
serverErrCh <- fmt.Errorf("unexpected ping: %+v", ping)
return
}
serverErrCh <- nil
}()
dialer := newTestControlSessionDialer(t, wire.ProtocolV2, connector, nil)
@@ -177,6 +228,7 @@ func TestControlSessionDialerDialV2(t *testing.T) {
require.NotNil(t, sessionCtx.Conn)
require.NotNil(t, sessionCtx.Connector)
require.False(t, connector.closed.Load())
require.NoError(t, sessionCtx.Conn.WriteMsg(&msg.Ping{PrivilegeKey: "v2-ping", Timestamp: 12345}))
require.NoError(t, <-serverErrCh)
}
+1 -1
View File
@@ -3,7 +3,7 @@ package event
import (
"errors"
"github.com/fatedier/frp/pkg/msg"
"kanhole/pkg/msg"
)
var ErrPayloadType = errors.New("error payload type")
+254
View File
@@ -0,0 +1,254 @@
//go:build kanholec_gui
package gui
import (
"fmt"
"io"
"net/http"
"strings"
"sync"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"kanhole/pkg/config"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/util/log"
)
type App struct {
fyneApp fyne.App
window fyne.Window
serverURL *widget.Entry
authToken *widget.Entry
clientKey *widget.Entry
statusLabel *widget.Label
configView *widget.Entry
proxyList *widget.List
startBtn *widget.Button
stopBtn *widget.Button
connectBtn *widget.Button
mu sync.Mutex
running bool
configData string
}
func New() *App {
a := &App{}
a.fyneApp = app.New()
a.window = a.fyneApp.NewWindow("kanholec GUI")
a.window.Resize(fyne.NewSize(850, 620))
a.window.SetMaster()
a.setupUI()
return a
}
func (a *App) saveSettings() {
p := a.fyneApp.Preferences()
p.SetString("server_url", a.serverURL.Text)
}
func (a *App) loadSettings() {
p := a.fyneApp.Preferences()
if url := p.StringWithFallback("server_url", "http://localhost:7500"); url != "" {
a.serverURL.SetText(url)
}
}
func (a *App) setupUI() {
a.serverURL = widget.NewEntry()
a.serverURL.SetText("http://localhost:7500")
a.serverURL.PlaceHolder = "http://frps-server:7500"
a.authToken = widget.NewEntry()
a.authToken.PlaceHolder = "One-time token"
a.clientKey = widget.NewEntry()
a.clientKey.PlaceHolder = "Client key (from server)"
a.statusLabel = widget.NewLabel("Not connected")
a.statusLabel.Importance = widget.MediumImportance
a.configView = widget.NewMultiLineEntry()
a.configView.SetMinRowsVisible(8)
a.configView.Disable()
a.proxyList = widget.NewList(
func() int { return 0 },
func() fyne.CanvasObject { return widget.NewLabel("") },
func(id widget.ListItemID, o fyne.CanvasObject) {},
)
a.connectBtn = widget.NewButtonWithIcon("Connect", theme.NavigateNextIcon(), a.onConnect)
a.startBtn = widget.NewButtonWithIcon("Start", theme.MediaPlayIcon(), a.onStart)
a.startBtn.Disable()
a.stopBtn = widget.NewButtonWithIcon("Stop", theme.MediaStopIcon(), a.onStop)
a.stopBtn.Disable()
// Settings form
settingsForm := widget.NewForm(
widget.NewFormItem("Server URL", a.serverURL),
)
settingsForm.OnSubmit = func() { a.saveSettings() }
settingsBtn := widget.NewButtonWithIcon("Save Settings", theme.DocumentSaveIcon(), a.saveSettings)
settingsTab := container.NewVBox(
widget.NewLabelWithStyle("Connection Settings", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
settingsForm,
settingsBtn,
layout.NewSpacer(),
)
// Auth form
authForm := widget.NewForm(
widget.NewFormItem("Server URL", a.serverURL),
widget.NewFormItem("One-Time Token", a.authToken),
widget.NewFormItem("Client Key", a.clientKey),
)
statusLine := container.NewHBox(
widget.NewLabelWithStyle("Status:", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
a.statusLabel,
layout.NewSpacer(),
a.connectBtn,
)
controls := container.NewHBox(
a.startBtn,
a.stopBtn,
)
connectTab := container.NewVBox(
widget.NewLabelWithStyle("frp Client GUI", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
authForm,
statusLine,
controls,
container.NewBorder(
widget.NewLabelWithStyle("Proxies", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
nil, nil, nil,
a.proxyList,
),
)
// Config tab
configTab := container.NewBorder(
widget.NewLabelWithStyle("Configuration (read-only)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
nil, nil, nil,
a.configView,
)
tabs := container.NewAppTabs(
container.NewTabItemWithIcon("Connect", theme.ComputerIcon(), connectTab),
container.NewTabItemWithIcon("Config", theme.DocumentIcon(), configTab),
container.NewTabItemWithIcon("Settings", theme.SettingsIcon(), settingsTab),
)
tabs.SelectIndex(0)
a.window.SetContent(tabs)
a.loadSettings()
}
func (a *App) onConnect() {
url := strings.TrimRight(a.serverURL.Text, "/")
token := a.authToken.Text
key := a.clientKey.Text
a.saveSettings()
if token == "" && key == "" {
a.statusLabel.SetText("Error: provide token or client key")
return
}
if token != "" {
apiURL := url + "/admin/api/client/auth"
body := fmt.Sprintf(`{"token":"%s"}`, token)
resp, err := http.Post(apiURL, "application/json", strings.NewReader(body))
if err != nil {
a.statusLabel.SetText(fmt.Sprintf("Error: %v", err))
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
a.statusLabel.SetText(fmt.Sprintf("Auth failed (%d): %s", resp.StatusCode, string(respBody)))
return
}
configData, _ := io.ReadAll(resp.Body)
a.configData = string(configData)
a.configView.SetText(a.configData)
a.statusLabel.SetText("Connected (token auth)")
a.startBtn.Enable()
a.connectBtn.SetText("Reconnect")
} else if key != "" {
apiURL := fmt.Sprintf("%s/admin/api/frpc/proxy-config/%s", url, key)
resp, err := http.Get(apiURL)
if err != nil {
a.statusLabel.SetText(fmt.Sprintf("Error: %v", err))
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
a.statusLabel.SetText(fmt.Sprintf("Config fetch failed (%d)", resp.StatusCode))
return
}
configData, _ := io.ReadAll(resp.Body)
a.configData = string(configData)
a.configView.SetText(a.configData)
a.statusLabel.SetText("Connected (key auth)")
a.startBtn.Enable()
a.connectBtn.SetText("Reconnect")
}
}
func (a *App) onStart() {
if a.configData == "" {
a.statusLabel.SetText("Error: no config loaded")
return
}
var cfg v1.ClientConfig
if err := config.LoadConfigure([]byte(a.configData), &cfg, false, "toml"); err != nil {
dialog.ShowError(fmt.Errorf("invalid config: %w", err), a.window)
return
}
a.mu.Lock()
a.running = true
a.mu.Unlock()
a.startBtn.Disable()
a.stopBtn.Enable()
a.statusLabel.SetText("Running")
}
func (a *App) onStop() {
a.mu.Lock()
a.running = false
a.mu.Unlock()
a.startBtn.Enable()
a.stopBtn.Disable()
a.statusLabel.SetText("Stopped")
}
func (a *App) Run() {
a.window.ShowAndRun()
}
func Run() {
gui := New()
log.Infof("starting frpc GUI")
gui.Run()
}
+2 -2
View File
@@ -24,8 +24,8 @@ import (
"strings"
"time"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/xlog"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/util/xlog"
)
var ErrHealthCheckType = errors.New("error health check type")
+5 -5
View File
@@ -24,11 +24,11 @@ import (
"strconv"
"time"
"github.com/fatedier/frp/client/configmgmt"
"github.com/fatedier/frp/client/http/model"
"github.com/fatedier/frp/client/proxy"
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/jsonx"
"kanhole/client/configmgmt"
"kanhole/client/http/model"
"kanhole/client/proxy"
httppkg "kanhole/pkg/util/http"
"kanhole/pkg/util/jsonx"
)
// Controller handles HTTP API requests for frpc.
+5 -5
View File
@@ -12,11 +12,11 @@ import (
"github.com/gorilla/mux"
"github.com/fatedier/frp/client/configmgmt"
"github.com/fatedier/frp/client/http/model"
"github.com/fatedier/frp/client/proxy"
v1 "github.com/fatedier/frp/pkg/config/v1"
httppkg "github.com/fatedier/frp/pkg/util/http"
"kanhole/client/configmgmt"
"kanhole/client/http/model"
"kanhole/client/proxy"
v1 "kanhole/pkg/config/v1"
httppkg "kanhole/pkg/util/http"
)
type fakeConfigManager struct {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
type ProxyDefinition struct {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
type VisitorDefinition struct {
+1 -1
View File
@@ -17,7 +17,7 @@ package proxy
import (
"reflect"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
func init() {
+9 -9
View File
@@ -28,15 +28,15 @@ import (
libnet "github.com/fatedier/golib/net"
"golang.org/x/time/rate"
"github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
plugin "github.com/fatedier/frp/pkg/plugin/client"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/limit"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
"kanhole/pkg/config/types"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
plugin "kanhole/pkg/plugin/client"
"kanhole/pkg/transport"
"kanhole/pkg/util/limit"
netpkg "kanhole/pkg/util/net"
"kanhole/pkg/util/xlog"
"kanhole/pkg/vnet"
)
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
+6 -6
View File
@@ -23,12 +23,12 @@ import (
"github.com/samber/lo"
"github.com/fatedier/frp/client/event"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
"kanhole/client/event"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/transport"
"kanhole/pkg/util/xlog"
"kanhole/pkg/vnet"
)
type Manager struct {
+8 -8
View File
@@ -25,14 +25,14 @@ import (
"github.com/fatedier/golib/errors"
"github.com/fatedier/frp/client/event"
"github.com/fatedier/frp/client/health"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/naming"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
"kanhole/client/event"
"kanhole/client/health"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/naming"
"kanhole/pkg/transport"
"kanhole/pkg/util/xlog"
"kanhole/pkg/vnet"
)
const (
+4 -4
View File
@@ -25,10 +25,10 @@ import (
"github.com/fatedier/golib/errors"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
netpkg "github.com/fatedier/frp/pkg/util/net"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/proto/udp"
netpkg "kanhole/pkg/util/net"
)
func init() {
+12 -10
View File
@@ -24,10 +24,10 @@ import (
"github.com/fatedier/golib/errors"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
netpkg "github.com/fatedier/frp/pkg/util/net"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/proto/udp"
netpkg "kanhole/pkg/util/net"
)
func init() {
@@ -99,15 +99,17 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
pxy.mu.Lock()
pxy.workConn = netpkg.WrapReadWriteCloserToConn(remote, conn)
// Plain UDP payload follows the configured wire protocol for message framing.
payloadRW := msg.NewReadWriter(pxy.workConn, pxy.clientCfg.Transport.WireProtocol)
pxy.readCh = make(chan *msg.UDPPacket, 1024)
pxy.sendCh = make(chan msg.Message, 1024)
pxy.closed = false
pxy.mu.Unlock()
workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) {
workConnReaderFn := func(rw msg.ReadWriter, readCh chan *msg.UDPPacket) {
for {
var udpMsg msg.UDPPacket
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
if errRet := rw.ReadMsgInto(&udpMsg); errRet != nil {
xl.Warnf("read from workConn for udp error: %v", errRet)
return
}
@@ -120,7 +122,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
}
}
}
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
workConnSenderFn := func(rw msg.ReadWriter, sendCh chan msg.Message) {
defer func() {
xl.Infof("writer goroutine for udp work connection closed")
}()
@@ -132,7 +134,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
case *msg.Ping:
xl.Tracef("send ping message to udp workConn")
}
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
if errRet = rw.WriteMsg(rawMsg); errRet != nil {
xl.Errorf("udp work write error: %v", errRet)
return
}
@@ -151,8 +153,8 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
}
}
go workConnSenderFn(pxy.workConn, pxy.sendCh)
go workConnReaderFn(pxy.workConn, pxy.readCh)
go workConnSenderFn(payloadRW, pxy.sendCh)
go workConnReaderFn(payloadRW, pxy.readCh)
go heartbeatFn(pxy.sendCh)
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
+16 -8
View File
@@ -25,12 +25,12 @@ import (
fmux "github.com/hashicorp/yamux"
"github.com/quic-go/quic-go"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/naming"
"github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport"
netpkg "github.com/fatedier/frp/pkg/util/net"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/naming"
"kanhole/pkg/nathole"
"kanhole/pkg/transport"
netpkg "kanhole/pkg/util/net"
)
func init() {
@@ -57,8 +57,7 @@ func NewXTCPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkConn) {
xl := pxy.xl
defer conn.Close()
var natHoleSidMsg msg.NatHoleSid
err := msg.ReadMsgInto(conn, &natHoleSidMsg)
natHoleSidMsg, err := readNatHoleSid(conn, pxy.clientCfg.Transport.WireProtocol)
if err != nil {
xl.Errorf("xtcp read from workConn error: %v", err)
return
@@ -131,6 +130,15 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
pxy.listenByQUIC(listenConn, raddr, startWorkConnMsg)
}
func readNatHoleSid(conn net.Conn, wireProtocol string) (*msg.NatHoleSid, error) {
workMsgConn := msg.NewConn(conn, msg.NewReadWriter(conn, wireProtocol))
var natHoleSidMsg msg.NatHoleSid
if err := workMsgConn.ReadMsgInto(&natHoleSidMsg); err != nil {
return nil, err
}
return &natHoleSidMsg, nil
}
func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, startWorkConnMsg *msg.StartWorkConn) {
xl := pxy.xl
listenConn.Close()
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2026 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !frps
package proxy
import (
"net"
"testing"
"time"
"github.com/stretchr/testify/require"
"kanhole/pkg/msg"
"kanhole/pkg/proto/wire"
)
func TestReadNatHoleSidUsesSelectedWireProtocol(t *testing.T) {
for _, tc := range []struct {
name string
wireProtocol string
}{
{name: "v2", wireProtocol: wire.ProtocolV2},
{name: "v1", wireProtocol: wire.ProtocolV1},
{name: "default", wireProtocol: ""},
} {
t.Run(tc.name, func(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
setPipeDeadline(t, client, server)
errCh := make(chan error, 1)
go func() {
writer := msg.NewConn(server, msg.NewReadWriter(server, tc.wireProtocol))
errCh <- writer.WriteMsg(&msg.NatHoleSid{Sid: "sid"})
}()
out, err := readNatHoleSid(client, tc.wireProtocol)
require.NoError(t, err)
require.Equal(t, "sid", out.Sid)
require.NoError(t, <-errCh)
})
}
}
func setPipeDeadline(t *testing.T, conns ...net.Conn) {
t.Helper()
deadline := time.Now().Add(time.Second)
for _, conn := range conns {
require.NoError(t, conn.SetDeadline(deadline))
}
}
+93 -13
View File
@@ -18,6 +18,7 @@ import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
@@ -27,19 +28,19 @@ import (
"github.com/fatedier/golib/crypto"
"github.com/samber/lo"
"github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/pkg/auth"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/source"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/policy/security"
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/wait"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
"kanhole/client/proxy"
"kanhole/pkg/auth"
"kanhole/pkg/config"
"kanhole/pkg/config/source"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/policy/security"
httppkg "kanhole/pkg/util/http"
"kanhole/pkg/util/log"
netpkg "kanhole/pkg/util/net"
"kanhole/pkg/util/wait"
"kanhole/pkg/util/xlog"
"kanhole/pkg/vnet"
)
func init() {
@@ -146,6 +147,9 @@ type Service struct {
// string if no configuration file was used.
configFilePath string
// configURL is the URL to fetch configuration from on startup and periodically.
configURL string
// service context
ctx context.Context
// call cancel to stop service
@@ -199,6 +203,7 @@ func NewService(options ServiceOptions) (*Service, error) {
common: options.Common,
reloadCommon: options.Common,
configFilePath: options.ConfigFilePath,
configURL: options.Common.ConfigURL,
unsafeFeatures: options.UnsafeFeatures,
proxyCfgs: proxyCfgs,
visitorCfgs: visitorCfgs,
@@ -265,6 +270,10 @@ func (svr *Service) Run(ctx context.Context) error {
go svr.keepControllerWorking()
if svr.configURL != "" {
go svr.pollConfigURL()
}
<-svr.ctx.Done()
svr.stop()
return nil
@@ -513,3 +522,74 @@ func (svr *Service) reloadConfigFromSourcesLocked() error {
}
return nil
}
func (svr *Service) pollConfigURL() {
url := svr.configURL
if url == "" {
return
}
log.Infof("starting config URL poller: %s", url)
lastBody := ""
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-svr.ctx.Done():
return
case <-ticker.C:
}
resp, err := http.Get(url)
if err != nil {
log.Warnf("failed to fetch config from %s: %v", url, err)
continue
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
log.Warnf("failed to read config from %s: %v", url, err)
continue
}
bodyStr := string(body)
if bodyStr == lastBody {
continue
}
log.Infof("config changed, reloading from %s", url)
allCfg := v1.ClientConfig{}
if err := config.LoadConfigure(body, &allCfg, false, "toml"); err != nil {
log.Warnf("failed to parse config from %s: %v", url, err)
continue
}
proxyCfgs := make([]v1.ProxyConfigurer, 0)
for _, c := range allCfg.Proxies {
proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
}
visitorCfgs := make([]v1.VisitorConfigurer, 0)
for _, c := range allCfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
}
// Update common config for new proxies
if allCfg.ClientCommonConfig.Log.To != "" {
svr.cfgMu.Lock()
svr.reloadCommon = &allCfg.ClientCommonConfig
svr.cfgMu.Unlock()
}
if err := svr.UpdateConfigSource(&allCfg.ClientCommonConfig, proxyCfgs, visitorCfgs); err != nil {
log.Warnf("failed to apply config from %s: %v", url, err)
continue
}
lastBody = bodyStr
log.Infof("config reloaded successfully from %s", url)
}
}
+2 -2
View File
@@ -11,8 +11,8 @@ import (
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/config/source"
v1 "github.com/fatedier/frp/pkg/config/v1"
"kanhole/pkg/config/source"
v1 "kanhole/pkg/config/v1"
)
type failingConnector struct {
+2 -2
View File
@@ -20,8 +20,8 @@ import (
libio "github.com/fatedier/golib/io"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/xlog"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/util/xlog"
)
type STCPVisitor struct {
+5 -5
View File
@@ -23,11 +23,11 @@ import (
"github.com/fatedier/golib/errors"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/proto/udp"
netpkg "kanhole/pkg/util/net"
"kanhole/pkg/util/xlog"
)
type SUDPVisitor struct {
+9 -9
View File
@@ -24,15 +24,15 @@ import (
libio "github.com/fatedier/golib/io"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/naming"
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
"github.com/fatedier/frp/pkg/transport"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/naming"
plugin "kanhole/pkg/plugin/visitor"
"kanhole/pkg/transport"
netpkg "kanhole/pkg/util/net"
"kanhole/pkg/util/util"
"kanhole/pkg/util/xlog"
"kanhole/pkg/vnet"
)
// Helper wraps some functions for visitor to use.
+5 -5
View File
@@ -24,11 +24,11 @@ import (
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/vnet"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/transport"
"kanhole/pkg/util/xlog"
"kanhole/pkg/vnet"
)
type Manager struct {
+8 -8
View File
@@ -29,14 +29,14 @@ import (
quic "github.com/quic-go/quic-go"
"golang.org/x/time/rate"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/naming"
"github.com/fatedier/frp/pkg/nathole"
"github.com/fatedier/frp/pkg/transport"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/naming"
"kanhole/pkg/nathole"
"kanhole/pkg/transport"
netpkg "kanhole/pkg/util/net"
"kanhole/pkg/util/util"
"kanhole/pkg/util/xlog"
)
var ErrNoTunnelSession = errors.New("no tunnel session")
+3 -3
View File
@@ -15,9 +15,9 @@
package main
import (
_ "github.com/fatedier/frp/pkg/metrics"
"github.com/fatedier/frp/pkg/util/system"
_ "github.com/fatedier/frp/web/frps"
_ "kanhole/pkg/metrics"
"kanhole/pkg/util/system"
_ "kanhole/web/frps"
)
func main() {
+54 -33
View File
@@ -1,17 +1,3 @@
// Copyright 2018 fatedier, fatedier@gmail.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
@@ -22,13 +8,14 @@ import (
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/security"
"github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/server"
"kanhole/pkg/config"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/config/v1/validation"
"kanhole/pkg/db"
"kanhole/pkg/policy/security"
"kanhole/pkg/util/log"
"kanhole/pkg/util/version"
"kanhole/server"
)
var (
@@ -41,8 +28,8 @@ var (
)
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of kanhole")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of kanhole")
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", ")))
@@ -51,14 +38,20 @@ func init() {
}
var rootCmd = &cobra.Command{
Use: "frps",
Short: "frps is the server of frp (https://github.com/fatedier/frp)",
Use: "kanhole",
Short: "kanhole is the server component of kanhole",
RunE: func(cmd *cobra.Command, args []string) error {
if showVersion {
fmt.Println(version.Full())
return nil
}
if err := db.Init("sqlite", "admin.db"); err != nil {
fmt.Printf("failed to initialize admin database: %v\n", err)
os.Exit(1)
}
defer db.Close()
var (
svrCfg *v1.ServerConfig
isLegacyFormat bool
@@ -71,15 +64,14 @@ var rootCmd = &cobra.Command{
os.Exit(1)
}
if isLegacyFormat {
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
"please use yaml/json/toml format instead!\n")
fmt.Printf("WARNING: ini format is deprecated, please use yaml/json/toml instead!\n")
}
} else {
if err := serverCfg.Complete(); err != nil {
fmt.Printf("failed to complete server config: %v\n", err)
svrCfg, err = loadConfigFromDB()
if err != nil {
fmt.Printf("failed to load server config: %v\n", err)
os.Exit(1)
}
svrCfg = &serverCfg
}
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
@@ -108,20 +100,49 @@ func Execute() {
}
}
func loadConfigFromDB() (*v1.ServerConfig, error) {
hasConfig, err := db.HasServerConfig()
if err != nil {
return nil, fmt.Errorf("failed to check server config in db: %w", err)
}
if hasConfig {
cfg, err := db.LoadServerConfig()
if err != nil {
return nil, fmt.Errorf("failed to load server config from db: %w", err)
}
if err := cfg.Complete(); err != nil {
return nil, fmt.Errorf("failed to complete server config: %w", err)
}
log.Infof("kanhole uses database configuration")
return cfg, nil
}
cfg := db.DefaultServerConfig()
if err := cfg.Complete(); err != nil {
return nil, fmt.Errorf("failed to complete server config: %w", err)
}
if err := db.SaveServerConfig(cfg); err != nil {
log.Warnf("failed to save default config to db: %v", err)
}
log.Infof("kanhole started with default configuration (first run)")
return cfg, nil
}
func runServer(cfg *v1.ServerConfig) (err error) {
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
if cfgFile != "" {
log.Infof("frps uses config file: %s", cfgFile)
log.Infof("kanhole uses config file: %s", cfgFile)
} else {
log.Infof("frps uses command line arguments for config")
log.Infof("kanhole uses database configuration")
}
svr, err := server.NewService(cfg)
if err != nil {
return err
}
log.Infof("frps started successfully")
log.Infof("kanhole started successfully")
svr.Run(context.Background())
return
}
+5 -5
View File
@@ -20,9 +20,9 @@ import (
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/security"
"kanhole/pkg/config"
"kanhole/pkg/config/v1/validation"
"kanhole/pkg/policy/security"
)
func init() {
@@ -34,7 +34,7 @@ var verifyCmd = &cobra.Command{
Short: "Verify that the configures is valid",
RunE: func(cmd *cobra.Command, args []string) error {
if cfgFile == "" {
fmt.Println("frps: the configuration file is not specified")
fmt.Println("kanhole: the configuration file is not specified")
return nil
}
svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode)
@@ -53,7 +53,7 @@ var verifyCmd = &cobra.Command{
fmt.Println(err)
os.Exit(1)
}
fmt.Printf("frps: the configuration file %s syntax is ok\n", cfgFile)
fmt.Printf("kanhole: the configuration file %s syntax is ok\n", cfgFile)
return nil
},
}
+3 -3
View File
@@ -15,9 +15,9 @@
package main
import (
"github.com/fatedier/frp/cmd/frpc/sub"
"github.com/fatedier/frp/pkg/util/system"
_ "github.com/fatedier/frp/web/frpc"
"kanhole/cmd/kanholec/sub"
"kanhole/pkg/util/system"
_ "kanhole/web/frpc"
)
func main() {
@@ -1,4 +1,4 @@
// Copyright 2023 The frp Authors
// Copyright 2023 The kanhole Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -24,9 +24,9 @@ import (
"github.com/rodaine/table"
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
clientsdk "github.com/fatedier/frp/pkg/sdk/client"
"kanhole/pkg/config"
v1 "kanhole/pkg/config/v1"
clientsdk "kanhole/pkg/sdk/client"
)
var adminAPITimeout = 30 * time.Second
@@ -37,9 +37,9 @@ func init() {
description string
handler func(*v1.ClientCommonConfig) error
}{
{"reload", "Hot-Reload frpc configuration", ReloadHandler},
{"reload", "Hot-Reload kanholec configuration", ReloadHandler},
{"status", "Overview of all proxies status", StatusHandler},
{"stop", "Stop the running frpc", StopHandler},
{"stop", "Stop the running kanholec", StopHandler},
}
for _, cmdConfig := range commands {
+183
View File
@@ -0,0 +1,183 @@
package sub
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"github.com/spf13/cobra"
"kanhole/pkg/util/log"
)
var (
authServer string
authOutput string
)
func init() {
authCmd := &cobra.Command{
Use: "auth",
Short: "Authenticate kanholec with a kanhole server",
Long: `Authenticate this kanholec instance with a kanhole server.
One-time token:
kanholec auth token <token> --server http://server:7500
Interactive login:
kanholec auth login --server http://server:7500 --client-name myclient
`,
}
tokenCmd := &cobra.Command{
Use: "token <token>",
Short: "Authenticate using a one-time token",
Args: cobra.ExactArgs(1),
RunE: runAuthToken,
}
tokenCmd.Flags().StringVarP(&authServer, "server", "s", "http://localhost:7500", "kanhole server admin URL")
tokenCmd.Flags().StringVarP(&authOutput, "output", "o", "", "output config file path (default: ./kanholec-<client-name>.toml)")
loginCmd := &cobra.Command{
Use: "login",
Short: "Authenticate using admin credentials",
RunE: runAuthLogin,
}
loginCmd.Flags().StringVarP(&authServer, "server", "s", "http://localhost:7500", "kanhole server admin URL")
loginCmd.Flags().StringVarP(&authOutput, "output", "o", "", "output config file path (default: ./kanholec-<client-name>.toml)")
loginCmd.Flags().String("username", "", "admin username (prompts if empty)")
loginCmd.Flags().String("password", "", "admin password (prompts if empty)")
loginCmd.Flags().String("client-name", "", "client name (fetches list if empty)")
rootCmd.AddCommand(authCmd)
authCmd.AddCommand(tokenCmd)
authCmd.AddCommand(loginCmd)
}
func runAuthToken(cmd *cobra.Command, args []string) error {
token := args[0]
url := authServer + "/admin/api/client/auth"
body := map[string]string{"token": token}
data, _ := json.Marshal(body)
resp, err := http.Post(url, "application/json", bytes.NewReader(data))
if err != nil {
return fmt.Errorf("failed to connect to server: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("auth failed (HTTP %d): %s", resp.StatusCode, string(respBody))
}
configData, _ := io.ReadAll(resp.Body)
return saveConfig(configData)
}
func runAuthLogin(cmd *cobra.Command, args []string) error {
username, _ := cmd.Flags().GetString("username")
password, _ := cmd.Flags().GetString("password")
clientName, _ := cmd.Flags().GetString("client-name")
if username == "" {
fmt.Print("Admin username: ")
fmt.Scanln(&username)
}
if password == "" {
fmt.Print("Admin password: ")
bytePassword, err := readPassword()
if err != nil {
return err
}
password = string(bytePassword)
fmt.Println()
}
url := authServer + "/admin/api/client/auth"
body := map[string]string{
"username": username,
"password": password,
"client_name": clientName,
}
data, _ := json.Marshal(body)
resp, err := http.Post(url, "application/json", bytes.NewReader(data))
if err != nil {
return fmt.Errorf("failed to connect to server: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("auth failed (HTTP %d): %s", resp.StatusCode, string(respBody))
}
// Check if server returned a client list
contentType := resp.Header.Get("Content-Type")
if contentType == "application/json" || len(contentType) == 0 {
var result struct {
Clients []map[string]any `json:"clients"`
RequiresClientName bool `json:"requires_client_name"`
}
respBody, _ := io.ReadAll(resp.Body)
if err := json.Unmarshal(respBody, &result); err == nil && result.RequiresClientName {
fmt.Println("Available clients:")
for i, c := range result.Clients {
fmt.Printf(" %d. %s\n", i+1, c["name"])
}
fmt.Print("Enter client name: ")
var name string
fmt.Scanln(&name)
body["client_name"] = name
data, _ = json.Marshal(body)
resp, err = http.Post(url, "application/json", bytes.NewReader(data))
if err != nil {
return fmt.Errorf("failed to connect to server: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("auth failed (HTTP %d): %s", resp.StatusCode, string(respBody))
}
configData, _ := io.ReadAll(resp.Body)
return saveConfig(configData)
}
configData := respBody
return saveConfig(configData)
}
configData, _ := io.ReadAll(resp.Body)
return saveConfig(configData)
}
func saveConfig(data []byte) error {
outputPath := authOutput
if outputPath == "" {
// Try to extract client name from config
outputPath = "kanholec.toml"
}
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
if err := os.WriteFile(outputPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
log.Infof("config saved to %s", outputPath)
fmt.Printf("Config saved to %s\n", outputPath)
fmt.Printf("Run: kanholec -c %s\n", outputPath)
return nil
}
func readPassword() ([]byte, error) {
return io.ReadAll(os.Stdin)
}
+21
View File
@@ -0,0 +1,21 @@
//go:build kanholec_gui
package sub
import (
"github.com/spf13/cobra"
"kanhole/client/gui"
)
func init() {
guiCmd := &cobra.Command{
Use: "gui",
Short: "Start the kanholec graphical user interface",
RunE: func(cmd *cobra.Command, args []string) error {
gui.Run()
return nil
},
}
rootCmd.AddCommand(guiCmd)
}
+23
View File
@@ -0,0 +1,23 @@
//go:build !kanholec_gui
package sub
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
guiCmd := &cobra.Command{
Use: "gui",
Short: "Start the kanholec graphical user interface",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("kanholec GUI is not available in this build.")
fmt.Println("To build with GUI support, install OpenGL and X11 dev libraries, then:")
fmt.Println(" CGO_ENABLED=1 go build -tags kanholec_gui -o kanholec ./cmd/kanholec")
return nil
},
}
rootCmd.AddCommand(guiCmd)
}
@@ -1,4 +1,4 @@
// Copyright 2023 The frp Authors
// Copyright 2023 The kanhole Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,9 +20,9 @@ import (
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/nathole"
"kanhole/pkg/config"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/nathole"
)
var (
@@ -1,4 +1,4 @@
// Copyright 2023 The frp Authors
// Copyright 2023 The kanhole Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -21,11 +21,11 @@ import (
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/source"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/security"
"kanhole/pkg/config"
"kanhole/pkg/config/source"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/config/v1/validation"
"kanhole/pkg/policy/security"
)
var proxyTypes = []v1.ProxyType{
@@ -73,7 +73,7 @@ func init() {
func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
return &cobra.Command{
Use: name,
Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
Short: fmt.Sprintf("Run kanholec with a single %s proxy", name),
Run: func(cmd *cobra.Command, args []string) {
if err := clientCfg.Complete(); err != nil {
fmt.Println(err)
@@ -106,7 +106,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
return &cobra.Command{
Use: "visitor",
Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
Short: fmt.Sprintf("Run kanholec with a single %s visitor", name),
Run: func(cmd *cobra.Command, args []string) {
if err := clientCfg.Complete(); err != nil {
fmt.Println(err)
@@ -17,7 +17,9 @@ package sub
import (
"context"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"os/signal"
"path/filepath"
@@ -28,15 +30,15 @@ import (
"github.com/spf13/cobra"
"github.com/fatedier/frp/client"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/source"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/featuregate"
"github.com/fatedier/frp/pkg/policy/security"
"github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version"
"kanhole/client"
"kanhole/pkg/config"
"kanhole/pkg/config/source"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/config/v1/validation"
"kanhole/pkg/policy/featuregate"
"kanhole/pkg/policy/security"
"kanhole/pkg/util/log"
"kanhole/pkg/util/version"
)
var (
@@ -45,21 +47,23 @@ var (
showVersion bool
strictConfigMode bool
allowUnsafe []string
serverConfigURL string
)
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc")
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./kanholec.toml", "config file of kanholec")
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one kanholec service for each file in config directory")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of kanholec")
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
rootCmd.PersistentFlags().StringVarP(&serverConfigURL, "server-config", "", "", "fetch config from frps server URL (auto-reloads on change)")
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
}
var rootCmd = &cobra.Command{
Use: "frpc",
Short: "frpc is the client of frp (https://github.com/fatedier/frp)",
Use: "kanholec",
Short: "kanholec is the client component of kanhole",
RunE: func(cmd *cobra.Command, args []string) error {
if showVersion {
fmt.Println(version.Full())
@@ -68,13 +72,23 @@ var rootCmd = &cobra.Command{
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
// If cfgDir is not empty, run multiple kanholec service for each config file in cfgDir.
// Note that it's only designed for testing. It's not guaranteed to be stable.
if cfgDir != "" {
_ = runMultipleClients(cfgDir, unsafeFeatures)
return nil
}
// If server-config is set, use it instead of local config file.
if serverConfigURL != "" {
err := runClientWithServerConfig(serverConfigURL, unsafeFeatures)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
}
// Do not show command usage here.
err := runClient(cfgFile, unsafeFeatures)
if err != nil {
@@ -97,7 +111,7 @@ func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures)
defer wg.Done()
err := runClient(path, unsafeFeatures)
if err != nil {
fmt.Printf("frpc service error for config file [%s]\n", path)
fmt.Printf("kanholec service error for config file [%s]\n", path)
}
}()
return nil
@@ -120,6 +134,37 @@ func handleTermSignal(svr *client.Service) {
svr.GracefulClose(500 * time.Millisecond)
}
func runClientWithServerConfig(url string, unsafeFeatures *security.UnsafeFeatures) error {
log.Infof("fetching config from server: %s", url)
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to fetch config from server: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read config from server: %w", err)
}
allCfg := v1.ClientConfig{}
if err := config.LoadConfigure(body, &allCfg, strictConfigMode, "toml"); err != nil {
return fmt.Errorf("failed to parse config from server: %w", err)
}
result := &config.ClientConfigLoadResult{
Common: &allCfg.ClientCommonConfig,
Proxies: make([]v1.ProxyConfigurer, 0),
}
for _, c := range allCfg.Proxies {
result.Proxies = append(result.Proxies, c.ProxyConfigurer)
}
result.Common.ConfigURL = url
return runClientWithAggregator(result, unsafeFeatures, "")
}
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
// Load configuration
result, err := config.LoadClientConfigResult(cfgFilePath, strictConfigMode)
@@ -198,8 +243,8 @@ func startServiceWithAggregator(
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
if cfgFile != "" {
log.Infof("start frpc service for config file [%s] with aggregated configuration", cfgFile)
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
log.Infof("start kanholec service for config file [%s] with aggregated configuration", cfgFile)
defer log.Infof("kanholec service for config file [%s] stopped", cfgFile)
}
svr, err := client.NewService(client.ServiceOptions{
Common: cfg,
@@ -1,4 +1,4 @@
// Copyright 2021 The frp Authors
// Copyright 2021 The kanhole Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,9 +20,9 @@ import (
"github.com/spf13/cobra"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/security"
"kanhole/pkg/config"
"kanhole/pkg/config/v1/validation"
"kanhole/pkg/policy/security"
)
func init() {
@@ -34,7 +34,7 @@ var verifyCmd = &cobra.Command{
Short: "Verify that the configures is valid",
RunE: func(cmd *cobra.Command, args []string) error {
if cfgFile == "" {
fmt.Println("frpc: the configuration file is not specified")
fmt.Println("kanholec: the configuration file is not specified")
return nil
}
@@ -53,7 +53,7 @@ var verifyCmd = &cobra.Command{
os.Exit(1)
}
fmt.Printf("frpc: the configuration file %s syntax is ok\n", cfgFile)
fmt.Printf("kanholec: the configuration file %s syntax is ok\n", cfgFile)
return nil
},
}
+47 -3
View File
@@ -33,7 +33,51 @@ git commit -m "bump version to vX.Y.Z"
git push origin dev
```
## 3. Merge dev → master
## 3. Pre-release Validation
Run the standard e2e suite locally:
```bash
make e2e
```
For releases that touch compatibility-sensitive areas such as login, control
connections, work connections, visitors, transport, or wire protocol handling,
also run the manual compatibility e2e suite:
```bash
make e2e-compatibility
make e2e-compatibility-floor
```
`make e2e-compatibility` builds the current `frps` and `frpc`, resolves the
recent stable release baselines from GitHub, downloads or reuses their binaries,
and tests current binaries against those historical releases. The default number
of recent baselines is controlled by `FRP_COMPAT_BASELINE_COUNT` in the
`Makefile`.
Downloaded release binaries are cached under:
```text
.cache/e2e-compat/<version>/<os>_<arch>/
```
For a release validation run that must be exactly reproducible, pass an explicit
baseline matrix instead of using the floating recent-release list:
```bash
FRP_COMPAT_BASELINE_VERSIONS="0.X.0 0.Y.0" make e2e-compatibility
```
Use `make e2e-compatibility-smoke` for a quick single-baseline check while
iterating locally. If GitHub release metadata requests are rate-limited, set
`GITHUB_TOKEN` or use `FRP_COMPAT_BASELINE_VERSIONS`.
The compatibility floor is a support-policy decision, not a value that should
change every release. Update `FRP_COMPAT_FLOOR_VERSION` only when the declared
compatibility window changes.
## 4. Merge dev → master
Create a PR from `dev` to `master`:
@@ -43,7 +87,7 @@ gh pr create --base master --head dev --title "bump version"
Wait for CI to pass, then merge using **merge commit** (not squash).
## 4. Tag the Release
## 5. Tag the Release
```bash
git checkout master
@@ -52,7 +96,7 @@ git tag -a vX.Y.Z -m "bump version"
git push origin vX.Y.Z
```
## 5. Trigger GoReleaser
## 6. Trigger GoReleaser
Manually trigger the `goreleaser` workflow in GitHub Actions:
+55 -9
View File
@@ -1,11 +1,13 @@
module github.com/fatedier/frp
module kanhole
go 1.25.0
require (
entgo.io/ent v0.14.6
fyne.io/fyne/v2 v2.7.4
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/coreos/go-oidc/v3 v3.14.1
github.com/fatedier/golib v0.6.0
github.com/fatedier/golib v0.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.0
@@ -26,31 +28,66 @@ require (
github.com/tidwall/gjson v1.17.1
github.com/vishvananda/netlink v1.3.0
github.com/xtaci/kcp-go/v5 v5.6.13
golang.org/x/crypto v0.49.0
golang.org/x/net v0.52.0
golang.org/x/crypto v0.52.0
golang.org/x/net v0.54.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
golang.org/x/time v0.10.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
gopkg.in/ini.v1 v1.67.0
k8s.io/apimachinery v0.28.8
k8s.io/client-go v0.28.8
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
modernc.org/sqlite v1.51.0
)
require (
ariga.io/atlas v0.36.2-0.20250730182955-2c6300d0a3e1 // indirect
fyne.io/systray v1.12.1 // indirect
github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fredbi/uri v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.2.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-text/render v0.2.1 // indirect
github.com/go-text/typesetting v0.3.4 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/reedsolomon v1.12.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pion/dtls/v3 v3.0.10 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
@@ -59,6 +96,10 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rymdport/portal v0.4.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/templexxx/cpu v0.1.1 // indirect
github.com/templexxx/xorsimd v0.4.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
@@ -66,16 +107,21 @@ require (
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/zclconf/go-cty v1.14.4 // indirect
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.42.0 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.44.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
+147 -18
View File
@@ -1,15 +1,37 @@
ariga.io/atlas v0.36.2-0.20250730182955-2c6300d0a3e1 h1:NPPfBaVZgz4LKBCIc0FbMogCjvXN+yGf7CZwotOwJo8=
ariga.io/atlas v0.36.2-0.20250730182955-2c6300d0a3e1/go.mod h1:Ex5l1xHsnWQUc3wYnrJ9gD7RUEzG76P7ZRQp8wNr0wc=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
entgo.io/ent v0.14.6 h1:/f2696BpwuWAEEG6PVGWflg6+Inrpq4pRWuNlWz/Skk=
entgo.io/ent v0.14.6/go.mod h1:z46QBUdGC+BATwsedbDuREfSS0oSCV+csdEYlL4p73s=
fyne.io/fyne/v2 v2.7.4 h1:OVCI5mT+Onb2kA4wlmGA5pLCqKik9f4NDb5jiR1OMTc=
fyne.io/fyne/v2 v2.7.4/go.mod h1:ZD1mmhBY75mSa97IXl3MPlICd1uNHfCXYh5hKIlVOII=
fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ=
fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
@@ -17,19 +39,51 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatedier/golib v0.6.0 h1:/mgBZZbkbMhIEZoXf7nV8knpUDzas/b+2ruYKxx1lww=
github.com/fatedier/golib v0.6.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
github.com/fatedier/golib v0.7.0 h1:tMDF9ObcwVt59VUHroJOzHQjVFPLymZVMpGm9WAVwhY=
github.com/fatedier/golib v0.7.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-text/render v0.2.1 h1:qwHhxqGUjjg4L0XyJWj7M7bpY75NZM+kBpv2Yfw5mcg=
github.com/go-text/render v0.2.1/go.mod h1:HCCAq8MUlm/WRcXshBb4K/n+IkjeXQ1c2Ba+yICSm0A=
github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -60,8 +114,20 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
@@ -70,8 +136,23 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
@@ -90,6 +171,8 @@ github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwy
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
@@ -105,21 +188,30 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -152,6 +244,12 @@ github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4H
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
@@ -159,22 +257,24 @@ go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
@@ -189,15 +289,16 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -205,8 +306,8 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
@@ -248,6 +349,34 @@ k8s.io/client-go v0.28.8 h1:TE59Tjd87WKvS2FPBTfIKLFX0nQJ4SSHsnDo5IHjgOw=
k8s.io/client-go v0.28.8/go.mod h1:uDVQ/rPzWpWIy40c6lZ4mUwaEvRWGnpoqSO4FM65P3o=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+162
View File
@@ -0,0 +1,162 @@
#!/bin/sh
set -eu
SCRIPT=$(readlink -f "$0")
ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
if ! command -v ginkgo >/dev/null 2>&1; then
echo "ginkgo not found, try to install..."
go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4
fi
debug=false
if [ "x${DEBUG:-}" = "xtrue" ]; then
debug=true
fi
logLevel=debug
if [ "${LOG_LEVEL:-}" ]; then
logLevel="${LOG_LEVEL}"
fi
currentFrpsPath=${CURRENT_FRPS_PATH:-${ROOT}/bin/frps}
currentFrpcPath=${CURRENT_FRPC_PATH:-${ROOT}/bin/frpc}
baselineCount=${FRP_COMPAT_BASELINE_COUNT:-8}
targetOS=${TARGET_OS:-$(go env GOOS)}
targetArch=${TARGET_ARCH:-$(go env GOARCH)}
targetPlatform="${targetOS}_${targetArch}"
cacheRoot=${FRP_COMPAT_CACHE_DIR:-${ROOT}/.cache/e2e-compat}
check_file() {
if [ ! -f "$2" ]; then
echo "$1 not found: $2"
exit 1
fi
}
check_file "current frps" "${currentFrpsPath}"
check_file "current frpc" "${currentFrpcPath}"
run_current_current=true
run_compatibility() {
baselineVersion=$1
baselineFrpsPath=$2
baselineFrpcPath=$3
check_file "baseline frps" "${baselineFrpsPath}"
check_file "baseline frpc" "${baselineFrpcPath}"
echo "Running compatibility e2e with baseline ${baselineVersion}"
ginkgo -nodes=1 --poll-progress-after=60s "${ROOT}/test/e2e/compatibility" -- \
-current-frps-path="${currentFrpsPath}" \
-current-frpc-path="${currentFrpcPath}" \
-baseline-frps-path="${baselineFrpsPath}" \
-baseline-frpc-path="${baselineFrpcPath}" \
-baseline-version="${baselineVersion}" \
-run-current-current="${run_current_current}" \
-log-level="${logLevel}" \
-debug="${debug}"
run_current_current=false
}
github_api_curl() {
if [ "${GITHUB_TOKEN:-}" ]; then
curl -fsSL \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
"$1"
else
curl -fsSL "$1"
fi
}
resolve_versions() {
if [ "${FRP_COMPAT_BASELINE_VERSIONS:-}" ]; then
printf "%s\n" "${FRP_COMPAT_BASELINE_VERSIONS}"
return
fi
case "${baselineCount}" in
'' | *[!0-9]*)
echo "FRP_COMPAT_BASELINE_COUNT must be a positive integer: ${baselineCount}" >&2
exit 1
;;
esac
if [ "${baselineCount}" -eq 0 ]; then
echo "FRP_COMPAT_BASELINE_COUNT must be greater than 0" >&2
exit 1
fi
if [ "${baselineCount}" -gt 100 ]; then
echo "FRP_COMPAT_BASELINE_COUNT must be less than or equal to 100" >&2
exit 1
fi
releaseURL="https://api.github.com/repos/fatedier/frp/releases?per_page=100"
resolvedVersions=""
if releases=$(github_api_curl "${releaseURL}" 2>/dev/null); then
resolvedVersions=$(printf "%s\n" "${releases}" |
sed -n 's/.*"tag_name":[[:space:]]*"v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/p' |
awk '!seen[$0]++' |
head -n "${baselineCount}" |
tr '\n' ' ' |
sed 's/[[:space:]]*$//')
else
echo "Failed to fetch release metadata from GitHub API, falling back to GitHub releases page." >&2
fi
if [ -z "${resolvedVersions}" ]; then
releasesPageURL="https://github.com/fatedier/frp/releases"
if ! releases=$(curl -fsSL "${releasesPageURL}"); then
echo "Failed to fetch release metadata from GitHub: ${releasesPageURL}" >&2
echo "Set FRP_COMPAT_BASELINE_VERSIONS to run with explicit baseline versions." >&2
exit 1
fi
resolvedVersions=$(printf "%s\n" "${releases}" |
grep -o 'releases/tag/v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"' |
sed 's#.*/v##; s/"$//' |
awk '!seen[$0]++' |
head -n "${baselineCount}" |
tr '\n' ' ' |
sed 's/[[:space:]]*$//')
fi
set -- ${resolvedVersions}
if [ "$#" -lt "${baselineCount}" ]; then
echo "Only resolved $# stable release versions from GitHub, expected ${baselineCount}." >&2
echo "Set FRP_COMPAT_BASELINE_VERSIONS to run with explicit baseline versions." >&2
exit 1
fi
printf "%s\n" "${resolvedVersions}"
}
if [ "${BASELINE_FRPS_PATH:-}" ] || [ "${BASELINE_FRPC_PATH:-}" ]; then
if [ -z "${BASELINE_FRPS_PATH:-}" ] || [ -z "${BASELINE_FRPC_PATH:-}" ]; then
echo "BASELINE_FRPS_PATH and BASELINE_FRPC_PATH must be set together"
exit 1
fi
run_compatibility "${FRP_COMPAT_BASELINE_VERSION:-custom}" "${BASELINE_FRPS_PATH}" "${BASELINE_FRPC_PATH}"
exit 0
fi
versions=$(resolve_versions)
echo "Compatibility baseline versions: ${versions}"
mkdir -p "${cacheRoot}"
for version in ${versions}; do
baselineDir="${cacheRoot}/${version}/${targetPlatform}"
if [ ! -f "${baselineDir}/frps" ] || [ ! -f "${baselineDir}/frpc" ]; then
tmpDir="${cacheRoot}/.download-${version}-${targetPlatform}"
rm -rf "${tmpDir}"
(
cd "${cacheRoot}"
FRP_VERSION="${version}" TARGET_DIRNAME="$(basename "${tmpDir}")" "${ROOT}/hack/download.sh"
)
mkdir -p "$(dirname "${baselineDir}")"
rm -rf "${baselineDir}"
mv "${tmpDir}" "${baselineDir}"
fi
run_compatibility "${version}" "${baselineDir}/frps" "${baselineDir}/frpc"
done
+309
View File
@@ -0,0 +1,309 @@
<#
.SYNOPSIS
kanholec Windows Setup Wizard (Interactive PowerShell Installer)
.DESCRIPTION
Interactive installer for kanholec with EULA acceptance, directory selection,
and optional features (PATH, shortcuts, desktop icon).
.PARAMETER Unattended
Run in silent mode with defaults.
.PARAMETER InstallDir
Installation directory (default: $env:ProgramFiles\kanholec).
.EXAMPLE
.\install.ps1
.\install.ps1 -Unattended
#>
param(
[switch]$Unattended,
[string]$InstallDir = "$env:ProgramFiles\kanholec"
)
$ErrorActionPreference = "Stop"
$host.UI.RawUI.WindowTitle = "kanholec Setup Wizard"
$env:KANHOLEC_VERSION = "0.62.0"
function Write-Banner {
Clear-Host
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " kanholec - kanhole Client v$env:KANHOLEC_VERSION" -ForegroundColor Cyan
Write-Host " Setup Wizard" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
}
function Show-License {
param([string]$Path)
Write-Banner
Write-Host "License Agreement" -ForegroundColor Yellow
Write-Host "==================" -ForegroundColor Yellow
Write-Host ""
if (Test-Path $Path) {
Get-Content $Path | ForEach-Object { Write-Host $_ }
} else {
Write-Host "kanholec is licensed under the Apache License, Version 2.0."
Write-Host "See http://www.apache.org/licenses/LICENSE-2.0"
}
Write-Host ""
if (-not $Unattended) {
$choice = $host.UI.PromptForChoice(
"License Agreement",
"Do you accept the terms?",
[System.Management.Automation.Host.ChoiceDescription[]]@(
@{Label="&Agree"; Help="I accept the agreement"},
@{Label="&Decline"; Help="I do not accept the agreement"}
),
1
)
if ($choice -ne 0) {
Write-Host "Setup cancelled. You must accept the license to install." -ForegroundColor Red
exit 1
}
}
}
function Select-Directory {
param([string]$DefaultPath)
Write-Banner
Write-Host "Installation Directory" -ForegroundColor Yellow
Write-Host "======================" -ForegroundColor Yellow
Write-Host ""
Write-Host "Default: $DefaultPath" -ForegroundColor Gray
Write-Host ""
if (-not $Unattended) {
$input = Read-Host "Press Enter to accept, or type a custom path"
if ($input -ne "") {
return $input
}
}
return $DefaultPath
}
function Select-Components {
Write-Banner
Write-Host "Select Components" -ForegroundColor Yellow
Write-Host "=================" -ForegroundColor Yellow
Write-Host ""
$components = @{
"Main" = @{Desc="kanholec binary and config"; Default=$true}
"Path" = @{Desc="Add to system PATH"; Default=$true}
"StartMenu" = @{Desc="Start Menu shortcuts"; Default=$true}
"Desktop" = @{Desc="Desktop shortcut"; Default=$false}
}
$selected = @{}
$i = 1
$choices = @()
foreach ($key in $components.Keys) {
$c = $components[$key]
$default = if ($c.Default) { "Y" } else { "N" }
$displayDefault = if ($c.Default) { "Yes" } else { "No" }
Write-Host " $i. $($c.Desc) [$displayDefault]" -ForegroundColor Gray
$choices += @{Key=$key; Default=$default}
$i++
}
Write-Host ""
if (-not $Unattended) {
Write-Host "Press Enter for defaults, or type numbers to toggle (e.g. 1,3):" -ForegroundColor Yellow
$input = Read-Host
if ($input -ne "") {
$toggleIndices = $input -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
for ($j = 0; $j -lt $choices.Count; $j++) {
$idx = $j + 1
$isToggled = [string]$idx -in $toggleIndices
$c = $components[$choices[$j].Key]
$default = $c.Default
$selected[$choices[$j].Key] = if ($isToggled) { -not $default } else { $default }
}
} else {
foreach ($ch in $choices) { $selected[$ch.Key] = $components[$ch.Key].Default }
}
} else {
foreach ($ch in $choices) { $selected[$ch.Key] = $components[$ch.Key].Default }
}
Write-Host ""
Write-Host "Selected components:" -ForegroundColor Green
foreach ($key in $selected.Keys) {
$mark = if ($selected[$key]) { "[X]" } else { "[ ]" }
Write-Host " $mark $($components[$key].Desc)" -ForegroundColor $(if ($selected[$key]) { "Green" } else { "DarkGray" })
}
Start-Sleep 1
return $selected
}
function Install-Frpc {
param(
[string]$InstallDir,
[hashtable]$Components,
[string]$BinarySource
)
Write-Banner
Write-Host "Installing kanholec..." -ForegroundColor Yellow
Write-Host "==================" -ForegroundColor Yellow
Write-Host ""
# Create directories
Write-Host " Creating directories..." -NoNewline
New-Item -Path $InstallDir -ItemType Directory -Force | Out-Null
$configDir = "$env:ProgramData\kanholec"
New-Item -Path $configDir -ItemType Directory -Force | Out-Null
Write-Host " OK" -ForegroundColor Green
# Copy binary
Write-Host " Copying kanholec.exe..." -NoNewline
if ($BinarySource -and (Test-Path $BinarySource)) {
Copy-Item -Path $BinarySource -Destination "$InstallDir\kanholec.exe" -Force
} else {
# Download from GitHub
$url = "https://github.com/fatedier/kanhole/releases/download/v$env:KANHOLEC_VERSION/kanhole_${env:KANHOLEC_VERSION}_windows_amd64.zip"
$zipPath = "$env:TEMP\kanholec.zip"
Write-Host ""
Write-Host " Downloading from GitHub..." -NoNewline
Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing
Expand-Archive -Path $zipPath -DestinationPath "$env:TEMP\kanholec" -Force
$exePath = Get-ChildItem -Path "$env:TEMP\kanholec" -Recurse -Filter "kanholec.exe" | Select-Object -First 1 -ExpandProperty FullName
if (-not $exePath) { throw "kanholec.exe not found in archive" }
Copy-Item -Path $exePath -Destination "$InstallDir\kanholec.exe" -Force
}
Write-Host " OK" -ForegroundColor Green
# Config file
Write-Host " Creating config..." -NoNewline
$configFile = "$configDir\kanholec.toml"
if (-not (Test-Path $configFile)) {
@"
# kanholec configuration
# Edit this file to configure your kanhole client.
serverAddr = "127.0.0.1"
serverPort = 7000
auth.token = ""
"@ | Out-File -FilePath $configFile -Encoding utf8
}
Write-Host " OK" -ForegroundColor Green
# PATH
if ($Components["Path"]) {
Write-Host " Adding to PATH..." -NoNewline
$currentPath = [Environment]::GetEnvironmentVariable("PATH", "Machine")
if ($currentPath -notlike "*$InstallDir*") {
$newPath = "$currentPath;$InstallDir"
[Environment]::SetEnvironmentVariable("PATH", $newPath, "Machine")
$env:PATH = "$env:PATH;$InstallDir"
}
Write-Host " OK" -ForegroundColor Green
}
# Start Menu
if ($Components["StartMenu"]) {
Write-Host " Creating Start Menu shortcuts..." -NoNewline
$startMenu = "$([Environment]::GetFolderPath('CommonStartMenu'))\Programs\kanholec"
New-Item -Path $startMenu -ItemType Directory -Force | Out-Null
$wshell = New-Object -ComObject WScript.Shell
$shortcut = $wshell.CreateShortcut("$startMenu\kanholec.lnk")
$shortcut.TargetPath = "$InstallDir\kanholec.exe"
$shortcut.Save()
$unlink = $wshell.CreateShortcut("$startMenu\Uninstall kanholec.lnk")
$unlink.TargetPath = "$InstallDir\uninstall.ps1"
$unlink.Save()
Write-Host " OK" -ForegroundColor Green
}
# Desktop
if ($Components["Desktop"]) {
Write-Host " Creating desktop shortcut..." -NoNewline
$desktop = [Environment]::GetFolderPath('Desktop')
$wshell = New-Object -ComObject WScript.Shell
$shortcut = $wshell.CreateShortcut("$desktop\kanholec.lnk")
$shortcut.TargetPath = "$InstallDir\kanholec.exe"
$shortcut.Save()
Write-Host " OK" -ForegroundColor Green
}
# Write uninstall script
Write-Host " Creating uninstaller..." -NoNewline
@"
param([switch]`$Silent)
`$InstallDir = "$InstallDir"
if (-not `$Silent) {
`$choice = `$host.UI.PromptForChoice("Uninstall kanholec", "Remove kanholec from this computer?", @(@{Label="&Yes"; Help=""}, @{Label="&No"; Help=""}), 1)
if (`$choice -ne 0) { exit }
}
# Remove files
Remove-Item -Path "`$InstallDir\kanholec.exe" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "`$InstallDir\uninstall.ps1" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "`$InstallDir" -Force -ErrorAction SilentlyContinue
# Remove shortcuts
Remove-Item -Path "$([Environment]::GetFolderPath('CommonStartMenu'))\Programs\kanholec" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path "$([Environment]::GetFolderPath('Desktop'))\kanholec.lnk" -Force -ErrorAction SilentlyContinue
Write-Host "kanholec has been uninstalled."
"@ | Out-File -FilePath "$InstallDir\uninstall.ps1" -Encoding utf8
Write-Host " OK" -ForegroundColor Green
Write-Host ""
Write-Host "Installation complete!" -ForegroundColor Green
Write-Host ""
Write-Host "Quick start:" -ForegroundColor Yellow
Write-Host " kanholec --help"
Write-Host " kanholec --server-config http://your-server:7500/admin/api/kanholec/proxy-config/YOUR_KEY"
Write-Host " kanholec -c $configFile"
Write-Host ""
Write-Host "Auth with provisioning token:" -ForegroundColor Yellow
Write-Host " kanholec auth login --server http://your-server:7500 --client-name myclient"
Write-Host ""
}
# ===== Main =====
$scriptDir = Split-Path -Parent $PSCommandPath
$binarySource = Join-Path $scriptDir "..\bin\kanholec-windows-amd64.exe"
if (-not (Test-Path $binarySource)) {
$binarySource = Join-Path $scriptDir "kanholec-windows-amd64.exe"
}
if (-not (Test-Path $binarySource)) {
$binarySource = $null
}
# Check admin rights
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Host "This installer requires administrator privileges." -ForegroundColor Red
Write-Host "Please run PowerShell as Administrator and try again." -ForegroundColor Yellow
if (-not $Unattended) {
Start-Sleep 2
# Self-elevate
$script = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
Start-Process powershell -Verb RunAs -ArgumentList $script
}
exit 1
}
if ($Unattended) {
Install-Frpc -InstallDir $InstallDir -Components @{
Main=$true; Path=$true; StartMenu=$true; Desktop=$false
} -BinarySource $binarySource
} else {
Show-License -Path (Join-Path $scriptDir "license.txt")
$dir = Select-Directory -DefaultPath $InstallDir
$components = Select-Components
Install-Frpc -InstallDir $dir -Components $components -BinarySource $binarySource
Write-Host ""
$runNow = $host.UI.PromptForChoice(
"Setup Complete",
"Run kanholec now?",
[System.Management.Automation.Host.ChoiceDescription[]]@(
@{Label="&Yes"; Help="Run kanholec"},
@{Label="&No"; Help="Close"}
),
0
)
if ($runNow -eq 0) {
Start-Process "$dir\kanholec.exe"
}
}
+10
View File
@@ -0,0 +1,10 @@
# kanholec configuration
# Generated by kanholec installer
# Edit this file to configure your kanhole client.
serverAddr = "127.0.0.1"
serverPort = 7000
auth.token = ""
# Uncomment and set the URL to auto-fetch config from kanholes:
# configURL = "http://your-server:7500/admin/api/kanholec/proxy-config/YOUR_KEY"
+148
View File
@@ -0,0 +1,148 @@
; kanholec Windows Installer (NSIS)
; Build: makensis kanholec.nsi
; Requires: NSIS 3.x (https://nsis.sourceforge.io)
!define PRODUCT_NAME "kanholec"
!define PRODUCT_VERSION "0.62.0"
!define PRODUCT_PUBLISHER "kanhole Contributors"
!define PRODUCT_WEB_SITE "https://github.com/kanhole/kanhole"
!define PRODUCT_DIR "$PROGRAMFILES64\${PRODUCT_NAME}"
!define PRODUCT_UNINSTALL_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
OutFile "..\..\bin\kanholec-${PRODUCT_VERSION}-setup.exe"
InstallDir "${PRODUCT_DIR}"
InstallDirRegKey HKLM "${PRODUCT_UNINSTALL_KEY}" "InstallLocation"
RequestExecutionLevel admin
SetCompressor lzma
; Pages (setup wizard)
Page license
Page components
Page directory
Page instfiles
Page custom setFinishPage
; Uninstaller pages
UninstPage uninstConfirm
UninstPage instfiles
; License data
LicenseData "..\..\packaging\windows\license.txt"
; Languages
!insertmacro MUI_LANGUAGE "English"
; Include modern UI
!include "MUI2.nsh"
!include "FileFunc.nsh"
!include "WinMessages.nsh"
!include "LogicLib.nsh"
Var FinishPage
Var RunNowCheckbox
Var ShowReadmeCheckbox
Var RunNowState
Var ReadmeState
Function setFinishPage
nsDialogs::Create 1018
Pop $FinishPage
${NSD_CreateLabel} 0 0 100% 24 "Setup Complete"
Pop $0
${NSD_CreateCheckbox} 0 30 100% 12 "Run kanholec now"
Pop $RunNowCheckbox
${NSD_Check} $RunNowCheckbox
${NSD_CreateCheckbox} 0 46 100% 12 "Open config directory"
Pop $ShowReadmeCheckbox
nsDialogs::Show
FunctionEnd
Function .onGUIEnd
${NSD_GetState} $RunNowCheckbox $RunNowState
${NSD_GetState} $ShowReadmeCheckbox $ReadmeState
${If} $RunNowState == ${BST_CHECKED}
ExecShell "open" "$INSTDIR\kanholec.exe"
${EndIf}
${If} $ReadmeState == ${BST_CHECKED}
ExecShell "open" "$PROGRAMDATA\kanholec"
${EndIf}
FunctionEnd
Section "kanholec (required)" SEC_KANHOLEC
SectionIn RO
SetOutPath "$INSTDIR"
File "..\..\bin\kanholec-windows-amd64.exe"
Rename "$INSTDIR\kanholec-windows-amd64.exe" "$INSTDIR\kanholec.exe"
; Config directory
CreateDirectory "$PROGRAMDATA\kanholec"
SetOutPath "$PROGRAMDATA\kanholec"
File "/oname=kanholec.toml" "..\..\packaging\windows\kanholec.default.toml"
WriteUninstaller "$INSTDIR\uninstall.exe"
; Registry for uninstaller
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "DisplayName" "${PRODUCT_NAME}"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "UninstallString" "$INSTDIR\uninstall.exe"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "DisplayIcon" "$INSTDIR\kanholec.exe,0"
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "InstallLocation" "$INSTDIR"
WriteRegDWORD HKLM "${PRODUCT_UNINSTALL_KEY}" "NoModify" 1
WriteRegDWORD HKLM "${PRODUCT_UNINSTALL_KEY}" "NoRepair" 1
SectionEnd
Section "Start Menu Shortcuts" SEC_SHORTCUTS
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\kanholec.lnk" "$INSTDIR\kanholec.exe" "" "$INSTDIR\kanholec.exe" 0
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Config Directory.lnk" "$PROGRAMDATA\kanholec"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
SectionEnd
Section "Add to PATH" SEC_PATH
EnVar::AddValue "PATH" "$INSTDIR"
Pop $0
SectionEnd
Section "Desktop Shortcut" SEC_DESKTOP
CreateShortCut "$DESKTOP\kanholec.lnk" "$INSTDIR\kanholec.exe" "" "$INSTDIR\kanholec.exe" 0
SectionEnd
; Descriptions
LangString DESC_SEC_KANHOLEC ${LANG_ENGLISH} "kanholec binary and default configuration."
LangString DESC_SEC_SHORTCUTS ${LANG_ENGLISH} "Start menu shortcuts for kanholec."
LangString DESC_SEC_PATH ${LANG_ENGLISH} "Add kanholec installation directory to system PATH."
LangString DESC_SEC_DESKTOP ${LANG_ENGLISH} "Create a desktop shortcut for kanholec."
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_KANHOLEC} $(DESC_SEC_KANHOLEC)
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_SHORTCUTS} $(DESC_SEC_SHORTCUTS)
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_PATH} $(DESC_SEC_PATH)
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_DESKTOP} $(DESC_SEC_DESKTOP)
!insertmacro MUI_FUNCTION_DESCRIPTION_END
Section "Uninstall"
Delete "$INSTDIR\kanholec.exe"
Delete "$INSTDIR\uninstall.exe"
RMDir "$INSTDIR"
Delete "$SMPROGRAMS\${PRODUCT_NAME}\kanholec.lnk"
Delete "$SMPROGRAMS\${PRODUCT_NAME}\Config Directory.lnk"
Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk"
RMDir "$SMPROGRAMS\${PRODUCT_NAME}"
Delete "$DESKTOP\kanholec.lnk"
EnVar::DeleteValue "PATH" "$INSTDIR"
DeleteRegKey HKLM "${PRODUCT_UNINSTALL_KEY}"
SectionEnd
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v3/wxs">
<Product
Name="kanholec"
Id="*"
UpgradeCode="A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
Language="1033"
Codepage="1252"
Version="0.62.0"
Manufacturer="kanhole Contributors">
<Package
InstallerVersion="200"
Compressed="yes"
InstallScope="perMachine"
Description="kanhole Client"
Comments="kanholec is the client component of kanhole - a fast reverse proxy." />
<Media Id="1" Cabinet="kanholec.cab" EmbedCab="yes" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder">
<Directory Id="INSTALLDIR" Name="kanholec">
<Component Id="FrpcBinary" Guid="E1B2C3D4-E5F6-7890-ABCD-EF1234567894">
<File Id="FrpcExe" Name="kanholec.exe" DiskId="1" Source="bin/kanholec-windows-amd64.exe" KeyPath="yes" />
</Component>
</Directory>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="kanholec">
<Component Id="StartMenuShortcuts" Guid="A2B2C3D4-E5F6-7890-ABCD-EF1234567892">
<Shortcut Id="FrpcShortcut" Name="kanholec" Target="[INSTALLDIR]kanholec.exe" WorkingDirectory="INSTALLDIR" Description="kanhole client" />
<Shortcut Id="UninstallShortcut" Name="Uninstall kanholec" Target="[System64Folder]msiexec.exe" Arguments="/x [ProductCode]" />
<RemoveFolder Id="RemoveApplicationProgramsFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\kanhole\kanholec" Name="installed" Type="integer" Value="1" />
</Component>
</Directory>
</Directory>
</Directory>
<Feature Id="Main" Title="kanholec" Description="kanhole client binary" Level="1">
<ComponentRef Id="FrpcBinary" />
<ComponentRef Id="StartMenuShortcuts" />
</Feature>
</Product>
</Wix>
+33
View File
@@ -0,0 +1,33 @@
{\rtf1\ansi\ansicpg1252\deff0\deflang1033\deftab720{\fonttbl{\f0\fnil\fcharset0 Arial;}{\f1\fmodern\fcharset0 Courier New;}}
{\colortbl\red0\green0\blue0;\red255\green255\blue255;}
\viewkind4\uc1\pard\qc\b\fs28 kanholec\par
\b0\fs20 Version 0.1.0\par
\pard\sa200\sl240\slmult1\fs20\par
\pard\sa200\sl240\slmult1\b\fs24 License Agreement\b0\fs20\par
\pard\sa200\sl240\slmult1\par
\pard\sa200\sl240\slmult1\fs20 This is a legal agreement between you (the "Licensee") and the kanhole project contributors ("Licensor"). By installing or using kanholec, you agree to the following terms and conditions.\par
\pard\sa200\sl240\slmult1\par
\pard\sa200\sl240\slmult1\b 1. Grant of License\b0\par
\pard\sa200\sl240\slmult1 The kanholec software is licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. You may obtain a copy of the License at:\par
\pard\sa200\sl240\slmult1 http://www.apache.org/licenses/LICENSE-2.0\par
\pard\sa200\sl240\slmult1\par
\pard\sa200\sl240\slmult1\b 2. Distribution\b0\par
\pard\sa200\sl240\slmult1 You may reproduce and distribute copies of the work or derivative works thereof in any medium, with or without modifications, and in source or object form, provided that you meet the following conditions:\par
\pard\sa200\sl240\slmult1 - You must give any other recipients of the work or derivative works a copy of this License\par
\pard\sa200\sl240\slmult1 - You must cause any modified files to carry prominent notices stating that you changed the files\par
\pard\sa200\sl240\slmult1 - You must retain all copyright, patent, trademark, and attribution notices\par
\pard\sa200\sl240\slmult1\par
\pard\sa200\sl240\slmult1\b 3. Disclaimer of Warranty\b0\par
\pard\sa200\sl240\slmult1 Unless required by applicable law or agreed to in writing, the software is provided on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\par
\pard\sa200\sl240\slmult1\par
\pard\sa200\sl240\slmult1\b 4. Limitation of Liability\b0\par
\pard\sa200\sl240\slmult1 In no event and under no legal theory shall the authors or copyright holders be liable for any damages arising from the use of the software.\par
\pard\sa200\sl240\slmult1\par
\pard\sa200\sl240\slmult1\b 5. Termination\b0\par
\pard\sa200\sl240\slmult1 This license automatically terminates if you violate any of its terms. Upon termination, you must destroy all copies of the software.\par
\pard\sa200\sl240\slmult1\par
\pard\sa200\sl240\slmult1\b 6. Governing Law\b0\par
\pard\sa200\sl240\slmult1 This license shall be governed by the laws applicable to the Licensor's jurisdiction.\par
\pard\sa200\sl240\slmult1\par
\pard\sa200\sl240\slmult1 By clicking "I Agree" or installing the software, you acknowledge that you have read this agreement, understand it, and agree to be bound by its terms.\par
}
+34
View File
@@ -0,0 +1,34 @@
kanholec - kanhole Client (version 0.1.0)
============================================
Copyright (c) kanhole project contributors.
Licensed under the Apache License, Version 2.0.
TERMS AND CONDITIONS
--------------------
1. Grant of License
frpc is licensed under the Apache License, Version 2.0 (the "License").
You may not use this file except in compliance with the License.
A copy of the License is available at:
http://www.apache.org/licenses/LICENSE-2.0
2. Distribution
You may reproduce and distribute copies of the work or derivative works
in any medium, with or without modifications, and in source or object
form, provided that you meet the following conditions:
- You must give any other recipients a copy of this License
- You must cause any modified files to carry prominent notices
- You must retain all copyright, patent, trademark notices
3. Disclaimer of Warranty
Unless required by applicable law, the software is provided "AS IS",
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied.
4. Limitation of Liability
In no event shall the authors be liable for any damages arising from
the use of this software.
5. Termination
This license automatically terminates if you violate its terms.
By installing this software, you accept these terms.
+2 -2
View File
@@ -18,8 +18,8 @@ import (
"context"
"fmt"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
)
type Setter interface {
+3 -3
View File
@@ -29,9 +29,9 @@ import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/msg"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/config/v1/validation"
"kanhole/pkg/msg"
)
// createOIDCHTTPClient creates an HTTP client with custom TLS and proxy configuration for OIDC token requests
+3 -3
View File
@@ -12,9 +12,9 @@ import (
"github.com/coreos/go-oidc/v3/oidc"
"github.com/stretchr/testify/require"
"github.com/fatedier/frp/pkg/auth"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"kanhole/pkg/auth"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
)
type mockTokenVerifier struct{}
+1 -1
View File
@@ -15,7 +15,7 @@
package auth
import (
"github.com/fatedier/frp/pkg/msg"
"kanhole/pkg/msg"
)
var AlwaysPassVerifier = &alwaysPass{}
+3 -3
View File
@@ -19,9 +19,9 @@ import (
"slices"
"time"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/util/util"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/msg"
"kanhole/pkg/util/util"
)
type TokenAuthSetterVerifier struct {
+6 -6
View File
@@ -22,9 +22,9 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"kanhole/pkg/config/types"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/config/v1/validation"
)
// WordSepNormalizeFunc changes all flags that contain "_" separators
@@ -154,8 +154,8 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
}
if !options.sshMode {
cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address")
cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port")
cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "kanhole server's address")
cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "kanhole server's port")
cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp",
fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols))
cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
@@ -248,7 +248,7 @@ func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig, opts ...R
cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host")
cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports")
cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only")
cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "kanhole tls only")
webServerTLS := v1.TLSConfig{}
cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
+2 -2
View File
@@ -23,8 +23,8 @@ import (
"gopkg.in/ini.v1"
legacyauth "github.com/fatedier/frp/pkg/auth/legacy"
"github.com/fatedier/frp/pkg/util/util"
legacyauth "kanhole/pkg/auth/legacy"
"kanhole/pkg/util/util"
)
// ClientCommonConf is the configuration parsed from ini.
+2 -2
View File
@@ -19,8 +19,8 @@ import (
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/config/types"
v1 "github.com/fatedier/frp/pkg/config/v1"
"kanhole/pkg/config/types"
v1 "kanhole/pkg/config/v1"
)
func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig {
+1 -1
View File
@@ -20,7 +20,7 @@ import (
"gopkg.in/ini.v1"
"github.com/fatedier/frp/pkg/config/types"
"kanhole/pkg/config/types"
)
type ProxyType string
+2 -2
View File
@@ -19,7 +19,7 @@ import (
"gopkg.in/ini.v1"
legacyauth "github.com/fatedier/frp/pkg/auth/legacy"
legacyauth "kanhole/pkg/auth/legacy"
)
type HTTPPluginOptions struct {
@@ -131,7 +131,7 @@ type ServerCommonConf struct {
// SubDomainHost specifies the domain that will be attached to sub-domains
// requested by the client when using Vhost proxying. For example, if this
// value is set to "frps.com" and the client requested the subdomain
// value is set to "kanhole.com" and the client requested the subdomain
// "test", the resulting URL would be "test.frps.com". By default, this
// value is "".
SubDomainHost string `ini:"subdomain_host" json:"subdomain_host"`
+1 -1
View File
@@ -46,7 +46,7 @@ func GetValues() *Values {
}
func RenderContent(in []byte) (out []byte, err error) {
tmpl, errRet := template.New("frp").Parse(string(in))
tmpl, errRet := template.New("kanhole").Parse(string(in))
if errRet != nil {
err = errRet
return
+7 -7
View File
@@ -30,12 +30,12 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/yaml"
"github.com/fatedier/frp/pkg/config/legacy"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/util/jsonx"
"github.com/fatedier/frp/pkg/util/util"
"kanhole/pkg/config/legacy"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/config/v1/validation"
"kanhole/pkg/msg"
"kanhole/pkg/util/jsonx"
"kanhole/pkg/util/util"
)
var glbEnvs map[string]string
@@ -82,7 +82,7 @@ func DetectLegacyINIFormatFromFile(path string) bool {
}
func RenderWithTemplate(in []byte, values *Values) ([]byte, error) {
tmpl, err := template.New("frp").Funcs(template.FuncMap{
tmpl, err := template.New("kanhole").Funcs(template.FuncMap{
"parseNumberRange": parseNumberRange,
"parseNumberRangePair": parseNumberRangePair,
}).Parse(string(in))
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"github.com/stretchr/testify/require"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
const tomlServerContent = `
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"slices"
"sync"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
type Aggregator struct {
+1 -1
View File
@@ -20,7 +20,7 @@ import (
"github.com/stretchr/testify/require"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
// mockProxy creates a TCP proxy config for testing
+1 -1
View File
@@ -17,7 +17,7 @@ package source
import (
"sync"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
// baseSource provides shared state and behavior for Source implementations.
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
func TestBaseSourceLoadReturnsClonedConfigurers(t *testing.T) {
+1 -1
View File
@@ -17,7 +17,7 @@ package source
import (
"fmt"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
func cloneConfigurers(
+7 -17
View File
@@ -14,11 +14,7 @@
package source
import (
"fmt"
v1 "github.com/fatedier/frp/pkg/config/v1"
)
import v1 "kanhole/pkg/config/v1"
// ConfigSource implements Source for in-memory configuration.
// All operations are thread-safe.
@@ -39,23 +35,17 @@ func (s *ConfigSource) ReplaceAll(proxies []v1.ProxyConfigurer, visitors []v1.Vi
nextProxies := make(map[string]v1.ProxyConfigurer, len(proxies))
for _, p := range proxies {
if p == nil {
return fmt.Errorf("proxy cannot be nil")
}
name := p.GetBaseConfig().Name
if name == "" {
return fmt.Errorf("proxy name cannot be empty")
name, err := validateProxyName(p)
if err != nil {
return err
}
nextProxies[name] = p
}
nextVisitors := make(map[string]v1.VisitorConfigurer, len(visitors))
for _, v := range visitors {
if v == nil {
return fmt.Errorf("visitor cannot be nil")
}
name := v.GetBaseConfig().Name
if name == "" {
return fmt.Errorf("visitor name cannot be empty")
name, err := validateVisitorName(v)
if err != nil {
return err
}
nextVisitors[name] = v
}
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"github.com/stretchr/testify/require"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
func TestNewConfigSource(t *testing.T) {
+1 -1
View File
@@ -15,7 +15,7 @@
package source
import (
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
// Source is the interface for configuration sources.
+102 -120
View File
@@ -20,8 +20,8 @@ import (
"os"
"path/filepath"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/jsonx"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/util/jsonx"
)
type StoreSourceConfig struct {
@@ -43,6 +43,11 @@ var (
ErrNotFound = errors.New("not found")
)
const (
storeKindProxy = "proxy"
storeKindVisitor = "visitor"
)
func NewStoreSource(cfg StoreSourceConfig) (*StoreSource, error) {
if cfg.Path == "" {
return nil, fmt.Errorf("path is required")
@@ -172,79 +177,111 @@ func (s *StoreSource) saveToFileUnlocked() error {
return nil
}
func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error {
if proxy == nil {
return fmt.Errorf("proxy cannot be nil")
func (s *StoreSource) persistOrRollbackUnlocked(rollback func()) error {
if err := s.saveToFileUnlocked(); err != nil {
rollback()
return fmt.Errorf("failed to persist: %w", err)
}
return nil
}
// Store map selectors return the target map for generic helpers.
func proxyStoreEntries(s *StoreSource) map[string]v1.ProxyConfigurer {
return s.proxies
}
func visitorStoreEntries(s *StoreSource) map[string]v1.VisitorConfigurer {
return s.visitors
}
// Store entry helpers share mutation, persistence, and rollback for proxy and visitor maps.
// T is intentionally limited by callers to v1.ProxyConfigurer or v1.VisitorConfigurer.
func addStoreEntry[T any](
s *StoreSource,
entriesFn func(*StoreSource) map[string]T,
kind string,
name string,
value T,
) error {
s.mu.Lock()
defer s.mu.Unlock()
entries := entriesFn(s)
if _, exists := entries[name]; exists {
return fmt.Errorf("%w: %s %q", ErrAlreadyExists, kind, name)
}
name := proxy.GetBaseConfig().Name
entries[name] = value
return s.persistOrRollbackUnlocked(func() {
delete(entries, name)
})
}
func updateStoreEntry[T any](
s *StoreSource,
entriesFn func(*StoreSource) map[string]T,
kind string,
name string,
value T,
) error {
s.mu.Lock()
defer s.mu.Unlock()
entries := entriesFn(s)
old, exists := entries[name]
if !exists {
return fmt.Errorf("%w: %s %q", ErrNotFound, kind, name)
}
entries[name] = value
return s.persistOrRollbackUnlocked(func() {
entries[name] = old
})
}
func removeStoreEntry[T any](
s *StoreSource,
entriesFn func(*StoreSource) map[string]T,
kind string,
name string,
) error {
if name == "" {
return fmt.Errorf("proxy name cannot be empty")
return fmt.Errorf("%s name cannot be empty", kind)
}
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.proxies[name]; exists {
return fmt.Errorf("%w: proxy %q", ErrAlreadyExists, name)
entries := entriesFn(s)
old, exists := entries[name]
if !exists {
return fmt.Errorf("%w: %s %q", ErrNotFound, kind, name)
}
s.proxies[name] = proxy
delete(entries, name)
return s.persistOrRollbackUnlocked(func() {
entries[name] = old
})
}
if err := s.saveToFileUnlocked(); err != nil {
delete(s.proxies, name)
return fmt.Errorf("failed to persist: %w", err)
func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error {
name, err := validateProxyName(proxy)
if err != nil {
return err
}
return nil
return addStoreEntry(s, proxyStoreEntries, storeKindProxy, name, proxy)
}
func (s *StoreSource) UpdateProxy(proxy v1.ProxyConfigurer) error {
if proxy == nil {
return fmt.Errorf("proxy cannot be nil")
name, err := validateProxyName(proxy)
if err != nil {
return err
}
name := proxy.GetBaseConfig().Name
if name == "" {
return fmt.Errorf("proxy name cannot be empty")
}
s.mu.Lock()
defer s.mu.Unlock()
oldProxy, exists := s.proxies[name]
if !exists {
return fmt.Errorf("%w: proxy %q", ErrNotFound, name)
}
s.proxies[name] = proxy
if err := s.saveToFileUnlocked(); err != nil {
s.proxies[name] = oldProxy
return fmt.Errorf("failed to persist: %w", err)
}
return nil
return updateStoreEntry(s, proxyStoreEntries, storeKindProxy, name, proxy)
}
func (s *StoreSource) RemoveProxy(name string) error {
if name == "" {
return fmt.Errorf("proxy name cannot be empty")
}
s.mu.Lock()
defer s.mu.Unlock()
oldProxy, exists := s.proxies[name]
if !exists {
return fmt.Errorf("%w: proxy %q", ErrNotFound, name)
}
delete(s.proxies, name)
if err := s.saveToFileUnlocked(); err != nil {
s.proxies[name] = oldProxy
return fmt.Errorf("failed to persist: %w", err)
}
return nil
return removeStoreEntry(s, proxyStoreEntries, storeKindProxy, name)
}
func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer {
@@ -259,78 +296,23 @@ func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer {
}
func (s *StoreSource) AddVisitor(visitor v1.VisitorConfigurer) error {
if visitor == nil {
return fmt.Errorf("visitor cannot be nil")
name, err := validateVisitorName(visitor)
if err != nil {
return err
}
name := visitor.GetBaseConfig().Name
if name == "" {
return fmt.Errorf("visitor name cannot be empty")
}
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.visitors[name]; exists {
return fmt.Errorf("%w: visitor %q", ErrAlreadyExists, name)
}
s.visitors[name] = visitor
if err := s.saveToFileUnlocked(); err != nil {
delete(s.visitors, name)
return fmt.Errorf("failed to persist: %w", err)
}
return nil
return addStoreEntry(s, visitorStoreEntries, storeKindVisitor, name, visitor)
}
func (s *StoreSource) UpdateVisitor(visitor v1.VisitorConfigurer) error {
if visitor == nil {
return fmt.Errorf("visitor cannot be nil")
name, err := validateVisitorName(visitor)
if err != nil {
return err
}
name := visitor.GetBaseConfig().Name
if name == "" {
return fmt.Errorf("visitor name cannot be empty")
}
s.mu.Lock()
defer s.mu.Unlock()
oldVisitor, exists := s.visitors[name]
if !exists {
return fmt.Errorf("%w: visitor %q", ErrNotFound, name)
}
s.visitors[name] = visitor
if err := s.saveToFileUnlocked(); err != nil {
s.visitors[name] = oldVisitor
return fmt.Errorf("failed to persist: %w", err)
}
return nil
return updateStoreEntry(s, visitorStoreEntries, storeKindVisitor, name, visitor)
}
func (s *StoreSource) RemoveVisitor(name string) error {
if name == "" {
return fmt.Errorf("visitor name cannot be empty")
}
s.mu.Lock()
defer s.mu.Unlock()
oldVisitor, exists := s.visitors[name]
if !exists {
return fmt.Errorf("%w: visitor %q", ErrNotFound, name)
}
delete(s.visitors, name)
if err := s.saveToFileUnlocked(); err != nil {
s.visitors[name] = oldVisitor
return fmt.Errorf("failed to persist: %w", err)
}
return nil
return removeStoreEntry(s, visitorStoreEntries, storeKindVisitor, name)
}
func (s *StoreSource) GetVisitor(name string) v1.VisitorConfigurer {
+98 -2
View File
@@ -17,12 +17,13 @@ package source
import (
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/require"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/jsonx"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/util/jsonx"
)
func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) {
@@ -59,6 +60,101 @@ func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
}
func TestStoreSource_UpdateAndRemoveProxyAndVisitor(t *testing.T) {
require := require.New(t)
storeSource := newTestStoreSource(t)
proxyCfg := mockProxy("proxy1")
visitorCfg := mockVisitor("visitor1")
require.NoError(storeSource.AddProxy(proxyCfg))
require.NoError(storeSource.AddVisitor(visitorCfg))
require.ErrorIs(storeSource.AddProxy(proxyCfg), ErrAlreadyExists)
require.ErrorIs(storeSource.AddVisitor(visitorCfg), ErrAlreadyExists)
require.ErrorContains(storeSource.RemoveProxy(""), "proxy name cannot be empty")
require.ErrorContains(storeSource.RemoveVisitor(""), "visitor name cannot be empty")
updatedProxy := mockProxy("proxy1").(*v1.TCPProxyConfig)
updatedProxy.RemotePort = 19090
require.NoError(storeSource.UpdateProxy(updatedProxy))
require.Equal(19090, storeSource.GetProxy("proxy1").(*v1.TCPProxyConfig).RemotePort)
updatedVisitor := mockVisitor("visitor1").(*v1.STCPVisitorConfig)
updatedVisitor.ServerName = "updated-server"
require.NoError(storeSource.UpdateVisitor(updatedVisitor))
require.Equal("updated-server", storeSource.GetVisitor("visitor1").(*v1.STCPVisitorConfig).ServerName)
require.NoError(storeSource.RemoveProxy("proxy1"))
require.Nil(storeSource.GetProxy("proxy1"))
require.ErrorIs(storeSource.RemoveProxy("proxy1"), ErrNotFound)
require.NoError(storeSource.RemoveVisitor("visitor1"))
require.Nil(storeSource.GetVisitor("visitor1"))
require.ErrorIs(storeSource.RemoveVisitor("visitor1"), ErrNotFound)
require.ErrorIs(storeSource.UpdateProxy(updatedProxy), ErrNotFound)
require.ErrorIs(storeSource.UpdateVisitor(updatedVisitor), ErrNotFound)
}
func TestStoreSource_MutationRollsBackOnPersistFailure(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("chmod does not make directories unwritable on Windows")
}
if os.Getuid() == 0 {
t.Skip("chmod does not block writes for uid 0")
}
require := require.New(t)
dir := t.TempDir()
path := filepath.Join(dir, "store.json")
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
require.NoError(err)
proxyCfg := mockProxy("proxy1")
visitorCfg := mockVisitor("visitor1")
originalRemotePort := proxyCfg.(*v1.TCPProxyConfig).RemotePort
originalServerName := visitorCfg.(*v1.STCPVisitorConfig).ServerName
require.NoError(storeSource.AddProxy(proxyCfg))
require.NoError(storeSource.AddVisitor(visitorCfg))
require.NoError(os.Chmod(dir, 0o500))
t.Cleanup(func() {
_ = os.Chmod(dir, 0o700)
})
requirePersistError := func(err error) {
t.Helper()
require.Error(err)
require.ErrorContains(err, "failed to persist")
require.NotErrorIs(err, ErrAlreadyExists)
require.NotErrorIs(err, ErrNotFound)
}
requirePersistError(storeSource.AddProxy(mockProxy("proxy2")))
require.Nil(storeSource.GetProxy("proxy2"))
updatedProxy := mockProxy("proxy1").(*v1.TCPProxyConfig)
updatedProxy.RemotePort = 19090
requirePersistError(storeSource.UpdateProxy(updatedProxy))
require.Equal(originalRemotePort, storeSource.GetProxy("proxy1").(*v1.TCPProxyConfig).RemotePort)
requirePersistError(storeSource.RemoveProxy("proxy1"))
require.NotNil(storeSource.GetProxy("proxy1"))
requirePersistError(storeSource.AddVisitor(mockVisitor("visitor2")))
require.Nil(storeSource.GetVisitor("visitor2"))
updatedVisitor := mockVisitor("visitor1").(*v1.STCPVisitorConfig)
updatedVisitor.ServerName = "updated-server"
requirePersistError(storeSource.UpdateVisitor(updatedVisitor))
require.Equal(originalServerName, storeSource.GetVisitor("visitor1").(*v1.STCPVisitorConfig).ServerName)
requirePersistError(storeSource.RemoveVisitor("visitor1"))
require.NotNil(storeSource.GetVisitor("visitor1"))
}
func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
require := require.New(t)
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2026 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package source
import (
"fmt"
v1 "kanhole/pkg/config/v1"
)
func validateProxyName(proxy v1.ProxyConfigurer) (string, error) {
if proxy == nil {
return "", fmt.Errorf("proxy cannot be nil")
}
name := proxy.GetBaseConfig().Name
if name == "" {
return "", fmt.Errorf("proxy name cannot be empty")
}
return name, nil
}
func validateVisitorName(visitor v1.VisitorConfigurer) (string, error) {
if visitor == nil {
return "", fmt.Errorf("visitor cannot be nil")
}
name := visitor.GetBaseConfig().Name
if name == "" {
return "", fmt.Errorf("visitor name cannot be empty")
}
return name, nil
}
+1 -1
View File
@@ -17,7 +17,7 @@ package config
import (
"fmt"
"github.com/fatedier/frp/pkg/util/util"
"kanhole/pkg/util/util"
)
type NumberPair struct {
+7 -2
View File
@@ -19,7 +19,7 @@ import (
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/util/util"
"kanhole/pkg/util/util"
)
type ClientConfig struct {
@@ -78,8 +78,13 @@ type ClientCommonConfig struct {
// Include other config files for proxies.
IncludeConfigFiles []string `json:"includes,omitempty"`
// Store config enables the built-in store source (not configurable via sources list).
// Store config enables the built-in store source.
Store StoreConfig `json:"store,omitempty"`
// ConfigURL fetches the full client configuration from a remote URL.
// When set, frpc fetches this URL on startup and periodically thereafter,
// automatically reloading when the configuration changes.
ConfigURL string `json:"configURL,omitempty"`
}
func (c *ClientCommonConfig) Complete() error {
+1 -1
View File
@@ -17,7 +17,7 @@ package v1
import (
"maps"
"github.com/fatedier/frp/pkg/util/util"
"kanhole/pkg/util/util"
)
type AuthScope string
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"fmt"
"reflect"
"github.com/fatedier/frp/pkg/util/jsonx"
"kanhole/pkg/util/jsonx"
)
type DecodeOptions struct {
+4 -4
View File
@@ -19,10 +19,10 @@ import (
"reflect"
"slices"
"github.com/fatedier/frp/pkg/config/types"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/util/jsonx"
"github.com/fatedier/frp/pkg/util/util"
"kanhole/pkg/config/types"
"kanhole/pkg/msg"
"kanhole/pkg/util/jsonx"
"kanhole/pkg/util/util"
)
type ProxyTransport struct {
+2 -2
View File
@@ -19,8 +19,8 @@ import (
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/util/jsonx"
"github.com/fatedier/frp/pkg/util/util"
"kanhole/pkg/util/jsonx"
"kanhole/pkg/util/util"
)
const (
+3 -3
View File
@@ -17,8 +17,8 @@ package v1
import (
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/config/types"
"github.com/fatedier/frp/pkg/util/util"
"kanhole/pkg/config/types"
"kanhole/pkg/util/util"
)
type ServerConfig struct {
@@ -60,7 +60,7 @@ type ServerConfig struct {
TCPMuxPassthrough bool `json:"tcpmuxPassthrough,omitempty"`
// SubDomainHost specifies the domain that will be attached to sub-domains
// requested by the client when using Vhost proxying. For example, if this
// value is set to "frps.com" and the client requested the subdomain
// value is set to "kanhole.com" and the client requested the subdomain
// "test", the resulting URL would be "test.frps.com".
SubDomainHost string `json:"subDomainHost,omitempty"`
// Custom404Page specifies a path to a custom 404 page to display. If this
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2026 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validation
import (
"fmt"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/policy/security"
)
func (v *ConfigValidator) validateAuthTokenSource(token string, tokenSource *v1.ValueSource) error {
var errs error
// Preserve the previous client/server validation order for joined errors.
if token != "" && tokenSource != nil {
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
}
if tokenSource == nil {
return errs
}
if tokenSource.Type == "exec" {
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
errs = AppendError(errs, err)
}
}
if err := tokenSource.Validate(); err != nil {
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
}
return errs
}
+228
View File
@@ -0,0 +1,228 @@
// Copyright 2026 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validation
import (
"testing"
"github.com/stretchr/testify/require"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/policy/security"
)
const (
tokenSourceConflictErr = "cannot specify both auth.token and auth.tokenSource"
tokenSourceExecErr = "unsafe feature \"TokenSourceExec\" is not enabled. To enable it, ensure it is allowed in the configuration or command line flags"
invalidFileSourceErr = "invalid auth.tokenSource: file configuration is required when type is 'file'"
unsupportedSourceErr = "invalid auth.tokenSource: unsupported value source type: env (only 'file' and 'exec' are supported)"
)
func TestValidateAuthTokenSource(t *testing.T) {
for _, tc := range authTokenSourceTestCases() {
t.Run(tc.name, func(t *testing.T) {
validator := newAuthTokenSourceValidator(tc.unsafeAllowed)
err := validator.validateAuthTokenSource(tc.token, tc.tokenSource())
requireValidationErrors(t, err, tc.wantErrs)
})
}
}
func TestValidateClientAuthTokenSource(t *testing.T) {
for _, tc := range authTokenSourceTestCases() {
t.Run(tc.name, func(t *testing.T) {
auth := v1.AuthClientConfig{
Method: v1.AuthMethodToken,
Token: tc.token,
TokenSource: tc.tokenSource(),
}
validator := newAuthTokenSourceValidator(tc.unsafeAllowed)
_, err := validator.ValidateClientCommonConfig(validClientConfigWithAuth(auth))
requireValidationErrors(t, err, tc.wantErrs)
})
}
}
func TestValidateServerAuthTokenSource(t *testing.T) {
for _, tc := range authTokenSourceTestCases() {
t.Run(tc.name, func(t *testing.T) {
auth := v1.AuthServerConfig{
Method: v1.AuthMethodToken,
Token: tc.token,
TokenSource: tc.tokenSource(),
}
validator := newAuthTokenSourceValidator(tc.unsafeAllowed)
_, err := validator.ValidateServerConfig(validServerConfigWithAuth(auth))
requireValidationErrors(t, err, tc.wantErrs)
})
}
}
type authTokenSourceTestCase struct {
name string
token string
tokenSource func() *v1.ValueSource
unsafeAllowed bool
wantErrs []string
}
func authTokenSourceTestCases() []authTokenSourceTestCase {
return []authTokenSourceTestCase{
{
name: "empty token config",
tokenSource: nilTokenSource,
},
{
name: "valid file tokenSource",
tokenSource: validFileTokenSource,
},
{
name: "literal token without tokenSource",
token: "token",
tokenSource: nilTokenSource,
},
{
name: "literal token conflicts with file tokenSource",
token: "token",
tokenSource: validFileTokenSource,
wantErrs: []string{tokenSourceConflictErr},
},
{
name: "exec tokenSource requires unsafe feature",
tokenSource: validExecTokenSource,
wantErrs: []string{tokenSourceExecErr},
},
{
name: "exec tokenSource with unsafe feature allowed",
tokenSource: validExecTokenSource,
unsafeAllowed: true,
},
{
name: "literal token conflicts with exec tokenSource and unsafe feature disabled",
token: "token",
tokenSource: validExecTokenSource,
wantErrs: []string{
tokenSourceConflictErr,
tokenSourceExecErr,
},
},
{
name: "literal token conflicts with exec tokenSource and unsafe feature allowed",
token: "token",
tokenSource: validExecTokenSource,
unsafeAllowed: true,
wantErrs: []string{tokenSourceConflictErr},
},
{
name: "invalid file tokenSource is wrapped",
tokenSource: invalidFileTokenSource,
wantErrs: []string{invalidFileSourceErr},
},
{
name: "unsupported tokenSource type is wrapped",
tokenSource: unsupportedTokenSource,
wantErrs: []string{unsupportedSourceErr},
},
}
}
func newAuthTokenSourceValidator(unsafeAllowed bool) *ConfigValidator {
if !unsafeAllowed {
return NewConfigValidator(nil)
}
return NewConfigValidator(security.NewUnsafeFeatures([]string{security.TokenSourceExec}))
}
func requireValidationErrors(t *testing.T, err error, wantErrs []string) {
t.Helper()
if len(wantErrs) == 0 {
require.NoError(t, err)
return
}
require.Error(t, err)
// Client/server validators may wrap joined errors in another join layer; compare leaf errors.
gotErrs := unwrapValidationErrors(err)
require.Len(t, gotErrs, len(wantErrs))
for i, wantErr := range wantErrs {
require.EqualError(t, gotErrs[i], wantErr)
}
}
func unwrapValidationErrors(err error) []error {
type joinedError interface {
Unwrap() []error
}
joined, ok := err.(joinedError)
if !ok {
return []error{err}
}
var errs []error
for _, err := range joined.Unwrap() {
errs = append(errs, unwrapValidationErrors(err)...)
}
return errs
}
// nilTokenSource keeps the shared table shape uniform for cases without a tokenSource.
func nilTokenSource() *v1.ValueSource {
return nil
}
func validFileTokenSource() *v1.ValueSource {
return &v1.ValueSource{
Type: "file",
File: &v1.FileSource{Path: "token.txt"},
}
}
func validExecTokenSource() *v1.ValueSource {
return &v1.ValueSource{
Type: "exec",
Exec: &v1.ExecSource{Command: "print-token"},
}
}
func invalidFileTokenSource() *v1.ValueSource {
return &v1.ValueSource{
Type: "file",
}
}
func unsupportedTokenSource() *v1.ValueSource {
return &v1.ValueSource{Type: "env"}
}
func validClientConfigWithAuth(auth v1.AuthClientConfig) *v1.ClientCommonConfig {
return &v1.ClientCommonConfig{
Auth: auth,
Log: v1.LogConfig{
Level: "info",
},
Transport: v1.ClientTransportConfig{
Protocol: "tcp",
WireProtocol: "v1",
},
}
}
func validServerConfigWithAuth(auth v1.AuthServerConfig) *v1.ServerConfig {
return &v1.ServerConfig{
Auth: auth,
Log: v1.LogConfig{
Level: "info",
},
}
}
+4 -19
View File
@@ -22,9 +22,9 @@ import (
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/policy/featuregate"
"github.com/fatedier/frp/pkg/policy/security"
v1 "kanhole/pkg/config/v1"
"kanhole/pkg/policy/featuregate"
"kanhole/pkg/policy/security"
)
func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
@@ -68,22 +68,7 @@ func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, e
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
}
// Validate token/tokenSource mutual exclusivity
if c.Token != "" && c.TokenSource != nil {
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
}
// Validate tokenSource if specified
if c.TokenSource != nil {
if c.TokenSource.Type == "exec" {
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
errs = AppendError(errs, err)
}
}
if err := c.TokenSource.Validate(); err != nil {
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
}
}
errs = AppendError(errs, v.validateAuthTokenSource(c.Token, c.TokenSource))
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
errs = AppendError(errs, err)
+1 -1
View File
@@ -18,7 +18,7 @@ import (
"fmt"
"slices"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
func validateWebServerConfig(c *v1.WebServerConfig) error {
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"net/url"
"strings"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
func ValidateOIDCClientCredentialsConfig(c *v1.AuthOIDCClientConfig) error {
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"github.com/stretchr/testify/require"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
func TestValidateOIDCClientCredentialsConfig(t *testing.T) {
+1 -1
View File
@@ -17,7 +17,7 @@ package validation
import (
"errors"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
func ValidateClientPluginOptions(c v1.ClientPluginOptions) error {
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation"
v1 "github.com/fatedier/frp/pkg/config/v1"
v1 "kanhole/pkg/config/v1"
)
func validateProxyBaseConfigForClient(c *v1.ProxyBaseConfig) error {
+2 -18
View File
@@ -20,8 +20,7 @@ import (
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/policy/security"
v1 "kanhole/pkg/config/v1"
)
func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
@@ -36,22 +35,7 @@ func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, err
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
}
// Validate token/tokenSource mutual exclusivity
if c.Auth.Token != "" && c.Auth.TokenSource != nil {
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
}
// Validate tokenSource if specified
if c.Auth.TokenSource != nil {
if c.Auth.TokenSource.Type == "exec" {
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
errs = AppendError(errs, err)
}
}
if err := c.Auth.TokenSource.Validate(); err != nil {
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
}
}
errs = AppendError(errs, v.validateAuthTokenSource(c.Auth.Token, c.Auth.TokenSource))
if err := validateLogConfig(&c.Log); err != nil {
errs = AppendError(errs, err)
+2 -2
View File
@@ -17,8 +17,8 @@ package validation
import (
"errors"
v1 "github.com/fatedier/frp/pkg/config/v1"
splugin "github.com/fatedier/frp/pkg/plugin/server"
v1 "kanhole/pkg/config/v1"
splugin "kanhole/pkg/plugin/server"
)
var (
+1 -1
View File
@@ -3,7 +3,7 @@ package validation
import (
"fmt"
"github.com/fatedier/frp/pkg/policy/security"
"kanhole/pkg/policy/security"
)
// ConfigValidator holds the context dependencies for configuration validation.

Some files were not shown because too many files have changed in this diff Show More